Feature: Theme switching between light and dark mode (#578)

* Theme switching feature integrated

* Condition removed

* menu height bugs fixed

* warnings removed
This commit is contained in:
devrk96 2020-09-22 17:26:11 +05:30 committed by GitHub
parent 31c00b72b3
commit a297737ca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 265 additions and 30 deletions

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 471.2 471.2" style="enable-background:new 0 0 471.2 471.2;" xml:space="preserve">
<g>
<g>
<path d="M227.619,444.2h-122.9c-33.4,0-60.5-27.2-60.5-60.5V87.5c0-33.4,27.2-60.5,60.5-60.5h124.9c7.5,0,13.5-6,13.5-13.5
s-6-13.5-13.5-13.5h-124.9c-48.3,0-87.5,39.3-87.5,87.5v296.2c0,48.3,39.3,87.5,87.5,87.5h122.9c7.5,0,13.5-6,13.5-13.5
S235.019,444.2,227.619,444.2z"/>
<path d="M450.019,226.1l-85.8-85.8c-5.3-5.3-13.8-5.3-19.1,0c-5.3,5.3-5.3,13.8,0,19.1l62.8,62.8h-273.9c-7.5,0-13.5,6-13.5,13.5
s6,13.5,13.5,13.5h273.9l-62.8,62.8c-5.3,5.3-5.3,13.8,0,19.1c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4l85.8-85.8
C455.319,239.9,455.319,231.3,450.019,226.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 965 B

View File

@ -154,7 +154,7 @@ export const EditableText = (props: EditableTextProps) => {
const bgColor = useMemo(
() =>
editModeBgcolor(!!isInvalid, isEditing, savingState, themeDetails.theme),
[isInvalid, isEditing, savingState],
[isInvalid, isEditing, savingState, themeDetails],
);
const editMode = useCallback(

View File

@ -17,6 +17,7 @@ import { ReactComponent as InviteUserIcon } from "assets/icons/ads/invite-users.
import { ReactComponent as ViewAllIcon } from "assets/icons/ads/view-all.svg";
import { ReactComponent as ContextMenuIcon } from "assets/icons/ads/context-menu.svg";
import { ReactComponent as DuplicateIcon } from "assets/icons/ads/duplicate.svg";
import { ReactComponent as LogoutIcon } from "assets/icons/ads/logout.svg";
import styled from "styled-components";
import { CommonComponentProps, Classes } from "./common";
import { noop } from "lodash";
@ -86,6 +87,7 @@ export const IconCollection = [
"downArrow",
"context-menu",
"duplicate",
"logout",
] as const;
export type IconName = typeof IconCollection[number];
@ -184,6 +186,9 @@ const Icon = (props: IconProps & CommonComponentProps) => {
case "duplicate":
returnIcon = <DuplicateIcon />;
break;
case "logout":
returnIcon = <LogoutIcon />;
break;
default:
returnIcon = null;
break;

View File

@ -16,7 +16,6 @@ const MenuWrapper = styled.div`
width: 234px;
background: ${props => props.theme.colors.blackShades[3]};
box-shadow: 0px 12px 28px rgba(0, 0, 0, 0.75);
padding: ${props => props.theme.spaces[5]}px 0px;
`;
const MenuOption = styled.div`

View File

@ -17,15 +17,15 @@ const ItemRow = styled.a<{ disabled?: boolean }>`
align-items: center;
justify-content: space-between;
text-decoration: none;
padding: ${props => props.theme.spaces[4]}px
${props => props.theme.spaces[6]}px;
padding: 0px ${props => props.theme.spaces[6]}px;
height: 38px;
${props =>
!props.disabled
? `
&:hover {
text-decoration: none;
cursor: pointer;
text-decoration: none;
background-color: ${props.theme.colors.blackShades[4]};
.${Classes.TEXT} {
color: ${props.theme.colors.blackShades[9]};
@ -38,7 +38,8 @@ const ItemRow = styled.a<{ disabled?: boolean }>`
}`
: `
&:hover {
cursor: not-allowed;
text-decoration: none;
cursor: default;
}
`}
`;
@ -68,7 +69,7 @@ function MenuItem(props: MenuItemProps) {
</Text>
) : null}
</IconContainer>
{props.label ? <Text type={TextType.P1}>{props.label}</Text> : null}
{props.label ? props.label : null}
</ItemRow>
);
}

View File

@ -0,0 +1,129 @@
import { CommonComponentProps, Classes } from "./common";
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import Text, { TextType } from "./Text";
type SwitchProps = CommonComponentProps & {
onSwitch: (value: boolean) => void;
value: boolean;
};
const StyledSwitch = styled.label<{
isLoading?: boolean;
value: boolean;
firstRender: boolean;
}>`
position: relative;
display: block;
width: 78px;
height: 26px;
cursor: pointer;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
border: 1px solid ${props => props.theme.colors.blackShades[5]};
background-color: ${props => props.theme.colors.info.main};
width: 78px;
height: 26px;
}
${props =>
`.slider:before {
position: absolute;
content: "";
width: 36px;
height: 20px;
top: 2px;
background-color: ${props.theme.colors.blackShades[0]};
left: ${props.value && !props.firstRender ? "38px" : "2px"};
transition: ${props.firstRender ? "0.4s" : "none"};
}
`}
input:checked + .slider:before {
transform: ${props => (props.firstRender ? "translateX(36px)" : "none")};
}
input:checked + .slider:before {
background-color: ${props => props.theme.colors.blackShades[0]};
}
input:hover + .slider {
border: 1px solid ${props => props.theme.colors.blackShades[7]};
}
`;
const Light = styled.div<{ value: boolean }>`
.${Classes.TEXT} {
color: ${props => (props.value ? "#FFFFFF" : "#939090")};
font-size: 10px;
line-height: 12px;
letter-spacing: -0.171429px;
}
position: absolute;
top: 3px;
left: 10px;
`;
const Dark = styled.div<{ value: boolean }>`
.${Classes.TEXT} {
font-size: 10px;
line-height: 12px;
letter-spacing: -0.171429px;
color: ${props => (!props.value ? "#FFFFFF" : "#939090")};
}
position: absolute;
top: 3px;
left: 46px;
`;
export default function Switch(props: SwitchProps) {
const [value, setValue] = useState(false);
const [firstRender, setFirstRender] = useState(false);
useEffect(() => {
setValue(props.value);
}, [props.value]);
const onChangeHandler = (value: boolean) => {
setValue(value);
props.onSwitch && props.onSwitch(value);
};
return (
<StyledSwitch
data-cy={props.cypressSelector}
isLoading={props.isLoading}
value={value}
className={props.className}
firstRender={firstRender}
>
<input
type="checkbox"
checked={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (!firstRender) {
setFirstRender(true);
}
onChangeHandler(e.target.checked);
}}
/>
<span className="slider"></span>
<Light value={value}>
<Text type={TextType.H6}>Light</Text>
</Light>
<Dark value={value}>
<Text type={TextType.H6}>Dark</Text>
</Dark>
</StyledSwitch>
);
}

