diff --git a/ui/public/fonts/Unbounded-Variable.woff2 b/ui/public/fonts/Unbounded-Variable.woff2 new file mode 100644 index 00000000..96d8ff5f Binary files /dev/null and b/ui/public/fonts/Unbounded-Variable.woff2 differ diff --git a/ui/src/themes/index.js b/ui/src/themes/index.js index 5f906038..c3877f5b 100644 --- a/ui/src/themes/index.js +++ b/ui/src/themes/index.js @@ -12,6 +12,7 @@ import CatppuccinMacchiatoTheme from './catppuccinMacchiato' import NuclearTheme from './nuclear' import AmusicTheme from './amusic' import SquiddiesGlassTheme from './SquiddiesGlass' +import NautilineTheme from './nautiline' export default { // Classic default themes @@ -27,6 +28,7 @@ export default { GruvboxDarkTheme, LigeraTheme, MonokaiTheme, + NautilineTheme, NordTheme, NuclearTheme, SpotifyTheme, diff --git a/ui/src/themes/nautiline.js b/ui/src/themes/nautiline.js new file mode 100644 index 00000000..6a05bf38 --- /dev/null +++ b/ui/src/themes/nautiline.js @@ -0,0 +1,905 @@ +/** + * Nautiline Theme for Navidrome + * Light theme inspired by the Nautiline iOS app + */ + +// ============================================ +// CONFIGURATION +// ============================================ + +const ACCENT_COLOR = '#009688' // Material teal + +// ============================================ +// DESIGN TOKENS +// ============================================ + +const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null +} + +const rgb = hexToRgb(ACCENT_COLOR) +const rgba = (alpha) => + rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` : 'transparent' + +const tokens = { + colors: { + accent: { + main: ACCENT_COLOR, + faded: rgba(0.1), + hover: rgba(0.15), + }, + background: { + primary: '#FFFFFF', + secondary: '#F5F5F7', + tertiary: '#E5E5EA', + }, + text: { + primary: '#1A1A1A', + secondary: '#8E8E93', + tertiary: '#AEAEB2', + }, + ui: { + separator: 'rgba(0, 0, 0, 0.08)', + shadow: 'rgba(0, 0, 0, 0.04)', + glassBg: 'rgba(255, 255, 255, 0.72)', + }, + }, + typography: { + fontFamily: { + base: [ + '-apple-system', + 'BlinkMacSystemFont', + '"SF Pro Text"', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + ].join(','), + heading: '"Unbounded", sans-serif', + }, + fontFace: ` + @font-face { + font-family: 'Unbounded'; + font-style: normal; + font-weight: 300 800; + font-display: swap; + src: url('/fonts/Unbounded-Variable.woff2') format('woff2'); + } + `, + }, + spacing: { + xs: '0.25rem', + sm: '0.5rem', + md: '0.75rem', + lg: '1rem', + xl: '1.5rem', + }, + radii: { + sm: '0.25rem', + md: '0.5rem', + lg: '0.625rem', + xl: '0.75rem', + full: '50%', + pill: '1rem', + }, + breakpoints: { + xs: 599, + sm: 600, + md: 720, + lg: 1280, + }, + sizing: { + cover: { + sm: '14em', + lg: '18em', + }, + icon: '1.25rem', + iconMinWidth: '2.5rem', + }, + blur: '1.25rem', +} + +const { colors, typography, spacing, radii, sizing, breakpoints } = tokens + +// ============================================ +// REUSABLE STYLE FACTORIES +// ============================================ + +const headingStyle = (weight, letterSpacing) => ({ + fontFamily: typography.fontFamily.heading, + fontWeight: weight, + ...(letterSpacing && { letterSpacing }), +}) + +const coverSizing = () => ({ + [`@media (min-width: ${breakpoints.sm}px)`]: { + height: sizing.cover.sm, + width: sizing.cover.sm, + minWidth: sizing.cover.sm, + }, + [`@media (min-width: ${breakpoints.lg}px)`]: { + height: sizing.cover.lg, + width: sizing.cover.lg, + minWidth: sizing.cover.lg, + }, +}) + +const customTooltipStyle = () => ({ + display: 'inline', + position: 'absolute', + bottom: '100%', + left: '50%', + transform: 'translateX(-50%)', + marginBottom: spacing.xs, + fontSize: '0.75rem', + whiteSpace: 'nowrap', + backgroundColor: colors.text.primary, + color: colors.background.primary, + padding: `${spacing.xs} ${spacing.sm}`, + borderRadius: radii.sm, + zIndex: 9999, +}) + +const actionButtonsStyle = () => ({ + padding: `${spacing.lg} 0`, + alignItems: 'center', + '@global': { + button: { + border: '1px solid transparent', + backgroundColor: colors.background.secondary, + color: colors.text.secondary, + margin: `0 ${spacing.sm}`, + borderRadius: radii.full, + minWidth: 0, + padding: spacing.lg, + position: 'relative', + '&:hover': { + backgroundColor: `${colors.background.tertiary} !important`, + border: '1px solid transparent', + }, + }, + 'button:first-child:not(:only-child)': { + [`@media screen and (max-width: ${breakpoints.md}px)`]: { + transform: 'scale(1.5)', + margin: spacing.lg, + '&:hover': { + transform: 'scale(1.6) !important', + }, + }, + transform: 'scale(2)', + margin: spacing.xl, + minWidth: 0, + padding: '0.3125rem', + transition: 'transform .3s ease', + background: colors.accent.main, + color: '#fff', + borderRadius: radii.full, + border: 0, + '&:hover': { + transform: 'scale(2.1)', + backgroundColor: `${colors.accent.main} !important`, + border: 0, + }, + }, + 'button:only-child': { + margin: spacing.xl, + }, + 'button:first-child>span:first-child': { + padding: 0, + }, + 'button>span:first-child>span': { + display: 'none', + }, + 'button:not(:first-child):hover>span:first-child>span': + customTooltipStyle(), + 'button:not(:first-child)>span:first-child>svg': { + color: colors.text.secondary, + }, + }, +}) + +const menuIconStyle = () => ({ + color: colors.text.primary, + minWidth: sizing.iconMinWidth, + '& svg': { + fontSize: sizing.icon, + }, +}) + +const activeLinkStyle = { + color: `${colors.accent.main} !important`, + '& .MuiListItemIcon-root': { + color: `${colors.accent.main} !important`, + }, +} + +// ============================================ +// THEME DEFINITION +// ============================================ + +// Note: !important declarations are required to override react-admin and third-party component styles +const NautilineTheme = { + themeName: 'Nautiline', + palette: { + type: 'light', + primary: { + main: colors.accent.main, + contrastText: '#FFFFFF', + }, + secondary: { + main: colors.accent.main, + contrastText: '#FFFFFF', + }, + background: { + default: colors.background.primary, + paper: colors.background.primary, + }, + text: { + primary: colors.text.primary, + secondary: colors.text.secondary, + }, + action: { + active: colors.accent.main, + hover: colors.accent.faded, + selected: colors.accent.faded, + }, + }, + typography: { + fontFamily: typography.fontFamily.base, + h1: headingStyle(700, '-0.02em'), + h2: headingStyle(700, '-0.02em'), + h3: headingStyle(600, '-0.01em'), + h4: headingStyle(600), + h5: headingStyle(600), + h6: headingStyle(600), + subtitle1: { fontWeight: 500 }, + subtitle2: { fontWeight: 500 }, + body1: { fontWeight: 400 }, + body2: { fontWeight: 400 }, + button: { fontWeight: 500, textTransform: 'none' }, + }, + shape: { + borderRadius: radii.xl, + }, + overrides: { + MuiCssBaseline: { + '@global': { + '@font-face': { + fontFamily: 'Unbounded', + fontStyle: 'normal', + fontWeight: '300 800', + fontDisplay: 'swap', + src: "url('/fonts/Unbounded-Variable.woff2') format('woff2')", + }, + body: { + backgroundColor: colors.background.primary, + }, + }, + }, + MuiAppBar: { + root: { + boxShadow: 'none', + borderBottom: `1px solid ${colors.ui.separator}`, + }, + colorSecondary: { + backgroundColor: colors.background.primary, + color: colors.text.primary, + }, + }, + MuiToolbar: { + root: { + backgroundColor: colors.background.primary, + }, + }, + MuiPaper: { + root: { + backgroundColor: colors.background.primary, + }, + elevation1: { + boxShadow: `0 0.0625rem 0.1875rem ${colors.ui.shadow}`, + }, + elevation2: { + boxShadow: `0 0.125rem ${spacing.sm} ${colors.ui.shadow}`, + }, + }, + MuiCard: { + root: { + backgroundColor: colors.background.primary, + borderRadius: radii.xl, + boxShadow: `0 0.125rem ${spacing.sm} ${colors.ui.shadow}`, + }, + }, + MuiButton: { + root: { + borderRadius: radii.md, + textTransform: 'none', + fontWeight: 600, + }, + contained: { + boxShadow: 'none', + '&:hover': { boxShadow: 'none' }, + }, + containedPrimary: { + backgroundColor: colors.accent.main, + '&:hover': { + backgroundColor: colors.accent.main, + filter: 'brightness(0.9)', + }, + }, + text: { + color: colors.accent.main, + }, + }, + MuiIconButton: { + root: { + color: colors.text.primary, + '&:hover': { + backgroundColor: colors.accent.faded, + }, + }, + colorPrimary: { + color: colors.accent.main, + }, + sizeSmall: { + padding: spacing.md, + }, + }, + MuiSvgIcon: { + colorPrimary: { + color: colors.accent.main, + }, + }, + MuiCheckbox: { + root: { + color: 'rgba(0, 0, 0, 0.15)', + '&$checked': { + color: colors.accent.main, + }, + }, + }, + MuiChip: { + root: { + backgroundColor: colors.background.secondary, + color: colors.text.primary, + borderRadius: radii.pill, + }, + colorPrimary: { + backgroundColor: colors.accent.faded, + color: colors.accent.main, + }, + }, + MuiTableRow: { + root: { + '&:hover': { + backgroundColor: `${colors.accent.faded} !important`, + }, + }, + }, + MuiTableCell: { + root: { + borderBottomColor: 'rgba(0, 0, 0, 0.04)', + }, + head: { + backgroundColor: colors.background.secondary, + color: colors.text.secondary, + fontWeight: 600, + fontSize: '0.75rem', + textTransform: 'uppercase', + letterSpacing: '0.05em', + }, + body: { + color: colors.text.primary, + }, + }, + MuiListItem: { + root: { + color: colors.text.primary, + '&:hover': { + backgroundColor: colors.accent.faded, + }, + '&$selected': { + backgroundColor: colors.accent.faded, + color: colors.accent.main, + '& .MuiListItemIcon-root': { + color: colors.accent.main, + }, + '&:hover': { + backgroundColor: colors.accent.faded, + }, + }, + }, + button: { + color: colors.text.primary, + '&:hover': { + backgroundColor: colors.accent.faded, + color: colors.text.primary, + }, + }, + }, + MuiListItemIcon: { + root: menuIconStyle(), + }, + MuiListItemText: { + primary: { + color: 'inherit', + }, + }, + MuiMenuItem: { + root: { + fontSize: '0.875rem', + paddingTop: '4px', + paddingBottom: '4px', + paddingLeft: '10px', + margin: '5px', + borderRadius: radii.md, + color: colors.text.primary, + }, + }, + MuiDrawer: { + paper: { + backgroundColor: colors.background.primary, + borderRight: `1px solid ${colors.ui.separator}`, + }, + }, + MuiSlider: { + root: { + color: colors.accent.main, + }, + track: { + backgroundColor: colors.accent.main, + }, + thumb: { + backgroundColor: colors.accent.main, + '&:hover': { + boxShadow: `0 0 0 ${spacing.sm} ${colors.accent.faded}`, + }, + }, + rail: { + backgroundColor: colors.background.tertiary, + }, + }, + MuiLinearProgress: { + root: { + backgroundColor: colors.background.tertiary, + borderRadius: radii.sm, + }, + bar: { + backgroundColor: colors.accent.main, + borderRadius: radii.sm, + }, + }, + MuiTabs: { + root: { + borderBottom: `1px solid ${colors.ui.separator}`, + }, + indicator: { + backgroundColor: colors.accent.main, + height: '0.1875rem', + borderRadius: '0.1875rem 0.1875rem 0 0', + }, + }, + MuiTab: { + root: { + textTransform: 'none', + fontWeight: 500, + fontFamily: typography.fontFamily.heading, + '&$selected': { + color: colors.accent.main, + fontWeight: 600, + }, + }, + }, + MuiInputBase: { + root: { + backgroundColor: colors.background.secondary, + borderRadius: radii.lg, + }, + }, + MuiOutlinedInput: { + root: { + borderRadius: radii.lg, + '& $notchedOutline': { + borderColor: colors.ui.separator, + }, + '&:hover $notchedOutline': { + borderColor: colors.text.tertiary, + }, + '&$focused $notchedOutline': { + borderColor: colors.accent.main, + borderWidth: '0.125rem', + }, + }, + }, + MuiFilledInput: { + root: { + backgroundColor: colors.background.secondary, + borderRadius: radii.lg, + '&:hover': { + backgroundColor: colors.background.tertiary, + }, + '&$focused': { + backgroundColor: colors.background.secondary, + }, + }, + }, + MuiFab: { + primary: { + backgroundColor: colors.accent.main, + '&:hover': { + backgroundColor: colors.accent.main, + filter: 'brightness(0.9)', + }, + }, + }, + MuiAvatar: { + root: { + borderRadius: radii.md, + }, + }, + MuiRating: { + iconFilled: { + color: colors.accent.main, + }, + iconHover: { + color: colors.accent.main, + }, + }, + MuiTooltip: { + tooltip: { + backgroundColor: colors.text.primary, + color: colors.background.primary, + fontSize: '0.75rem', + padding: `${spacing.xs} ${spacing.sm}`, + borderRadius: radii.sm, + }, + }, + MuiBottomNavigation: { + root: { + backgroundColor: colors.ui.glassBg, + backdropFilter: `blur(${tokens.blur})`, + borderTop: `1px solid ${colors.ui.separator}`, + }, + }, + MuiBottomNavigationAction: { + root: { + color: colors.text.secondary, + '&$selected': { + color: colors.accent.main, + }, + }, + label: { + fontFamily: typography.fontFamily.heading, + fontSize: '0.65rem', + '&$selected': { + fontSize: '0.65rem', + }, + }, + }, + NDAppBar: { + root: { + color: colors.text.primary, + }, + }, + NDLogin: { + main: { + backgroundColor: colors.background.primary, + }, + card: { + backgroundColor: colors.background.primary, + borderRadius: radii.pill, + boxShadow: `0 ${spacing.xs} ${spacing.xl} ${colors.ui.shadow}`, + }, + }, + NDAlbumGridView: { + albumContainer: { + borderRadius: radii.md, + '& img': { + borderRadius: radii.md, + }, + }, + albumTitle: { + fontWeight: 600, + color: colors.text.primary, + }, + albumSubtitle: { + color: colors.text.secondary, + }, + albumPlayButton: { + backgroundColor: colors.accent.main, + borderRadius: radii.full, + boxShadow: `0 ${spacing.sm} ${spacing.sm} rgba(0, 0, 0, 0.15)`, + padding: '0.35rem', + transition: 'padding .3s ease', + '&:hover': { + backgroundColor: `${colors.accent.main} !important`, + padding: '0.45rem', + }, + }, + }, + NDAlbumDetails: { + root: { + [`@media (max-width: ${breakpoints.xs}px)`]: { + padding: '0.7em', + width: '100%', + minWidth: 'unset', + }, + }, + cardContents: { + [`@media (max-width: ${breakpoints.xs}px)`]: { + flexDirection: 'column', + alignItems: 'center', + }, + }, + details: { + [`@media (max-width: ${breakpoints.xs}px)`]: { + width: '100%', + }, + }, + cover: { + borderRadius: radii.md, + }, + coverParent: { + marginRight: spacing.xl, + [`@media (max-width: ${breakpoints.xs}px)`]: { + width: '100%', + height: 'auto', + minWidth: 'unset', + aspectRatio: '1', + marginRight: 0, + marginBottom: spacing.lg, + }, + ...coverSizing(), + }, + recordName: { + fontSize: '1.75rem', + fontWeight: 700, + marginBottom: '0.15rem', + }, + recordArtist: { + marginBottom: spacing.md, + }, + recordMeta: { + marginBottom: spacing.sm, + }, + genreList: { + marginTop: spacing.md, + }, + loveButton: { + marginLeft: spacing.sm, + }, + }, + NDAlbumShow: { + albumActions: actionButtonsStyle(), + }, + NDPlaylistShow: { + playlistActions: actionButtonsStyle(), + }, + NDSubMenu: { + icon: menuIconStyle(), + menuHeader: { + color: colors.text.primary, + '& .MuiTypography-root': { + color: colors.text.primary, + }, + }, + actionIcon: { + marginLeft: spacing.sm, + }, + }, + RaMenuItemLink: { + root: { + color: `${colors.text.primary} !important`, + '& .MuiListItemIcon-root': menuIconStyle(), + '&[class*="makeStyles-active"]': activeLinkStyle, + }, + active: activeLinkStyle, + }, + NDDesktopArtistDetails: { + root: { + [`@media (min-width: ${breakpoints.sm}px)`]: { + padding: '1em', + }, + [`@media (min-width: ${breakpoints.lg}px)`]: { + padding: '1em', + }, + }, + cover: { + borderRadius: radii.md, + ...coverSizing(), + }, + artistImage: { + borderRadius: radii.md, + marginRight: spacing.xl, + [`@media (min-width: ${breakpoints.sm}px)`]: { + height: sizing.cover.sm, + width: sizing.cover.sm, + minWidth: sizing.cover.sm, + maxHeight: sizing.cover.sm, + minHeight: sizing.cover.sm, + }, + [`@media (min-width: ${breakpoints.lg}px)`]: { + height: sizing.cover.lg, + width: sizing.cover.lg, + minWidth: sizing.cover.lg, + maxHeight: sizing.cover.lg, + minHeight: sizing.cover.lg, + }, + }, + artistName: { + fontSize: '1.75rem', + fontWeight: 700, + marginBottom: spacing.sm, + }, + }, + NDMobileArtistDetails: { + cover: { + borderRadius: radii.md, + }, + artistImage: { + borderRadius: radii.md, + }, + }, + RaList: { + content: { + overflow: 'visible', + }, + }, + RaBulkActionsToolbar: { + topToolbar: { + backgroundColor: 'transparent', + boxShadow: 'none', + padding: spacing.sm, + '@global': { + button: { + border: '1px solid transparent', + backgroundColor: colors.background.secondary, + color: colors.text.secondary, + margin: `0 ${spacing.xs}`, + borderRadius: radii.full, + minWidth: 0, + padding: spacing.sm, + position: 'relative', + '&:hover': { + backgroundColor: `${colors.background.tertiary} !important`, + border: '1px solid transparent', + }, + }, + 'button>span:first-child>span': { + display: 'none', + }, + 'button:hover>span:first-child>span': customTooltipStyle(), + 'button>span:first-child>svg': { + color: colors.text.secondary, + }, + }, + }, + }, + RaPaginationActions: { + currentPageButton: { + backgroundColor: colors.accent.faded, + }, + }, + }, + player: { + theme: 'light', + stylesheet: ` + @font-face { + font-family: 'Unbounded'; + font-style: normal; + font-weight: 300 800; + font-display: swap; + src: url('/fonts/Unbounded-Variable.woff2') format('woff2'); + } + + .react-jinke-music-player-main { + background-color: ${colors.background.primary} !important; + font-family: ${typography.fontFamily.base} !important; + } + + .react-jinke-music-player-main .music-player-panel { + background-color: ${colors.ui.glassBg} !important; + backdrop-filter: blur(${tokens.blur}) !important; + -webkit-backdrop-filter: blur(${tokens.blur}) !important; + border-top: 1px solid ${colors.ui.separator} !important; + box-shadow: 0 -0.125rem 1.25rem rgba(0, 0, 0, 0.06) !important; + } + + .react-jinke-music-player-main svg { + color: ${colors.text.primary} !important; + } + + .react-jinke-music-player-main svg:hover { + color: ${colors.accent.main} !important; + } + + .react-jinke-music-player-main .rc-slider-track, + .react-jinke-music-player-main .rc-slider-handle { + background-color: ${colors.accent.main} !important; + } + + .react-jinke-music-player-main .rc-slider-handle { + border-color: ${colors.accent.main} !important; + } + + .react-jinke-music-player-main .rc-slider-rail { + background-color: ${colors.background.secondary} !important; + } + + .react-jinke-music-player-main .rc-slider { + height: 4px !important; + } + + .react-jinke-music-player-main .rc-slider-rail, + .react-jinke-music-player-main .rc-slider-track { + height: 4px !important; + border-radius: 2px !important; + } + + .react-jinke-music-player-main .rc-slider-handle { + width: 12px !important; + height: 12px !important; + margin-top: -4px !important; + } + + .react-jinke-music-player-main .audio-lists-panel, + .react-jinke-music-player-main .audio-lists-panel-content { + background-color: ${colors.background.primary} !important; + } + + .react-jinke-music-player-main .audio-lists-panel-content .audio-item { + background-color: transparent !important; + color: ${colors.text.primary} !important; + } + + .react-jinke-music-player-main .audio-lists-panel-content .audio-item:hover { + background-color: ${colors.accent.faded} !important; + } + + .react-jinke-music-player-main .audio-lists-panel-content .audio-item.playing { + background-color: ${colors.accent.faded} !important; + color: ${colors.accent.main} !important; + } + + .react-jinke-music-player-main .lyric-btn-active, + .react-jinke-music-player-main .play-mode-title { + color: ${colors.accent.main} !important; + } + + .react-jinke-music-player-main .music-player-panel .player-content .music-player-controller .music-player-info .music-player-title { + color: ${colors.text.primary} !important; + font-weight: 600 !important; + font-family: ${typography.fontFamily.heading} !important; + } + + .react-jinke-music-player-main .music-player-panel .player-content .music-player-controller .music-player-info .music-player-artist { + color: ${colors.text.secondary} !important; + } + + .react-jinke-music-player-main.mini-player { + background-color: ${colors.ui.glassBg} !important; + backdrop-filter: blur(${tokens.blur}) !important; + -webkit-backdrop-filter: blur(${tokens.blur}) !important; + border-radius: ${radii.xl} !important; + box-shadow: 0 ${spacing.xs} 1.25rem rgba(0, 0, 0, 0.08) !important; + } + + + .MuiTypography-h1, + .MuiTypography-h2, + .MuiTypography-h3, + .MuiTypography-h4, + .MuiTypography-h5, + .MuiTypography-h6 { + font-family: ${typography.fontFamily.heading} !important; + } + `, + }, +} + +export default NautilineTheme