polymorph-sdk 0.1.0 → 0.2.2

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/package.json CHANGED
@@ -1,47 +1,51 @@
1
1
  {
2
2
  "name": "polymorph-sdk",
3
- "version": "0.1.0",
4
- "description": "Tool logging wrapper for Vercel AI SDK",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
3
+ "version": "0.2.2",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
8
  "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
13
- }
9
+ ".": "./src/index.ts"
14
10
  },
15
11
  "files": [
16
- "dist"
12
+ "dist",
13
+ "src",
14
+ "README.md"
17
15
  ],
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "main": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ }
27
+ },
18
28
  "scripts": {
19
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
- "lint": "tsc --noEmit",
22
- "test": "vitest run",
23
- "test:watch": "vitest"
29
+ "build": "vite build",
30
+ "dev": "vite build --watch",
31
+ "prepublishOnly": "bun run build"
24
32
  },
25
- "keywords": [
26
- "ai",
27
- "llm",
28
- "vercel",
29
- "ai-sdk",
30
- "polymorph",
31
- "tool-logging"
32
- ],
33
- "author": "",
34
- "license": "MIT",
35
33
  "dependencies": {
36
- "ai": "^6.0.0"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^22.0.0",
40
- "tsup": "^8.0.0",
41
- "typescript": "^5.0.0",
42
- "vitest": "^2.0.0"
34
+ "@livekit/components-react": "^2.9.19",
35
+ "@mantine/core": "^8.3.7",
36
+ "@mantine/hooks": "^8.3.7",
37
+ "livekit-client": "^2.17.0"
43
38
  },
44
39
  "peerDependencies": {
45
- "ai": ">=4.0.0"
40
+ "react": "^18.0.0 || ^19.0.0",
41
+ "react-dom": "^18.0.0 || ^19.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "^19.2.2",
45
+ "@types/react-dom": "^19.2.2",
46
+ "@vitejs/plugin-react": "^5.1.0",
47
+ "typescript": "~5.9.3",
48
+ "vite": "npm:rolldown-vite@7.2.2",
49
+ "vite-plugin-dts": "^4.5.4"
46
50
  }
47
51
  }
