sunpeak 0.5.36 → 0.5.41

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 (33) hide show
  1. package/dist/chatgpt/mock-openai.d.ts +2 -2
  2. package/dist/chatgpt/simple-sidebar.d.ts +2 -1
  3. package/dist/hooks/use-max-height.d.ts +1 -1
  4. package/dist/index.cjs +176 -121
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +176 -121
  7. package/dist/index.js.map +1 -1
  8. package/dist/providers/openai/types.d.ts +1 -1
  9. package/dist/providers/types.d.ts +1 -1
  10. package/dist/style.css +155 -36
  11. package/package.json +1 -1
  12. package/template/dist/chatgpt/albums.js +10 -10
  13. package/template/dist/chatgpt/carousel.js +2 -2
  14. package/template/dist/chatgpt/counter.js +7 -7
  15. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +3 -3
  16. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +4 -4
  17. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +19 -19
  18. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Textarea.js +3 -3
  19. package/template/node_modules/.vite/deps/_metadata.json +30 -30
  20. package/template/node_modules/.vite/deps/{chunk-675LFNY2.js → chunk-EVJ3DVH5.js} +8 -8
  21. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  22. package/template/src/components/album/albums.test.tsx +7 -2
  23. package/template/src/components/album/albums.tsx +1 -1
  24. package/template/src/components/album/fullscreen-viewer.test.tsx +12 -24
  25. package/template/src/components/album/fullscreen-viewer.tsx +55 -34
  26. package/template/src/components/carousel/carousel.tsx +1 -1
  27. package/template/src/components/resources/albums-resource.tsx +1 -0
  28. package/template/src/components/resources/counter-resource.tsx +8 -0
  29. package/template/src/simulations/albums-simulation.ts +5 -1
  30. package/template/src/simulations/carousel-simulation.ts +5 -1
  31. package/template/src/simulations/counter-simulation.ts +6 -1
  32. package/template/src/simulations/widget-config.ts +42 -0
  33. /package/template/node_modules/.vite/deps/{chunk-675LFNY2.js.map → chunk-EVJ3DVH5.js.map} +0 -0
@@ -2,11 +2,11 @@ import {
2
2
  Button,
3
3
  ButtonLink,
4
4
  CopyButton
5
- } from "./chunk-675LFNY2.js";
6
- import "./chunk-QPJAV452.js";
7
- import "./chunk-XB525PXG.js";
5
+ } from "./chunk-EVJ3DVH5.js";
8
6
  import "./chunk-YOJ6QPGS.js";
9
7
  import "./chunk-BAG6OO6S.js";
8
+ import "./chunk-XB525PXG.js";
9
+ import "./chunk-QPJAV452.js";
10
10
  import "./chunk-EGRHWZRV.js";
11
11
  import "./chunk-CNYJBM5F.js";
12
12
  import "./chunk-PTVT3RFX.js";
@@ -1,7 +1,3 @@
1
- import {
2
- dist_exports4 as dist_exports
3
- } from "./chunk-SGWD4VEU.js";
4
- import "./chunk-KFGKZMLK.js";
5
1
  import {
6
2
  useResizeObserver
7
3
  } from "./chunk-YOJ6QPGS.js";
@@ -9,6 +5,10 @@ import {
9
5
  handlePressableMouseEnter,
10
6
  waitForAnimationFrame
11
7
  } from "./chunk-BAG6OO6S.js";
8
+ import {
9
+ dist_exports4 as dist_exports
10
+ } from "./chunk-SGWD4VEU.js";
11
+ import "./chunk-KFGKZMLK.js";
12
12
  import "./chunk-EGRHWZRV.js";
13
13
  import {
14
14
  clsx_default
@@ -1,14 +1,23 @@
1
- import {
2
- Input
3
- } from "./chunk-CQ3GYAYB.js";
4
1
  import {
5
2
  Button,
6
3
  LoadingIndicator,
7
4
  TransitionGroup
8
- } from "./chunk-675LFNY2.js";
5
+ } from "./chunk-EVJ3DVH5.js";
9
6
  import {
10
- o
11
- } from "./chunk-QPJAV452.js";
7
+ useTimeout
8
+ } from "./chunk-YOJ6QPGS.js";
9
+ import {
10
+ handlePressableMouseEnter,
11
+ preventDefaultHandler,
12
+ toCssVariables,
13
+ waitForAnimationFrame
14
+ } from "./chunk-BAG6OO6S.js";
15
+ import {
16
+ dist_exports,
17
+ dist_exports3 as dist_exports2,
18
+ dist_exports5 as dist_exports3
19
+ } from "./chunk-SGWD4VEU.js";
20
+ import "./chunk-KFGKZMLK.js";
12
21
  import {
13
22
  Check_default,
14
23
  ChevronDownVector_default,
@@ -18,20 +27,11 @@ import {
18
27
  X_default
19
28
  } from "./chunk-XB525PXG.js";
20
29
  import {
21
- dist_exports,
22
- dist_exports3 as dist_exports2,
23
- dist_exports5 as dist_exports3
24
- } from "./chunk-SGWD4VEU.js";
25
- import "./chunk-KFGKZMLK.js";
26
- import {
27
- useTimeout
28
- } from "./chunk-YOJ6QPGS.js";
30
+ Input
31
+ } from "./chunk-CQ3GYAYB.js";
29
32
  import {
30
- handlePressableMouseEnter,
31
- preventDefaultHandler,
32
- toCssVariables,
33
- waitForAnimationFrame
34
- } from "./chunk-BAG6OO6S.js";
33
+ o
34
+ } from "./chunk-QPJAV452.js";
35
35
  import "./chunk-EGRHWZRV.js";
