sunpeak 0.5.39 → 0.6.1

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 (42) hide show
  1. package/README.md +1 -0
  2. package/bin/commands/build.mjs +4 -4
  3. package/dist/chatgpt/globals.css +7 -0
  4. package/dist/chatgpt/mock-openai.d.ts +2 -2
  5. package/dist/chatgpt/simple-sidebar.d.ts +2 -1
  6. package/dist/hooks/use-max-height.d.ts +1 -1
  7. package/dist/index.cjs +235 -150
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +235 -150
  10. package/dist/index.js.map +1 -1
  11. package/dist/providers/openai/types.d.ts +1 -1
  12. package/dist/providers/types.d.ts +1 -1
  13. package/dist/style.css +201 -38
  14. package/package.json +1 -1
  15. package/template/.sunpeak/dev.tsx +2 -2
  16. package/template/README.md +3 -3
  17. package/template/dist/chatgpt/albums.js +11 -11
  18. package/template/dist/chatgpt/carousel.js +2 -2
  19. package/template/dist/chatgpt/counter.js +7 -7
  20. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +2 -2
  21. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +9 -9
  22. package/template/node_modules/.vite/deps/_metadata.json +22 -22
  23. package/template/node_modules/.vite/deps/{chunk-EVJ3DVH5.js → chunk-DQAZDQU3.js} +5 -5
  24. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  25. package/template/src/components/album/albums.test.tsx +7 -2
  26. package/template/src/components/album/albums.tsx +1 -1
  27. package/template/src/components/album/fullscreen-viewer.test.tsx +12 -24
  28. package/template/src/components/album/fullscreen-viewer.tsx +30 -71
  29. package/template/src/components/carousel/carousel.tsx +1 -1
  30. package/template/src/components/index.ts +0 -1
  31. package/template/src/{components/resources → resources}/albums-resource.test.tsx +1 -1
  32. package/template/src/{components/resources → resources}/albums-resource.tsx +2 -1
  33. package/template/src/{components/resources → resources}/carousel-resource.test.tsx +2 -2
  34. package/template/src/{components/resources → resources}/carousel-resource.tsx +2 -2
  35. package/template/src/{components/resources → resources}/counter-resource.tsx +8 -0
  36. package/template/src/simulations/albums-simulation.ts +5 -1
  37. package/template/src/simulations/carousel-simulation.ts +5 -1
  38. package/template/src/simulations/counter-simulation.ts +6 -1
  39. package/template/src/simulations/widget-config.ts +42 -0
  40. /package/template/node_modules/.vite/deps/{chunk-EVJ3DVH5.js.map → chunk-DQAZDQU3.js.map} +0 -0
  41. /package/template/src/{components/resources → resources}/counter-resource.test.tsx +0 -0
  42. /package/template/src/{components/resources → resources}/index.ts +0 -0
@@ -2,10 +2,10 @@ import {
2
2
  Button,
3
3
  ButtonLink,
4
4
  CopyButton
5
- } from "./chunk-EVJ3DVH5.js";
5
+ } from "./chunk-DQAZDQU3.js";
6
+ import "./chunk-XB525PXG.js";
6
7
  import "./chunk-YOJ6QPGS.js";
7
8
  import "./chunk-BAG6OO6S.js";
8
- import "./chunk-XB525PXG.js";
9
9
  import "./chunk-QPJAV452.js";
10
10
  import "./chunk-EGRHWZRV.js";
11
11
  import "./chunk-CNYJBM5F.js";
@@ -2,7 +2,15 @@ import {
2
2
  Button,
3
3
  LoadingIndicator,
4
4
  TransitionGroup
5
- } from "./chunk-EVJ3DVH5.js";
5
+ } from "./chunk-DQAZDQU3.js";
6
+ import {
7
+ Check_default,
8
+ ChevronDownVector_default,
9
+ DropdownVector_default,
10
+ Info_default,
11
+ Search_default,
12
+ X_default
13
+ } from "./chunk-XB525PXG.js";
6
14
  import {
7
15
  useTimeout
8
16
  } from "./chunk-YOJ6QPGS.js";