@@ -0,0 +1,70 @@
1
+ import { useEffect, useRef } from "react";
2
+ import styles from "./styles.module.css";
3
+ import type { ChatMessage, SessionStatus } from "./types";
4
+
5
+ export function ChatThread({
6
+ messages,
7
+ primaryColor,
8
+ status,
9
+ }: {
10
+ messages: ChatMessage[];
11
+ primaryColor: string;
12
+ status: SessionStatus;
13
+ }) {
14
+ const scrollRef = useRef<HTMLDivElement>(null);
15
+ const agentCountAtConnect = useRef<number | null>(null);
16
+ const agentMessageCount = messages.filter((m) => m.role === "agent").length;
17
+
18
+ // Snapshot agent message count when connecting starts
19
+ if (status === "connecting" && agentCountAtConnect.current === null) {
20
+ agentCountAtConnect.current = agentMessageCount;
21
+ }
22
+ if (status === "idle" || status === "error") {
23
+ agentCountAtConnect.current = null;
24
+ }
25
+
26
+ const initialConnectThinking =
27
+ (status === "connecting" || status === "connected") &&
28
+ agentCountAtConnect.current !== null &&
29
+ agentMessageCount <= agentCountAtConnect.current;
30
+ const waitingForAgentReply =
31
+ status === "connected" &&
32
+ messages.length > 0 &&
33
+ messages[messages.length - 1].role === "user";
34
+ const showThinking = initialConnectThinking || waitingForAgentReply;
35
+
36
+ useEffect(() => {
37
+ scrollRef.current?.scrollTo({
38
+ top: scrollRef.current.scrollHeight,
39
+ behavior: "smooth",
40
+ });
41
+ }, []);
42
+
43
+ return (
44
+ <div ref={scrollRef} className={styles.chatThread}>
45
+ {messages.map((msg) => (
46
+ <div
47
+ key={msg.id}
48
+ className={`${styles.messageBubble} ${msg.role === "user" ? styles.userMessage : styles.agentMessage}`}
49
+ style={
50
+ msg.role === "user" ? { backgroundColor: primaryColor } : undefined
51
+ }
52
+ >
53
+ {msg.text}
54
+ {msg.source === "voice" && (
55
+ <div className={styles.voiceLabel}>voice</div>
56
+ )}
57
+ </div>
58
+ ))}
59
+ {showThinking && (
60
+ <div className={`${styles.messageBubble} ${styles.agentMessage}`}>
61
+ <div className={styles.thinkingDots}>
62
+ <span />
63
+ <span />
64
+ <span />
65
+ </div>
66
+ </div>
67
+ )}
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,103 @@
1
+ import { LiveKitRoom } from "@livekit/components-react";
2
+ import { MantineProvider } from "@mantine/core";
3
+ import "@mantine/core/styles.css";
4
+ import { useRef, useState } from "react";
5
+ import { RoomHandler } from "./RoomHandler";
6
+ import styles from "./styles.module.css";
7
+ import { buildWidgetTheme } from "./theme";
8
+ import type { WidgetConfig } from "./types";
9
+ import { usePolymorphSession } from "./usePolymorphSession";
10
+ import { WidgetPanel } from "./WidgetPanel";
11
+
12
+ // Chat bubble icon for FAB
13
+ function ChatIcon() {
14
+ return (
15
+ <svg
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ width="24"
18
+ height="24"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth="2"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ >
26
+ <path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
27
+ </svg>
28
+ );
29
+ }
30
+
31
+ export function PolymorphWidget(props: WidgetConfig) {
32
+ const { position = "bottom-right", branding } = props;
33
+ const [open, setOpen] = useState(false);
34
+ const session = usePolymorphSession(props);
35
+ const primaryColor = branding?.primaryColor || "#171717";
36
+ const theme = buildWidgetTheme(primaryColor);
37
+ const rootRef = useRef<HTMLDivElement>(null);
38
+
39
+ return (
40
+ <div
41
+ ref={rootRef}
42
+ className={`polymorph-widget ${styles.widgetRoot} ${position === "bottom-left" ? styles.bottomLeft : styles.bottomRight}`}
43
+ style={{ colorScheme: props.darkMode ? "dark" : "light" }}
44
+ >
45
+ <MantineProvider
46
+ theme={theme}
47
+ forceColorScheme={props.darkMode ? "dark" : "light"}
48
+ cssVariablesSelector=".polymorph-widget"
49
+ getRootElement={() => rootRef.current ?? undefined}
50
+ >
51
+ {open && (
52
+ <WidgetPanel
53
+ config={props}
54
+ session={session}
55
+ onClose={() => {
56
+ session.disconnect();
57
+ setOpen(false);
58
+ }}
59
+ />
60
+ )}
61
+ <button
62
+ type="button"
63
+ className={styles.fab}
64
+ style={{ backgroundColor: primaryColor }}
65
+ onClick={() => {
66
+ if (open) {
67
+ session.disconnect();
68
+ }
69
+ setOpen((prev) => !prev);
70
+ }}
71
+ >
72
+ <ChatIcon />
73
+ </button>
74
+ {/* LiveKit room lives outside WidgetPanel so it mounts before the panel renders */}
75
+ {session.roomConnection && (
76
+ <LiveKitRoom
77
+ token={session.roomConnection.token}
78
+ serverUrl={session.roomConnection.livekitUrl}
79
+ connect={true}
80
+ audio={
81
+ session.isVoiceEnabled
82
+ ? {
83
+ echoCancellation: true,
84
+ noiseSuppression: true,
85
+ autoGainControl: true,
86
+ }
87
+ : false
88
+ }
89
+ video={false}
90
+ style={{ display: "none" }}
91
+ onDisconnected={session.disconnect}
92
+ >
93
+ <RoomHandler
94
+ setRoom={session.setRoom}
95
+ addMessage={session.addMessage}
96
+ isVoiceEnabled={session.isVoiceEnabled}
97
+ />
98
+ </LiveKitRoom>
99
+ )}
100
+ </MantineProvider>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,99 @@
1
+ import { RoomAudioRenderer, useRoomContext } from "@livekit/components-react";
2
+ import type { Room } from "livekit-client";
3
+ import {
4
+ type DataPacket_Kind,
5
+ type Participant,
6
+ ParticipantKind,
7
+ RoomEvent,
8
+ type TranscriptionSegment,
9
+ } from "livekit-client";
10
+ import { useEffect } from "react";
11
+
12
+ export function RoomHandler({
13
+ setRoom,
14
+ addMessage,
15
+ isVoiceEnabled,
16
+ }: {
17
+ setRoom: (room: Room | null) => void;
18
+ addMessage: (
19
+ role: "user" | "agent",
20
+ text: string,
21
+ source: "chat" | "voice",
22
+ ) => void;
23
+ isVoiceEnabled: boolean;
24
+ }) {
25
+ const room = useRoomContext();
26
+
27
+ useEffect(() => {
28
+ setRoom(room);
29
+ return () => {
30
+ setRoom(null);
31
+ };
32
+ }, [room, setRoom]);
33
+
34
+ // Send initial voice state to agent once connected
35
+ useEffect(() => {
36
+ const sendVoiceState = () => {
37
+ const payload = new TextEncoder().encode(
38
+ JSON.stringify({ voice_enabled: isVoiceEnabled }),
39
+ );
40
+ room.localParticipant.publishData(payload, {
41
+ reliable: true,
42
+ topic: "voice_mode",
43
+ });
44
+ };
45
+
46
+ if (room.state === "connected") {
47
+ sendVoiceState();
48
+ } else {
49
+ const onConnected = () => sendVoiceState();
50
+ room.on(RoomEvent.Connected, onConnected);
51
+ return () => {
52
+ room.off(RoomEvent.Connected, onConnected);
53
+ };
54
+ }
55
+ }, [room, isVoiceEnabled]);
56
+
57
+ useEffect(() => {
58
+ const handleDataReceived = (
59
+ payload: Uint8Array,
60
+ _participant?: Participant,
61
+ _kind?: DataPacket_Kind,
62
+ topic?: string,
63
+ ) => {
64
+ if (topic === "chat_message") {
65
+ try {
66
+ const data = JSON.parse(new TextDecoder().decode(payload));
67
+ if (data.type === "chat_response") {
68
+ addMessage("agent", data.text, "chat");
69
+ }
70
+ } catch {
71
+ /* ignore malformed */
72
+ }
73
+ }
74
+ };
75
+
76
+ const handleTranscription = (
77
+ segments: TranscriptionSegment[],
78
+ participant?: Participant,
79
+ ) => {
80
+ const finalSegments = segments.filter((s) => s.final);
81
+ if (finalSegments.length > 0) {
82
+ const text = finalSegments.map((s) => s.text).join(" ");
83
+ const isAgent =
84
+ participant?.kind === ParticipantKind.AGENT ||
85
+ participant?.identity?.includes("agent");
86
+ addMessage(isAgent ? "agent" : "user", text, "voice");
87
+ }
88
+ };
89
+
90
+ room.on(RoomEvent.DataReceived, handleDataReceived);
91
+ room.on(RoomEvent.TranscriptionReceived, handleTranscription);
92
+ return () => {
93
+ room.off(RoomEvent.DataReceived, handleDataReceived);
94
+ room.off(RoomEvent.TranscriptionReceived, handleTranscription);
95
+ };
96
+ }, [room, addMessage]);
97
+
98
+ return <RoomAudioRenderer />;
99
+ }
@@ -0,0 +1,85 @@
1
+ import styles from "./styles.module.css";
2
+ import type { SessionStatus } from "./types";
3
+
4
+ function VolumeIcon() {
5
+ return (
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ width="14"
9
+ height="14"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ >
17
+ <path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
18
+ <path d="M16 9a5 5 0 0 1 0 6" />
19
+ <path d="M19.364 18.364a9 9 0 0 0 0-12.728" />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ function VolumeOffIcon() {
25
+ return (
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="14"
29
+ height="14"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ strokeWidth="2"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ >
37
+ <path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
38
+ <line x1="22" x2="16" y1="9" y2="15" />
39
+ <line x1="16" x2="22" y1="9" y2="15" />
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ interface VoiceOverlayProps {
45
+ isVoiceEnabled: boolean;
46
+ isMicActive: boolean;
47
+ status: SessionStatus;
48
+ onToggleVoice: () => void;
49
+ }
50
+
51
+ export function VoiceOverlay({
52
+ isVoiceEnabled,
53
+ isMicActive,
54
+ status,
55
+ onToggleVoice,
56
+ }: VoiceOverlayProps) {
57
+ if (status !== "connected") return null;
58
+
59
+ return (
60
+ <div className={styles.voiceOverlay}>
61
+ {isVoiceEnabled ? (
62
+ <>
63
+ <span className={styles.voiceBars}>
64
+ <span className={styles.voiceBar} />
65
+ <span className={styles.voiceBar} />
66
+ <span className={styles.voiceBar} />
67
+ </span>
68
+ <span className={styles.voiceLabel}>
69
+ {isMicActive ? "Voice active" : "Mic muted"}
70
+ </span>
71
+ </>
72
+ ) : (
73
+ <span className={styles.voiceLabel}>Chat only</span>
74
+ )}
75
+ <button
76
+ type="button"
77
+ className={`${styles.voiceToggle} ${isVoiceEnabled ? styles.voiceToggleActive : ""}`}
78
+ onClick={onToggleVoice}
79
+ title={isVoiceEnabled ? "Turn off voice" : "Turn on voice"}
80
+ >
81
+ {isVoiceEnabled ? <VolumeIcon /> : <VolumeOffIcon />}
82
+ </button>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,228 @@
1
+ import type { Room } from "livekit-client";
2
+ import { useCallback, useState } from "react";
3
+ import { ChatThread } from "./ChatThread";
4
+ import styles from "./styles.module.css";
5
+ import type { ChatMessage, SessionStatus, WidgetConfig } from "./types";
6
+ import { VoiceOverlay } from "./VoiceOverlay";
7
+
8
+ // Internal SVG icons (no @tabler dependency)
9
+ function MicIcon() {
10
+ return (
11
+ <svg
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ width="18"
14
+ height="18"
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ strokeWidth="2"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ >
22
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
23
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
24
+ <line x1="12" x2="12" y1="19" y2="22" />
25
+ </svg>
26
+ );
27
+ }
28
+ function MicOffIcon() {
29
+ return (
30
+ <svg
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ width="18"
33
+ height="18"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="2"
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ >
41
+ <line x1="2" x2="22" y1="2" y2="22" />
42
+ <path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" />
43
+ <path d="M5 10v2a7 7 0 0 0 12 5" />
44
+ <path d="M15 9.34V5a3 3 0 0 0-5.68-1.33" />
45
+ <path d="M9 9v3a3 3 0 0 0 5.12 2.12" />
46
+ <line x1="12" x2="12" y1="19" y2="22" />
47
+ </svg>
48
+ );
49
+ }
50
+ function SendIcon() {
51
+ return (
52
+ <svg
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ width="18"
55
+ height="18"
56
+ viewBox="0 0 24 24"
57
+ fill="none"
58
+ stroke="currentColor"
59
+ strokeWidth="2"
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ >
63
+ <path d="m22 2-7 20-4-9-9-4Z" />
64
+ <path d="M22 2 11 13" />
65
+ </svg>
66
+ );
67
+ }
68
+ function CloseIcon() {
69
+ return (
70
+ <svg
71
+ xmlns="http://www.w3.org/2000/svg"
72
+ width="18"
73
+ height="18"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ >
81
+ <path d="M18 6 6 18" />
82
+ <path d="m6 6 12 12" />
83
+ </svg>
84
+ );
85
+ }
86
+
87
+ interface WidgetPanelProps {
88
+ config: WidgetConfig;
89
+ session: {
90
+ status: SessionStatus;
91
+ roomConnection: { token: string; livekitUrl: string } | null;
92
+ messages: ChatMessage[];
93
+ isVoiceEnabled: boolean;
94
+ isMicActive: boolean;
95
+ error: string | null;
96
+ connect: () => Promise<void>;
97
+ disconnect: () => void;
98
+ addMessage: (
99
+ role: "user" | "agent",
100
+ text: string,
101
+ source: "chat" | "voice",
102
+ ) => void;
103
+ sendMessage: (text: string) => void;
104
+ toggleMic: () => void;
105
+ toggleVoice: () => void;
106
+ setRoom: (room: Room | null) => void;
107
+ };
108
+ onClose: () => void;
109
+ }
110
+
111
+ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
112
+ const [inputValue, setInputValue] = useState("");
113
+ const primaryColor = config.branding?.primaryColor || "#171717";
114
+ const isChatOnlyAgent = (config.agentName || "")
115
+ .toLowerCase()
116
+ .includes("chat-agent");
117
+
118
+ const handleSend = useCallback(() => {
119
+ if (!inputValue.trim()) return;
120
+ session.sendMessage(inputValue);
121
+ setInputValue("");
122
+ }, [inputValue, session]);
123
+
124
+ const handleKeyDown = useCallback(
125
+ (e: React.KeyboardEvent) => {
126
+ if (e.key === "Enter" && !e.shiftKey) {
127
+ e.preventDefault();
128
+ handleSend();
129
+ }
130
+ },
131
+ [handleSend],
132
+ );
133
+
134
+ return (
135
+ <div className={styles.panel}>
136
+ {/* Header */}
137
+ <div className={styles.header}>
138
+ <div>
139
+ <div style={{ fontWeight: 500, fontSize: 16 }}>
140
+ {config.branding?.title || "Hi there"}
141
+ </div>
142
+ {config.branding?.subtitle && (
143
+ <div
144
+ style={{
145
+ fontSize: 13,
146
+ color: "var(--mantine-color-dimmed)",
147
+ marginTop: 2,
148
+ }}
149
+ >
150
+ {config.branding.subtitle}
151
+ </div>
152
+ )}
153
+ </div>
154
+ <button type="button" className={styles.iconButton} onClick={onClose}>
155
+ <CloseIcon />
156
+ </button>
157
+ </div>
158
+
159
+ {/* Chat Thread */}
160
+ <ChatThread
161
+ messages={session.messages}
162
+ primaryColor={primaryColor}
163
+ status={session.status}
164
+ />
165
+
166
+ {/* Voice Overlay */}
167
+ {!isChatOnlyAgent && (
168
+ <VoiceOverlay
169
+ isVoiceEnabled={session.isVoiceEnabled}
170
+ isMicActive={session.isMicActive}
171
+ status={session.status}
172
+ onToggleVoice={session.toggleVoice}
173
+ />
174
+ )}
175
+
176
+ {/* Error */}
177
+ {session.error && <div className={styles.errorText}>{session.error}</div>}
178
+
179
+ {/* Connect button or Input bar */}
180
+ {session.status === "idle" || session.status === "error" ? (
181
+ <button
182
+ type="button"
183
+ className={styles.connectButton}
184
+ style={{ backgroundColor: primaryColor }}
185
+ onClick={session.connect}
186
+ >
187
+ Start conversation
188
+ </button>
189
+ ) : session.status === "connecting" ? (
190
+ <button
191
+ type="button"
192
+ className={styles.connectButton}
193
+ style={{ backgroundColor: primaryColor }}
194
+ disabled
195
+ >
196
+ Connecting...
197
+ </button>
198
+ ) : (
199
+ <div className={styles.inputBar}>
200
+ <input
201
+ className={styles.inputField}
202
+ placeholder="Type a message..."
203
+ value={inputValue}
204
+ onChange={(e) => setInputValue(e.target.value)}
205
+ onKeyDown={handleKeyDown}
206
+ />
207
+ <button
208
+ type="button"
209
+ className={styles.iconButton}
210
+ onClick={handleSend}
211
+ disabled={!inputValue.trim()}
212
+ >
213
+ <SendIcon />
214
+ </button>
215
+ {session.isVoiceEnabled && !isChatOnlyAgent && (
216
+ <button
217
+ type="button"
218
+ className={`${styles.iconButton} ${session.isMicActive ? styles.iconButtonActive : ""}`}
219
+ onClick={session.toggleMic}
220
+ >
221
+ {session.isMicActive ? <MicIcon /> : <MicOffIcon />}
222
+ </button>
223
+ )}
224
+ </div>
225
+ )}
226
+ </div>
227
+ );
228
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ describe("polymorph-sdk exports", () => {
4
+ it("exports PolymorphWidget component", async () => {
5
+ const mod = await import("../index");
6
+ expect(mod.PolymorphWidget).toBeDefined();
7
+ });
8
+
9
+ it("exports usePolymorphSession hook", async () => {
10
+ const mod = await import("../index");
11
+ expect(mod.usePolymorphSession).toBeDefined();
12
+ });
13
+ });
package/src/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module "*.module.css" {
4
+ const classes: Record<string, string>;
5
+ export default classes;
6
+ }