sunpeak 0.2.4 → 0.2.5

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.
@@ -19,12 +19,48 @@ Edit [src/App.tsx](./src/App.tsx) to build your app UI.
19
19
  ```
20
20
  src/
21
21
  ├── App.tsx # Your main app component
22
- └── components # Your shadcn/ui React components
22
+ └── components/ # Your shadcn/ui React components
23
+
24
+ mcp/
25
+ └── server.ts # MCP server for testing in ChatGPT
23
26
 
24
27
  dist/ # Build output (generated)
25
28
  └── chatgpt/ # ChatGPT builds
26
29
  ```
27
30
 
31
+ ## Testing
32
+
33
+ ### Testing Locally
34
+
35
+ Run the following scripts, and manually QA the UI from the dev server:
36
+
37
+ ```bash
38
+ pnpm lint
39
+ pnpm typecheck
40
+ pnpm test
41
+ pnpm build
42
+ pnpm dev
43
+ ```
44
+
45
+ ### Testing in ChatGPT
46
+
47
+ Test your app directly in ChatGPT using the built-in MCP server:
48
+
49
+ ```bash
50
+ # 1. Build your app. You must rebuild your app for changes to take effect.
51
+ pnpm build
52
+
53
+ # 2. Start the MCP server.
54
+ pnpm mcp
55
+
56
+ # 3. In another terminal, run a tunnel. For example:
57
+ ngrok http 6766
58
+ ```
59
+
60
+ The server will run on `http://localhost:6766` and serve your App component with dummy data from `mcp/server.ts`.
61
+
62
+ You can then connect to the tunnel `/mcp` path from ChatGPT in developer mode to see your UI in action.
63
+
28
64
  ## Build & Deploy
29
65
 
30
66
  Build your app for production:
@@ -36,7 +72,7 @@ pnpm build
36
72
  This creates optimized builds in the `dist/` directory:
37
73
 
38
74
  - `dist/chatgpt/index.js` - ChatGPT iframe component
39
- - Host this file somewhere and reference it as a resource in your MCP server.
75
+ - Host this file somewhere and reference it as a resource in your production MCP server.
40
76
 
41
77
  ## Resources
42
78
 
@@ -4,53 +4,55 @@ import { ChatGPTSimulator } from 'sunpeak';
4
4
  import { App } from '@/App';
5
5
  import './styles.css';
6
6
 
7
- const places = [
8
- {
9
- id: '1',
10
- name: 'Lady Bird Lake',
11
- rating: 4.5,
12
- category: 'Waterfront',
13
- location: 'Austin',
14
- image: 'https://images.unsplash.com/photo-1520950237264-dfe336995c34?w=400&h=400&fit=crop',
15
- description: 'Scenic lake perfect for kayaking, paddleboarding, and trails.',
16
- },
17
- {
18
- id: '2',
19
- name: 'Texas State Capitol',
20
- rating: 4.8,
21
- category: 'Historic Site',
22
- location: 'Austin',
23
- image: 'https://images.unsplash.com/photo-1664231978322-4d0b45c7027b?w=400&h=400&fit=crop',
24
- description: 'Stunning capitol building with free tours and beautiful grounds.',
25
- },
26
- {
27
- id: '3',
28
- name: 'The Paramount Theatre',
29
- rating: 4.7,
30
- category: 'Architecture',
31
- location: 'Austin',
32
- image: 'https://images.unsplash.com/photo-1583097090970-4d3b940ea1a0?w=400&h=400&fit=crop',
33
- description: 'Century-old performance and movie theatre in the heart of downtown Austin.',
34
- },
35
- {
36
- id: '4',
37
- name: 'Zilker Park',
38
- rating: 4.7,
39
- category: 'Park',
40
- location: 'Austin',
41
- image: 'https://images.unsplash.com/photo-1563828568124-f800803ba13c?w=400&h=400&fit=crop',
42
- description: 'Popular park with trails, sports fields, and Barton Springs Pool.',
43
- },
44
- {
45
- id: '5',
46
- name: 'South Congress Avenue',
47
- rating: 4.6,
48
- category: 'Landmark',
49
- location: 'Austin',
50
- image: 'https://images.unsplash.com/photo-1588993608283-7f0eda4438be?w=400&h=400&fit=crop',
51
- description: 'Vibrant street with unique shops, restaurants, and live music.',
52
- },
53
- ];
7
+ const toolOutput = {
8
+ places: [
9
+ {
10
+ id: '1',
11
+ name: 'Lady Bird Lake',
12
+ rating: 4.5,
13
+ category: 'Waterfront',
14
+ location: 'Austin',
15
+ image: 'https://images.unsplash.com/photo-1520950237264-dfe336995c34?w=400&h=400&fit=crop',
16
+ description: 'Scenic lake perfect for kayaking, paddleboarding, and trails.',
17
+ },
18
+ {
19
+ id: '2',
20
+ name: 'Texas State Capitol',
21
+ rating: 4.8,
22
+ category: 'Historic Site',
23
+ location: 'Austin',
24
+ image: 'https://images.unsplash.com/photo-1664231978322-4d0b45c7027b?w=400&h=400&fit=crop',
25
+ description: 'Stunning capitol building with free tours and beautiful grounds.',
26
+ },
27
+ {
28
+ id: '3',
29
+ name: 'The Paramount Theatre',
30
+ rating: 4.7,
31
+ category: 'Architecture',
32
+ location: 'Austin',
33
+ image: 'https://images.unsplash.com/photo-1583097090970-4d3b940ea1a0?w=400&h=400&fit=crop',
34
+ description: 'Century-old performance and movie theatre in the heart of downtown Austin.',
35
+ },
36
+ {
37
+ id: '4',
38
+ name: 'Zilker Park',
39
+ rating: 4.7,
40
+ category: 'Park',
41
+ location: 'Austin',
42
+ image: 'https://images.unsplash.com/photo-1563828568124-f800803ba13c?w=400&h=400&fit=crop',
43
+ description: 'Popular park with trails, sports fields, and Barton Springs Pool.',
44
+ },
45
+ {
46
+ id: '5',
47
+ name: 'South Congress Avenue',
48
+ rating: 4.6,
49
+ category: 'Landmark',
50
+ location: 'Austin',
51
+ image: 'https://images.unsplash.com/photo-1588993608283-7f0eda4438be?w=400&h=400&fit=crop',
52
+ description: 'Vibrant street with unique shops, restaurants, and live music.',
53
+ },
54
+ ],
55
+ };
54
56
 
