Skip to content

Commit c049d33

Browse files
committed
feat: add useTheme hook for reactive theme detection
Added a new useTheme() hook that provides reactive access to the ChatGPT theme and automatically updates when the theme changes. Changes: - Created useTheme hook that wraps useOpenAiGlobal('theme') - Returns Theme | null ('light' | 'dark' | null) - Added 3 comprehensive tests (14 total OpenAI hook tests now) - Updated both READMEs with useTheme examples and complete hook list - All 513 tests passing Usage: ```tsx import { useTheme } from '@ainativekit/ui'; function MyWidget() { const theme = useTheme(); return <div className={theme === 'dark' ? 'dark-mode' : 'light-mode'} />; } ``` Resolves: #5
1 parent 2090e58 commit c049d33

File tree

5 files changed

+124
-10
lines changed

5 files changed

+124
-10
lines changed

README.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,45 @@ import { SettingsCog, Terminal, Star } from '@ainativekit/ui/icons';
186186
Utilities to integrate with the **ChatGPT Apps SDK** runtime.
187187

188188
```tsx
189-
import { useOpenAiGlobal, useWidgetState, useMaxHeight } from '@ainativekit/ui';
189+
import {
190+
useOpenAiGlobal,
191+
useWidgetState,
192+
useMaxHeight,
193+
useTheme,
194+
useDisplayMode
195+
} from '@ainativekit/ui';
190196

191197
function MyChatGPTWidget() {
192-
const openai = useOpenAiGlobal(); // Access global OpenAI instance
193-
const [state, setState] = useWidgetState({}); // Manage widget state
194-
const maxHeight = useMaxHeight(); // Layout constraint from host
198+
// Access specific OpenAI global values (reactive)
199+
const theme = useTheme(); // 'light' | 'dark' | null
200+
const displayMode = useDisplayMode(); // 'inline' | 'pip' | 'fullscreen' | null
201+
const maxHeight = useMaxHeight(); // number | null
202+
203+
// Or access any global property directly
204+
const locale = useOpenAiGlobal('locale'); // string | null
205+
206+
// Manage persistent widget state
207+
const [state, setState] = useWidgetState({ count: 0 });
195208

196-
return <div style={{ maxHeight }}>{/* your widget */}</div>;
209+
return (
210+
<div
211+
className={theme === 'dark' ? 'dark-mode' : 'light-mode'}
212+
style={{ maxHeight: maxHeight ?? 600 }}
213+
>
214+
{/* your widget */}
215+
</div>
216+
);
197217
}
198218
```
199219

220+
**Available Hooks:**
221+
- `useTheme()` - Get current theme and listen for changes
222+
- `useDisplayMode()` - Get current display mode (inline/pip/fullscreen)
223+
- `useMaxHeight()` - Get maximum height constraint for layout
224+
- `useWidgetState(defaultState)` - Persistent state across ChatGPT sessions
225+
- `useOpenAiGlobal(key)` - Access any `window.openai` property reactively
226+
- `useWidgetProps(defaultProps)` - Get tool output data
227+
200228
## 📘 TypeScript Support
201229

202230
AINativeKit UI exports comprehensive TypeScript definitions for the ChatGPT Apps SDK, giving you **full autocomplete and type safety** for `window.openai`.

packages/ui/README.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,45 @@ import { SettingsCog, Terminal, Star } from '@ainativekit/ui/icons';
187187
Utilities to integrate with the **ChatGPT Apps SDK** runtime.
188188

