sunpeak 0.2.6 → 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.
Files changed (79) hide show
  1. package/README.md +32 -17
  2. package/dist/chatgpt/chatgpt-simulator-types.d.ts +8 -0
  3. package/dist/chatgpt/chatgpt-simulator.d.ts +11 -0
  4. package/dist/chatgpt/conversation.d.ts +11 -0
  5. package/dist/chatgpt/index.d.ts +3 -0
  6. package/dist/chatgpt/mcp-provider.d.ts +25 -0
  7. package/dist/chatgpt/mock-openai.d.ts +61 -0
  8. package/dist/chatgpt/openai-provider.d.ts +19 -0
  9. package/dist/chatgpt/openai-types.d.ts +81 -0
  10. package/dist/chatgpt/simple-sidebar.d.ts +22 -0
  11. package/dist/chatgpt/theme-provider.d.ts +13 -0
  12. package/dist/hooks/index.d.ts +14 -0
  13. package/dist/hooks/use-display-mode.d.ts +2 -0
  14. package/dist/hooks/use-locale.d.ts +1 -0
  15. package/dist/hooks/use-max-height.d.ts +1 -0
  16. package/dist/hooks/use-mobile.d.ts +1 -0
  17. package/dist/hooks/use-safe-area.d.ts +2 -0
  18. package/dist/hooks/use-theme.d.ts +2 -0
  19. package/dist/hooks/use-tool-input.d.ts +2 -0
  20. package/dist/hooks/use-tool-response-metadata.d.ts +2 -0
  21. package/dist/hooks/use-user-agent.d.ts +2 -0
  22. package/dist/hooks/use-view.d.ts +2 -0
  23. package/dist/hooks/use-widget-api.d.ts +8 -0
  24. package/dist/hooks/use-widget-global.d.ts +9 -0
  25. package/dist/hooks/use-widget-props.d.ts +1 -0
  26. package/dist/hooks/use-widget-state.d.ts +4 -0
  27. package/dist/index.cjs +3310 -666
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +5 -366
  30. package/dist/index.js +3325 -640
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/index.d.ts +2 -0
  33. package/dist/lib/media-queries.d.ts +3 -0
  34. package/dist/lib/utils.d.ts +2 -0
  35. package/dist/mcp/index.cjs +799 -64
  36. package/dist/mcp/index.cjs.map +1 -1
  37. package/dist/mcp/index.d.ts +1 -12
  38. package/dist/mcp/index.js +786 -44
  39. package/dist/mcp/index.js.map +1 -1
  40. package/dist/mcp/server.d.ts +10 -0
  41. package/dist/mcp/types.d.ts +74 -0
  42. package/dist/providers/index.d.ts +40 -0
  43. package/dist/providers/types.d.ts +71 -0
  44. package/dist/style.css +5014 -0
  45. package/dist/test/setup.d.ts +0 -0
  46. package/dist/types/index.d.ts +2 -0
  47. package/package.json +11 -19
  48. package/template/README.md +3 -6
  49. package/template/dev/main.tsx +0 -1
  50. package/template/mcp/server.ts +1 -1
  51. package/template/package.json +4 -14
  52. package/template/src/App.tsx +7 -8
  53. package/template/src/components/index.ts +2 -2
  54. package/template/src/components/openai-card.test.tsx +73 -0
  55. package/template/src/components/openai-card.tsx +126 -0
  56. package/template/src/components/openai-carousel.test.tsx +84 -0
  57. package/template/src/components/openai-carousel.tsx +178 -0
  58. package/template/src/styles/globals.css +5 -216
  59. package/template/vite.config.build.ts +61 -0
  60. package/template/vite.config.ts +0 -2
  61. package/dist/index.d.cts +0 -366
  62. package/dist/mcp/index.d.cts +0 -12
  63. package/dist/styles/chatgpt/index.css +0 -146
  64. package/dist/styles/globals.css +0 -219
  65. package/template/components.json +0 -21
  66. package/template/dev/styles.css +0 -6
  67. package/template/postcss.config.js +0 -5
  68. package/template/src/components/shadcn/button.tsx +0 -60
  69. package/template/src/components/shadcn/card.tsx +0 -76
  70. package/template/src/components/shadcn/carousel.tsx +0 -260
  71. package/template/src/components/shadcn/index.ts +0 -5
  72. package/template/src/components/shadcn/label.tsx +0 -24
  73. package/template/src/components/shadcn/select.tsx +0 -157
  74. package/template/src/components/sunpeak-card.test.tsx +0 -76
  75. package/template/src/components/sunpeak-card.tsx +0 -171
  76. package/template/src/components/sunpeak-carousel.test.tsx +0 -42
  77. package/template/src/components/sunpeak-carousel.tsx +0 -160
  78. package/template/src/styles/chatgpt.css +0 -146
  79. package/template/tsup.config.ts +0 -50
