diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 9b109fe..424dbb0 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -2,7 +2,22 @@ 'use client'; import { useEffect } from 'react'; -import { TransactionHeartbeat, useTransaction } from '@bridgewise/ui-components'; +import { + BridgeWiseProvider, + TransactionHeartbeat, + useTransaction, + BridgeStatus, +} from '@bridgewise/ui-components'; + +const customTheme = { + primaryColor: '#22c55e', + secondaryColor: '#0f172a', + backgroundColor: '#020617', + textColor: '#e5e7eb', + borderRadius: '16px', + fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + spacingUnit: '0.9rem', +}; function TransactionDemo() { const { state, updateState, startTransaction, clearState } = useTransaction(); @@ -32,34 +47,73 @@ function TransactionDemo() { }, [state, updateState]); return ( -
-
-

- BridgeWise Transaction Heartbeat Demo -

- -

- Click "Start Transaction" to simulate a bridge transfer. Then refresh the page to verify that the state persists and the heartbeat component reappears. -

- -
- - -
- - -
-
+ +
+
+

+ BridgeWise Theming Demo +

+ +

+ This page demonstrates the BridgeWise theme system. The heartbeat and status components + are styled via CSS variables injected by BridgeWiseProvider, with a custom + primary color and dark background. +

+ +
+ + +
+ +
+
+

+ Inline BridgeStatus +

+

+ An inline status card using the same theme variables as the floating heartbeat. +

+ +
+ +
+

+ Component-level Overrides +

+

+ The floating heartbeat below uses a custom className to adjust its + position while still inheriting all theme variables. +

+

+ Trigger a transaction and you'll see the heartbeat appear in the bottom-left corner. +

+
+
+ + +
+
+
); } diff --git a/libs/ui-components/README.md b/libs/ui-components/README.md index 623f73e..c29136a 100644 --- a/libs/ui-components/README.md +++ b/libs/ui-components/README.md @@ -2,6 +2,106 @@ BridgeWise UI SDK components and hooks for cross-chain UX. +## Theming Guide + +BridgeWise exposes a flexible, type-safe theming system that can be used globally or per app. + +### BridgeWiseTheme + +For simple integrations, you can use the high-level `BridgeWiseTheme` interface: + +```ts +import type { BridgeWiseTheme } from '@bridgewise/ui-components'; + +const theme: BridgeWiseTheme = { + primaryColor: '#22c55e', + secondaryColor: '#0f172a', + backgroundColor: '#020617', + textColor: '#e5e7eb', + borderRadius: '16px', + fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + spacingUnit: '0.9rem', +}; +``` + +### BridgeWiseProvider + +Wrap your dApp (or specific sections) with `BridgeWiseProvider` to inject theme tokens and CSS variables: + +```tsx +import { + BridgeWiseProvider, + TransactionHeartbeat, + BridgeStatus, +} from '@bridgewise/ui-components'; + +const customTheme = { + primaryColor: '#22c55e', + backgroundColor: '#020617', + textColor: '#e5e7eb', +}; + +export function App() { + return ( + + {/* Your app + BridgeWise components */} + + + + ); +} +``` + +Under the hood this is mapped into the full token-based `Theme` object and converted into CSS variables with the `--bw-` prefix, e.g.: + +- `--bw-colors-transaction-background` +- `--bw-colors-foreground-primary` +- `--bw-spacing-md` +- `--bw-typography-font-size-sm` + +You can also pass a full `DeepPartial` instead of `BridgeWiseTheme` for complete control. + +### Dark Mode + +The theme system supports light/dark mode with a `ThemeMode`: + +- `'light'` +- `'dark'` +- `'system'` (default) + +`BridgeWiseProvider` forwards `defaultMode`, `enableSystem`, and related props to the underlying `ThemeProvider`. Dark mode uses the built-in `darkTheme` token overrides. + +### Component-level overrides + +All public UI components support `className` and/or `style` overrides to match your design system: + +- `TransactionHeartbeat` – `className`, `style` +- `BridgeStatus` – `className`, `style` +- `BridgeHistory` – `className`, `style` +- `BridgeCompare` – `className`, `style` + +Example: + +```tsx + + +``` + +### CSS variable mapping + +If you need to integrate with Tailwind or other CSS-in-JS systems, you can rely on the generated CSS variables: + +```ts +import { generateCSSVariables, defaultTheme } from '@bridgewise/ui-components'; + +const cssVars = generateCSSVariables(defaultTheme); +// cssVars['--bw-colors-transaction-background'] => '#ffffff' +``` + +These variables are applied at the `document.documentElement` level by `ThemeProvider`, and are safe to use in custom styles as `var(--bw-...)`. + +--- + ## Transaction History The transaction history module provides a unified view across Stellar and EVM bridge executions. diff --git a/libs/ui-components/src/components/BridgeCompare/BridgeCompare.tsx b/libs/ui-components/src/components/BridgeCompare/BridgeCompare.tsx index 019d551..a99c96e 100644 --- a/libs/ui-components/src/components/BridgeCompare/BridgeCompare.tsx +++ b/libs/ui-components/src/components/BridgeCompare/BridgeCompare.tsx @@ -13,6 +13,8 @@ interface BridgeCompareProps { showBenchmarkComparison?: boolean; minLiquidityThreshold?: number; onRouteSelect?: (route: BridgeRoute) => void; + className?: string; + style?: React.CSSProperties; } // Define the types locally to avoid import issues @@ -28,7 +30,9 @@ const BridgeCompare: React.FC = ({ destinationChain, showBenchmarkComparison = true, minLiquidityThreshold = 0, - onRouteSelect + onRouteSelect, + className, + style, }: BridgeCompareProps) => { // Get benchmark data for comparison const { @@ -78,7 +82,10 @@ const BridgeCompare: React.FC = ({ }; return ( -
+