@@ -18,14 +26,6 @@ import {
18
26
  dist_exports5 as dist_exports3
19
27
  } from "./chunk-SGWD4VEU.js";
20
28
  import "./chunk-KFGKZMLK.js";
21
- import {
22
- Check_default,
23
- ChevronDownVector_default,
24
- DropdownVector_default,
25
- Info_default,
26
- Search_default,
27
- X_default
28
- } from "./chunk-XB525PXG.js";
29
29
  import {
30
30
  Input
31
31
  } from "./chunk-CQ3GYAYB.js";
@@ -7,109 +7,112 @@
7
7
  "react": {
8
8
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/index.js",
9
9
  "file": "react.js",
10
- "fileHash": "1fcc4295",
10
+ "fileHash": "94bcffe9",
11
11
  "needsInterop": true
12
12
  },
13
13
  "react-dom": {
14
14
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.0_react@19.2.0/node_modules/react-dom/index.js",
15
15
  "file": "react-dom.js",
16
- "fileHash": "a3591f16",
16
+ "fileHash": "8fb77322",
17
17
  "needsInterop": true
18
18
  },
19
19
  "react/jsx-dev-runtime": {
20
20
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/jsx-dev-runtime.js",
21
21
  "file": "react_jsx-dev-runtime.js",
22
- "fileHash": "94686c0a",
22
+ "fileHash": "ca004f1d",
23
23
  "needsInterop": true
24
24
  },
25
25
  "react/jsx-runtime": {
26
26
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/jsx-runtime.js",
27
27
  "file": "react_jsx-runtime.js",
28
- "fileHash": "1e5db247",
28
+ "fileHash": "4f69b4ed",
29
29
  "needsInterop": true
30
30
  },
31
31
  "@openai/apps-sdk-ui/components/Button": {
32
32
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Button/index.js",
33
33
  "file": "@openai_apps-sdk-ui_components_Button.js",
34
- "fileHash": "d6d133fa",
34
+ "fileHash": "943d4cd6",
35
35
  "needsInterop": false
36
36
  },
37
37
  "@openai/apps-sdk-ui/components/Checkbox": {
38
38
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Checkbox/index.js",
39
39
  "file": "@openai_apps-sdk-ui_components_Checkbox.js",
40
- "fileHash": "ce84b869",
40
+ "fileHash": "9eeaf63b",
41
41
  "needsInterop": false
42
42
  },
43
43
  "@openai/apps-sdk-ui/components/Icon": {
44
44
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Icon/index.js",
45
45
  "file": "@openai_apps-sdk-ui_components_Icon.js",
46
- "fileHash": "060967f2",
46
+ "fileHash": "188194a5",
47
47
  "needsInterop": false
48
48
  },
49
49
  "@openai/apps-sdk-ui/components/Input": {
50
50
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Input/index.js",
51
51
  "file": "@openai_apps-sdk-ui_components_Input.js",
52
- "fileHash": "a6e2b3d5",
52
+ "fileHash": "bf451833",
53
53
  "needsInterop": false
54
54
  },
55
55
  "@openai/apps-sdk-ui/components/SegmentedControl": {
56
56
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/SegmentedControl/index.js",
57
57
  "file": "@openai_apps-sdk-ui_components_SegmentedControl.js",
58
- "fileHash": "a5b2ba7d",
58
+ "fileHash": "456dc53c",
59
59
  "needsInterop": false
60
60
  },
61
61
  "@openai/apps-sdk-ui/components/Select": {
62
62
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Select/index.js",
63
63
  "file": "@openai_apps-sdk-ui_components_Select.js",
64
- "fileHash": "681ae142",
64
+ "fileHash": "8a137c67",
65
65
  "needsInterop": false
66
66
  },