55
57
  createRoot(document.getElementById('root')!).render(
56
58
  <StrictMode>
@@ -58,8 +60,9 @@ createRoot(document.getElementById('root')!).render(
58
60
  appName="Splorin"
59
61
  appIcon="✈️"
60
62
  userMessage="Show me popular places to visit in Austin Texas"
63
+ toolOutput={toolOutput}
61
64
  >
62
- <App data={places} />
65
+ <App />
63
66
  </ChatGPTSimulator>
64
67
  </StrictMode>
65
68
  );
@@ -0,0 +1,66 @@
1
+ import { runMCPServer } from 'sunpeak/mcp';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ // Dummy data matching the dev script
8
+ const toolOutput = {
9
+ places: [
10
+ {
11
+ id: '1',
12
+ name: 'Lady Bird Lake',
13
+ rating: 4.5,
14
+ category: 'Waterfront',
15
+ location: 'Austin',
16
+ image: 'https://images.unsplash.com/photo-1520950237264-dfe336995c34?w=400&h=400&fit=crop',
17
+ description: 'Scenic lake perfect for kayaking, paddleboarding, and trails.',
18
+ },
19
+ {
20
+ id: '2',
21
+ name: 'Texas State Capitol',
22
+ rating: 4.8,
23
+ category: 'Historic Site',
24
+ location: 'Austin',
25
+ image: 'https://images.unsplash.com/photo-1664231978322-4d0b45c7027b?w=400&h=400&fit=crop',
26
+ description: 'Stunning capitol building with free tours and beautiful grounds.',
27
+ },
28
+ {
29
+ id: '3',
30
+ name: 'The Paramount Theatre',
31
+ rating: 4.7,
32
+ category: 'Architecture',
33
+ location: 'Austin',
34
+ image: 'https://images.unsplash.com/photo-1583097090970-4d3b940ea1a0?w=400&h=400&fit=crop',
35
+ description: 'Century-old performance and movie theatre in the heart of downtown Austin.',
36
+ },
37
+ {
38
+ id: '4',
39
+ name: 'Zilker Park',
40
+ rating: 4.7,
41
+ category: 'Park',
42
+ location: 'Austin',
43
+ image: 'https://images.unsplash.com/photo-1563828568124-f800803ba13c?w=400&h=400&fit=crop',
44
+ description: 'Popular park with trails, sports fields, and Barton Springs Pool.',
45
+ },
46
+ {
47
+ id: '5',
48
+ name: 'South Congress Avenue',
49
+ rating: 4.6,
50
+ category: 'Landmark',
51
+ location: 'Austin',
52
+ image: 'https://images.unsplash.com/photo-1588993608283-7f0eda4438be?w=400&h=400&fit=crop',
53
+ description: 'Vibrant street with unique shops, restaurants, and live music.',
54
+ },
55
+ ],
56
+ };
57
+
58
+ runMCPServer({
59
+ name: 'my-sunpeak-app',
60
+ version: '0.1.0',
61
+ distPath: path.resolve(__dirname, '../dist/chatgpt/index.global.js'),
62
+ toolName: 'show-places',
63
+ toolDescription: 'Show popular places in Austin',
64
+ dummyData: toolOutput,
65
+ port: 6766,
66
+ });
@@ -6,6 +6,7 @@
6
6
  "scripts": {
7
7
  "build": "tsup",
8
8
  "dev": "vite",
9
+ "mcp": "tsx mcp/server.ts",
9
10
  "lint": "eslint . --ext .ts,.tsx --fix",
10
11
  "typecheck": "tsc --noEmit",
11
12
  "test": "vitest run"
@@ -27,7 +28,9 @@
27
28
  "tw-animate-css": "^1.4.0"
28
29
  },
29
30
  "devDependencies": {
31
+ "@tailwindcss/postcss": "^4.1.17",
30
32
  "@tailwindcss/vite": "^4.1.17",
33
+ "postcss": "^8.4.49",
31
34
  "@testing-library/jest-dom": "^6.9.1",
32
35
  "@testing-library/react": "^16.3.0",
33
36
  "@testing-library/user-event": "^14.6.1",
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ };
@@ -1,4 +1,6 @@
1
+ import { useWidgetProps } from 'sunpeak';
1
2
  import { SunpeakCarousel, SunpeakCard } from './components';
3
+ import 'tw-animate-css';
2
4
  import '@/styles/globals.css';
3
5
  import '@/styles/chatgpt.css';
4
6
 
@@ -12,14 +14,16 @@ export interface Place {
12
14
  description: string;
13
15
  }
14
16
 
15
- export interface AppProps {
16
- data: Place[];
17
+ export interface AppData extends Record<string, unknown> {
18
+ places: Place[];
17
19
  }
18
20
 
19
- export function App({ data }: AppProps) {
21
+ export function App() {
22
+ const data = useWidgetProps<AppData>(() => ({ places: [] }));
23
+
20
24
  return (
21
25
  <SunpeakCarousel gap={16} showArrows={true} showEdgeGradients={true} cardWidth={220}>
22
- {data.map((place) => (
26
+ {data.places.map((place) => (
23
27
  <SunpeakCard
24
28
  key={place.id}
25
29
  image={place.image}
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import { useWidgetState } from "sunpeak"
2
3
  import { cn } from "@/lib/index"
3
4
  import {
4
5
  Card,
@@ -15,10 +16,27 @@ export interface SunpeakButtonProps extends Omit<React.ComponentProps<typeof But
15
16
  onClick: () => void
16
17
  }
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
+
18
36
  export interface SunpeakCardProps extends React.HTMLAttributes<HTMLDivElement> {
19
37
  children?: React.ReactNode
20
- image: string
21
- imageAlt: string
38
+ image?: string
39
+ imageAlt?: string
22
40
  imageMaxWidth?: number
23
41
  imageMaxHeight?: number
24
42
  header?: React.ReactNode
@@ -31,22 +49,35 @@ export interface SunpeakCardProps extends React.HTMLAttributes<HTMLDivElement> {
31
49
  export const SunpeakCard = React.forwardRef<HTMLDivElement, SunpeakCardProps>(
32
50
  (
33
51
  {
34
- children,
35
- image,
36
- imageAlt,
37
- imageMaxWidth = 400,
38
- imageMaxHeight = 400,
39
- header,
40
- metadata,
41
- button1,
42
- button2,
43
- variant = "default",
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",
44
62
  className,
45
63
  onClick,
46
64
  ...props
47
65
  },
48
66
  ref
49
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
+
50
81
  const hasButtons = button1 || button2
51
82
 
52
83
  const handleCardClick = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -1,5 +1,6 @@
1
1
  import * as React from "react"
2
2
  import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
3
+ import { useWidgetState, useDisplayMode } from "sunpeak"
3
4
  import { cn } from "@/lib/index"
4
5
  import {
5
6
  Carousel,
@@ -10,6 +11,18 @@ import {
10
11
  type CarouselApi,
11
12
  } from "@/components/shadcn/carousel"
12
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
+
13
26
  export type SunpeakCarouselProps = {
14
27
  children?: React.ReactNode
15
28
  gap?: number
@@ -25,15 +38,26 @@ export const SunpeakCarousel = React.forwardRef<
25
38
  >(
26
39
  (
27
40
  {
28
- children,
29
- gap = 16,
30
- showArrows = true,
31
- showEdgeGradients = true,
32
- cardWidth,
41
+ children: childrenProp,
42
+ gap: gapProp = 16,
43
+ showArrows: showArrowsProp = true,
44
+ showEdgeGradients: showEdgeGradientsProp = true,
45
+ cardWidth: cardWidthProp,
33
46
  className,
34
47
  },
35
48
  ref
36
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
+
37
61
  const [api, setApi] = React.useState<CarouselApi>()
38
62
  const [canScrollPrev, setCanScrollPrev] = React.useState(false)
39
63
  const [canScrollNext, setCanScrollNext] = React.useState(false)
@@ -44,6 +68,11 @@ export const SunpeakCarousel = React.forwardRef<
44
68
  const onSelect = () => {
45
69
  setCanScrollPrev(api.canScrollPrev())
46
70
  setCanScrollNext(api.canScrollNext())
71
+
72
+ const currentIndex = api.selectedScrollSnap()
73
+ if (widgetState?.currentIndex !== currentIndex) {
74
+ setWidgetState({ currentIndex })
75
+ }
47
76
  }
48
77
 
49
78
  onSelect()
@@ -54,7 +83,7 @@ export const SunpeakCarousel = React.forwardRef<
54
83
  api.off("select", onSelect)
55
84
  api.off("reInit", onSelect)
56
85
  }
57
- }, [api])
86
+ }, [api, widgetState?.currentIndex, setWidgetState])
58
87
 
59
88
  const childArray = React.Children.toArray(children)
60
89
 
@@ -62,8 +91,13 @@ export const SunpeakCarousel = React.forwardRef<
62
91
  if (typeof cardWidth === "number") {
63
92
  return `${cardWidth}px`
64
93
  }
65
- if (cardWidth?.inline) {
66
- return `${cardWidth.inline}px`
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
+ }
67
101
  }
68
102
  return "220px"
69
103
  }
@@ -0,0 +1,8 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import { App } from './App';
3
+
4
+ // Mount the App to the root element
5
+ const root = document.getElementById('root');
6
+ if (root) {
7
+ createRoot(root).render(<App />);
8
+ }
@@ -1,5 +1,4 @@
1
1
  @import "tailwindcss";
2
- @import "tw-animate-css";
3
2
 
4
3
  @custom-variant dark (&:is(.dark *));
5
4
 
@@ -1,23 +1,50 @@
1
1
  import { defineConfig } from 'tsup';
2
+ import postcss from 'postcss';
3
+ import tailwindcss from '@tailwindcss/postcss';
4
+ import type { Plugin } from 'esbuild';
5
+ import fs from 'fs';
6
+
7
+ function postcssPlugin(): Plugin {
8
+ return {
9
+ name: 'postcss',
10
+ setup(build) {
11
+ build.onLoad({ filter: /\.css$/ }, async (args) => {
12
+ const css = fs.readFileSync(args.path, 'utf8');
13
+ const result = await postcss([tailwindcss()]).process(css, {
14
+ from: args.path,
15
+ });
16
+ return {
17
+ contents: result.css,
18
+ loader: 'css',
19
+ };
20
+ });
21
+ },
22
+ };
23
+ }
2
24
 
3
25
  export default defineConfig({
4
26
  entry: {
5
- 'chatgpt/index': 'src/App.tsx',
27
+ 'chatgpt/index': 'src/index.chatgpt.tsx',
6
28
  },
7
- format: ['esm'],
29
+ format: ['iife'],
8
30
  dts: false,
9
31
  sourcemap: false,
10
32
  clean: true,
11
33
  external: [],
34
+ noExternal: [/.*/],
12
35
  treeshake: true,
13
36
  splitting: false,
14
37
  minify: true,
15
38
  outDir: 'dist',
16
39
  injectStyle: true,
40
+ globalName: 'SunpeakApp',
41
+ esbuildPlugins: [postcssPlugin()],
17
42
  esbuildOptions(options) {
18
43
  options.bundle = true;
19
44
  options.platform = 'browser';
20
45
  options.target = 'es2020';
21
46
  options.jsx = 'automatic';
47
+ // Enable "style" condition for CSS-only packages like tw-animate-css
48
+ options.conditions = ['style', 'import', 'module', 'browser', 'default'];
22
49
  },
23
50
  });