Bridge Comparison

diff --git a/libs/ui-components/src/components/BridgeHistory/BridgeHistory.tsx b/libs/ui-components/src/components/BridgeHistory/BridgeHistory.tsx index 5e76f73..c83f777 100644 --- a/libs/ui-components/src/components/BridgeHistory/BridgeHistory.tsx +++ b/libs/ui-components/src/components/BridgeHistory/BridgeHistory.tsx @@ -19,6 +19,8 @@ export interface BridgeHistoryProps { includeBackend?: boolean; historyConfig?: TransactionHistoryConfig; emptyStateMessage?: string; + className?: string; + style?: React.CSSProperties; } export const BridgeHistory: React.FC = ({ @@ -32,6 +34,8 @@ export const BridgeHistory: React.FC = ({ includeBackend = false, historyConfig, emptyStateMessage = 'No transactions found for this account.', + className, + style, }) => { const filter: TransactionHistoryFilter = { chain, @@ -51,30 +55,28 @@ export const BridgeHistory: React.FC = ({ historyConfig, ); - if (!account) { - return

Connect a wallet to view transaction history.

; - } + return ( +
+ {!account &&

Connect a wallet to view transaction history.

} - if (loading) { - return

Loading transaction history...

; - } + {account && loading &&

Loading transaction history...

} - if (transactions.length === 0) { - return

{emptyStateMessage}

; - } + {account && !loading && transactions.length === 0 &&

{emptyStateMessage}

} - return ( -
-

Bridge History

-
    - {transactions.map((transaction) => ( -
  • - {transaction.bridgeName} • {transaction.sourceChain} →{' '} - {transaction.destinationChain} • {transaction.amount} {transaction.sourceToken} •{' '} - {transaction.status} • {transaction.timestamp.toLocaleString()} -
  • - ))} -
+ {account && !loading && transactions.length > 0 && ( + <> +

Bridge History

+
    + {transactions.map((transaction) => ( +
  • + {transaction.bridgeName} • {transaction.sourceChain} →{' '} + {transaction.destinationChain} • {transaction.amount} {transaction.sourceToken} •{' '} + {transaction.status} • {transaction.timestamp.toLocaleString()} +
  • + ))} +
+ + )}
); }; diff --git a/libs/ui-components/src/components/TransactionHeartbeat/TransactionHeartbeat.tsx b/libs/ui-components/src/components/TransactionHeartbeat/TransactionHeartbeat.tsx index ca43221..50f3997 100644 --- a/libs/ui-components/src/components/TransactionHeartbeat/TransactionHeartbeat.tsx +++ b/libs/ui-components/src/components/TransactionHeartbeat/TransactionHeartbeat.tsx @@ -9,6 +9,11 @@ import React from 'react'; import { TransactionHeartbeatHeadless } from './TransactionHeartbeat.headless'; +export interface TransactionHeartbeatProps { + className?: string; + style?: React.CSSProperties; +} + /** * Transaction heartbeat notification component * Displays transaction progress with themed styling @@ -27,12 +32,15 @@ import { TransactionHeartbeatHeadless } from './TransactionHeartbeat.headless'; * } * ``` */ -export const TransactionHeartbeat: React.FC = () => { +export const TransactionHeartbeat: React.FC = ({ + className, + style, +}) => { return ( {({ state, clearState, isSuccess, isFailed, isPending }) => (
{ border: `1px solid var(--bw-colors-transaction-border)`, transition: `all var(--bw-transitions-base)`, fontFamily: 'var(--bw-typography-font-family-sans)', + ...style, }} >
diff --git a/libs/ui-components/src/components/TransactionHeartbeat/index.ts b/libs/ui-components/src/components/TransactionHeartbeat/index.ts index 240eb29..75ff95d 100644 --- a/libs/ui-components/src/components/TransactionHeartbeat/index.ts +++ b/libs/ui-components/src/components/TransactionHeartbeat/index.ts @@ -11,3 +11,4 @@ export type { TransactionHeartbeatHeadlessProps, TransactionHeartbeatRenderProps, } from './TransactionHeartbeat.headless'; +export type { TransactionHeartbeatProps } from './TransactionHeartbeat'; diff --git a/libs/ui-components/src/index.ts b/libs/ui-components/src/index.ts index 2544251..999277b 100644 --- a/libs/ui-components/src/index.ts +++ b/libs/ui-components/src/index.ts @@ -7,6 +7,7 @@ export { ThemeProvider, useTheme, + BridgeWiseProvider, ThemeScript, defaultTheme, darkTheme, @@ -27,6 +28,7 @@ export type { ThemeContextValue, DeepPartial, ThemeConfig, + BridgeWiseTheme, CSSVariables, } from './theme'; diff --git a/libs/ui-components/src/theme/BridgeWiseProvider.tsx b/libs/ui-components/src/theme/BridgeWiseProvider.tsx new file mode 100644 index 0000000..4d2ca09 --- /dev/null +++ b/libs/ui-components/src/theme/BridgeWiseProvider.tsx @@ -0,0 +1,104 @@ +'use client'; + +import React from 'react'; +import { ThemeProvider, ThemeProviderProps } from './ThemeProvider'; +import type { BridgeWiseTheme, DeepPartial, Theme } from './types'; + +function normalizeBridgeWiseTheme(theme: BridgeWiseTheme): DeepPartial { + const normalized: DeepPartial = {}; + + if (theme.backgroundColor || theme.secondaryColor) { + normalized.colors = { + ...normalized.colors, + background: { + primary: theme.backgroundColor, + secondary: theme.secondaryColor, + }, + } as any; + } + + if (theme.textColor || theme.primaryColor) { + normalized.colors = { + ...normalized.colors, + foreground: { + primary: theme.textColor, + link: theme.primaryColor, + }, + } as any; + } + + if (theme.primaryColor || theme.secondaryColor) { + normalized.colors = { + ...normalized.colors, + status: { + pending: theme.primaryColor, + success: theme.primaryColor, + }, + transaction: { + progressBar: { + pending: theme.primaryColor, + }, + }, + } as any; + } + + if (theme.borderRadius) { + normalized.radii = { + lg: theme.borderRadius, + md: theme.borderRadius, + xl: theme.borderRadius, + }; + } + + if (theme.fontFamily) { + normalized.typography = { + fontFamily: { + sans: theme.fontFamily, + }, + } as any; + } + + if (theme.spacingUnit) { + normalized.spacing = { + xs: theme.spacingUnit, + sm: theme.spacingUnit, + md: theme.spacingUnit, + lg: theme.spacingUnit, + xl: theme.spacingUnit, + '2xl': theme.spacingUnit, + }; + } + + return normalized; +} + +export interface BridgeWiseProviderProps extends Omit { + theme?: BridgeWiseTheme | DeepPartial; +} + +export const BridgeWiseProvider: React.FC = ({ + theme, + ...rest +}) => { + let normalizedTheme: DeepPartial | undefined; + + if (theme) { + const maybeTheme = theme as BridgeWiseTheme; + if ( + 'primaryColor' in maybeTheme || + 'secondaryColor' in maybeTheme || + 'backgroundColor' in maybeTheme || + 'textColor' in maybeTheme || + 'borderRadius' in maybeTheme || + 'fontFamily' in maybeTheme || + 'spacingUnit' in maybeTheme + ) { + normalizedTheme = normalizeBridgeWiseTheme(maybeTheme); + } else { + normalizedTheme = theme as DeepPartial; + } + } + + return ; +}; + diff --git a/libs/ui-components/src/theme/__tests__/theme-provider.spec.ts b/libs/ui-components/src/theme/__tests__/theme-provider.spec.ts new file mode 100644 index 0000000..5a00585 --- /dev/null +++ b/libs/ui-components/src/theme/__tests__/theme-provider.spec.ts @@ -0,0 +1,35 @@ +import { mergeTheme, generateCSSVariables } from '../utils'; +import { defaultTheme, darkTheme } from '../tokens'; +import type { DeepPartial, Theme } from '../types'; + +describe('BridgeWise theme utilities', () => { + it('merges custom theme overrides with defaults', () => { + const override: DeepPartial = { + colors: { + background: { + primary: '#000000', + }, + }, + }; + + const merged = mergeTheme(defaultTheme, override); + + expect(merged.colors.background.primary).toBe('#000000'); + expect(merged.colors.background.secondary).toBe(defaultTheme.colors.background.secondary); + }); + + it('generates CSS variables from a theme object', () => { + const cssVars = generateCSSVariables(defaultTheme); + + expect(cssVars['--bw-colors-background-primary']).toBe(defaultTheme.colors.background.primary); + expect(cssVars['--bw-typography-font-size-base']).toBe(defaultTheme.typography.fontSize.base); + }); + + it('applies dark theme overrides on top of defaults', () => { + const merged = mergeTheme(defaultTheme, darkTheme); + + expect(merged.colors.background.primary).toBe(darkTheme.colors?.background?.primary); + expect(merged.colors.foreground.primary).toBe(darkTheme.colors?.foreground?.primary); + }); +}); + diff --git a/libs/ui-components/src/theme/index.ts b/libs/ui-components/src/theme/index.ts index ac114b6..a953726 100644 --- a/libs/ui-components/src/theme/index.ts +++ b/libs/ui-components/src/theme/index.ts @@ -4,6 +4,7 @@ */ export { ThemeProvider, useTheme } from './ThemeProvider'; +export { BridgeWiseProvider } from './BridgeWiseProvider'; export { ThemeScript } from './ThemeScript'; export { defaultTheme, darkTheme, primitiveColors } from './tokens'; export { mergeTheme, generateCSSVariables } from './utils'; @@ -19,5 +20,6 @@ export type { ThemeContextValue, DeepPartial, ThemeConfig, + BridgeWiseTheme, CSSVariables, } from './types'; diff --git a/libs/ui-components/src/theme/types.ts b/libs/ui-components/src/theme/types.ts index ce39d4c..a228274 100644 --- a/libs/ui-components/src/theme/types.ts +++ b/libs/ui-components/src/theme/types.ts @@ -162,6 +162,16 @@ export interface Theme { transitions: ThemeTransitions; } +export interface BridgeWiseTheme { + primaryColor?: string; + secondaryColor?: string; + backgroundColor?: string; + textColor?: string; + borderRadius?: string; + fontFamily?: string; + spacingUnit?: string; +} + /** * Deep partial utility type * Allows partial theme customization at any depth