Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dark mode and custom theme colors #26

Merged
merged 18 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
// Copyright (C) 2022 Nethesis S.r.l.
// SPDX-License-Identifier: AGPL-3.0-or-later

import '../styles/globals.css';
import '../styles/globals.css'

export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
}

export const decorators = [
(Story, context) => (
<div className={context.globals.theme}>
<div className={'bg-gray-100 dark:bg-gray-800 p-12 h-screen'}>
<Story className={context.globals.theme} />
</div>
</div>
),
]

export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'mirror',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
],
showName: true,
// Change title based on selected value
dynamicTitle: false,
},
},
}
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,44 @@ podman run --rm --name nethvoice-cti -p 3001:3000/tcp ghcr.io/nethserver/nethvoi
_If port 3001 is already in use, replace it with a free one._

The project will be available on localhost:3001

## Dark mode and custom color palette

CTI color palette is defined in `tailwind.config.js` and consists of five shades of a primary color (currently green). These five shades are named:
- `primary`
- `primaryLighter`
- `primaryLight`
- `primaryDark`
- `primaryDarker`

Most of UI components are derived from [Tailwind UI](https://tailwindui.com/). Tailwind code needs to be adapted to use CTI color palette and to support dark mode, so:
- every Tailwind class that is color related (e.g. `bg-sky-100`, `text-indigo-600`) needs to be changed in order to use CTI color palette
- for every Tailwind class that is color related, a `dark:` class must be added to specify the color used in dark mode

See next paragraphs for details.

### Change Tailwind color related classes

Tailwind CSS represents color shades with numbers from 50 (lightest) to 900 (darkest). Here are some examples to adapt color related classes included in Tailwind components for CTI color palette:
- `bg-sky-600` becomes `bg-primary`
- `bg-sky-700` becomes `bg-primaryDark`
- `text-sky-900` becomes `text-primaryDarker`
- `border-sky-100` becomes `border-primaryLighter`
- `ring-sky-500` becomes `ring-primaryLight`

### Add Tailwind classes to support dark mode

- Never use `white` and `black` colors in dark mode. Use `gray-100` (very light gray) and `gray-900` (very dark gray) instead
- The sum of color shades in light and dark mode should be about 900 (e.g. 100+800, 300+600, 400+500). In dark mode light colors become dark, and dark colors become light
- Sometimes, little manual adjustments are needed (e.g. to make an icon more legible in dark mode)
- Some examples:
- `bg-white`: add class `dark:bg-gray-900` (don't use `black`)
- `bg-gray-100`: add class `dark:bg-gray-800`
- `bg-gray-400`: add class `dark:bg-gray-500`
- `text-gray-600`: add class `dark:text-gray-300`
- `text-gray-900`: add class `dark:text-gray-100` (don't use `white`)
- `bg-primary`: add class `dark:bg-primary` (primary color is unchanged in dark mode)
- `ring-primaryLight`: add class `dark:ring-primaryDark`
- `text-primaryDarker`: add class `dark:text-primaryLighter`

More examples are included [in this PR](https://github.com/nethesis/nethvoice-cti/pull/26)
6 changes: 3 additions & 3 deletions components/common/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export const EmptyState: FC<EmptyStateProps> = ({
return (
<>
<div className={classNames('text-center', 'p-8', className)}>
<div className='text-gray-400'>{icon}</div>
<h3 className='mt-2 text-sm font-medium text-gray-900'>{title}</h3>
{description && <p className='mt-1 text-sm text-gray-500'>{description}</p>}
<div className='text-gray-400 dark:text-gray-400'>{icon}</div>
<h3 className='mt-2 text-sm font-medium text-gray-900 dark:text-gray-100'>{title}</h3>
{description && <p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>{description}</p>}
<div className='mt-6 flex flex-col items-center'>{children}</div>
</div>
</>
Expand Down
4 changes: 2 additions & 2 deletions components/common/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface SwitchProps {
export const Switch: FC<SwitchProps> = ({ changed, on, disabled, label, className }): JSX.Element => {
const [enabled, setEnabled] = useState(on || false)
const { switch: switchTheme } = useTheme().theme
const backgroundOn = disabled ? switchTheme.off.indigo : switchTheme.on.indigo
const backgroundOn = disabled ? switchTheme.off.primary : switchTheme.on.primary
const backgroundOff = disabled ? switchTheme.off.gray : switchTheme.on.gray

useEffect(() => {
Expand All @@ -37,7 +37,7 @@ export const Switch: FC<SwitchProps> = ({ changed, on, disabled, label, classNam
return (
<HeadlessSwitch.Group>
<div className={classNames('w-fit ', 'flex', 'items-center', 'flex-row-reverse', className)}>
{label && <HeadlessSwitch.Label className='ml-3 text-sm text-gray-900'>{label}</HeadlessSwitch.Label>}
{label && <HeadlessSwitch.Label className='ml-3 text-sm text-gray-900 dark:text-gray-100'>{label}</HeadlessSwitch.Label>}
<HeadlessSwitch
checked={enabled}
onChange={() => setEnabled(!enabled)}
Expand Down
2 changes: 1 addition & 1 deletion components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const Layout: FC<LayoutProps> = ({ children }) => {
{/* Primary column */}
<section
aria-labelledby='primary-heading'
className='flex h-full min-w-0 flex-1 flex-col lg:order-last'
className='flex h-full min-w-0 flex-1 flex-col lg:order-last p-8 bg-gray-100 dark:bg-gray-800'
>
{/* The page content */}
{children}
Expand Down
10 changes: 5 additions & 5 deletions components/layout/MobileNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const MobileNavBar: FC<MobileNavBarProps> = ({ closeMobileMenu, show, ite
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-gray-600 bg-opacity-75' />
<div className='fixed inset-0 bg-opacity-75 dark:bg-opacity-75 bg-gray-600 dark:bg-gray-600' />
</Transition.Child>

<div className='fixed inset-0 z-40 flex'>
Expand All @@ -54,7 +54,7 @@ export const MobileNavBar: FC<MobileNavBarProps> = ({ closeMobileMenu, show, ite
leaveFrom='translate-x-0'
leaveTo='-translate-x-full'
>
<Dialog.Panel className='relative flex w-full max-w-xs flex-1 flex-col bg-sky-600 pt-5 pb-4'>
<Dialog.Panel className='relative flex w-full max-w-xs flex-1 flex-col pt-5 pb-4 bg-primary'>
<Transition.Child
as={Fragment}
enter='ease-in-out duration-300'
Expand Down Expand Up @@ -99,8 +99,8 @@ export const MobileNavBar: FC<MobileNavBarProps> = ({ closeMobileMenu, show, ite
href={item.href}
className={classNames(
item.current
? 'bg-sky-700 text-white'
: 'text-gray-100 hover:bg-sky-700 hover:text-white',
? 'bg-primaryDark text-white'
: 'text-gray-100 hover:bg-primaryDark hover:text-white',
'group py-2 px-3 rounded-md flex items-center text-sm font-medium',
)}
aria-current={item.current ? 'page' : undefined}
Expand All @@ -110,7 +110,7 @@ export const MobileNavBar: FC<MobileNavBarProps> = ({ closeMobileMenu, show, ite
className={classNames(
item.current
? 'text-white'
: 'text-gray-300 group-hover:text-white',
: 'text-gray-100 group-hover:text-white',
'mr-3 h-6 w-6',
)}
aria-hidden='true'
Expand Down
8 changes: 4 additions & 4 deletions components/layout/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface NavBarProps {

export const NavBar: FC<NavBarProps> = ({ items }) => {
return (
<div className='hidden w-28 overflow-y-auto bg-sky-600 md:block'>
<div className='hidden w-28 overflow-y-auto md:block bg-primary'>
<div className='flex w-full flex-col items-center py-6 h-full'>
<div className='flex flex-shrink-0 items-center'>
<Image
Expand All @@ -40,16 +40,16 @@ export const NavBar: FC<NavBarProps> = ({ items }) => {
<a
className={classNames(
item.current
? 'bg-sky-700 text-white'
: 'text-gray-100 hover:bg-sky-700 hover:text-white',
? 'bg-primaryDark text-white'
: 'text-gray-100 hover:bg-primaryDark hover:text-white',
'group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium',
)}
aria-current={item.current ? 'page' : undefined}
>
<FontAwesomeIcon
icon={item.icon}
className={classNames(
item.current ? 'text-white' : 'text-gray-300 group-hover:text-white',
item.current ? 'text-white' : 'text-gray-100 group-hover:text-white',
'h-6 w-6',
)}
aria-hidden='true'
Expand Down
46 changes: 29 additions & 17 deletions components/layout/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from '../../lib/speedDial'
import { useSelector } from 'react-redux'
import { RootState } from '../../store'
import Skeleton from 'react-loading-skeleton'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faPhone,
Expand Down Expand Up @@ -113,11 +112,11 @@ export const SideBar = () => {
return (
<>
{/* Secondary column (hidden on smaller screens) */}
<aside className='hidden w-96 border-l border-gray-200 bg-white lg:block h-full '>
<div className='flex h-full flex-col bg-white'>
<aside className='hidden w-96 border-l lg:block h-full border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'>
<div className='flex h-full flex-col bg-white dark:bg-gray-900'>
<div className='py-6 px-5'>
<div className='flex items-start justify-between'>
<h2 className='text-lg font-medium text-gray-900'>Speed Dial</h2>
<h2 className='text-lg font-medium text-gray-900 dark:text-gray-100'>Speed Dial</h2>
<div className='ml-3 flex h-7 items-center gap-2'>
{isSpeedDialLoaded && !!speedDials.length && (
<Button variant='white' onClick={() => openCreateSpeedDialDrawer()}>
Expand All @@ -129,8 +128,11 @@ export const SideBar = () => {
</div>
</div>
</div>
<span className='border-b border-gray-200'></span>
<ul role='list' className='flex-1 divide-y divide-gray-200 overflow-y-auto'>
<span className='border-b border-gray-200 dark:border-gray-700'></span>
<ul
role='list'
className='flex-1 divide-y overflow-y-auto divide-gray-200 dark:divide-gray-700'
>
{/* get speed dial error */}
{getSpeedDialError && (
<InlineNotification type='error' title={getSpeedDialError} className='my-6' />
Expand All @@ -141,10 +143,12 @@ export const SideBar = () => {
Array.from(Array(4)).map((e, index) => (
<li key={index}>
<div className='flex items-center px-4 py-4 sm:px-6'>
<Skeleton circle height='100%' containerClassName='w-12 h-12 leading-none' />
{/* avatar skeleton */}
<div className='animate-pulse rounded-full h-12 w-12 bg-gray-300 dark:bg-gray-600'></div>
<div className='min-w-0 flex-1 px-4'>
<div className='flex flex-col justify-center'>
<Skeleton />
{/* line skeleton */}
<div className='animate-pulse h-3 rounded bg-gray-300 dark:bg-gray-600'></div>
</div>
</div>
</div>
Expand All @@ -169,25 +173,31 @@ export const SideBar = () => {
speedDials.map((speedDial, key) => (
<li key={key}>
<div className='group relative flex items-center py-6 px-5'>
<div className='absolute inset-0 group-hover:bg-gray-50' aria-hidden='true' />
<div
className='absolute inset-0 group-hover:bg-gray-50 dark:group-hover:bg-gray-800'
aria-hidden='true'
/>
<div className='relative flex min-w-0 flex-1 items-center justify-between'>
<div className='flex'>
<span className='text-gray-300 '>
<span className='text-gray-300 dark:text-gray-600'>
<Avatar size='base' placeholderType='company' />
</span>
<div className='ml-4 truncate'>
<p className='truncate text-sm font-medium text-gray-900'>
<p className='truncate text-sm font-medium text-gray-900 dark:text-gray-100'>
{speedDial.name}
</p>
<p className='truncate text-sm text-gray-500'>
<p className='truncate text-sm text-gray-500 dark:text-gray-400'>
{speedDial.speeddial_num}
</p>
</div>
</div>
<div className='flex gap-2'>
{/* Actions */}
<Button variant='white'>
<FontAwesomeIcon icon={faPhone} className='h-4 w-4 text-gray-600' />
<FontAwesomeIcon
icon={faPhone}
className='h-4 w-4 text-gray-600 dark:text-gray-300'
/>
<span className='sr-only'>Call speed dial</span>
</Button>
<Dropdown items={getItemsMenu(speedDial)} position='left'>
Expand All @@ -211,17 +221,19 @@ export const SideBar = () => {
afterLeave={() => setDeletingName('')}
>
<Modal.Content>
<div className='mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0'>
<div className='mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 bg-red-100 dark:bg-red-900'>
<FontAwesomeIcon
icon={faTriangleExclamation}
className='h-6 w-6 text-red-600'
className='h-6 w-6 text-red-600 dark:text-red-200'
aria-hidden='true'
/>
</div>
<div className='mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left'>
<h3 className='text-lg font-medium leading-6 text-gray-900'>Delete speed dial</h3>
<h3 className='text-lg font-medium leading-6 text-gray-900 dark:text-gray-100'>
Delete speed dial
</h3>
<div className='mt-2'>
<p className='text-sm text-gray-500'>
<p className='text-sm text-gray-500 dark:text-gray-400'>
Speed dial <strong>{deletingName || ''}</strong> will be deleted.
</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/layout/SideDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const SideDrawer: FC<SideDrawerProps> = ({ isShown, contentType, config,
leaveFrom='translate-x-0'
leaveTo='translate-x-full'
>
<Dialog.Panel className='relative flex w-80 md:w-96 lg:w-[33vw] 2xl:w-[30vw] flex-1 flex-col bg-white p-5 shadow-[0px_20px_40px_0_rgba(0,0,0,0.2)]'>
<Dialog.Panel className='relative flex w-80 md:w-96 lg:w-[33vw] 2xl:w-[30vw] flex-1 flex-col p-5 shadow-[0px_20px_40px_0_rgba(0,0,0,0.2)] bg-white dark:bg-gray-900'>
<div className='h-0 flex-1 overflow-y-auto'>
<nav className='flex h-full flex-col'>
<div className='space-y-1'>
Expand Down
23 changes: 19 additions & 4 deletions components/layout/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
faMagnifyingGlass,
faArrowRightFromBracket,
faBars,
faSun,
faMoon,
} from '@fortawesome/free-solid-svg-icons'
import { setTheme } from '../../lib/darkTheme'

interface TopBarProps {
openMobileCb: () => void
Expand All @@ -29,6 +32,7 @@ interface TopBarProps {
export const TopBar: FC<TopBarProps> = ({ openMobileCb }) => {
const router = useRouter()
const { name, mainextension, mainPresence } = useSelector((state: RootState) => state.user)
const { theme } = useSelector((state: RootState) => state.darkTheme)

const doLogout = async () => {
const res = await logout()
Expand All @@ -42,6 +46,14 @@ export const TopBar: FC<TopBarProps> = ({ openMobileCb }) => {
}
}

const toggleDarkTheme = () => {
if (theme === 'dark') {
setTheme('light')
} else {
setTheme('dark')
}
}

const dropdownItems = (
<>
<div className='cursor-default'>
Expand All @@ -53,6 +65,9 @@ export const TopBar: FC<TopBarProps> = ({ openMobileCb }) => {
</span>
</Dropdown.Header>
</div>
<Dropdown.Item icon={theme === 'dark' ? faSun : faMoon} onClick={toggleDarkTheme}>
{theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
</Dropdown.Item>
<Dropdown.Item icon={faArrowRightFromBracket} onClick={doLogout}>
Logout
</Dropdown.Item>
Expand All @@ -61,10 +76,10 @@ export const TopBar: FC<TopBarProps> = ({ openMobileCb }) => {

return (
<header className='w-full'>
<div className='relative z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white shadow-sm'>
<div className='relative z-10 flex h-16 flex-shrink-0 border-b shadow-sm border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'>
<button
type='button'
className='border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-sky-500 md:hidden'
className='border-r px-4 focus:outline-none focus:ring-2 focus:ring-inset md:hidden focus:ring-primaryLight border-gray-200 text-gray-500 dark:focus:ring-primaryDark dark:border-gray-700 dark:text-gray-400'
onClick={openMobileCb}
>
<span className='sr-only'>Open sidebar</span>
Expand All @@ -76,7 +91,7 @@ export const TopBar: FC<TopBarProps> = ({ openMobileCb }) => {
<label htmlFor='search-field' className='sr-only'>
Find and call
</label>
<div className='relative w-full text-gray-400 focus-within:text-gray-600'>
<div className='relative w-full text-gray-400 focus-within:text-gray-600 dark:text-gray-500 dark:focus-within:text-gray-300'>
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center'>
<FontAwesomeIcon
icon={faMagnifyingGlass}
Expand All @@ -87,7 +102,7 @@ export const TopBar: FC<TopBarProps> = ({ openMobileCb }) => {
<input
name='search-field'
id='search-field'
className='h-full w-full border-transparent py-2 pl-8 pr-3 text-base text-gray-900 placeholder-gray-500 focus:border-transparent focus:placeholder-gray-400 focus:outline-none focus:ring-0'
className='h-full w-full border-transparent py-2 pl-8 pr-3 text-base focus:border-transparent focus:outline-none focus:ring-0 bg-white focus:placeholder-gray-400 text-gray-900 placeholder-gray-500 dark:bg-gray-900 dark:focus:placeholder-gray-500 dark:text-gray-100 dark:placeholder-gray-400'
placeholder='Call'
type='search'
/>
Expand Down
Loading