sunpeak 0.2.5 → 0.3.2
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.
- package/README.md +32 -17
- package/dist/chatgpt/chatgpt-simulator-types.d.ts +8 -0
- package/dist/chatgpt/chatgpt-simulator.d.ts +11 -0
- package/dist/chatgpt/conversation.d.ts +11 -0
- package/dist/chatgpt/index.d.ts +3 -0
- package/dist/chatgpt/mcp-provider.d.ts +25 -0
- package/dist/chatgpt/mock-openai.d.ts +61 -0
- package/dist/chatgpt/openai-provider.d.ts +19 -0
- package/dist/chatgpt/openai-types.d.ts +81 -0
- package/dist/chatgpt/simple-sidebar.d.ts +22 -0
- package/dist/chatgpt/theme-provider.d.ts +13 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/use-display-mode.d.ts +2 -0
- package/dist/hooks/use-locale.d.ts +1 -0
- package/dist/hooks/use-max-height.d.ts +1 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-safe-area.d.ts +2 -0
- package/dist/hooks/use-theme.d.ts +2 -0
- package/dist/hooks/use-tool-input.d.ts +2 -0
- package/dist/hooks/use-tool-response-metadata.d.ts +2 -0
- package/dist/hooks/use-user-agent.d.ts +2 -0
- package/dist/hooks/use-view.d.ts +2 -0
- package/dist/hooks/use-widget-api.d.ts +8 -0
- package/dist/hooks/use-widget-global.d.ts +9 -0
- package/dist/hooks/use-widget-props.d.ts +1 -0
- package/dist/hooks/use-widget-state.d.ts +4 -0
- package/dist/index.cjs +3310 -666
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -366
- package/dist/index.js +3325 -640
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/media-queries.d.ts +3 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/mcp/index.cjs +799 -64
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.d.ts +1 -12
- package/dist/mcp/index.js +786 -44
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/types.d.ts +74 -0
- package/dist/providers/index.d.ts +40 -0
- package/dist/providers/types.d.ts +71 -0
- package/dist/style.css +5014 -0
- package/dist/test/setup.d.ts +0 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +11 -19
- package/template/README.md +3 -6
- package/template/dev/main.tsx +0 -1
- package/template/mcp/server.ts +1 -1
- package/template/package.json +4 -14
- package/template/src/App.tsx +7 -8
- package/template/src/components/index.ts +2 -2
- package/template/src/components/openai-card.test.tsx +73 -0
- package/template/src/components/openai-card.tsx +126 -0
- package/template/src/components/openai-carousel.test.tsx +84 -0
- package/template/src/components/openai-carousel.tsx +178 -0
- package/template/src/styles/globals.css +5 -216
- package/template/vite.config.build.ts +61 -0
- package/dist/index.d.cts +0 -366
- package/dist/mcp/index.d.cts +0 -12
- package/dist/styles/chatgpt/index.css +0 -146
- package/dist/styles/globals.css +0 -219
- package/template/components.json +0 -21
- package/template/dev/styles.css +0 -6
- package/template/postcss.config.js +0 -5
- package/template/src/components/shadcn/button.tsx +0 -60
- package/template/src/components/shadcn/card.tsx +0 -76
- package/template/src/components/shadcn/carousel.tsx +0 -260
- package/template/src/components/shadcn/index.ts +0 -5
- package/template/src/components/shadcn/label.tsx +0 -24
- package/template/src/components/shadcn/select.tsx +0 -157
- package/template/src/components/sunpeak-card.test.tsx +0 -76
- package/template/src/components/sunpeak-card.tsx +0 -171
- package/template/src/components/sunpeak-carousel.test.tsx +0 -42
- package/template/src/components/sunpeak-carousel.tsx +0 -160
- package/template/src/styles/chatgpt.css +0 -146
- package/template/tsup.config.ts +0 -50
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
3
|
-
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
4
|
-
|
|
5
|
-
import { cn } from "@/lib/index"
|
|
6
|
-
|
|
7
|
-
const Select = SelectPrimitive.Root
|
|
8
|
-
|
|
9
|
-
const SelectGroup = SelectPrimitive.Group
|
|
10
|
-
|
|
11
|
-
const SelectValue = SelectPrimitive.Value
|
|
12
|
-
|
|
13
|
-
const SelectTrigger = React.forwardRef<
|
|
14
|
-
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
15
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
16
|
-
>(({ className, children, ...props }, ref) => (
|
|
17
|
-
<SelectPrimitive.Trigger
|
|
18
|
-
ref={ref}
|
|
19
|
-
className={cn(
|
|
20
|
-
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
21
|
-
className
|
|
22
|
-
)}
|
|
23
|
-
{...props}
|
|
24
|
-
>
|
|
25
|
-
{children}
|
|
26
|
-
<SelectPrimitive.Icon asChild>
|
|
27
|
-
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
28
|
-
</SelectPrimitive.Icon>
|
|
29
|
-
</SelectPrimitive.Trigger>
|
|
30
|
-
))
|
|
31
|
-
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
32
|
-
|
|
33
|
-
const SelectScrollUpButton = React.forwardRef<
|
|
34
|
-
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
35
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
36
|
-
>(({ className, ...props }, ref) => (
|
|
37
|
-
<SelectPrimitive.ScrollUpButton
|
|
38
|
-
ref={ref}
|
|
39
|
-
className={cn(
|
|
40
|
-
"flex cursor-default items-center justify-center py-1",
|
|
41
|
-
className
|
|
42
|
-
)}
|
|
43
|
-
{...props}
|
|
44
|
-
>
|
|
45
|
-
<ChevronUp className="h-4 w-4" />
|
|
46
|
-
</SelectPrimitive.ScrollUpButton>
|
|
47
|
-
))
|
|
48
|
-
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
49
|
-
|
|
50
|
-
const SelectScrollDownButton = React.forwardRef<
|
|
51
|
-
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
52
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
53
|
-
>(({ className, ...props }, ref) => (
|
|
54
|
-
<SelectPrimitive.ScrollDownButton
|
|
55
|
-
ref={ref}
|
|
56
|
-
className={cn(
|
|
57
|
-
"flex cursor-default items-center justify-center py-1",
|
|
58
|
-
className
|
|
59
|
-
)}
|
|
60
|
-
{...props}
|
|
61
|
-
>
|
|
62
|
-
<ChevronDown className="h-4 w-4" />
|
|
63
|
-
</SelectPrimitive.ScrollDownButton>
|
|
64
|
-
))
|
|
65
|
-
SelectScrollDownButton.displayName =
|
|
66
|
-
SelectPrimitive.ScrollDownButton.displayName
|
|
67
|
-
|
|
68
|
-
const SelectContent = React.forwardRef<
|
|
69
|
-
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
70
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
71
|
-
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
72
|
-
<SelectPrimitive.Portal>
|
|
73
|
-
<SelectPrimitive.Content
|
|
74
|
-
ref={ref}
|
|
75
|
-
className={cn(
|
|
76
|
-
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 origin-[--radix-select-content-transform-origin]",
|
|
77
|
-
position === "popper" &&
|
|
78
|
-
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
79
|
-
className
|
|
80
|
-
)}
|
|
81
|
-
position={position}
|
|
82
|
-
{...props}
|
|
83
|
-
>
|
|
84
|
-
<SelectScrollUpButton />
|
|
85
|
-
<SelectPrimitive.Viewport
|
|
86
|
-
className={cn(
|
|
87
|
-
"p-1",
|
|
88
|
-
position === "popper" &&
|
|
89
|
-
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
90
|
-
)}
|
|
91
|
-
>
|
|
92
|
-
{children}
|
|
93
|
-
</SelectPrimitive.Viewport>
|
|
94
|
-
<SelectScrollDownButton />
|
|
95
|
-
</SelectPrimitive.Content>
|
|
96
|
-
</SelectPrimitive.Portal>
|
|
97
|
-
))
|
|
98
|
-
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
99
|
-
|
|
100
|
-
const SelectLabel = React.forwardRef<
|
|
101
|
-
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
102
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
103
|
-
>(({ className, ...props }, ref) => (
|
|
104
|
-
<SelectPrimitive.Label
|
|
105
|
-
ref={ref}
|
|
106
|
-
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
|
107
|
-
{...props}
|
|
108
|
-
/>
|
|
109
|
-
))
|
|
110
|
-
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
111
|
-
|
|
112
|
-
const SelectItem = React.forwardRef<
|
|
113
|
-
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
114
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
115
|
-
>(({ className, children, ...props }, ref) => (
|
|
116
|
-
<SelectPrimitive.Item
|
|
117
|
-
ref={ref}
|
|
118
|
-
className={cn(
|
|
119
|
-
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
120
|
-
className
|
|
121
|
-
)}
|
|
122
|
-
{...props}
|
|
123
|
-
>
|
|
124
|
-
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
125
|
-
<SelectPrimitive.ItemIndicator>
|
|
126
|
-
<Check className="h-4 w-4" />
|
|
127
|
-
</SelectPrimitive.ItemIndicator>
|
|
128
|
-
</span>
|
|
129
|
-
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
130
|
-
</SelectPrimitive.Item>
|
|
131
|
-
))
|
|
132
|
-
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
133
|
-
|
|
134
|
-
const SelectSeparator = React.forwardRef<
|
|
135
|
-
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
136
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
137
|
-
>(({ className, ...props }, ref) => (
|
|
138
|
-
<SelectPrimitive.Separator
|
|
139
|
-
ref={ref}
|
|
140
|
-
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
141
|
-
{...props}
|
|
142
|
-
/>
|
|
143
|
-
))
|
|
144
|
-
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
145
|
-
|
|
146
|
-
export {
|
|
147
|
-
Select,
|
|
148
|
-
SelectGroup,
|
|
149
|
-
SelectValue,
|
|
150
|
-
SelectTrigger,
|
|
151
|
-
SelectContent,
|
|
152
|
-
SelectLabel,
|
|
153
|
-
SelectItem,
|
|
154
|
-
SelectSeparator,
|
|
155
|
-
SelectScrollUpButton,
|
|
156
|
-
SelectScrollDownButton,
|
|
157
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
-
import { SunpeakCard } from './sunpeak-card';
|
|
5
|
-
|
|
6
|
-
describe('SunpeakCard', () => {
|
|
7
|
-
it('renders image, header, metadata, and content', () => {
|
|
8
|
-
render(
|
|
9
|
-
<SunpeakCard
|
|
10
|
-
image="https://example.com/image.jpg"
|
|
11
|
-
imageAlt="Test image"
|
|
12
|
-
header="Test Header"
|
|
13
|
-
metadata="Test metadata"
|
|
14
|
-
>
|
|
15
|
-
Card content here
|
|
16
|
-
</SunpeakCard>
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
expect(screen.getByRole('img')).toHaveAttribute('src', 'https://example.com/image.jpg');
|
|
20
|
-
expect(screen.getByRole('img')).toHaveAttribute('alt', 'Test image');
|
|
21
|
-
expect(screen.getByText('Test Header')).toBeInTheDocument();
|
|
22
|
-
expect(screen.getByText('Test metadata')).toBeInTheDocument();
|
|
23
|
-
expect(screen.getByText('Card content here')).toBeInTheDocument();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('applies variant styles correctly', () => {
|
|
27
|
-
const { rerender } = render(
|
|
28
|
-
<SunpeakCard
|
|
29
|
-
image="https://example.com/image.jpg"
|
|
30
|
-
imageAlt="Test"
|
|
31
|
-
variant="bordered"
|
|
32
|
-
data-testid="card"
|
|
33
|
-
/>
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
expect(screen.getByTestId('card')).toHaveClass('border-2');
|
|
37
|
-
|
|
38
|
-
rerender(
|
|
39
|
-
<SunpeakCard
|
|
40
|
-
image="https://example.com/image.jpg"
|
|
41
|
-
imageAlt="Test"
|
|
42
|
-
variant="elevated"
|
|
43
|
-
data-testid="card"
|
|
44
|
-
/>
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
expect(screen.getByTestId('card')).toHaveClass('shadow-lg');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('renders buttons and handles clicks without propagating to card', async () => {
|
|
51
|
-
const user = userEvent.setup();
|
|
52
|
-
const cardClick = vi.fn();
|
|
53
|
-
const buttonClick = vi.fn();
|
|
54
|
-
|
|
55
|
-
render(
|
|
56
|
-
<SunpeakCard
|
|
57
|
-
image="https://example.com/image.jpg"
|
|
58
|
-
imageAlt="Test"
|
|
59
|
-
onClick={cardClick}
|
|
60
|
-
button1={{ children: 'Primary', onClick: buttonClick, isPrimary: true }}
|
|
61
|
-
button2={{ children: 'Secondary', onClick: buttonClick }}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const primaryButton = screen.getByRole('button', { name: 'Primary' });
|
|
66
|
-
const secondaryButton = screen.getByRole('button', { name: 'Secondary' });
|
|
67
|
-
|
|
68
|
-
expect(primaryButton).toBeInTheDocument();
|
|
69
|
-
expect(secondaryButton).toBeInTheDocument();
|
|
70
|
-
|
|
71
|
-
await user.click(primaryButton);
|
|
72
|
-
|
|
73
|
-
expect(buttonClick).toHaveBeenCalledTimes(1);
|
|
74
|
-
expect(cardClick).not.toHaveBeenCalled();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { useWidgetState } from "sunpeak"
|
|
3
|
-
import { cn } from "@/lib/index"
|
|
4
|
-
import {
|
|
5
|
-
Card,
|
|
6
|
-
CardContent,
|
|
7
|
-
CardDescription,
|
|
8
|
-
CardFooter,
|
|
9
|
-
CardHeader,
|
|
10
|
-
CardTitle,
|
|
11
|
-
} from "@/components/shadcn/card"
|
|
12
|
-
import { Button } from "@/components/shadcn/button"
|
|
13
|
-
|
|
14
|
-
export interface SunpeakButtonProps extends Omit<React.ComponentProps<typeof Button>, 'onClick'> {
|
|
15
|
-
isPrimary?: boolean
|
|
16
|
-
onClick: () => void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface SunpeakCardData extends Record<string, unknown> {
|
|
20
|
-
image?: string
|
|
21
|
-
imageAlt?: string
|
|
22
|
-
imageMaxWidth?: number
|
|
23
|
-
imageMaxHeight?: number
|
|
24
|
-
header?: React.ReactNode
|
|
25
|
-
metadata?: React.ReactNode
|
|
26
|
-
children?: React.ReactNode
|
|
27
|
-
button1?: SunpeakButtonProps
|
|
28
|
-
button2?: SunpeakButtonProps
|
|
29
|
-
variant?: "default" | "bordered" | "elevated"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface SunpeakCardState extends Record<string, unknown> {
|
|
33
|
-
selectedVariant?: "default" | "bordered" | "elevated"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SunpeakCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
37
|
-
children?: React.ReactNode
|
|
38
|
-
image?: string
|
|
39
|
-
imageAlt?: string
|
|
40
|
-
imageMaxWidth?: number
|
|
41
|
-
imageMaxHeight?: number
|
|
42
|
-
header?: React.ReactNode
|
|
43
|
-
metadata?: React.ReactNode
|
|
44
|
-
button1?: SunpeakButtonProps
|
|
45
|
-
button2?: SunpeakButtonProps
|
|
46
|
-
variant?: "default" | "bordered" | "elevated"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const SunpeakCard = React.forwardRef<HTMLDivElement, SunpeakCardProps>(
|
|
50
|
-
(
|
|
51
|
-
{
|
|
52
|
-
children: childrenProp,
|
|
53
|
-
image: imageProp,
|
|
54
|
-
imageAlt: imageAltProp,
|
|
55
|
-
imageMaxWidth: imageMaxWidthProp = 400,
|
|
56
|
-
imageMaxHeight: imageMaxHeightProp = 400,
|
|
57
|
-
header: headerProp,
|
|
58
|
-
metadata: metadataProp,
|
|
59
|
-
button1: button1Prop,
|
|
60
|
-
button2: button2Prop,
|
|
61
|
-
variant: variantProp = "default",
|
|
62
|
-
className,
|
|
63
|
-
onClick,
|
|
64
|
-
...props
|
|
65
|
-
},
|
|
66
|
-
ref
|
|
67
|
-
) => {
|
|
68
|
-
const [widgetState] = useWidgetState<SunpeakCardState>(() => ({}))
|
|
69
|
-
|
|
70
|
-
const children = childrenProp
|
|
71
|
-
const image = imageProp
|
|
72
|
-
const imageAlt = imageAltProp
|
|
73
|
-
const imageMaxWidth = imageMaxWidthProp
|
|
74
|
-
const imageMaxHeight = imageMaxHeightProp
|
|
75
|
-
const header = headerProp
|
|
76
|
-
const metadata = metadataProp
|
|
77
|
-
const button1 = button1Prop
|
|
78
|
-
const button2 = button2Prop
|
|
79
|
-
const variant = widgetState?.selectedVariant ?? variantProp
|
|
80
|
-
|
|
81
|
-
const hasButtons = button1 || button2
|
|
82
|
-
|
|
83
|
-
const handleCardClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
84
|
-
onClick?.(e)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const renderButton = (buttonProps: SunpeakButtonProps) => {
|
|
88
|
-
const { isPrimary = false, onClick: buttonOnClick, children, ...restProps } = buttonProps
|
|
89
|
-
|
|
90
|
-
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
91
|
-
e.stopPropagation()
|
|
92
|
-
buttonOnClick()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<Button
|
|
97
|
-
{...restProps}
|
|
98
|
-
variant={isPrimary ? "default" : "outline"}
|
|
99
|
-
onClick={handleClick}
|
|
100
|
-
>
|
|
101
|
-
{children}
|
|
102
|
-
</Button>
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const variantClasses = {
|
|
107
|
-
default: "",
|
|
108
|
-
bordered: "border-2",
|
|
109
|
-
elevated: "shadow-lg",
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<Card
|
|
114
|
-
ref={ref}
|
|
115
|
-
className={cn(
|
|
116
|
-
"overflow-hidden cursor-pointer select-none",
|
|
117
|
-
variantClasses[variant],
|
|
118
|
-
className
|
|
119
|
-
)}
|
|
120
|
-
onClick={handleCardClick}
|
|
121
|
-
{...props}
|
|
122
|
-
>
|
|
123
|
-
{image && (
|
|
124
|
-
<div className="w-full overflow-hidden">
|
|
125
|
-
<img
|
|
126
|
-
src={image}
|
|
127
|
-
alt={imageAlt}
|
|
128
|
-
loading="lazy"
|
|
129
|
-
className="w-full h-auto aspect-square object-cover"
|
|
130
|
-
style={{
|
|
131
|
-
maxWidth: `${imageMaxWidth}px`,
|
|
132
|
-
maxHeight: `${imageMaxHeight}px`,
|
|
133
|
-
}}
|
|
134
|
-
/>
|
|
135
|
-
</div>
|
|
136
|
-
)}
|
|
137
|
-
<div className="flex flex-col flex-1">
|
|
138
|
-
{(header || metadata) && (
|
|
139
|
-
<CardHeader className={cn("p-3", image && "pt-2")}>
|
|
140
|
-
{header && (
|
|
141
|
-
<CardTitle className="text-sm font-medium leading-tight overflow-hidden text-ellipsis whitespace-nowrap">
|
|
142
|
-
{header}
|
|
143
|
-
</CardTitle>
|
|
144
|
-
)}
|
|
145
|
-
{metadata && (
|
|
146
|
-
<CardDescription className="text-xs leading-normal">
|
|
147
|
-
{metadata}
|
|
148
|
-
</CardDescription>
|
|
149
|
-
)}
|
|
150
|
-
</CardHeader>
|
|
151
|
-
)}
|
|
152
|
-
{children && (
|
|
153
|
-
<CardContent className={cn(
|
|
154
|
-
"px-3 pb-3 text-sm leading-normal",
|
|
155
|
-
(header || metadata) ? "pt-0" : "pt-3"
|
|
156
|
-
)}>
|
|
157
|
-
<div className="line-clamp-2">{children}</div>
|
|
158
|
-
</CardContent>
|
|
159
|
-
)}
|
|
160
|
-
{hasButtons && (
|
|
161
|
-
<CardFooter className="flex gap-2 flex-wrap px-3 pb-3 pt-0">
|
|
162
|
-
{button1 && renderButton(button1)}
|
|
163
|
-
{button2 && renderButton(button2)}
|
|
164
|
-
</CardFooter>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
</Card>
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
)
|
|
171
|
-
SunpeakCard.displayName = "SunpeakCard"
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { SunpeakCarousel } from './sunpeak-carousel';
|
|
4
|
-
|
|
5
|
-
describe('SunpeakCarousel', () => {
|
|
6
|
-
it('renders all children as carousel items', () => {
|
|
7
|
-
render(
|
|
8
|
-
<SunpeakCarousel>
|
|
9
|
-
<div>Item 1</div>
|
|
10
|
-
<div>Item 2</div>
|
|
11
|
-
<div>Item 3</div>
|
|
12
|
-
</SunpeakCarousel>
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
16
|
-
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
17
|
-
expect(screen.getByText('Item 3')).toBeInTheDocument();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('applies custom gap spacing to carousel content', () => {
|
|
21
|
-
render(
|
|
22
|
-
<SunpeakCarousel gap={24} data-testid="carousel">
|
|
23
|
-
<div>Item 1</div>
|
|
24
|
-
<div>Item 2</div>
|
|
25
|
-
</SunpeakCarousel>
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const carouselContent = document.querySelector('[class*="ml-0"]');
|
|
29
|
-
expect(carouselContent).toHaveStyle({ gap: '24px' });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('applies custom card width to carousel items', () => {
|
|
33
|
-
render(
|
|
34
|
-
<SunpeakCarousel cardWidth={300}>
|
|
35
|
-
<div>Item 1</div>
|
|
36
|
-
</SunpeakCarousel>
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const carouselItem = document.querySelector('[class*="pl-0"]');
|
|
40
|
-
expect(carouselItem).toHaveStyle({ flexBasis: '300px', minWidth: '300px' });
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
|
|
3
|
-
import { useWidgetState, useDisplayMode } from "sunpeak"
|
|
4
|
-
import { cn } from "@/lib/index"
|
|
5
|
-
import {
|
|
6
|
-
Carousel,
|
|
7
|
-
CarouselContent,
|
|
8
|
-
CarouselItem,
|
|
9
|
-
CarouselNext,
|
|
10
|
-
CarouselPrevious,
|
|
11
|
-
type CarouselApi,
|
|
12
|
-
} from "@/components/shadcn/carousel"
|
|
13
|
-
|
|
14
|
-
export interface SunpeakCarouselData extends Record<string, unknown> {
|
|
15
|
-
children?: React.ReactNode
|
|
16
|
-
gap?: number
|
|
17
|
-
showArrows?: boolean
|
|
18
|
-
showEdgeGradients?: boolean
|
|
19
|
-
cardWidth?: number | { inline?: number; fullscreen?: number }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SunpeakCarouselState extends Record<string, unknown> {
|
|
23
|
-
currentIndex?: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type SunpeakCarouselProps = {
|
|
27
|
-
children?: React.ReactNode
|
|
28
|
-
gap?: number
|
|
29
|
-
showArrows?: boolean
|
|
30
|
-
showEdgeGradients?: boolean
|
|
31
|
-
cardWidth?: number | { inline?: number; fullscreen?: number }
|
|
32
|
-
className?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const SunpeakCarousel = React.forwardRef<
|
|
36
|
-
HTMLDivElement,
|
|
37
|
-
SunpeakCarouselProps
|
|
38
|
-
>(
|
|
39
|
-
(
|
|
40
|
-
{
|
|
41
|
-
children: childrenProp,
|
|
42
|
-
gap: gapProp = 16,
|
|
43
|
-
showArrows: showArrowsProp = true,
|
|
44
|
-
showEdgeGradients: showEdgeGradientsProp = true,
|
|
45
|
-
cardWidth: cardWidthProp,
|
|
46
|
-
className,
|
|
47
|
-
},
|
|
48
|
-
ref
|
|
49
|
-
) => {
|
|
50
|
-
const [widgetState, setWidgetState] = useWidgetState<SunpeakCarouselState>(() => ({
|
|
51
|
-
currentIndex: 0,
|
|
52
|
-
}))
|
|
53
|
-
const displayMode = useDisplayMode()
|
|
54
|
-
|
|
55
|
-
const children = childrenProp
|
|
56
|
-
const gap = gapProp
|
|
57
|
-
const showArrows = showArrowsProp
|
|
58
|
-
const showEdgeGradients = showEdgeGradientsProp
|
|
59
|
-
const cardWidth = cardWidthProp
|
|
60
|
-
|
|
61
|
-
const [api, setApi] = React.useState<CarouselApi>()
|
|
62
|
-
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
63
|
-
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
64
|
-
|
|
65
|
-
React.useEffect(() => {
|
|
66
|
-
if (!api) return
|
|
67
|
-
|
|
68
|
-
const onSelect = () => {
|
|
69
|
-
setCanScrollPrev(api.canScrollPrev())
|
|
70
|
-
setCanScrollNext(api.canScrollNext())
|
|
71
|
-
|
|
72
|
-
const currentIndex = api.selectedScrollSnap()
|
|
73
|
-
if (widgetState?.currentIndex !== currentIndex) {
|
|
74
|
-
setWidgetState({ currentIndex })
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
onSelect()
|
|
79
|
-
api.on("select", onSelect)
|
|
80
|
-
api.on("reInit", onSelect)
|
|
81
|
-
|
|
82
|
-
return () => {
|
|
83
|
-
api.off("select", onSelect)
|
|
84
|
-
api.off("reInit", onSelect)
|
|
85
|
-
}
|
|
86
|
-
}, [api, widgetState?.currentIndex, setWidgetState])
|
|
87
|
-
|
|
88
|
-
const childArray = React.Children.toArray(children)
|
|
89
|
-
|
|
90
|
-
const getCardWidth = () => {
|
|
91
|
-
if (typeof cardWidth === "number") {
|
|
92
|
-
return `${cardWidth}px`
|
|
93
|
-
}
|
|
94
|
-
if (cardWidth && typeof cardWidth === "object") {
|
|
95
|
-
if (displayMode === "fullscreen" && cardWidth.fullscreen) {
|
|
96
|
-
return `${cardWidth.fullscreen}px`
|
|
97
|
-
}
|
|
98
|
-
if (cardWidth.inline) {
|
|
99
|
-
return `${cardWidth.inline}px`
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return "220px"
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div ref={ref} className={cn("relative w-full", className)}>
|
|
107
|
-
{showEdgeGradients && canScrollPrev && (
|
|
108
|
-
<div
|
|
109
|
-
className="pointer-events-none absolute left-0 top-0 z-10 h-full w-12 bg-gradient-to-r from-background to-transparent transition-opacity"
|
|
110
|
-
aria-hidden="true"
|
|
111
|
-
/>
|
|
112
|
-
)}
|
|
113
|
-
{showEdgeGradients && canScrollNext && (
|
|
114
|
-
<div
|
|
115
|
-
className="pointer-events-none absolute right-0 top-0 z-10 h-full w-12 bg-gradient-to-l from-background to-transparent transition-opacity"
|
|
116
|
-
aria-hidden="true"
|
|
117
|
-
/>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
<Carousel
|
|
121
|
-
opts={{
|
|
122
|
-
align: "start",
|
|
123
|
-
dragFree: true,
|
|
124
|
-
}}
|
|
125
|
-
plugins={[WheelGesturesPlugin()]}
|
|
126
|
-
setApi={setApi}
|
|
127
|
-
className="w-full"
|
|
128
|
-
>
|
|
129
|
-
<CarouselContent
|
|
130
|
-
className="ml-0"
|
|
131
|
-
style={{
|
|
132
|
-
gap: `${gap}px`,
|
|
133
|
-
}}
|
|
134
|
-
>
|
|
135
|
-
{childArray.map((child, index) => (
|
|
136
|
-
<CarouselItem
|
|
137
|
-
key={index}
|
|
138
|
-
className="pl-0"
|
|
139
|
-
style={{
|
|
140
|
-
flexBasis: getCardWidth(),
|
|
141
|
-
minWidth: getCardWidth(),
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
144
|
-
{child}
|
|
145
|
-
</CarouselItem>
|
|
146
|
-
))}
|
|
147
|
-
</CarouselContent>
|
|
148
|
-
|
|
149
|
-
{showArrows && canScrollPrev && (
|
|
150
|
-
<CarouselPrevious className="left-2" />
|
|
151
|
-
)}
|
|
152
|
-
{showArrows && canScrollNext && (
|
|
153
|
-
<CarouselNext className="right-2" />
|
|
154
|
-
)}
|
|
155
|
-
</Carousel>
|
|
156
|
-
</div>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
)
|
|
160
|
-
SunpeakCarousel.displayName = "SunpeakCarousel"
|