36
36
  import {
37
37
  clsx_default
@@ -1,9 +1,9 @@
1
- import {
2
- o
3
- } from "./chunk-QPJAV452.js";
4
1
  import {
5
2
  toCssVariables
6
3
  } from "./chunk-BAG6OO6S.js";
4
+ import {
5
+ o
6
+ } from "./chunk-QPJAV452.js";
7
7
  import "./chunk-EGRHWZRV.js";
8
8
  import {
9
9
  clsx_default
@@ -7,118 +7,115 @@
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": "e328b168",
10
+ "fileHash": "264ae3aa",
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": "24da7214",
16
+ "fileHash": "565f0670",
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": "a22ed365",
22
+ "fileHash": "dc93890d",
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": "995b8d9a",
28
+ "fileHash": "769723ed",
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": "46265110",
34
+ "fileHash": "2aaa39fe",
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": "df3f5c67",
40
+ "fileHash": "9bc36d10",
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": "baa257c2",
46
+ "fileHash": "0c8a7bb7",
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": "b09a0706",
52
+ "fileHash": "66aa5c5a",
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": "30f3b4fa",
58
+ "fileHash": "97a2aa5f",
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": "c07720fa",
64
+ "fileHash": "be97a54f",
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": "4d8a8d87",
70
+ "fileHash": "cb2abaca",
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": "9e5b445c",
76
+ "fileHash": "446e068b",
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": "8c4d2ac1",
82
+ "fileHash": "07860a30",
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": "3b8004d6",
88
+ "fileHash": "361056e7",
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": "8678cd8b",
94
+ "fileHash": "a42e64fa",
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": "df32aaf4",
100
+ "fileHash": "39ff4ecc",
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": "11ec8e32",
106
+ "fileHash": "01cd3e08",
107
107
  "needsInterop": false
108
108
  }
109
109
  },