File without changes
@@ -0,0 +1,2 @@
1
+ export * from '../chatgpt/openai-types';
2
+ export * from '../chatgpt/chatgpt-simulator-types';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.2.6",
4
- "description": "The ChatGPT Apps UI SDK. Build and test your ChatGPT App UI locally with approved shadcn React components.",
3
+ "version": "0.3.2",
4
+ "description": "The ChatGPT Apps UI SDK. Build and test your ChatGPT App UI locally with OpenAI apps-sdk-ui components.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "default": "./dist/index.js"
14
14
  },
15
15
  "require": {
16
- "types": "./dist/index.d.cts",
16
+ "types": "./dist/index.d.ts",
17
17
  "default": "./dist/index.cjs"
18
18
  }
19
19
  },
@@ -23,8 +23,6 @@
23
23
  "default": "./dist/mcp/index.js"
24
24
  }
25
25
  },
26
- "./styles/globals.css": "./dist/styles/globals.css",
27
- "./styles/chatgpt": "./dist/styles/chatgpt/index.css",
28
26
  "./package.json": "./package.json"
29
27
  },
30
28
  "bin": {
@@ -38,15 +36,17 @@
38
36
  ],
39
37
  "sideEffects": false,
40
38
  "keywords": [
39
+ "openai-apps-sdk-ui",
41
40
  "openai-app",
41
+ "chatgpt-apps-sdk",
42
42
  "chatgpt-app",
43
+ "mcp-ui",
44
+ "mcp",
43
45
  "chatgpt-simulator",
44
46
  "testing",
45
47
  "multi-platform",
46
48
  "react",
47
- "components",
48
- "shadcn-ui",
49
- "tailwindcss",
49
+ "tailwind",
50
50
  "sunpeak"
51
51
  ],
52
52
  "author": "Sunpeak AI",
@@ -57,17 +57,9 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@modelcontextprotocol/sdk": "^0.5.0",
60
- "@radix-ui/react-dialog": "^1.1.15",
61
- "@radix-ui/react-label": "^2.1.8",
62
- "@radix-ui/react-select": "^2.2.6",
63
- "@radix-ui/react-separator": "^1.1.8",
64
- "@radix-ui/react-slot": "^1.2.4",
65
- "@radix-ui/react-tooltip": "^1.2.8",
66
- "class-variance-authority": "^0.7.1",
60
+ "@openai/apps-sdk-ui": "^0.2.0",
67
61
  "clsx": "^2.1.1",
68
- "lucide-react": "^0.554.0",
69
62
  "tailwind-merge": "^3.4.0",
70
- "tw-animate-css": "^1.4.0",
71
63
  "zod": "^3.23.8"
72
64
  },
73
65
  "devDependencies": {
@@ -91,10 +83,10 @@
91
83
  "react-dom": "^18.3.1",
92
84
  "tailwindcss": "^4.1.17",
93
85
  "ts-node": "^10.9.2",
94
- "tsup": "^8.3.5",
95
86
  "tsx": "^4.20.6",
96
87
  "typescript": "^5.6.3",
97
88
  "vite": "^5.4.21",
89
+ "vite-plugin-dts": "^4.5.4",
98
90
  "vitest": "^4.0.12"
99
91
  },
100
92
  "repository": {
@@ -106,7 +98,7 @@
106
98
  },
107
99
  "homepage": "https://sunpeak.ai/",
