diff --git a/examples/with-react-ui/src/App.tsx b/examples/with-react-ui/src/App.tsx index c47fdc6f..9c03f525 100644 --- a/examples/with-react-ui/src/App.tsx +++ b/examples/with-react-ui/src/App.tsx @@ -9,7 +9,7 @@ const config: PhantomSDKConfig = { function App() { return ( - +
; + theme?: "light" | "dark" | "auto" | PhantomTheme; + customTheme?: Partial; config: PhantomSDKConfig; + appIcon?: string; // URL to app icon + appName?: string; // App name to display } // Connection UI state @@ -31,7 +35,7 @@ interface PhantomUIContextValue { const PhantomUIContext = createContext(null); // Internal UI Provider that consumes react-sdk context -function PhantomUIProvider({ children, theme = "light", customTheme }: Omit) { +function PhantomUIProvider({ children, theme = "dark", customTheme, appIcon, appName }: Omit) { const baseConnect = useBaseConnect(); const { sdk, isPhantomAvailable: _isPhantomAvailable } = usePhantom(); const isExtensionInstalled = useIsExtensionInstalled(); @@ -40,6 +44,12 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit isMobileDevice(), []); + // Get the resolved theme object + const resolvedTheme = useMemo(() => { + const baseTheme = typeof theme === 'string' ? getTheme(theme) : theme; + return mergeTheme(baseTheme, customTheme); + }, [theme, customTheme]); + // Connection state const [connectionState, setConnectionState] = useState({ isVisible: false, @@ -184,94 +194,31 @@ function PhantomUIProvider({ children, theme = "light", customTheme }: Omit {children} - {/* Connection Modal - rendered conditionally based on state */} - {connectionState.isVisible && ( -
-
e.stopPropagation()}> -
-

Connect to Phantom

- -
- -
- {connectionState.error &&
{connectionState.error.message}
} - -
- {/* Mobile device with no Phantom extension - show deeplink button */} - {isMobile && !isExtensionInstalled.isInstalled && ( - - )} - - {/* Primary auth options - Phantom, Google */} - {!isMobile && ( - <> - {/* Login with Phantom (embedded provider using Phantom extension) */} - {isPhantomLoginAvailable.isAvailable && ( - - )} - - {/* Continue with Google */} - - - )} - - {/* Extension option - smaller UI section */} - {!isMobile && isExtensionInstalled.isInstalled && ( -
-
- or -
- -
- )} -
-
-
-
- )} + ); } // Main exported Provider that wraps both react-sdk and react-ui providers -export function PhantomProvider({ children, theme = "light", customTheme, config }: PhantomUIProviderProps) { +export function PhantomProvider({ children, theme = "dark", customTheme, config, appIcon, appName }: PhantomUIProviderProps) { return ( - + {children} diff --git a/packages/react-ui/src/components/Modal.tsx b/packages/react-ui/src/components/Modal.tsx new file mode 100644 index 00000000..82ebf2d4 --- /dev/null +++ b/packages/react-ui/src/components/Modal.tsx @@ -0,0 +1,342 @@ +import React, { type CSSProperties } from "react"; +import type { PhantomTheme } from "../themes"; + +export interface ModalProps { + isVisible: boolean; + isConnecting: boolean; + error: Error | null; + providerType: "injected" | "embedded" | "deeplink" | null; + theme: PhantomTheme; + appIcon?: string; + appName?: string; + isMobile: boolean; + isExtensionInstalled: boolean; + isPhantomLoginAvailable: boolean; + onClose: () => void; + onConnectWithDeeplink: () => void; + onConnectWithAuthProvider: (provider?: "google" | "apple" | "phantom") => void; + onConnectWithInjected: () => void; +} + +export function Modal({ + isVisible, + isConnecting, + error, + providerType, + theme, + appIcon, + appName, + isMobile, + isExtensionInstalled, + isPhantomLoginAvailable, + onClose, + onConnectWithDeeplink, + onConnectWithAuthProvider, + onConnectWithInjected, +}: ModalProps) { + if (!isVisible) return null; + + // Helper function to convert hex color to rgba + const hexToRgba = (hex: string, opacity: number): string => { + // Remove # if present + const cleanHex = hex.replace('#', ''); + + // Parse hex values + const r = parseInt(cleanHex.slice(0, 2), 16); + const g = parseInt(cleanHex.slice(2, 4), 16); + const b = parseInt(cleanHex.slice(4, 6), 16); + + return `rgba(${r}, ${g}, ${b}, ${opacity})`; + }; + + // Styles + const overlayStyle: CSSProperties = { + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: theme.overlay, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 9999, + }; + + const modalStyle: CSSProperties = { + backgroundColor: theme.background, + borderRadius: '16px', + padding: '24px', + maxWidth: '400px', + width: '100%', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', + position: 'relative' as const, + }; + + const headerStyle: CSSProperties = { + position: 'relative', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginBottom: '24px', + }; + + const titleStyle: CSSProperties = { + margin: 0, + fontFamily: '"SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontSize: '14px', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '17px', + letterSpacing: '-0.14px', + color: theme.secondary, + fontFeatureSettings: '"liga" off, "clig" off', + textAlign: 'center' as const, + }; + + const closeButtonStyle: CSSProperties = { + position: 'absolute' as const, + right: 0, + top: '50%', + transform: 'translateY(-50%)', + background: 'none', + border: 'none', + color: theme.text, + fontSize: '24px', + cursor: 'pointer', + padding: '4px 8px', + lineHeight: 1, + transition: 'color 0.2s', + }; + + const appIconStyle: CSSProperties = appIcon ? { + width: '56px', + height: '56px', + borderRadius: '50%', + display: 'block', + margin: '0 auto 24px', + objectFit: 'cover' as const, + } : {}; + + const buttonStyle: CSSProperties = { + width: '100%', + padding: '12px 16px', + backgroundColor: hexToRgba(theme.secondary, 0.10), // Secondary with 10% opacity + color: theme.text, + border: 'none', + borderRadius: theme.borderRadius, + fontSize: '16px', + fontWeight: '600', + cursor: 'pointer', + transition: 'background-color 0.2s', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '8px', + }; + + const secondaryButtonStyle: CSSProperties = { + width: '100%', + padding: '12px 16px', + backgroundColor: 'transparent', + color: theme.text, + border: `1px solid ${theme.secondary}`, + borderRadius: theme.borderRadius, + fontSize: '14px', + fontWeight: '500', + cursor: 'pointer', + transition: 'background-color 0.2s', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '8px', + }; + + const buttonContainerStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column' as const, + gap: '12px', + }; + + const dividerStyle: CSSProperties = { + display: 'flex', + alignItems: 'center', + margin: '24px 0', + color: theme.secondary, + fontSize: '14px', + textTransform: 'uppercase' as const, + }; + + const dividerLineStyle: CSSProperties = { + flex: 1, + height: '1px', + backgroundColor: theme.secondary, + }; + + const dividerTextStyle: CSSProperties = { + padding: '0 12px', + }; + + const footerStyle: CSSProperties = { + marginTop: '24px', + textAlign: 'center' as const, + color: theme.secondary, + fontSize: '12px', + }; + + const errorStyle: CSSProperties = { + backgroundColor: 'rgba(220, 53, 69, 0.1)', + color: '#ff6b6b', + border: '1px solid rgba(220, 53, 69, 0.3)', + borderRadius: '8px', + padding: '12px', + marginBottom: '12px', + fontSize: '14px', + }; + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+

