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:
parent
31c00b72b3
commit
a297737ca3
15
app/client/src/assets/icons/ads/logout.svg
Normal file
15
app/client/src/assets/icons/ads/logout.svg
Normal 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 |
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
129
app/client/src/components/ads/RectangularSwitcher.tsx
Normal file
129
app/client/src/components/ads/RectangularSwitcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
67
app/client/src/pages/common/ProfileDropdown.tsx
Normal file
67
app/client/src/pages/common/ProfileDropdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export enum ThemeMode {
|
|||
DARK = "DARK",
|
||||
}
|
||||
const initialState: ThemeState = {
|
||||
mode: ThemeMode.LIGHT,
|
||||
mode: ThemeMode.DARK,
|
||||
theme: {
|
||||
...theme,
|
||||
colors: {
|
||||
|
|
|
|||
11
app/client/src/sagas/ThemeSaga.tsx
Normal file
11
app/client/src/sagas/ThemeSaga.tsx
Normal 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);
|
||||
}
|
||||
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user