108
100
  "scripts": {
109
- "build": "tsup",
101
+ "build": "vite build",
110
102
  "dev": "pnpm --filter my-sunpeak-app dev",
111
103
  "lint": "eslint . --ext .ts,.tsx --fix",
112
104
  "typecheck": "tsc --noEmit",
@@ -4,8 +4,6 @@ A ChatGPT App UI built with [sunpeak](https://github.com/Sunpeak-AI/sunpeak).
4
4
 
5
5
  ## Quickstart
6
6
 
7
- Requirements: Node (20+), pnpm (10+)
8
-
9
7
  ```bash
10
8
  pnpm dev
11
9
  ```
@@ -19,7 +17,7 @@ Edit [src/App.tsx](./src/App.tsx) to build your app UI.
19
17
  ```
20
18
  src/
21
19
  ├── App.tsx # Your main app component
22
- └── components/ # Your shadcn/ui React components
20
+ └── components/ # Your React components
23
21
 
24
22
  mcp/
25
23
  └── server.ts # MCP server for testing in ChatGPT
@@ -57,9 +55,9 @@ pnpm mcp
57
55
  ngrok http 6766
58
56
  ```
59
57
 
60
- The server will run on `http://localhost:6766` and serve your App component with dummy data from `mcp/server.ts`.
58
+ You can then connect to the tunnel forwarding URL at the `/mcp` path from ChatGPT **in developer mode** to see your UI in action: `User > Settings > Apps & Connectors > Create`
61
59
 
62
- You can then connect to the tunnel `/mcp` path from ChatGPT in developer mode to see your UI in action.
60
+ Once your app is connected, send `show app` to ChatGPT. Many changes require you to Refresh your app on the same settings modal.
63
61
 
64
62
  ## Build & Deploy
65
63
 
@@ -80,4 +78,3 @@ This creates optimized builds in the `dist/` directory:
80
78
  - [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
81
79
  - [ChatGPT Apps SDK UI Documentation](https://developers.openai.com/apps-sdk/build/chatgpt-ui)
82
80
  - [ChatGPT Apps SDK Examples](https://github.com/openai/openai-apps-sdk-examples)
83
- - [shadcn/ui](https://ui.shadcn.com/)
@@ -2,7 +2,6 @@ import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import { ChatGPTSimulator } from 'sunpeak';
4
4
  import { App } from '@/App';
5
- import './styles.css';
6
5
 
7
6
  const toolOutput = {
8
7
  places: [
@@ -58,7 +58,7 @@ const toolOutput = {
58
58
  runMCPServer({
59
59
  name: 'my-sunpeak-app',
60
60
  version: '0.1.0',
61
- distPath: path.resolve(__dirname, '../dist/chatgpt/index.global.js'),
61
+ distPath: path.resolve(__dirname, '../dist/chatgpt/index.js'),
62
62
  toolName: 'show-places',
63
63
  toolDescription: 'Show popular places in Austin',
64
64
  dummyData: toolOutput,
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "tsup",
7
+ "build": "vite build --config vite.config.build.ts",
8
8
  "dev": "vite",
9
9
  "mcp": "tsx mcp/server.ts",
10
10
  "lint": "eslint . --ext .ts,.tsx --fix",
@@ -12,25 +12,15 @@
12
12
  "test": "vitest run"
13
13
  },
14
14
  "dependencies": {
15
- "@radix-ui/react-dialog": "^1.1.15",
16
- "@radix-ui/react-label": "^2.1.8",
17
- "@radix-ui/react-select": "^2.2.6",
18
- "@radix-ui/react-separator": "^1.1.8",
19
- "@radix-ui/react-slot": "^1.2.4",
20
- "@radix-ui/react-tooltip": "^1.2.8",
21
- "class-variance-authority": "^0.7.1",
15
+ "@openai/apps-sdk-ui": "^0.2.0",
22
16
  "clsx": "^2.1.1",
23
17
  "embla-carousel-react": "^8.6.0",
24
18
  "embla-carousel-wheel-gestures": "^8.1.0",
25
- "lucide-react": "^0.554.0",
26
19
  "sunpeak": "workspace:*",
27
- "tailwind-merge": "^3.4.0",
28
- "tw-animate-css": "^1.4.0"
20
+ "tailwind-merge": "^3.4.0"
29
21
  },
30
22
  "devDependencies": {
31
- "@tailwindcss/postcss": "^4.1.17",
32
23
  "@tailwindcss/vite": "^4.1.17",
33
- "postcss": "^8.4.49",
34
24
  "@testing-library/jest-dom": "^6.9.1",
35
25
  "@testing-library/react": "^16.3.0",
36
26
  "@testing-library/user-event": "^14.6.1",
@@ -45,12 +35,12 @@
45
35
  "eslint-plugin-react": "^7.37.5",
46
36
  "eslint-plugin-react-hooks": "^7.0.1",
47
37
  "jsdom": "^27.2.0",
38
+ "postcss": "^8.4.49",
48
39
  "prettier": "^3.6.2",
49
40
  "react": "^18.3.1",
50
41
  "react-dom": "^18.3.1",
51
42
  "tailwindcss": "^4.1.17",
52
43
  "ts-node": "^10.9.2",
53
- "tsup": "^8.3.5",
54
44
  "tsx": "^4.20.6",
55
45
  "typescript": "^5.6.3",
56
46
  "vite": "^5.4.21",
@@ -1,8 +1,7 @@
1
- import { useWidgetProps } from 'sunpeak';
2
- import { SunpeakCarousel, SunpeakCard } from './components';
3
- import 'tw-animate-css';
4
1
  import '@/styles/globals.css';
5
- import '@/styles/chatgpt.css';
2
+
3
+ import { useWidgetProps } from 'sunpeak';
4
+ import { OpenAICarousel, OpenAICard } from './components';
6
5
 
7
6
  export interface Place {
8
7
  id: string;
@@ -22,9 +21,9 @@ export function App() {
22
21
  const data = useWidgetProps<AppData>(() => ({ places: [] }));
23
22
 
24
23
  return (
25
- <SunpeakCarousel gap={16} showArrows={true} showEdgeGradients={true} cardWidth={220}>
24
+ <OpenAICarousel gap={16} showArrows={true} showEdgeGradients={true} cardWidth={220}>
26
25
  {data.places.map((place) => (
27
- <SunpeakCard
26
+ <OpenAICard
28
27
  key={place.id}
29
28
  image={place.image}
30
29
  imageAlt={place.name}
@@ -42,8 +41,8 @@ export function App() {
42
41
  }}
43
42
  >
44
43
  {place.description}
45
- </SunpeakCard>
44
+ </OpenAICard>
46
45
  ))}
47
- </SunpeakCarousel>
46
+ </OpenAICarousel>
48
47
  );
49
48
  }
@@ -1,2 +1,2 @@
1
- export * from './sunpeak-carousel';
2
- export * from './sunpeak-card';
1
+ export * from './openai-carousel';
2
+ export * from './openai-card';
@@ -0,0 +1,73 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { OpenAICard } from './openai-card';
4
+
5
+ describe('OpenAICard', () => {
6
+ it('renders correct variant classes', () => {
7
+ const { container, rerender } = render(
8
+ <OpenAICard variant="default" data-testid="card">
9
+ Content
10
+ </OpenAICard>
11
+ );
12
+
13
+ const card = container.firstChild as HTMLElement;
14
+ expect(card.className).toContain('border border-subtle bg-surface');
15
+
16
+ rerender(
17
+ <OpenAICard variant="bordered" data-testid="card">
18
+ Content
19
+ </OpenAICard>
20
+ );
21
+ expect(card.className).toContain('border-2 border-default bg-surface');
22
+
23
+ rerender(
24
+ <OpenAICard variant="elevated" data-testid="card">
25
+ Content
26
+ </OpenAICard>
27
+ );
28
+ expect(card.className).toContain('shadow-lg');
29
+ });
30
+
31
+ it('button clicks stop propagation and do not trigger card onClick', () => {
32
+ const cardOnClick = vi.fn();
33
+ const button1OnClick = vi.fn();
34
+
35
+ render(
36
+ <OpenAICard
37
+ onClick={cardOnClick}
38
+ button1={{ onClick: button1OnClick, children: 'Click Me' }}
39
+ >
40
+ Content
41
+ </OpenAICard>
42
+ );
43
+
44
+ const button = screen.getByText('Click Me');
45
+ fireEvent.click(button);
46
+
47
+ expect(button1OnClick).toHaveBeenCalledTimes(1);
48
+ expect(cardOnClick).not.toHaveBeenCalled();
49
+ });
50
+
51
+ it('calls button onClick handlers when buttons are clicked', () => {
52
+ const button1OnClick = vi.fn();
53
+ const button2OnClick = vi.fn();
54
+
55
+ render(
56
+ <OpenAICard
57
+ button1={{ onClick: button1OnClick, children: 'Button 1', isPrimary: true }}
58
+ button2={{ onClick: button2OnClick, children: 'Button 2' }}
59
+ >
60
+ Content
61
+ </OpenAICard>
62
+ );
63
+
64
+ const button1 = screen.getByText('Button 1');
65
+ const button2 = screen.getByText('Button 2');
66
+
67
+ fireEvent.click(button1);
68
+ expect(button1OnClick).toHaveBeenCalledTimes(1);
69
+
70
+ fireEvent.click(button2);
71
+ expect(button2OnClick).toHaveBeenCalledTimes(1);
72
+ });
73
+ });
@@ -0,0 +1,126 @@
1
+ import * as React from "react"
2
+ import { Button } from "@openai/apps-sdk-ui/components/Button"
3
+ import { cn } from "@/lib/index"
4
+
5
+ export interface OpenAIButtonProps {
6
+ isPrimary?: boolean
7
+ onClick: () => void
8
+ children: React.ReactNode
9
+ }
10
+
11
+ export interface OpenAICardProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ children?: React.ReactNode
13
+ image?: string
14
+ imageAlt?: string
15
+ imageMaxWidth?: number
16
+ imageMaxHeight?: number
17
+ header?: React.ReactNode
18
+ metadata?: React.ReactNode
19
+ button1?: OpenAIButtonProps
20
+ button2?: OpenAIButtonProps
21
+ variant?: "default" | "bordered" | "elevated"
22
+ }
23
+
24
+ export const OpenAICard = React.forwardRef<HTMLDivElement, OpenAICardProps>(
25
+ (
26
+ {
27
+ children,
28
+ image,
29
+ imageAlt,
30
+ imageMaxWidth = 400,
31
+ imageMaxHeight = 400,
32
+ header,
33
+ metadata,
34
+ button1,
35
+ button2,
36
+ variant = "default",
37
+ className,
38
+ onClick,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ const variantClasses = {
44
+ default: "border border-subtle bg-surface",
45
+ bordered: "border-2 border-default bg-surface",
46
+ elevated: "border border-subtle bg-surface shadow-lg",
47
+ }
48
+
49
+ const handleCardClick = (e: React.MouseEvent<HTMLDivElement>) => {
50
+ onClick?.(e)
51
+ }
52
+
53
+ const renderButton = (buttonProps: OpenAIButtonProps) => {
54
+ const { isPrimary = false, onClick: buttonOnClick, children } = buttonProps
55
+
56
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
57
+ e.stopPropagation()
58
+ buttonOnClick()
59
+ }
60
+
61
+ return (
62
+ <Button
63
+ color={isPrimary ? "primary" : "secondary"}
64
+ variant={isPrimary ? "solid" : "soft"}
65
+ onClick={handleClick}
66
+ >
67
+ {children}
68
+ </Button>
69
+ )
70
+ }
71
+
72
+ const hasButtons = button1 || button2
73
+
74
+ return (
75
+ <div
76
+ ref={ref}
77
+ className={cn(
78
+ "overflow-hidden rounded-2xl cursor-pointer select-none",
79
+ variantClasses[variant],
80
+ className
81
+ )}
82
+ onClick={handleCardClick}
83
+ {...props}
84
+ >
85
+ {image && (
86
+ <div className="w-full overflow-hidden">
87
+ <img
88
+ src={image}
89
+ alt={imageAlt}
90
+ loading="lazy"
91
+ className="w-full h-auto aspect-square object-cover"
92
+ style={{
93
+ maxWidth: `${imageMaxWidth}px`,
94
+ maxHeight: `${imageMaxHeight}px`,
95
+ }}
96
+ />
97
+ </div>
98
+ )}
99
+ <div className="flex flex-col flex-1 p-4">
100
+ {header && (
101
+ <h2 className="font-medium text-base leading-tight overflow-hidden text-ellipsis whitespace-nowrap mb-2">
102
+ {header}
103
+ </h2>
104
+ )}
105
+ {metadata && (
106
+ <p className="text-secondary text-xs mb-1">
107
+ {metadata}
108
+ </p>
109
+ )}
110
+ {children && (
111
+ <div className="text-sm leading-normal line-clamp-2 mb-3">
112
+ {children}
113
+ </div>
114
+ )}
115
+ {hasButtons && (
116
+ <div className="flex gap-2 flex-wrap mt-auto">
117
+ {button1 && renderButton(button1)}
118
+ {button2 && renderButton(button2)}
119
+ </div>
120
+ )}
121
+ </div>
122
+ </div>
123
+ )
124
+ }
125
+ )
126
+ OpenAICard.displayName = "OpenAICard"
@@ -0,0 +1,84 @@
1
+ import { render } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { OpenAICarousel } from './openai-carousel';
4
+
5
+ const mockUseDisplayMode = vi.fn(() => 'inline');
6
+
7
+ // Mock sunpeak hooks
8
+ vi.mock('sunpeak', () => ({
9
+ useWidgetState: vi.fn(() => [{ currentIndex: 0 }, vi.fn()]),
10
+ useDisplayMode: () => mockUseDisplayMode(),
11
+ }));
12
+
13
+ // Mock embla-carousel-react
14
+ vi.mock('embla-carousel-react', () => ({
15
+ default: vi.fn(() => [vi.fn(), null]),
16
+ }));
17
+
18
+ // Mock embla-carousel-wheel-gestures
19
+ vi.mock('embla-carousel-wheel-gestures', () => ({
20
+ WheelGesturesPlugin: vi.fn(() => ({})),
21
+ }));
22
+
23
+ describe('OpenAICarousel', () => {
24
+ beforeEach(() => {
25
+ mockUseDisplayMode.mockReturnValue('inline');
26
+ });
27
+
28
+ it('renders all children with correct card width', () => {
29
+ const { container } = render(
30
+ <OpenAICarousel cardWidth={300}>
31
+ <div>Card 1</div>
32
+ <div>Card 2</div>
33
+ <div>Card 3</div>
34
+ </OpenAICarousel>
35
+ );
36
+
37
+ const cardContainers = container.querySelectorAll('.flex-none');
38
+ expect(cardContainers).toHaveLength(3);
39
+
40
+ cardContainers.forEach((cardContainer) => {
41
+ const element = cardContainer as HTMLElement;
42
+ expect(element.style.minWidth).toBe('300px');
43
+ expect(element.style.maxWidth).toBe('300px');
44
+ });
45
+ });
46
+
47
+ it('handles cardWidth object with inline/fullscreen modes', () => {
48
+ // Test inline mode
49
+ mockUseDisplayMode.mockReturnValue('inline');
50
+ const { container: inlineContainer } = render(
51
+ <OpenAICarousel cardWidth={{ inline: 250, fullscreen: 400 }}>
52
+ <div>Card 1</div>
53
+ </OpenAICarousel>
54
+ );
55
+
56
+ let cardContainer = inlineContainer.querySelector('.flex-none') as HTMLElement;
57
+ expect(cardContainer.style.minWidth).toBe('250px');
58
+
59
+ // Test fullscreen mode
60
+ mockUseDisplayMode.mockReturnValue('fullscreen');
61
+ const { container: fullscreenContainer } = render(
62
+ <OpenAICarousel cardWidth={{ inline: 250, fullscreen: 400 }}>
63
+ <div>Card 1</div>
64
+ </OpenAICarousel>
65
+ );
66
+
67
+ cardContainer = fullscreenContainer.querySelector('.flex-none') as HTMLElement;
68
+ expect(cardContainer.style.minWidth).toBe('400px');
69
+ });
70
+
71
+ it('applies custom gap between cards', () => {
72
+ const { container } = render(
73
+ <OpenAICarousel gap={24}>
74
+ <div>Card 1</div>
75
+ <div>Card 2</div>
76
+ </OpenAICarousel>
77
+ );
78
+
79
+ const carouselTrack = container.querySelector('.flex.touch-pan-y') as HTMLElement;
80
+ expect(carouselTrack.style.gap).toBe('24px');
81
+ expect(carouselTrack.style.marginLeft).toBe('-24px');
82
+ expect(carouselTrack.style.paddingLeft).toBe('24px');
83
+ });
84
+ });