67
67
  "@openai/apps-sdk-ui/components/Textarea": {
68
68
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Textarea/index.js",
69
69
  "file": "@openai_apps-sdk-ui_components_Textarea.js",
70
- "fileHash": "8085e72e",
70
+ "fileHash": "a5e61764",
71
71
  "needsInterop": false
72
72
  },
73
73
  "@openai/apps-sdk-ui/theme": {
74
74
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/lib/theme.js",
75
75
  "file": "@openai_apps-sdk-ui_theme.js",
76
- "fileHash": "dcd36bbb",
76
+ "fileHash": "21efb3b2",
77
77
  "needsInterop": false
78
78
  },
79
79
  "clsx": {
80
80
  "src": "../../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs",
81
81
  "file": "clsx.js",
82
- "fileHash": "a4d88c05",
82
+ "fileHash": "68a7acff",
83
83
  "needsInterop": false
84
84
  },
85
85
  "embla-carousel-react": {
86
86
  "src": "../../../../node_modules/.pnpm/embla-carousel-react@8.6.0_react@19.2.0/node_modules/embla-carousel-react/esm/embla-carousel-react.esm.js",
87
87
  "file": "embla-carousel-react.js",
88
- "fileHash": "1c3b430c",
88
+ "fileHash": "f2ad1539",
89
89
  "needsInterop": false
90
90
  },
91
91
  "embla-carousel-wheel-gestures": {
92
92
  "src": "../../../../node_modules/.pnpm/embla-carousel-wheel-gestures@8.1.0_embla-carousel@8.6.0/node_modules/embla-carousel-wheel-gestures/dist/embla-carousel-wheel-gestures.esm.js",
93
93
  "file": "embla-carousel-wheel-gestures.js",
94
- "fileHash": "b4318164",
94
+ "fileHash": "90a4d0c8",
95
95
  "needsInterop": false
96
96
  },
97
97
  "react-dom/client": {
98
98
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.0_react@19.2.0/node_modules/react-dom/client.js",
99
99
  "file": "react-dom_client.js",
100
- "fileHash": "e6bdfdfa",
100
+ "fileHash": "02f8c5e8",
101
101
  "needsInterop": true
102
102
  },
103
103
  "tailwind-merge": {
104
104
  "src": "../../../../node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.mjs",
105
105
  "file": "tailwind-merge.js",
106
- "fileHash": "4eb4287f",
106
+ "fileHash": "ace864a4",
107
107
  "needsInterop": false
108
108
  }
109
109
  },