110
110
  "chunks": {
111
- "chunk-CQ3GYAYB": {
112
- "file": "chunk-CQ3GYAYB.js"
113
- },
114
- "chunk-675LFNY2": {
115
- "file": "chunk-675LFNY2.js"
111
+ "chunk-EVJ3DVH5": {
112
+ "file": "chunk-EVJ3DVH5.js"
116
113
  },
117
- "chunk-QPJAV452": {
118
- "file": "chunk-QPJAV452.js"
114
+ "chunk-YOJ6QPGS": {
115
+ "file": "chunk-YOJ6QPGS.js"
119
116
  },
120
- "chunk-XB525PXG": {
121
- "file": "chunk-XB525PXG.js"
117
+ "chunk-BAG6OO6S": {
118
+ "file": "chunk-BAG6OO6S.js"
122
119
  },
123
120
  "chunk-SGWD4VEU": {
124
121
  "file": "chunk-SGWD4VEU.js"
@@ -126,11 +123,14 @@
126
123
  "chunk-KFGKZMLK": {
127
124
  "file": "chunk-KFGKZMLK.js"
128
125
  },
129
- "chunk-YOJ6QPGS": {
130
- "file": "chunk-YOJ6QPGS.js"
126
+ "chunk-XB525PXG": {
127
+ "file": "chunk-XB525PXG.js"
131
128
  },
132
- "chunk-BAG6OO6S": {
133
- "file": "chunk-BAG6OO6S.js"
129
+ "chunk-CQ3GYAYB": {
130
+ "file": "chunk-CQ3GYAYB.js"
131
+ },
132
+ "chunk-QPJAV452": {
133
+ "file": "chunk-QPJAV452.js"
134
134
  },
135
135
  "chunk-EGRHWZRV": {
136
136
  "file": "chunk-EGRHWZRV.js"
@@ -1,10 +1,3 @@
1
- import {
2
- o
3
- } from "./chunk-QPJAV452.js";
4
- import {
5
- Check_default,
6
- Copy_default
7
- } from "./chunk-XB525PXG.js";
8
1
  import {
9
2
  useTimeout
10
3
  } from "./chunk-YOJ6QPGS.js";
@@ -19,6 +12,13 @@ import {
19
12
  toTransformProperty,
20
13
  waitForAnimationFrame
21
14
  } from "./chunk-BAG6OO6S.js";
15
+ import {
16
+ Check_default,
17
+ Copy_default
18
+ } from "./chunk-XB525PXG.js";
19
+ import {
20
+ o
21
+ } from "./chunk-QPJAV452.js";
22
22
  import {
23
23
  clsx_default
24
24
  } from "./chunk-CNYJBM5F.js";
@@ -625,4 +625,4 @@ export {
625
625
  ButtonLink,
626
626
  CopyButton
627
627
  };
628
- //# sourceMappingURL=chunk-675LFNY2.js.map
628
+ //# sourceMappingURL=chunk-EVJ3DVH5.js.map
@@ -1 +1 @@
1
- {"version":"4.0.13","results":[[":src/components/resources/carousel-resource.test.tsx",{"duration":288.1622299999999,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":326.12392699999987,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":260.0158970000002,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":69.38757799999985,"failed":false}],[":src/components/resources/counter-resource.test.tsx",{"duration":335.9426239999998,"failed":false}],[":src/components/resources/albums-resource.test.tsx",{"duration":287.460425,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":444.058082,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":340.6524770000001,"failed":false}],[":src/components/card/card.test.tsx",{"duration":59.76885399999992,"failed":false}]]}
1
+ {"version":"4.0.13","results":[[":src/components/album/albums.test.tsx",{"duration":370.11665100000005,"failed":false}],[":src/components/resources/counter-resource.test.tsx",{"duration":311.2705619999997,"failed":false}],[":src/components/resources/carousel-resource.test.tsx",{"duration":257.46737600000006,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":232.20117000000027,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":86.45302899999979,"failed":false}],[":src/components/resources/albums-resource.test.tsx",{"duration":273.0157959999999,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":458.94508499999984,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":290.17094999999995,"failed":false}],[":src/components/card/card.test.tsx",{"duration":54.771146000000044,"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,56 +11,77 @@ 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);
16
+ const [width, setWidth] = React.useState(0);
17
+ const containerRef = React.useRef<HTMLDivElement>(null);
17
18
 
18
19
  React.useEffect(() => {
19
20
  setSelectedIndex(0);
20
21
  }, [album?.id]);
21
22
 
23
+ // Measure component width to determine mobile vs desktop layout
24
+ React.useEffect(() => {
25
+ const element = containerRef.current;
26
+ if (!element) return;
27
+
28
+ const updateWidth = () => {
29
+ setWidth(element.getBoundingClientRect().width);
30
+ };
31
+
32
+ updateWidth();
33
+
34
+ const resizeObserver = new ResizeObserver(updateWidth);
35
+ resizeObserver.observe(element);
36
+
37
+ return () => {
38
+ resizeObserver.disconnect();
39
+ };
40
+ }, []);
41
+
42
+ // Combine refs
43
+ React.useImperativeHandle(ref, () => containerRef.current!);
44
+
22
45
  const selectedPhoto = album?.photos?.[selectedIndex];
46
+ const isMobile = width > 0 && width < 768;
23
47
 
24
48
  return (
25
49
  <div
26
- ref={ref}
27
- className={cn('relative w-full h-full bg-surface', className)}
50
+ ref={containerRef}
51
+ className={cn('flex w-full bg-surface', isMobile ? 'flex-col' : 'flex-row', className)}
28
52
  style={{
29
- maxHeight: maxHeight ?? undefined,
30
- height: maxHeight ?? undefined,
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`,
31
57
  }}
32
58
  >
33
- <div className="absolute inset-0 flex flex-row overflow-hidden">
34
- {/* Film strip */}
35
- <div
36
- className="hidden md:block absolute pointer-events-none z-10 left-0 top-0 bottom-0 w-40"
37
- style={{
38
- paddingLeft: `${safeArea?.insets.left ?? 0}px`,
39
- }}
40
- >
41
- <FilmStrip album={album} selectedIndex={selectedIndex} onSelect={setSelectedIndex} />
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>
42
66
  </div>
67
+ )}
43
68
 
44
- {/* Main photo */}
45
- <div
46
- className="flex-1 min-w-0 px-40 py-10 relative flex items-center justify-center"
47
- style={{
48
- paddingTop: `calc(2.5rem + ${safeArea?.insets.top ?? 0}px)`,
49
- paddingBottom: `calc(2.5rem + ${safeArea?.insets.bottom ?? 0}px)`,
50
- paddingLeft: `calc(10rem + ${safeArea?.insets.left ?? 0}px)`,
51
- paddingRight: `calc(10rem + ${safeArea?.insets.right ?? 0}px)`,
52
- }}
53
- >
54
- <div className="relative w-full h-full">
55
- {selectedPhoto ? (
56
- <img
57
- src={selectedPhoto.url}
58
- alt={selectedPhoto.title || album.title}
59
- className="absolute inset-0 m-auto rounded-3xl shadow-sm border border-primary/10 max-w-full max-h-full object-contain"
60
- />
61
- ) : null}
62
- </div>
69
+ {/* Film strip - desktop only */}
70
+ {!isMobile && (
71
+ <div className="w-40 flex-shrink-0">
72
+ <FilmStrip album={album} selectedIndex={selectedIndex} onSelect={setSelectedIndex} />
63
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}
64
85
  </div>
65
86
  </div>
66
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
 
@@ -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`,
@@ -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