diff --git a/lib/components/Notifications/Inbox.tsx b/lib/components/Notifications/Inbox.tsx index d934c25..51c21e8 100644 --- a/lib/components/Notifications/Inbox.tsx +++ b/lib/components/Notifications/Inbox.tsx @@ -6,9 +6,10 @@ import { Liquid } from 'liquidjs'; import { InAppNotification } from '@notificationapi/core/dist/interfaces'; import { Filter } from './interface'; import { NotificationAPIContext } from '../Provider/context'; -import { List, ListItem, Pagination } from '@mui/material'; +import { List, ListItem, Pagination, useTheme } from '@mui/material'; import { DefaultEmptyComponent } from './DefaultEmpty'; import VirtualList from 'rc-virtual-list'; +import { getThemeColors } from '../../utils/theme'; export type InboxProps = { pagination: unknown; @@ -30,6 +31,9 @@ export const Inbox: React.FC = (props) => { const [page, setPage] = useState(1); const context = useContext(NotificationAPIContext); + const theme = useTheme(); + const themeColors = getThemeColors(theme); + if (!context) { return null; } @@ -95,6 +99,10 @@ export const Inbox: React.FC = (props) => {
{props.pagination === 'INFINITE_SCROLL' ? ( = (props) => { ) : ( ` cursor: ${(props) => (props.$redirect ? 'pointer' : 'default')}; &:hover { - background: #eee !important; + background: ${(props) => props.$hoverBackground} !important; border-radius: 8px !important; } @@ -43,6 +45,9 @@ export type NotificationProps = { }; export const Notification = (props: NotificationProps) => { + const theme = useTheme(); + const themeColors = getThemeColors(theme); + if (props.renderer) { return props.renderer(props.notifications) as ReactElement; } @@ -89,11 +94,18 @@ export const Notification = (props: NotificationProps) => { const date = props.notifications[groupSize - 1].date; const ids = props.notifications.map((n) => n.id); + // Calculate hover background (slightly lighter/darker than paper) + const hoverBackground = + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.08)' + : 'rgba(0, 0, 0, 0.04)'; + return ( { props.markAsClicked(ids); if (redirectURL) { @@ -106,7 +118,8 @@ export const Notification = (props: NotificationProps) => { }} style={{ padding: '16px 6px 16px 0', - background: '#fff', + background: themeColors.paper, + color: themeColors.text, position: 'relative', display: 'flex', alignItems: 'center', @@ -135,14 +148,22 @@ export const Notification = (props: NotificationProps) => { variant="body2" fontWeight={archived ? 300 : 400} style={{ - whiteSpace: 'pre-line' + whiteSpace: 'pre-line', + color: themeColors.text }} >
- +
diff --git a/lib/components/Notifications/NotificationFeed.tsx b/lib/components/Notifications/NotificationFeed.tsx index e0d505b..37989c2 100644 --- a/lib/components/Notifications/NotificationFeed.tsx +++ b/lib/components/Notifications/NotificationFeed.tsx @@ -7,8 +7,10 @@ import { NotificationPreferencesPopup } from '../Preferences'; import { InAppNotification } from '@notificationapi/core/dist/interfaces'; import { Filter, Pagination } from './interface'; import Divider from '@mui/material/Divider'; +import { useTheme } from '@mui/material/styles'; import WebPushOptInMessage from '../WebPush/WebPushOptInMessage'; import Language from '@mui/icons-material/Language'; +import { getThemeColors } from '../../utils/theme'; export type NotificationFeedProps = { pagination?: keyof typeof Pagination; @@ -28,6 +30,8 @@ export type NotificationFeedProps = { export const NotificationFeed: React.FC = (props) => { const [openPreferences, setOpenPreferences] = useState(false); const context = useContext(NotificationAPIContext); + const theme = useTheme(); + const themeColors = getThemeColors(theme); // every 5 seconds useEffect(() => { @@ -74,8 +78,9 @@ export const NotificationFeed: React.FC = (props) => { padding: '0 12px', boxSizing: 'border-box', borderRadius: 8, - background: '#fff', - border: '1px solid #dcdcdc', + background: themeColors.paper, + border: `1px solid ${themeColors.border}`, + color: themeColors.text, ...props.style }} > @@ -93,7 +98,9 @@ export const NotificationFeed: React.FC = (props) => { {context.webPushOptInMessage && localStorage.getItem('hideWebPushOptInMessage') !== 'true' && (
- + } diff --git a/lib/components/Notifications/NotificationLauncher.tsx b/lib/components/Notifications/NotificationLauncher.tsx index 1bc7f8b..f5ccfdd 100644 --- a/lib/components/Notifications/NotificationLauncher.tsx +++ b/lib/components/Notifications/NotificationLauncher.tsx @@ -6,8 +6,9 @@ import { NotificationAPIContext } from '../Provider/context'; import { NotificationPreferencesPopup } from '../Preferences'; import { Position } from './interface'; import { LanguageOutlined, NotificationsOutlined } from '@mui/icons-material'; -import { Divider, IconButton, Popover } from '@mui/material'; +import { Divider, IconButton, Popover, useTheme } from '@mui/material'; import WebPushOptInMessage from '../WebPush/WebPushOptInMessage'; +import { getThemeColors } from '../../utils/theme'; type NotificationLaucherProps = NotificationPopupProps & { position?: keyof typeof Position; @@ -23,6 +24,8 @@ export const NotificationLauncher: React.FC = ( const [open, setOpen] = useState(false); const context = useContext(NotificationAPIContext); const [anchorEl, setAnchorEl] = useState(null); + const theme = useTheme(); + const themeColors = getThemeColors(theme); if (!context) { return null; @@ -33,7 +36,7 @@ export const NotificationLauncher: React.FC = ( ), @@ -45,7 +48,7 @@ export const NotificationLauncher: React.FC = ( popupWidth: props.popupWidth || 400, popupHeight: props.popupHeight || 600, buttonIconSize: props.buttonIconSize || 20, - iconColor: props.iconColor || '#000000', + iconColor: props.iconColor || themeColors.icon, pagination: props.pagination || 'INFINITE_SCROLL', pageSize: props.pageSize || 10, pagePosition: props.pagePosition || 'top', @@ -119,36 +122,52 @@ export const NotificationLauncher: React.FC = ( slotProps={{ paper: { style: { - width: config.popupWidth, - padding: '0 16px', - borderRadius: 8 + borderRadius: 8, + backgroundColor: themeColors.paper, + color: themeColors.text } } }} > - - {context.webPushOptInMessage && - localStorage.getItem('hideWebPushOptInMessage') !== 'true' && ( -
- - - } - alertContainerStyle={{ maxWidth: '345px' }} - /> -
- )} +
+ + {context.webPushOptInMessage && + localStorage.getItem('hideWebPushOptInMessage') !== 'true' && ( +
+ + + } + alertContainerStyle={{ maxWidth: '345px' }} + /> +
+ )} +
= (props) => { const [openPreferences, setOpenPreferences] = useState(false); const [anchorEl, setAnchorEl] = useState(null); const context = useContext(NotificationAPIContext); + const theme = useTheme(); + const themeColors = getThemeColors(theme); if (!context) { return null; @@ -52,7 +55,7 @@ export const NotificationPopup: React.FC = (props) => { ), @@ -64,7 +67,7 @@ export const NotificationPopup: React.FC = (props) => { popupWidth: props.popupWidth || 400, popupHeight: props.popupHeight || 600, buttonIconSize: props.buttonIconSize || 20, - iconColor: props.iconColor || '#000000', + iconColor: props.iconColor || themeColors.icon, pagination: props.pagination || 'INFINITE_SCROLL', pageSize: props.pageSize || 10, pagePosition: props.pagePosition || 'bottom', @@ -120,7 +123,9 @@ export const NotificationPopup: React.FC = (props) => { slotProps={{ paper: { style: { - borderRadius: 8 + borderRadius: 8, + backgroundColor: themeColors.paper, + color: themeColors.text } } }} @@ -132,7 +137,9 @@ export const NotificationPopup: React.FC = (props) => { width: config.popupWidth, padding: '0 16px', zIndex: props.popupZIndex, - height: config.popupHeight + height: config.popupHeight, + backgroundColor: themeColors.paper, + color: themeColors.text }} > = (props) => { {context.webPushOptInMessage && localStorage.getItem('hideWebPushOptInMessage') !== 'true' && (
- +
)} diff --git a/lib/components/Preferences/NotificationPreferencesInline.tsx b/lib/components/Preferences/NotificationPreferencesInline.tsx index 437bd43..3861a60 100644 --- a/lib/components/Preferences/NotificationPreferencesInline.tsx +++ b/lib/components/Preferences/NotificationPreferencesInline.tsx @@ -1,8 +1,9 @@ import { useContext } from 'react'; import { Preferences } from './Preferences'; import { NotificationAPIContext } from '../Provider/context'; -import { Divider } from '@mui/material'; +import { Divider, useTheme } from '@mui/material'; import WebPushOptInMessage from '../WebPush/WebPushOptInMessage'; +import { getThemeColors } from '../../utils/theme'; type NotificationPreferencesInlineProps = object; @@ -10,16 +11,28 @@ export function NotificationPreferencesInline( props: NotificationPreferencesInlineProps ) { const context = useContext(NotificationAPIContext); + const theme = useTheme(); + const themeColors = getThemeColors(theme); + if (!context) { return null; } props; return ( -
+
{' '} {context.webPushOptInMessage && (
- + - Notification Preferences - + + Notification Preferences + + {context.webPushOptInMessage && (
diff --git a/lib/components/Preferences/PreferenceInput.tsx b/lib/components/Preferences/PreferenceInput.tsx index 85736fe..9431d11 100644 --- a/lib/components/Preferences/PreferenceInput.tsx +++ b/lib/components/Preferences/PreferenceInput.tsx @@ -13,8 +13,10 @@ import { RadioGroup, Stack, Switch, - Typography + Typography, + useTheme } from '@mui/material'; +import { getThemeColors } from '../../utils/theme'; const sortChannels = (a: Channels, b: Channels) => { const order = [ @@ -72,6 +74,9 @@ export const PreferenceInput = ({ updateDelivery, subNotificationId }: Props) => { + const theme = useTheme(); + const themeColors = getThemeColors(theme); + return ( <> {(notification.channels as Channels[]) @@ -102,7 +107,7 @@ export const PreferenceInput = ({ let selector; if (deliveries.length === 1) { selector = ( - + {getDeliveryLabel(preference.delivery)} ); @@ -167,7 +172,11 @@ export const PreferenceInput = ({ >
- + Choose frequency:
@@ -223,11 +232,15 @@ export const PreferenceInput = ({ }} > {icon} - {name} + + {name} +
{selector}
- {i !== notification.channels.length - 1 && } + {i !== notification.channels.length - 1 && ( + + )}
); })} diff --git a/lib/components/Preferences/Preferences.tsx b/lib/components/Preferences/Preferences.tsx index c250627..cbdb7d4 100644 --- a/lib/components/Preferences/Preferences.tsx +++ b/lib/components/Preferences/Preferences.tsx @@ -6,12 +6,16 @@ import { AccordionDetails, AccordionSummary, Box, - Typography + Typography, + useTheme } from '@mui/material'; import { ExpandMore } from '@mui/icons-material'; +import { getThemeColors } from '../../utils/theme'; export function Preferences() { const context = useContext(NotificationAPIContext); + const theme = useTheme(); + const themeColors = getThemeColors(theme); if (!context || !context.preferences) { return null; @@ -40,22 +44,40 @@ export function Preferences() { return ( } - style={{ - backgroundColor: '#f0f0f0', + expandIcon={} + sx={{ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.05)' + : 'rgba(0, 0, 0, 0.02)', flexDirection: 'row-reverse', - gap: 16 + gap: 16, + '&:hover': { + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.08)' + : 'rgba(0, 0, 0, 0.04)' + } }} > - {n.title} + + {n.title} + - + <> } - style={{ + expandIcon={ + + } + sx={{ flexDirection: 'row-reverse', - gap: 16 + gap: 16, + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.03)' + : 'rgba(0, 0, 0, 0.01)', + '&:hover': { + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.06)' + : 'rgba(0, 0, 0, 0.02)' + } }} > - {sn.title} + + {sn.title} + - + {items}; + return ( + + {items} + + ); } diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index e20d8af..bdd525f 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -25,6 +25,11 @@ import { } from '@notificationapi/core/dist/interfaces'; import { Context, NotificationAPIContext } from './context'; import { createDebugLogger, formatApiCall } from '../../utils/debug'; +import { ThemeProvider } from '@mui/material/styles'; +import { + NotificationAPITheme, + createNotificationAPITheme +} from '../../utils/theme'; type Props = ( | { @@ -47,6 +52,7 @@ type Props = ( customServiceWorkerPath?: string; debug?: boolean; onNewNotifications?: (notifications: InAppNotification[]) => void; + theme?: NotificationAPITheme; }; // Ensure that the code runs only in the browser @@ -856,10 +862,20 @@ export const NotificationAPIProvider: React.FunctionComponent< webPushOptIn }); + // Create MUI theme from theme prop + const muiTheme = useMemo(() => { + if (props.theme) { + return createNotificationAPITheme(props.theme); + } + return createNotificationAPITheme('light'); // Default to light theme + }, [props.theme]); + return ( - - {props.children} - + + + {props.children} + + ); }; diff --git a/lib/main.ts b/lib/main.ts index fcd004d..97fb151 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -18,3 +18,11 @@ export { SlackConnect } from './components/Slack'; // Debug utilities export { createDebugLogger, type DebugLogger } from './utils/debug'; + +// Theme utilities +export type { + NotificationAPITheme, + NotificationAPIThemeMode, + NotificationAPIThemeColors +} from './utils/theme'; +export { createNotificationAPITheme, getThemeColors } from './utils/theme'; diff --git a/lib/utils/theme.ts b/lib/utils/theme.ts new file mode 100644 index 0000000..b6f4f45 --- /dev/null +++ b/lib/utils/theme.ts @@ -0,0 +1,150 @@ +import { createTheme, Theme, ThemeOptions } from '@mui/material/styles'; + +export type NotificationAPIThemeMode = 'light' | 'dark'; + +export type NotificationAPIThemeColors = { + background?: string; + paper?: string; + text?: string; + textSecondary?: string; + border?: string; + icon?: string; + divider?: string; +}; + +export type NotificationAPITheme = + | NotificationAPIThemeMode + | { + mode?: NotificationAPIThemeMode; + colors?: NotificationAPIThemeColors; + } + | Theme; + +const defaultLightColors: Required = { + background: '#ffffff', + paper: '#ffffff', + text: '#000000', + textSecondary: '#666666', + border: '#dcdcdc', + icon: '#000000', + divider: '#e0e0e0' +}; + +const defaultDarkColors: Required = { + background: '#1e1e1e', + paper: '#2d2d2d', + text: '#ffffff', + textSecondary: '#b0b0b0', + border: '#404040', + icon: '#ffffff', + divider: '#404040' +}; + +export function createNotificationAPITheme(theme: NotificationAPITheme): Theme { + // If it's already a MUI Theme, return it + if (theme && typeof theme === 'object' && 'palette' in theme) { + return theme as Theme; + } + + // Determine mode and colors + let mode: NotificationAPIThemeMode = 'light'; + let colors: NotificationAPIThemeColors = {}; + + if (theme === 'dark' || theme === 'light') { + mode = theme; + } else if (theme && typeof theme === 'object') { + mode = theme.mode || 'light'; + colors = theme.colors || {}; + } + + // Merge with defaults + const defaultColors = + mode === 'dark' ? defaultDarkColors : defaultLightColors; + const finalColors = { ...defaultColors, ...colors }; + + // Create MUI theme + const muiThemeOptions: ThemeOptions = { + palette: { + mode, + background: { + default: finalColors.background, + paper: finalColors.paper + }, + text: { + primary: finalColors.text, + secondary: finalColors.textSecondary + }, + divider: finalColors.divider + }, + components: { + MuiPaper: { + styleOverrides: { + root: { + backgroundColor: finalColors.paper, + color: finalColors.text + } + } + }, + MuiPopover: { + styleOverrides: { + paper: { + backgroundColor: finalColors.paper, + color: finalColors.text + } + } + }, + MuiDialog: { + styleOverrides: { + paper: { + backgroundColor: finalColors.paper, + color: finalColors.text + } + } + }, + MuiList: { + styleOverrides: { + root: { + backgroundColor: finalColors.paper, + color: finalColors.text + } + } + }, + MuiListItem: { + styleOverrides: { + root: { + backgroundColor: finalColors.paper, + color: finalColors.text + } + } + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: finalColors.divider + } + } + } + } + }; + + return createTheme(muiThemeOptions); +} + +// Export theme colors for easy access +export function getThemeColors( + theme: Theme +): Required { + const mode = theme.palette.mode; + const defaultColors = + mode === 'dark' ? defaultDarkColors : defaultLightColors; + + return { + background: theme.palette.background.default || defaultColors.background, + paper: theme.palette.background.paper || defaultColors.paper, + text: theme.palette.text.primary || defaultColors.text, + textSecondary: theme.palette.text.secondary || defaultColors.textSecondary, + border: defaultColors.border, + icon: defaultColors.icon, + divider: theme.palette.divider || defaultColors.divider + }; +} diff --git a/package-lock.json b/package-lock.json index 5d9e854..ed1023e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notificationapi/react", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notificationapi/react", - "version": "1.7.0", + "version": "1.8.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/package.json b/package.json index 27fa6fe..910911f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@notificationapi/react", "private": false, - "version": "1.7.0", + "version": "1.8.0", "type": "module", "overrides": { "esbuild": "^0.25.0",