🏡 index : quine-global/quintodrome.git

/**
 * Nautiline Theme for Navidrome
 * Light theme inspired by the Nautiline iOS app
 */

// ============================================
// CONFIGURATION
// ============================================

const ACCENT_COLOR = '#009688' // Material teal
const UNBOUNDED_FONT_PATH = 'fonts/Unbounded-Variable.woff2'

// ============================================
// 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('${UNBOUNDED_FONT_PATH}') 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('${UNBOUNDED_FONT_PATH}') 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: {
      link: {
        borderRadius: radii.md,
        overflow: 'hidden',
      },
      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',
          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('${UNBOUNDED_FONT_PATH}') 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