Login or Sign Up

+ +
+ + {/* App Icon */} + {appIcon && ( + {appName + )} + + {/* Body */} +
+ {/* Error Message */} + {error && ( +
+ {error.message} +
+ )} + + {/* Provider Options */} +
+ {/* Mobile device with no Phantom extension - show deeplink button */} + {isMobile && !isExtensionInstalled && ( + + )} + + {/* Primary auth options - Phantom, Google */} + {!isMobile && ( + <> + {/* Login with Phantom (embedded provider using Phantom extension) */} + {isPhantomLoginAvailable && ( + + )} + + {/* Continue with Google */} + + + )} + + {/* Extension option - divider and secondary button */} + {!isMobile && isExtensionInstalled && ( + <> + {/* Divider with "OR" text */} +
+
+ OR +
+
+ + {/* Injected wallet button */} + + + )} +
+
+ + {/* Footer */} +
+ Protected by Phantom +
+
+
+ ); +} diff --git a/packages/react-ui/src/index.ts b/packages/react-ui/src/index.ts index 28200cd7..431b4a97 100644 --- a/packages/react-ui/src/index.ts +++ b/packages/react-ui/src/index.ts @@ -7,6 +7,9 @@ export {PhantomProvider, type PhantomUIProviderProps} from "./PhantomProvider"; // Enhanced Hooks with UI integration export * from "./hooks"; +// Theme system +export { darkTheme, lightTheme, getTheme, mergeTheme, type PhantomTheme } from "./themes"; + // Re-export hooks and types from react-sdk (useConnect is overridden by UI hooks) export { useAccounts, @@ -26,6 +29,3 @@ export { export type { NetworkId } from "@phantom/client"; export { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk"; - -// Import and auto-inject CSS styles -import "./styles.css"; diff --git a/packages/react-ui/src/themes.ts b/packages/react-ui/src/themes.ts new file mode 100644 index 00000000..1843b328 --- /dev/null +++ b/packages/react-ui/src/themes.ts @@ -0,0 +1,73 @@ +/** + * Theme type definitions for Phantom UI + * Simple theme focused on colors and border radius + */ + +export interface PhantomTheme { + // Background color for modal + background: string; + + // Primary color for buttons and accents + primary: string; + + // Secondary color for text, borders, dividers + secondary: string; + + // Primary text color + text: string; + + // Overlay background (with opacity) + overlay: string; + + // Border radius for buttons and modal + borderRadius: string; +} + +/** + * Dark theme configuration + */ +export const darkTheme: PhantomTheme = { + background: '#181818', + primary: '#98979C', + secondary: '#98979C', + text: '#FFFFFF', + overlay: 'rgba(0, 0, 0, 0.7)', + borderRadius: '12px', +}; + +/** + * Light theme configuration + */ +export const lightTheme: PhantomTheme = { + background: '#FFFFFF', + primary: '#ab9ff2', + secondary: '#6c757d', + text: '#212529', + overlay: 'rgba(0, 0, 0, 0.5)', + borderRadius: '12px', +}; + +/** + * Get theme by name + */ +export function getTheme(themeName: 'light' | 'dark' | 'auto'): PhantomTheme { + if (themeName === 'auto') { + // Check system preference + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? darkTheme : lightTheme; + } + + return themeName === 'dark' ? darkTheme : lightTheme; +} + +/** + * Merge custom theme with base theme + */ +export function mergeTheme(baseTheme: PhantomTheme, customTheme?: Partial): PhantomTheme { + if (!customTheme) return baseTheme; + + return { + ...baseTheme, + ...customTheme, + }; +} diff --git a/packages/react-ui/src/themes/dark.css b/packages/react-ui/src/themes/dark.css deleted file mode 100644 index 41727828..00000000 --- a/packages/react-ui/src/themes/dark.css +++ /dev/null @@ -1,42 +0,0 @@ -/* Dark Theme */ -[data-theme="dark"] { - /* Colors */ - --primary: #ab9ff2; - --primary-hover: #9c8dff; - --secondary: #8e95a0; - --secondary-hover: #a1a8b3; - --success: #34d058; - --danger: #f85149; - --warning: #ffb347; - --info: #58a6ff; - - /* Background */ - --bg-primary: #0d1117; - --bg-secondary: #161b22; - --bg-modal: #21262d; - --bg-overlay: rgba(0, 0, 0, 0.8); - --bg-button-primary: var(--primary); - --bg-button-secondary: transparent; - --bg-button-tertiary: #21262d; - - /* Text */ - --text-primary: #f0f6fc; - --text-secondary: #8b949e; - --text-muted: #6e7681; - --text-inverse: #0d1117; - --text-button-primary: #ffffff; - --text-button-secondary: var(--primary); - --text-button-tertiary: var(--text-primary); - - /* Border */ - --border-color: #30363d; - --border-radius: 8px; - --border-radius-lg: 12px; - --border-button: 1px solid var(--primary); - - /* Shadows */ - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.3); - --shadow-modal: 0 20px 25px rgba(0, 0, 0, 0.5); -} diff --git a/packages/react-ui/src/themes/light.css b/packages/react-ui/src/themes/light.css deleted file mode 100644 index fb57da70..00000000 --- a/packages/react-ui/src/themes/light.css +++ /dev/null @@ -1,66 +0,0 @@ -/* Light Theme */ -:root, -[data-theme="light"] { - /* Colors */ - --primary: #ab9ff2; - --primary-hover: #9c8dff; - --secondary: #6c757d; - --secondary-hover: #545b62; - --success: #28a745; - --danger: #dc3545; - --warning: #ffc107; - --info: #17a2b8; - - /* Background */ - --bg-primary: #ffffff; - --bg-secondary: #f8f9fa; - --bg-modal: #ffffff; - --bg-overlay: rgba(0, 0, 0, 0.5); - --bg-button-primary: var(--primary); - --bg-button-secondary: transparent; - --bg-button-tertiary: #f8f9fa; - - /* Text */ - --text-primary: #212529; - --text-secondary: #6c757d; - --text-muted: #868e96; - --text-inverse: #ffffff; - --text-button-primary: #ffffff; - --text-button-secondary: var(--primary); - --text-button-tertiary: var(--text-primary); - - /* Border */ - --border-color: #dee2e6; - --border-radius: 8px; - --border-radius-lg: 12px; - --border-button: 1px solid var(--primary); - - /* Spacing */ - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - --spacing-xl: 32px; - - /* Typography */ - --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - --font-size-xs: 12px; - --font-size-sm: 14px; - --font-size-md: 16px; - --font-size-lg: 18px; - --font-size-xl: 24px; - --font-weight-normal: 400; - --font-weight-medium: 500; - --font-weight-bold: 600; - - /* Shadows */ - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1); - --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); - --shadow-modal: 0 20px 25px rgba(0, 0, 0, 0.15); - - /* Animation */ - --transition-fast: 0.15s ease; - --transition-normal: 0.25s ease; - --transition-slow: 0.35s ease; -} diff --git a/packages/react-ui/tsup.config.ts b/packages/react-ui/tsup.config.ts index ff266484..ab930407 100644 --- a/packages/react-ui/tsup.config.ts +++ b/packages/react-ui/tsup.config.ts @@ -9,8 +9,4 @@ export default defineConfig({ external: ["react", "react-dom", "@phantom/react-sdk", "@phantom/client"], minify: true, sourcemap: true, - // Include CSS files in the bundle - loader: { - ".css": "copy", - }, });