189189
```tsx
190-
import { useOpenAiGlobal, useWidgetState, useMaxHeight } from '@ainativekit/ui';
190+
import {
191+
useOpenAiGlobal,
192+
useWidgetState,
193+
useMaxHeight,
194+
useTheme,
195+
useDisplayMode
196+
} from '@ainativekit/ui';
191197

192198
function MyChatGPTWidget() {
193-
const openai = useOpenAiGlobal(); // Access global OpenAI instance
194-
const [state, setState] = useWidgetState({}); // Manage widget state
195-
const maxHeight = useMaxHeight(); // Layout constraint from host
199+
// Access specific OpenAI global values (reactive)
200+
const theme = useTheme(); // 'light' | 'dark' | null
201+
const displayMode = useDisplayMode(); // 'inline' | 'pip' | 'fullscreen' | null
202+
const maxHeight = useMaxHeight(); // number | null
203+
204+
// Or access any global property directly
205+
const locale = useOpenAiGlobal('locale'); // string | null
206+
207+
// Manage persistent widget state
208+
const [state, setState] = useWidgetState({ count: 0 });
196209

197-
return <div style={{ maxHeight }}>{/* your widget */}</div>;
210+
return (
211+
<div
212+
className={theme === 'dark' ? 'dark-mode' : 'light-mode'}
213+
style={{ maxHeight: maxHeight ?? 600 }}
214+
>
215+
{/* your widget */}
216+
</div>
217+
);
198218
}
199219
```
200220

221+
**Available Hooks:**
222+
- `useTheme()` - Get current theme and listen for changes
223+
- `useDisplayMode()` - Get current display mode (inline/pip/fullscreen)
224+
- `useMaxHeight()` - Get maximum height constraint for layout
225+
- `useWidgetState(defaultState)` - Persistent state across ChatGPT sessions
226+
- `useOpenAiGlobal(key)` - Access any `window.openai` property reactively
227+
- `useWidgetProps(defaultProps)` - Get tool output data
228+
201229
## 📘 TypeScript Support
202230

203231
AINativeKit UI exports comprehensive TypeScript definitions for the ChatGPT Apps SDK, giving you **full autocomplete and type safety** for `window.openai`.

packages/ui/src/hooks/openai/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { useWidgetProps } from './useWidgetProps';
44
export { useWidgetState } from './useWidgetState';
55
export { useMaxHeight } from './useMaxHeight';
66
export { useDisplayMode } from './useDisplayMode';
7+
export { useTheme } from './useTheme';

packages/ui/src/hooks/openai/openai-hooks.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useWidgetState,
77
useMaxHeight,
88
useDisplayMode,
9+
useTheme,
910
SetGlobalsEvent,
1011
type OpenAiGlobals,
1112
type OpenAiApi,
@@ -198,4 +199,34 @@ describe('OpenAI Hooks', () => {
198199
expect(result.current).toBe('pip');
199200
});
200201
});
202+
203+
describe('useTheme', () => {
204+
beforeEach(() => {
205+
window.openai = createOpenAiMock();
206+
});
207+
208+
it('returns the current theme from globals', () => {
209+
const { result } = renderHook(() => useTheme());
210+
expect(result.current).toBe('light');
211+
});
212+
213+
it('updates when theme changes', () => {
214+
const { result } = renderHook(() => useTheme());
215+
expect(result.current).toBe('light');
216+
217+
act(() => {
218+
window.openai.theme = 'dark';
219+
dispatchGlobalsEvent({ theme: 'dark' });
220+
});
221+
222+
expect(result.current).toBe('dark');
223+
});
224+
225+
it('returns null when window.openai is not available', () => {
226+
delete (window as { openai?: unknown }).openai;
227+
228+
const { result } = renderHook(() => useTheme());
229+
expect(result.current).toBeNull();
230+
});
231+
});
201232
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useOpenAiGlobal } from './useOpenAiGlobal';
2+
import type { Theme } from './types';
3+
4+
/**
5+
* Get the current ChatGPT theme and listen for changes
6+
*
7+
* @returns The current theme ('light' or 'dark'), or null if not in ChatGPT environment
8+
*
9+
* @example
10+
* ```tsx
11+
* import { useTheme } from '@ainativekit/ui';
12+
*
13+
* function MyComponent() {
14+
* const theme = useTheme();
15+
*
16+
* return (
17+
* <div className={theme === 'dark' ? 'dark-mode' : 'light-mode'}>
18+
* Current theme: {theme ?? 'unknown'}
19+
* </div>
20+
* );
21+
* }
22+
* ```
23+
*/
24+
export const useTheme = (): Theme | null => {
25+
return useOpenAiGlobal('theme');
26+
};

0 commit comments

Comments
 (0)