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.
package/README.md CHANGED
@@ -59,6 +59,15 @@ export default function App() {
59
59
  - 🔄 **Anthropic Claude** - Design system available (SDK support coming soon)
60
60
  - 🔧 **Custom platforms** - Implement your own platform adapter
61
61
 
62
+ ## What is sunpeak exactly?
63
+
64
+ sunpeak is an npm package consisting of:
65
+
66
+ 1. **A CLI utility** for working with sunpeak (`./bin`).
67
+ 2. **A `sunpeak` library** (`./src`). This library contains common runtime APIs and testing utilities—including a ChatGPT simulator—to be used as a dependency by sunpeak projects.
68
+ 3. **A templated npm package** (`./template`) that is initialized by the CLI to help developers set up sunpeak projects. These projects have the `sunpeak` dependency already wired up alongside a collection of pre-built shadcn UI components (`./template/src/components`) to copy, modify, or use as an example.
69
+ 1. Developers build their final UI in the `App` component, which is then built against CSS files that codify the design systems of each of the genAI platforms (of which ChatGPT is the only one today).
70
+
62
71
  ## Contributing
63
72
 
64
73
  We welcome your contributions!
@@ -69,4 +78,5 @@ For development quickstart on this package, see [DEVELOPMENT.md](./DEVELOPMENT.m
69
78
 
70
79
  - [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
71
80
  - [ChatGPT Apps SDK UI Documentation](https://developers.openai.com/apps-sdk/build/chatgpt-ui)
81
+ - [ChatGPT Apps SDK window.openai Reference](https://developers.openai.com/apps-sdk/build/mcp-server#understand-the-windowopenai-widget-runtime)
72
82
  - [ChatGPT Apps SDK Examples](https://github.com/openai/openai-apps-sdk-examples)
package/dist/index.cjs CHANGED
@@ -76,49 +76,120 @@ var SetGlobalsEvent = class extends CustomEvent {
76
76
  }
77
77
  };
78
78
 
79
- // src/types/simulator.ts
80
- var SCREEN_WIDTHS = {
81
- "mobile-s": 375,
82
- "mobile-l": 425,
83
- tablet: 768,
84
- full: 1024
79
+ // src/providers/openai.ts
80
+ function isOpenAiAvailable() {
81
+ return typeof window !== "undefined" && window.openai != null;
82
+ }
83
+ var OpenAiProvider = class {
84
+ constructor() {
85
+ __publicField(this, "id", "openai");
86
+ }
87
+ getGlobal(key) {
88
+ if (typeof window === "undefined" || !window.openai) {
89
+ return null;
90
+ }
91
+ return window.openai[key] ?? null;
92
+ }
93
+ subscribe(key, onChange) {
94
+ if (typeof window === "undefined") {
95
+ return () => {
96
+ };
97
+ }
98
+ const handleEvent = (event) => {
99
+ if (event.detail.globals[key] !== void 0) {
100
+ onChange();
101
+ }
102
+ };
103
+ window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleEvent, {
104
+ passive: true
105
+ });
106
+ return () => {
107
+ window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleEvent);
108
+ };
109
+ }
110
+ getAPI() {
111
+ if (typeof window === "undefined" || !window.openai) {
112
+ return null;
113
+ }
114
+ const api = window.openai;
115
+ return {
116
+ callTool: api.callTool?.bind(api),
117
+ sendFollowUpMessage: api.sendFollowUpMessage?.bind(api),
118
+ openExternal: api.openExternal?.bind(api),
119
+ requestDisplayMode: api.requestDisplayMode?.bind(api),
120
+ requestModal: api.requestModal?.bind(api),
121
+ notifyIntrinsicHeight: api.notifyIntrinsicHeight?.bind(api),
122
+ setWidgetState: api.setWidgetState?.bind(api)
123
+ };
124
+ }
85
125
  };
126
+ var openAiProvider = null;
127
+ function getOpenAiProvider() {
128
+ if (!openAiProvider) {
129
+ openAiProvider = new OpenAiProvider();
130
+ }
131
+ return openAiProvider;
132
+ }
133
+
134
+ // src/providers/index.ts
135
+ var cachedProvider = null;
136
+ var detectionComplete = false;
137
+ function getProvider() {
138
+ if (detectionComplete) {
139
+ return cachedProvider;
140
+ }
141
+ if (isOpenAiAvailable()) {
142
+ cachedProvider = getOpenAiProvider();
143
+ }
144
+ detectionComplete = true;
145
+ return cachedProvider;
146
+ }
147
+ function isProviderAvailable() {
148
+ return getProvider() !== null;
149
+ }
150
+ function getGlobal(key) {
151
+ const provider = getProvider();
152
+ return provider?.getGlobal(key) ?? null;
153
+ }
154
+ function subscribeToGlobal(key, onChange) {
155
+ const provider = getProvider();
156
+ return provider?.subscribe(key, onChange) ?? (() => {
157
+ });
158
+ }
159
+ function getAPI() {
160
+ const provider = getProvider();
161
+ return provider?.getAPI() ?? null;
162
+ }
163
+ function resetProviderCache() {
164
+ cachedProvider = null;
165
+ detectionComplete = false;
166
+ }
86
167
 
87
- // src/hooks/use-openai-global.ts
88
- function useOpenAiGlobal(key) {
168
+ // src/hooks/use-widget-global.ts
169
+ function useWidgetGlobal(key) {
89
170
  return React3.useSyncExternalStore(
90
- (onChange) => {
91
- if (typeof window === "undefined") {
92
- return () => {
93
- };
94
- }
95
- const handleSetGlobal = (event) => {
96
- const value = event.detail.globals[key];
97
- if (value === void 0) {
98
- return;
99
- }
100
- onChange();
101
- };
102
- window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
103
- passive: true
104
- });
105
- return () => {
106
- window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
107
- };
108
- },
109
- () => window.openai?.[key] ?? null,
110
- () => window.openai?.[key] ?? null
171
+ (onChange) => subscribeToGlobal(key, onChange),
172
+ () => getGlobal(key),
173
+ () => getGlobal(key)
111
174
  );
112
175
  }
176
+ function useWidgetAPI() {
177
+ return React3.useMemo(() => getAPI(), []);
178
+ }
113
179
 
114
180
  // src/hooks/use-display-mode.ts
115
181
  var useDisplayMode = () => {
116
- return useOpenAiGlobal("displayMode");
182
+ return useWidgetGlobal("displayMode");
183
+ };
184
+
185
+ // src/hooks/use-locale.ts
186
+ var useLocale = () => {
187
+ return useWidgetGlobal("locale");
117
188
  };
118
189
 
119
190
  // src/hooks/use-max-height.ts
120
191
  var useMaxHeight = () => {
121
- return useOpenAiGlobal("maxHeight");
192
+ return useWidgetGlobal("maxHeight");
122
193
  };
123
194
  var MOBILE_BREAKPOINT = 768;
124
195
  function useIsMobile() {
@@ -135,26 +206,84 @@ function useIsMobile() {
135
206
  return !!isMobile;
136
207
  }
137
208
 
209
+ // src/hooks/use-safe-area.ts
210
+ var useSafeArea = () => {
211
+ return useWidgetGlobal("safeArea");
212
+ };
213
+
138
214
  // src/hooks/use-theme.ts
139
215
  var useTheme = () => {
140
- return useOpenAiGlobal("theme");
216
+ return useWidgetGlobal("theme");
141
217
  };
142
- function useWidgetState() {
143
- const widgetState = useOpenAiGlobal("widgetState");
144
- const setWidgetState = useOpenAiGlobal("setWidgetState");
145
- const setter = React3.useCallback(
146
- async (state) => {
147
- if (setWidgetState) {
148
- await setWidgetState(state);
149
- }
218
+
219
+ // src/hooks/use-tool-input.ts
220
+ function useToolInput() {
221
+ return useWidgetGlobal("toolInput");
222
+ }
223
+
224
+ // src/hooks/use-tool-response-metadata.ts
225
+ function useToolResponseMetadata() {
226
+ return useWidgetGlobal("toolResponseMetadata");
227
+ }
228
+
229
+ // src/hooks/use-user-agent.ts
230
+ var useUserAgent = () => {
231
+ return useWidgetGlobal("userAgent");
232
+ };
233
+
234
+ // src/hooks/use-view.ts
235
+ function useView() {
236
+ return useWidgetGlobal("view");
237
+ }
238
+
239
+ // src/hooks/use-widget-props.ts
240
+ function useWidgetProps(defaultState) {
241
+ const props = useWidgetGlobal("toolOutput");
242
+ const fallback = typeof defaultState === "function" ? defaultState() : defaultState ?? null;
243
+ return props ?? fallback;
244
+ }
245
+ function useWidgetState(defaultState) {
246
+ const widgetStateFromProvider = useWidgetGlobal("widgetState");
247
+ const api = useWidgetAPI();
248
+ const [widgetState, _setWidgetState] = React3.useState(() => {
249
+ if (widgetStateFromProvider != null) {
250
+ return widgetStateFromProvider;
251
+ }
252
+ return typeof defaultState === "function" ? defaultState() : defaultState ?? null;
253
+ });
254
+ React3.useEffect(() => {
255
+ _setWidgetState(widgetStateFromProvider);
256
+ }, [widgetStateFromProvider]);
257
+ const setWidgetState = React3.useCallback(
258
+ (state) => {
259
+ _setWidgetState((prevState) => {
260
+ const newState = typeof state === "function" ? state(prevState) : state;
261
+ if (newState != null && api?.setWidgetState) {
262
+ api.setWidgetState(newState);
263
+ }
264
+ return newState;
265
+ });
150
266
  },
151
- [setWidgetState]
267
+ [api]
152
268
  );
153
- return React3.useMemo(() => [widgetState, setter], [widgetState, setter]);
269
+ return [widgetState, setWidgetState];
154
270
  }
155
271
  function cn(...inputs) {
156
272
  return tailwindMerge.twMerge(clsx.clsx(inputs));
157
273
  }
274
+
275
+ // src/lib/media-queries.ts
276
+ function matchMediaQuery(query) {
277
+ return window.matchMedia(query).matches;
278
+ }
279
+ function createMediaQueryFn(query) {
280
+ return () => matchMediaQuery(query);
281
+ }
282
+ var prefersReducedMotion = createMediaQueryFn(
283
+ "(prefers-reduced-motion: reduce)"
284
+ );
285
+ var isPrimarilyTouchDevice = createMediaQueryFn("(pointer: coarse)");
286
+ var isHoverAvailable = createMediaQueryFn("(hover: hover)");
158
287
  function Sheet({ ...props }) {
159
288
  return /* @__PURE__ */ jsxRuntime.jsx(SheetPrimitive__namespace.Root, { "data-slot": "sheet", ...props });
160
289
  }
@@ -647,14 +776,22 @@ var Label2 = React3__namespace.forwardRef(({ className, ...props }, ref) => /* @
647
776
  }
648
777
  ));
649
778
  Label2.displayName = LabelPrimitive__namespace.Root.displayName;
779
+
780
+ // src/types/simulator.ts
781
+ var SCREEN_WIDTHS = {
782
+ "mobile-s": 375,
783
+ "mobile-l": 425,
784
+ tablet: 768,
785
+ full: 1024
786
+ };
650
787
  function Conversation({
651
788
  children,
652
789
  screenWidth,
653
- displayMode,
654
790
  appName = "ChatGPT",
655
791
  appIcon,
656
792
  userMessage = "Show me some interesting places to visit."
657
793
  }) {
794
+ const displayMode = useDisplayMode() ?? "inline";
658
795
  const containerWidth = screenWidth === "full" ? "100%" : `${SCREEN_WIDTHS[screenWidth]}px`;
659
796
  if (displayMode === "fullscreen") {
660
797
  return /* @__PURE__ */ jsxRuntime.jsx(SidebarInset, { className: "flex flex-col bg-background", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children }) });
@@ -762,6 +899,7 @@ var MockOpenAI = class {
762
899
  right: 0
763
900
  }
764
901
  });
902
+ __publicField(this, "view", null);
765
903
  __publicField(this, "toolInput", {});
766
904
  __publicField(this, "toolOutput", null);
767
905
  __publicField(this, "toolResponseMetadata", null);
@@ -782,6 +920,14 @@ var MockOpenAI = class {
782
920
  this.setDisplayMode(args.mode);
783
921
  return { mode: args.mode };
784
922
  }
923
+ async requestModal(args) {
924
+ console.log("Mock requestModal:", args);
925
+ this.view = { mode: args.mode, params: args.params };
926
+ this.emitUpdate({ view: this.view });
927
+ }
928
+ notifyIntrinsicHeight(height) {
929
+ console.log("Mock notifyIntrinsicHeight:", height);
930
+ }
785
931
  async setWidgetState(state) {
786
932
  this.widgetState = state;
787
933
  this.emitUpdate({ widgetState: state });
@@ -794,33 +940,76 @@ var MockOpenAI = class {
794
940
  this.displayMode = displayMode;
795
941
  this.emitUpdate({ displayMode });
796
942
  }
943
+ setToolOutput(toolOutput) {
944
+ this.toolOutput = toolOutput;
945
+ this.emitUpdate({ toolOutput });
946
+ }
947
+ setWidgetStateExternal(widgetState) {
948
+ this.widgetState = widgetState;
949
+ this.emitUpdate({ widgetState });
950
+ }
797
951
  emitUpdate(globals) {
798
952
  if (typeof window !== "undefined") {
799
- const event = new CustomEvent(SET_GLOBALS_EVENT_TYPE, {
800
- detail: { globals }
953
+ queueMicrotask(() => {
954
+ const event = new CustomEvent(SET_GLOBALS_EVENT_TYPE, {
955
+ detail: { globals }
956
+ });
957
+ window.dispatchEvent(event);
801
958
  });
802
- window.dispatchEvent(event);
803
959
  }
804
960
  }
805
961
  };
806
- function initMockOpenAI() {
962
+ function initMockOpenAI(initialData) {
807
963
  if (typeof window !== "undefined") {
808
964
  const mock = new MockOpenAI();
965
+ if (initialData?.theme !== void 0) {
966
+ mock.theme = initialData.theme;
967
+ }
968
+ if (initialData?.displayMode !== void 0) {
969
+ mock.displayMode = initialData.displayMode;
970
+ }
971
+ if (initialData?.toolOutput !== void 0) {
972
+ mock.toolOutput = initialData.toolOutput;
973
+ }
974
+ if (initialData?.widgetState !== void 0) {
975
+ mock.widgetState = initialData.widgetState;
976
+ }
809
977
  window.openai = mock;
810
978
  return mock;
811
979
  }
812
980
  return new MockOpenAI();
813
981
  }
982
+ var DEFAULT_THEME = "dark";
983
+ var DEFAULT_DISPLAY_MODE = "inline";
814
984
  function ChatGPTSimulator({
815
985
  children,
816
986
  appName,
817
987
  appIcon,
818
- userMessage
988
+ userMessage,
989
+ toolOutput = null,
990
+ widgetState = null
819
991
  }) {
820
- const [theme, setTheme] = React3.useState("dark");
821
- const [displayMode, setDisplayMode] = React3.useState("inline");
822
- const [screenWidth, setScreenWidth] = React3.useState("full");
823
- const mock = React3.useMemo(() => initMockOpenAI(), []);
992
+ const [screenWidth, setScreenWidth] = React3__namespace.useState("full");
993
+ const mock = React3.useMemo(
994
+ () => initMockOpenAI({
995
+ theme: DEFAULT_THEME,
996
+ displayMode: DEFAULT_DISPLAY_MODE
997
+ }),
998
+ []
999
+ );
1000
+ const theme = useTheme() ?? DEFAULT_THEME;
1001
+ const displayMode = useDisplayMode() ?? DEFAULT_DISPLAY_MODE;
1002
+ React3.useLayoutEffect(() => {
1003
+ if (mock && typeof window !== "undefined") {
1004
+ window.openai = mock;
1005
+ if (toolOutput !== void 0) {
1006
+ mock.setToolOutput(toolOutput);
1007
+ }
1008
+ if (widgetState !== void 0) {
1009
+ mock.setWidgetStateExternal(widgetState);
1010
+ }
1011
+ }
1012
+ }, [mock, toolOutput, widgetState]);
824
1013
  React3.useEffect(() => {
825
1014
  return () => {
826
1015
  if (typeof window !== "undefined") {
@@ -828,31 +1017,13 @@ function ChatGPTSimulator({
828
1017
  }
829
1018
  };
830
1019
  }, []);
831
- React3.useEffect(() => {
832
- if (mock) {
833
- mock.setTheme(theme);
834
- }
835
- }, [mock, theme]);
836
- React3.useEffect(() => {
837
- if (mock) {
838
- mock.setDisplayMode(displayMode);
839
- }
840
- }, [mock, displayMode]);
841
1020
  return /* @__PURE__ */ jsxRuntime.jsx(ThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsxs(SidebarProvider, { defaultOpen: true, children: [
842
1021
  /* @__PURE__ */ jsxRuntime.jsx(Sidebar, { children: /* @__PURE__ */ jsxRuntime.jsx(SidebarContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(SidebarGroup, { children: [
843
1022
  /* @__PURE__ */ jsxRuntime.jsx(SidebarGroupLabel, { className: "text-md", children: "Controls" }),
844
1023
  /* @__PURE__ */ jsxRuntime.jsxs(SidebarGroupContent, { className: "space-y-4", children: [
845
1024
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
846
- /* @__PURE__ */ jsxRuntime.jsx(Label2, { htmlFor: "app-ui-select", className: "text-xs", children: "App UI" }),
847
- /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: "carousel", onValueChange: () => {
848
- }, children: [
849
- /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { id: "app-ui-select", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, {}) }),
850
- /* @__PURE__ */ jsxRuntime.jsx(SelectContent, { children: /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "carousel", children: "Carousel" }) })
851
- ] })
852
- ] }),
853
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
854
- /* @__PURE__ */ jsxRuntime.jsx(Label2, { htmlFor: "theme-select", className: "text-xs", children: "Color Scheme" }),
855
- /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: theme, onValueChange: (value) => setTheme(value), children: [
1025
+ /* @__PURE__ */ jsxRuntime.jsx(Label2, { htmlFor: "theme-select", className: "text-xs", children: "Theme" }),
1026
+ /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: theme, onValueChange: (value) => mock.setTheme(value), children: [
856
1027
  /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { id: "theme-select", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, {}) }),
857
1028
  /* @__PURE__ */ jsxRuntime.jsxs(SelectContent, { children: [
858
1029
  /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "light", children: "Light" }),
@@ -862,7 +1033,7 @@ function ChatGPTSimulator({
862
1033
  ] }),
863
1034
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
864
1035
  /* @__PURE__ */ jsxRuntime.jsx(Label2, { htmlFor: "display-mode-select", className: "text-xs", children: "Display Mode" }),
865
- /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: displayMode, onValueChange: (value) => setDisplayMode(value), children: [
1036
+ /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: displayMode, onValueChange: (value) => mock.setDisplayMode(value), children: [
866
1037
  /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { id: "display-mode-select", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, {}) }),
867
1038
  /* @__PURE__ */ jsxRuntime.jsxs(SelectContent, { children: [
868
1039
  /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "inline", children: "Inline" }),
@@ -889,7 +1060,6 @@ function ChatGPTSimulator({
889
1060
  Conversation,
890
1061
  {
891
1062
  screenWidth,
892
- displayMode,
893
1063
  appName,
894
1064
  appIcon,
895
1065
  userMessage,
@@ -905,13 +1075,32 @@ exports.SET_GLOBALS_EVENT_TYPE = SET_GLOBALS_EVENT_TYPE;
905
1075
  exports.SetGlobalsEvent = SetGlobalsEvent;
906
1076
  exports.ThemeProvider = ThemeProvider;
907
1077
  exports.cn = cn;
1078
+ exports.getAPI = getAPI;
1079
+ exports.getGlobal = getGlobal;
1080
+ exports.getOpenAiProvider = getOpenAiProvider;
1081
+ exports.getProvider = getProvider;
908
1082
  exports.initMockOpenAI = initMockOpenAI;
1083
+ exports.isHoverAvailable = isHoverAvailable;
1084
+ exports.isOpenAiAvailable = isOpenAiAvailable;
1085
+ exports.isPrimarilyTouchDevice = isPrimarilyTouchDevice;
1086
+ exports.isProviderAvailable = isProviderAvailable;
1087
+ exports.prefersReducedMotion = prefersReducedMotion;
1088
+ exports.resetProviderCache = resetProviderCache;
1089
+ exports.subscribeToGlobal = subscribeToGlobal;
909
1090
  exports.useDisplayMode = useDisplayMode;
910
1091
  exports.useIsMobile = useIsMobile;
1092
+ exports.useLocale = useLocale;
911
1093
  exports.useMaxHeight = useMaxHeight;
912
- exports.useOpenAiGlobal = useOpenAiGlobal;
1094
+ exports.useSafeArea = useSafeArea;
913
1095
  exports.useTheme = useTheme;
914
1096
  exports.useThemeContext = useThemeContext;
1097
+ exports.useToolInput = useToolInput;
1098
+ exports.useToolResponseMetadata = useToolResponseMetadata;
1099
+ exports.useUserAgent = useUserAgent;
1100
+ exports.useView = useView;
1101
+ exports.useWidgetAPI = useWidgetAPI;
1102
+ exports.useWidgetGlobal = useWidgetGlobal;
1103
+ exports.useWidgetProps = useWidgetProps;
915
1104
  exports.useWidgetState = useWidgetState;
916
1105
  //# sourceMappingURL=index.cjs.map
917
1106
  //# sourceMappingURL=index.cjs.map