110
110
  "chunks": {
111
- "chunk-EVJ3DVH5": {
112
- "file": "chunk-EVJ3DVH5.js"
111
+ "chunk-DQAZDQU3": {
112
+ "file": "chunk-DQAZDQU3.js"
113
+ },
114
+ "chunk-XB525PXG": {
115
+ "file": "chunk-XB525PXG.js"
113
116
  },
114
117
  "chunk-YOJ6QPGS": {
115
118
  "file": "chunk-YOJ6QPGS.js"
@@ -123,9 +126,6 @@
123
126
  "chunk-KFGKZMLK": {
124
127
  "file": "chunk-KFGKZMLK.js"
125
128
  },
126
- "chunk-XB525PXG": {
127
- "file": "chunk-XB525PXG.js"
128
- },
129
129
  "chunk-CQ3GYAYB": {
130
130
  "file": "chunk-CQ3GYAYB.js"
131
131
  },
@@ -1,3 +1,7 @@
1
+ import {
2
+ Check_default,
3
+ Copy_default
4
+ } from "./chunk-XB525PXG.js";
1
5
  import {
2
6
  useTimeout
3
7
  } from "./chunk-YOJ6QPGS.js";
@@ -12,10 +16,6 @@ import {
12
16
  toTransformProperty,
13
17
  waitForAnimationFrame
14
18
  } from "./chunk-BAG6OO6S.js";
15
- import {
16
- Check_default,
17
- Copy_default
18
- } from "./chunk-XB525PXG.js";
19
19
  import {
20
20
  o
21
21
  } from "./chunk-QPJAV452.js";
@@ -625,4 +625,4 @@ export {
625
625
  ButtonLink,
626
626
  CopyButton
627
627
  };
628
- //# sourceMappingURL=chunk-EVJ3DVH5.js.map
628
+ //# sourceMappingURL=chunk-DQAZDQU3.js.map
@@ -1 +1 @@
1
- {"version":"4.0.13","results":[[":src/components/album/fullscreen-viewer.test.tsx",{"duration":258.46369799999957,"failed":false}],[":src/components/resources/carousel-resource.test.tsx",{"duration":245.01015499999994,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":330.82841299999995,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":62.86271500000021,"failed":false}],[":src/components/resources/counter-resource.test.tsx",{"duration":310.1445560000002,"failed":false}],[":src/components/resources/albums-resource.test.tsx",{"duration":264.3613190000001,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":469.90154299999995,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":360.1429190000001,"failed":false}],[":src/components/card/card.test.tsx",{"duration":55.29722700000002,"failed":false}]]}
1
+ {"version":"4.0.13","results":[[":src/resources/counter-resource.test.tsx",{"duration":341.562457,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":347.8453920000002,"failed":false}],[":src/resources/carousel-resource.test.tsx",{"duration":281.398232,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":245.63889299999983,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":64.06780200000003,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":269.48677499999985,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":490.057513,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":302.1487770000001,"failed":false}],[":src/components/card/card.test.tsx",{"duration":55.60354700000016,"failed":false}]]}
@@ -91,8 +91,13 @@ describe('Albums', () => {
91
91
  const firstAlbum = screen.getByText('Summer Vacation').closest('button')!;
92
92
  fireEvent.click(firstAlbum);
93
93
 
94
- // Should update widget state with selected album ID
95
- expect(mockSetWidgetState).toHaveBeenCalledWith({ selectedAlbumId: 'album-1' });
94
+ // Should update widget state with selected album ID using function updater
95
+ expect(mockSetWidgetState).toHaveBeenCalledTimes(1);
96
+ const updateFn = mockSetWidgetState.mock.calls[0][0];
97
+ expect(typeof updateFn).toBe('function');
98
+ // Test the updater function
99
+ const result = updateFn({ currentIndex: 0 });
100
+ expect(result).toEqual({ currentIndex: 0, selectedAlbumId: 'album-1' });
96
101
 
97
102
  // Should request fullscreen mode
98
103
  expect(mockRequestDisplayMode).toHaveBeenCalledWith({ mode: 'fullscreen' });
@@ -48,7 +48,7 @@ export const Albums = React.forwardRef<HTMLDivElement, AlbumsProps>(({ className
48
48
 
49
49
  const handleSelectAlbum = React.useCallback(
50
50
  (album: Album) => {
51
- setWidgetState({ selectedAlbumId: album.id });
51
+ setWidgetState((prev) => ({ ...prev, selectedAlbumId: album.id }));
52
52
  api?.requestDisplayMode?.({ mode: 'fullscreen' });
53
53
  },
54
54
  [setWidgetState, api]
@@ -4,11 +4,9 @@ import { FullscreenViewer } from './fullscreen-viewer';
4
4
  import type { Album } from './albums';
5
5
 
6
6
  // Mock sunpeak hooks
7
- let mockMaxHeight = 800;
8
7
  let mockSafeArea = { insets: { top: 0, bottom: 0, left: 0, right: 0 } };
9
8
 
10
9
  vi.mock('sunpeak', () => ({
11
- useMaxHeight: () => mockMaxHeight,
12
10
  useSafeArea: () => mockSafeArea,
13
11
  }));
14
12
 
@@ -27,8 +25,8 @@ describe('FullscreenViewer', () => {
27
25
  it('resets to first photo when album changes', () => {
28
26
  const { rerender, container } = render(<FullscreenViewer album={mockAlbum} />);
29
27
 
30
- // Get the main photo area (not the film strip)
31
- const mainPhotoArea = container.querySelector('.flex-1.min-w-0');
28
+ // Get the main photo area
29
+ const mainPhotoArea = container.querySelector('.flex-1');
32
30
  let mainPhoto = mainPhotoArea?.querySelector('img');
33
31
  expect(mainPhoto).toHaveAttribute('alt', 'First Photo');
34
32
  expect(mainPhoto).toHaveAttribute('src', 'https://example.com/1.jpg');
@@ -56,8 +54,8 @@ describe('FullscreenViewer', () => {
56
54
  it('displays correct photo based on selected index from FilmStrip', () => {
57
55
  const { container } = render(<FullscreenViewer album={mockAlbum} />);
58
56
 
59
- // Get the main photo (not from film strip)
60
- const mainPhotoArea = container.querySelector('.flex-1.min-w-0');
57
+ // Get the main photo
58
+ const mainPhotoArea = container.querySelector('.flex-1');
61
59
  const firstPhoto = mainPhotoArea?.querySelector('img');
62
60
 
63
61
  expect(firstPhoto).toHaveAttribute('alt', 'First Photo');
@@ -79,28 +77,18 @@ describe('FullscreenViewer', () => {
79
77
  expect(images.length).toBe(0);
80
78
  });
81
79
 
82
- it('respects safe area insets for main photo area', () => {
80
+ it('respects safe area insets', () => {
83
81
  mockSafeArea = { insets: { top: 20, bottom: 30, left: 10, right: 15 } };
84
82
 
85
83
  const { container } = render(<FullscreenViewer album={mockAlbum} />);
86
84
 
87
- // Find the main photo area
88
- const mainPhotoArea = container.querySelector('.flex-1.min-w-0');
89
- expect(mainPhotoArea).toBeInTheDocument();
90
-
91
- // Note: calc values may not be computed in jsdom, so we check the element has the style attribute
92
- expect(mainPhotoArea).toHaveAttribute('style');
93
- });
94
-
95
- it('respects maxHeight constraint', () => {
96
- mockMaxHeight = 600;
97
-
98
- const { container } = render(<FullscreenViewer album={mockAlbum} />);
99
-
100
- const mainDiv = container.firstChild as HTMLElement;
101
- expect(mainDiv).toHaveStyle({
102
- maxHeight: '600px',
103
- height: '600px',
85
+ // Check root div has safe area padding
86
+ const rootDiv = container.firstChild as HTMLElement;
87
+ expect(rootDiv).toHaveStyle({
88
+ paddingTop: '20px',
89
+ paddingBottom: '30px',
90
+ paddingLeft: '10px',
91
+ paddingRight: '15px',
104
92
  });
105
93
  });
106
94
  });
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useMaxHeight, useSafeArea } from 'sunpeak';
2
+ import { useSafeArea } from 'sunpeak';
3
3
  import { cn } from '../../lib/index';
4
4
  import { FilmStrip } from './film-strip';
5
5
  import type { Album } from './albums';
@@ -11,7 +11,6 @@ export type FullscreenViewerProps = {
11
11
 
12
12
  export const FullscreenViewer = React.forwardRef<HTMLDivElement, FullscreenViewerProps>(
13
13
  ({ album, className }, ref) => {
14
- const maxHeight = useMaxHeight();
15
14
  const safeArea = useSafeArea();
16
15
  const [selectedIndex, setSelectedIndex] = React.useState(0);
17
16
  const [width, setWidth] = React.useState(0);
@@ -49,80 +48,40 @@ export const FullscreenViewer = React.forwardRef<HTMLDivElement, FullscreenViewe
49
48
  return (
50
49
  <div
51
50
  ref={containerRef}
52
- className={cn('relative w-full h-full bg-surface', className)}
51
+ className={cn('flex w-full bg-surface', isMobile ? 'flex-col' : 'flex-row', className)}
53
52
  style={{
54
- maxHeight: maxHeight ?? undefined,
55
- height: maxHeight ?? undefined,
56
- ['--safe-top' as string]: `${safeArea?.insets.top ?? 0}px`,
57
- ['--safe-bottom' as string]: `${safeArea?.insets.bottom ?? 0}px`,
58
- ['--safe-left' as string]: `${safeArea?.insets.left ?? 0}px`,
59
- ['--safe-right' as string]: `${safeArea?.insets.right ?? 0}px`,
53
+ paddingTop: `${safeArea?.insets.top ?? 0}px`,
54
+ paddingBottom: `${safeArea?.insets.bottom ?? 0}px`,
55
+ paddingLeft: `${safeArea?.insets.left ?? 0}px`,
56
+ paddingRight: `${safeArea?.insets.right ?? 0}px`,
60
57
  }}
61
58
  >
62
- <div
63
- className={cn(
64
- 'absolute inset-0 flex overflow-hidden',
65
- isMobile ? 'flex-col' : 'flex-row'
66
- )}
67
- >
68
- {/* Album header - mobile only */}
69
- {isMobile && (
70
- <div
71
- className="z-10 border-b border-subtle bg-surface/95 backdrop-blur-sm"
72
- style={{
73
- paddingTop: `calc(0.75rem + var(--safe-top))`,
74
- paddingBottom: '0.75rem',
75
- paddingLeft: `calc(1rem + var(--safe-left))`,
76
- paddingRight: `calc(1rem + var(--safe-right))`,
77
- }}
78
- >
79
- <h2 className="text-base font-semibold text-primary">{album.title}</h2>
80
- <p className="text-sm text-secondary">
81
- {selectedIndex + 1} / {album.photos.length}
82
- </p>
83
- </div>
84
- )}
85
-
86
- {/* Film strip - desktop only */}
87
- {!isMobile && (
88
- <div
89
- className="absolute pointer-events-none z-10 left-0 top-0 bottom-0 w-40"
90
- style={{
91
- paddingLeft: `var(--safe-left)`,
92
- }}
93
- >
94
- <FilmStrip album={album} selectedIndex={selectedIndex} onSelect={setSelectedIndex} />
95
- </div>
96
- )}
59
+ {/* Album header - mobile only */}
60
+ {isMobile && (
61
+ <div className="border-b border-subtle bg-surface/95 backdrop-blur-sm px-4 py-3">
62
+ <h2 className="text-base font-semibold text-primary">{album.title}</h2>
63
+ <p className="text-sm text-secondary">
64
+ {selectedIndex + 1} / {album.photos.length}
65
+ </p>
66
+ </div>
67
+ )}
97
68
 
98
- {/* Main photo */}
99
- <div
100
- className="flex-1 min-w-0 relative flex items-center justify-center"
101
- style={{
102
- paddingTop: isMobile
103
- ? `calc(1rem + var(--safe-top))`
104
- : `calc(2.5rem + var(--safe-top))`,
105
- paddingBottom: isMobile
106
- ? `calc(1rem + var(--safe-bottom))`
107
- : `calc(2.5rem + var(--safe-bottom))`,
108
- paddingLeft: isMobile
109
- ? `calc(1rem + var(--safe-left))`
110
- : `calc(10rem + var(--safe-left))`,
111
- paddingRight: isMobile
112
- ? `calc(1rem + var(--safe-right))`
113
- : `calc(10rem + var(--safe-right))`,
114
- }}
115
- >
116
- <div className="relative w-full h-full">
117
- {selectedPhoto ? (
118
- <img
119
- src={selectedPhoto.url}
120
- alt={selectedPhoto.title || album.title}
121
- className="absolute inset-0 m-auto rounded-3xl shadow-sm border border-primary/10 max-w-full max-h-full object-contain"
122
- />
123
- ) : null}
124
- </div>
69
+ {/* Film strip - desktop only */}
70
+ {!isMobile && (
71
+ <div className="w-40 flex-shrink-0">
72
+ <FilmStrip album={album} selectedIndex={selectedIndex} onSelect={setSelectedIndex} />
125
73
  </div>
74
+ )}
75
+
76
+ {/* Main photo */}
77
+ <div className="flex-1 flex items-center justify-center p-4 md:p-10">
78
+ {selectedPhoto ? (
79
+ <img
80
+ src={selectedPhoto.url}
81
+ alt={selectedPhoto.title || album.title}
82
+ className="rounded-3xl shadow-sm border border-primary/10 max-w-full max-h-full object-contain"
83
+ />
84
+ ) : null}
126
85
  </div>
127
86
  </div>
128
87
  );
@@ -57,7 +57,7 @@ export const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
57
57
 
58
58
  const currentIndex = emblaApi.selectedScrollSnap();
59
59
  if (widgetState?.currentIndex !== currentIndex) {
60
- setWidgetState({ currentIndex });
60
+ setWidgetState((prev) => ({ ...prev, currentIndex }));
61
61
  }
62
62
  }, [emblaApi, widgetState?.currentIndex, setWidgetState]);
63
63
 
@@ -1,4 +1,3 @@
1
1
  export * from './card';
2
2
  export * from './carousel';
3
3
  export * from './album';
4
- export * from './resources';
@@ -12,7 +12,7 @@ vi.mock('sunpeak', () => ({
12
12
  }));
13
13
 
14
14
  // Mock Albums component
15
- vi.mock('../album/albums', () => ({
15
+ vi.mock('../components/album/albums', () => ({
16
16
  Albums: () => <div data-testid="albums-component">Albums Component</div>,
17
17
  }));
18
18
 
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { useSafeArea, useMaxHeight } from 'sunpeak';
3
- import { Albums } from '../album/albums';
3
+ import { Albums } from '../components/album/albums';
4
4
 
5
5
  /**
6
6
  * Production-ready Albums Resource
@@ -15,6 +15,7 @@ export const AlbumsResource = React.forwardRef<HTMLDivElement>((_props, ref) =>
15
15
  return (
16
16
  <div
17
17
  ref={ref}
18
+ className="h-full"
18
19
  style={{
19
20
  paddingTop: `${safeArea?.insets.top ?? 0}px`,
20
21
  paddingBottom: `${safeArea?.insets.bottom ?? 0}px`,
@@ -32,13 +32,13 @@ vi.mock('sunpeak', () => ({
32
32
  }));
33
33
 
34
34
  // Mock child components
35
- vi.mock('../carousel/carousel', () => ({
35
+ vi.mock('../components/carousel/carousel', () => ({
36
36
  Carousel: ({ children }: { children: React.ReactNode }) => (
37
37
  <div data-testid="carousel">{children}</div>
38
38
  ),
39
39
  }));
40
40
 
41
- vi.mock('../card/card', () => ({
41
+ vi.mock('../components/card/card', () => ({
42
42
  Card: ({ header, buttonSize }: { header: React.ReactNode; buttonSize?: string }) => (
43
43
  <div data-testid="card" data-button-size={buttonSize}>
44
44
  {header}
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { useWidgetProps, useSafeArea, useMaxHeight, useUserAgent } from 'sunpeak';
3
- import { Carousel } from '../carousel/carousel';
4
- import { Card } from '../card/card';
3
+ import { Carousel } from '../components/carousel/carousel';
4
+ import { Card } from '../components/card/card';
5
5
 
6
6
  /**
7
7
  * Production-ready Carousel Resource
@@ -1,5 +1,6 @@
1
1
  import { useWidgetState, useSafeArea, useMaxHeight, useUserAgent } from 'sunpeak';
2
2
  import { Button } from '@openai/apps-sdk-ui/components/Button';
3
+ import { useEffect } from 'react';
3
4
 
4
5
  interface CounterState extends Record<string, unknown> {
5
6
  count?: number;
@@ -22,6 +23,13 @@ export function CounterResource() {
22
23
  const count = widgetState?.count ?? 0;
23
24
  const hasTouch = userAgent?.capabilities.touch ?? false;
24
25
 
26
+ // Initialize count to 0 if not set
27
+ useEffect(() => {
28
+ if (widgetState?.count === undefined) {
29
+ setWidgetState({ count: 0 });
30
+ }
31
+ }, [widgetState?.count, setWidgetState]);
32
+
25
33
  const increment = () => {
26
34
  setWidgetState({ count: count + 1 });
27
35
  };
@@ -3,6 +3,8 @@
3
3
  * This file contains only metadata and doesn't import React components or CSS.
4
4
  */
5
5
 
6
+ import { defaultWidgetMeta } from './widget-config';
7
+
6
8
  const albumsData = {
7
9
  albums: [
8
10
  {
@@ -144,7 +146,9 @@ export const albumsSimulation = {
144
146
  title: 'Albums',
145
147
  description: 'Show photo albums widget markup',
146
148
  mimeType: 'text/html+skybridge',
147
- _meta: {},
149
+ _meta: {
150
+ ...defaultWidgetMeta,
151
+ },
148
152
  },
149
153
 
150
154
  // MCP CallTool protocol - data for CallTool response
@@ -3,6 +3,8 @@
3
3
  * This file contains only metadata and doesn't import React components or CSS.
4
4
  */
5
5
 
6
+ import { defaultWidgetMeta } from './widget-config';
7
+
6
8
  const placesData = {
7
9
  places: [
8
10
  {
@@ -81,7 +83,9 @@ export const carouselSimulation = {
81
83
  title: 'Carousel',
82
84
  description: 'Show popular places to visit widget markup',
83
85
  mimeType: 'text/html+skybridge',
84
- _meta: {},
86
+ _meta: {
87
+ ...defaultWidgetMeta,
88
+ },
85
89
  },
86
90
 
87
91
  // MCP CallTool protocol - data for CallTool response
@@ -2,6 +2,9 @@
2
2
  * Server-safe configuration for the counter simulation.
3
3
  * This file contains only metadata and doesn't import React components or CSS.
4
4
  */
5
+
6
+ import { defaultWidgetMeta } from './widget-config';
7
+
5
8
  export const counterSimulation = {
6
9
  userMessage: 'Help me count something',
7
10
 
@@ -30,7 +33,9 @@ export const counterSimulation = {
30
33
  title: 'Counter',
31
34
  description: 'Show a simple counter tool widget markup',
32
35
  mimeType: 'text/html+skybridge',
33
- _meta: {},
36
+ _meta: {
37
+ ...defaultWidgetMeta,
38
+ },
34
39
  },
35
40
 
36
41
  // MCP CallTool protocol - data for CallTool response
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Default widget configuration for all simulations.
3
+ * Individual simulations can override these values as needed.
4
+ */
5
+
6
+ export interface WidgetCSP {
7
+ connect_domains: string[];
8
+ resource_domains: string[];
9
+ }
10
+
11
+ export interface WidgetMeta {
12
+ 'openai/widgetDomain': string;
13
+ 'openai/widgetCSP': WidgetCSP;
14
+ }
15
+
16
+ /**
17
+ * Default widget metadata that can be spread into resource._meta
18
+ *
19
+ * @example
20
+ * // Use default configuration
21
+ * resource: {
22
+ * _meta: {
23
+ * ...defaultWidgetMeta,
24
+ * }
25
+ * }
26
+ *
27
+ * @example
28
+ * // Override specific values
29
+ * resource: {
30
+ * _meta: {
31
+ * ...defaultWidgetMeta,
32
+ * 'openai/widgetDomain': 'https://custom.domain.com',
33
+ * }
34
+ * }
35
+ */
36
+ export const defaultWidgetMeta: WidgetMeta = {
37
+ 'openai/widgetDomain': 'https://sunpeak.ai', // YOUR DOMAIN HERE.
38
+ 'openai/widgetCSP': {
39
+ connect_domains: ['https://sunpeak.ai'], // YOUR API HERE.
40
+ resource_domains: ['https://*.oaistatic.com', 'https://images.unsplash.com'], // YOUR CDN HERE (if any).
41
+ },
42
+ };