View File

@ -113,8 +113,8 @@ const InputWrapper = styled.div`
`;
const ErrorWrapper = styled.div`
position absolute;
bottom: -17px;
position: absolute;
bottom: -17px;
`;
const TextInput = forwardRef(
(props: TextInputProps, ref: Ref<HTMLInputElement>) => {
@ -135,7 +135,7 @@ const TextInput = forwardRef(
const inputStyle = useMemo(
() => boxStyles(props, validation.isValid, theme),
[props, validation.isValid],
[props, validation.isValid, theme],
);
const memoizedChangeHandler = useCallback(

View File

@ -5,15 +5,13 @@ import { getCurrentUser } from "selectors/usersSelectors";
import styled from "styled-components";
import StyledHeader from "components/designSystems/appsmith/StyledHeader";
import AppsmithLogo from "assets/images/appsmith_logo_white.png";
import CustomizedDropdown from "./CustomizedDropdown";
import DropdownProps from "./CustomizedDropdown/HeaderDropdownData";
import { AppState } from "reducers";
import { User, ANONYMOUS_USERNAME } from "constants/userConstants";
import { AUTH_LOGIN_URL, APPLICATIONS_URL } from "constants/routes";
import Button from "components/editorComponents/Button";
import history from "utils/history";
import { Colors } from "constants/Colors";
// import ThemeSwitcher from "./ThemeSwitcher";
import ProfileDropdown from "./ProfileDropdown";
const StyledPageHeader = styled(StyledHeader)`
background: ${Colors.BALTIC_SEA};
@ -42,10 +40,6 @@ type PageHeaderProps = {
user?: User;
};
// const StyledSwitcher = styled(ThemeSwitcher)`
// flex: 1;
// `;
export const PageHeader = (props: PageHeaderProps) => {
const { user } = props;
const location = useLocation();
@ -62,7 +56,6 @@ export const PageHeader = (props: PageHeaderProps) => {
<AppsmithLogoImg src={AppsmithLogo} alt="Appsmith logo" />
</Link>
</HeaderSection>
{/* <StyledSwitcher /> */}
{user && (
<StyledDropDownContainer>
{user.username === ANONYMOUS_USERNAME ? (
@ -74,7 +67,7 @@ export const PageHeader = (props: PageHeaderProps) => {
onClick={() => history.push(loginUrl)}
/>
) : (
<CustomizedDropdown {...DropdownProps(user, user.username)} />
<ProfileDropdown userName={user.username} />
)}
</StyledDropDownContainer>
)}

View File

@ -0,0 +1,67 @@
import React from "react";
import { CommonComponentProps } from "components/ads/common";
import { getInitialsAndColorCode } from "utils/AppsmithUtils";
import { useSelector } from "react-redux";
import { getThemeDetails } from "selectors/themeSelectors";
import Text, { TextType } from "components/ads/Text";
import styled from "styled-components";
import { Position } from "@blueprintjs/core";
import Menu from "components/ads/Menu";
import ThemeSwitcher from "./ThemeSwitcher";
import MenuDivider from "components/ads/MenuDivider";
import MenuItem from "components/ads/MenuItem";
import {
getOnSelectAction,
DropdownOnSelectActions,
} from "./CustomizedDropdown/dropdownHelpers";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
type TagProps = CommonComponentProps & {
onClick?: (text: string) => void;
userName?: string;
};
const ProfileImage = styled.div<{ backgroundColor?: string }>`
width: 30px;
height: 30px;
display: flex;
align-items: center;
border-radius: 50%;
justify-content: center;
cursor: pointer;
background-color: ${props => props.backgroundColor};
`;
export default function ProfileDropdown(props: TagProps) {
const themeDetails = useSelector(getThemeDetails);
const initialsAndColorCode = getInitialsAndColorCode(
props.userName,
themeDetails.theme.colors.appCardColors,
);
return (
<Menu
position={Position.BOTTOM}
target={
<ProfileImage backgroundColor={initialsAndColorCode[1]}>
<Text type={TextType.H6} highlight>
{initialsAndColorCode[0]}
</Text>
</ProfileImage>
}
>
<ThemeSwitcher />
<MenuDivider />
<MenuItem
icon="logout"
text="Sign Out"
onSelect={() =>
getOnSelectAction(DropdownOnSelectActions.DISPATCH, {
type: ReduxActionTypes.LOGOUT_USER_INIT,
})
}
/>
</Menu>
);
}

View File

@ -1,18 +1,31 @@
import Toggle from "components/ads/Toggle";
import React from "react";
import { useDispatch } from "react-redux";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setThemeMode } from "actions/themeActions";
import { ThemeMode } from "reducers/uiReducers/themeReducer";
import Switch from "components/ads/RectangularSwitcher";
import MenuItem from "components/ads/MenuItem";
import { getThemeDetails } from "selectors/themeSelectors";
export default function ThemeSwitcher(props: { className?: string }) {
const dispatch = useDispatch();
const themeDetails = useSelector(getThemeDetails);
const [switchedOn, setSwitchOn] = useState(
themeDetails.mode === ThemeMode.DARK,
);
return (
<Toggle
className={props.className}
value={true}
onToggle={value => {
dispatch(setThemeMode(value ? ThemeMode.LIGHT : ThemeMode.DARK));
}}
></Toggle>
<MenuItem
text="Theme"
label={
<Switch
className={props.className}
value={switchedOn}
onSwitch={value => {
setSwitchOn(value);
dispatch(setThemeMode(value ? ThemeMode.DARK : ThemeMode.LIGHT));
}}
></Switch>
}
/>
);
}

View File

@ -7,7 +7,7 @@ export enum ThemeMode {
DARK = "DARK",
}
const initialState: ThemeState = {
mode: ThemeMode.LIGHT,
mode: ThemeMode.DARK,
theme: {
...theme,
colors: {

View File

@ -0,0 +1,11 @@
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { takeLatest } from "redux-saga/effects";
import { ThemeMode } from "reducers/uiReducers/themeReducer";
export function* setThemeSaga(actionPayload: ReduxAction<ThemeMode>) {
yield localStorage.setItem("THEME", actionPayload.payload);
}
export default function* themeSagas() {
yield takeLatest(ReduxActionTypes.SET_THEME, setThemeSaga);
}

View File

@ -18,6 +18,7 @@ import curlImportSagas from "./CurlImportSagas";
import queryPaneSagas from "./QueryPaneSagas";
import modalSagas from "./ModalSagas";
import batchSagas from "./BatchSagas";
import themeSagas from "./ThemeSaga";
export function* rootSaga() {
yield all([
@ -40,5 +41,6 @@ export function* rootSaga() {
spawn(queryPaneSagas),
spawn(modalSagas),
spawn(batchSagas),
spawn(themeSagas),
]);
}