feat: Custom accent color (#4897)

* types

* Working, but messy

* Add InputColor component

* types

* Show default theme values when not customized

* Support custom theme on team sign-in page

* Payload validation

* Custom theme on shared documents

* Improve theme validation

* Team -> Workspace in settings
This commit is contained in:
Tom Moor
2023-02-19 10:43:03 -05:00
committed by GitHub
parent 7c05b7326a
commit 70beb7524f
45 changed files with 684 additions and 390 deletions

View File

@@ -5,9 +5,9 @@ import Initials from "./Initials";
export interface IAvatar {
avatarUrl: string | null;
color: string;
initial: string;
id: string;
color?: string;
initial?: string;
id?: string;
}
type Props = {
@@ -61,7 +61,7 @@ const IconWrapper = styled.div`
position: absolute;
bottom: -2px;
right: -2px;
background: ${(props) => props.theme.primary};
background: ${(props) => props.theme.accent};
border: 2px solid ${(props) => props.theme.background};
border-radius: 100%;
width: 20px;

View File

@@ -5,7 +5,7 @@ const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
margin-left: 10px;
padding: 1px 5px 2px;
background-color: ${({ yellow, primary, theme }) =>
yellow ? theme.yellow : primary ? theme.primary : "transparent"};
yellow ? theme.yellow : primary ? theme.accent : "transparent"};
color: ${({ primary, yellow, theme }) =>
primary ? theme.white : yellow ? theme.almostBlack : theme.textTertiary};
border: 1px solid

View File

@@ -1,6 +1,6 @@
import { LocationDescriptor } from "history";
import { ExpandedIcon } from "outline-icons";
import { darken, lighten } from "polished";
import { darken, lighten, transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
import ActionButton, {
@@ -22,8 +22,8 @@ const RealButton = styled(ActionButton)<RealProps>`
margin: 0;
padding: 0;
border: 0;
background: ${(props) => props.theme.buttonBackground};
color: ${(props) => props.theme.buttonText};
background: ${(props) => props.theme.accent};
color: ${(props) => props.theme.accentText};
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px;
border-radius: 4px;
font-size: 14px;
@@ -51,14 +51,14 @@ const RealButton = styled(ActionButton)<RealProps>`
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${(props) => darken(0.05, props.theme.buttonBackground)};
background: ${(props) => darken(0.05, props.theme.accent)};
}
&:disabled {
cursor: default;
pointer-events: none;
color: ${(props) => props.theme.white50};
background: ${(props) => lighten(0.2, props.theme.buttonBackground)};
color: ${(props) => transparentize(0.5, props.theme.accentText)};
background: ${(props) => lighten(0.2, props.theme.accent)};
svg {
fill: ${(props) => props.theme.white50};

View File

@@ -63,7 +63,7 @@ const CircularProgressBar = ({
<Circle color={theme.progressBarBackground} offset={offset} />
{percentage > 0 && (
<Circle
color={theme.primary}
color={theme.accent}
percentage={percentage}
offset={offset}
/>

View File

@@ -147,13 +147,13 @@ export const MenuAnchorCSS = css<MenuAnchorProps>`
&:hover,
&:focus,
&.focus-visible {
color: ${props.theme.white};
background: ${props.dangerous ? props.theme.danger : props.theme.primary};
color: ${props.theme.accentText};
background: ${props.dangerous ? props.theme.danger : props.theme.accent};
box-shadow: none;
cursor: var(--pointer);
svg {
fill: ${props.theme.white};
fill: ${props.theme.accentText};
}
}
}
@@ -163,13 +163,13 @@ export const MenuAnchorCSS = css<MenuAnchorProps>`
props.$active &&
!props.disabled &&
`
color: ${props.theme.white};
background: ${props.dangerous ? props.theme.danger : props.theme.primary};
color: ${props.theme.accentText};
background: ${props.dangerous ? props.theme.danger : props.theme.accent};
box-shadow: none;
cursor: var(--pointer);
svg {
fill: ${props.theme.white};
fill: ${props.theme.accentText};
}
`}

View File

@@ -117,7 +117,7 @@ export const Node = styled.span<{
${(props) =>
props.selected &&
`
background: ${props.theme.primary};
background: ${props.theme.accent};
color: ${props.theme.white};
svg {

View File

@@ -44,7 +44,7 @@ function DocumentTasks({ document }: Props) {
<>
{completed === total ? (
<Done
color={theme.primary}
color={theme.accent}
size={20}
$animated={done && previousDone === false}
/>

View File

@@ -46,6 +46,7 @@ import Flex from "~/components/Flex";
import { LabelText } from "~/components/Input";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import DelayedMount from "./DelayedMount";
const style = {
width: 30,
@@ -263,7 +264,13 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) {
})}
</Icons>
<Colors>
<React.Suspense fallback={<Loading>{t("Loading")}</Loading>}>
<React.Suspense
fallback={
<DelayedMount>
<Text>{t("Loading")}</Text>
</DelayedMount>
}
>
<ColorPicker
color={color}
onChange={(color) => onChange(color.hex, icon)}
@@ -328,10 +335,6 @@ const IconButton = styled(NudeButton)`
height: 30px;
`;
const Loading = styled(Text)`
padding: 16px;
`;
const ColorPicker = styled(TwitterPicker)`
box-shadow: none !important;
background: transparent !important;

View File

@@ -0,0 +1,87 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
import ContextMenu from "./ContextMenu";
import DelayedMount from "./DelayedMount";
import Input, { Props as InputProps } from "./Input";
import NudeButton from "./NudeButton";
import Relative from "./Sidebar/components/Relative";
import Text from "./Text";
type Props = Omit<InputProps, "onChange"> & {
value: string | undefined;
onChange: (value: string) => void;
};
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }) => {
const { t } = useTranslation();
const menu = useMenuState({
modal: true,
placement: "bottom-end",
});
return (
<Relative>
<Input
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder="#"
maxLength={7}
{...rest}
/>
<MenuButton {...menu}>
{(props) => (
<SwatchButton
aria-label={t("Show menu")}
{...props}
$background={value}
/>
)}
</MenuButton>
<ContextMenu {...menu} aria-label={t("Select a color")}>
<React.Suspense
fallback={
<DelayedMount>
<Text>{t("Loading")}</Text>
</DelayedMount>
}
>
<StyledColorPicker
disableAlpha
color={value}
onChange={(color) => onChange(color.hex)}
/>
</React.Suspense>
</ContextMenu>
</Relative>
);
};
const SwatchButton = styled(NudeButton)<{ $background: string | undefined }>`
background: ${(props) => props.$background};
border: 1px solid ${(props) => props.theme.inputBorder};
border-radius: 50%;
position: absolute;
bottom: 20px;
right: 6px;
`;
const ColorPicker = React.lazy(
() => import("react-color/lib/components/chrome/Chrome")
);
const StyledColorPicker = styled(ColorPicker)`
background: inherit !important;
box-shadow: none !important;
border: 0 !important;
border-radius: 0 !important;
user-select: none;
input {
user-select: text;
color: ${(props) => props.theme.text} !important;
}
`;
export default InputColor;

View File

@@ -276,7 +276,7 @@ const Positioner = styled(Position)`
${StyledSelectOption} {
&[aria-selected="true"] {
color: ${(props) => props.theme.white};
background: ${(props) => props.theme.primary};
background: ${(props) => props.theme.accent};
box-shadow: none;
cursor: var(--pointer);

View File

@@ -52,7 +52,7 @@ const ListItem = (
$border={border}
$small={small}
activeStyle={{
background: theme.primary,
background: theme.accent,
}}
{...rest}
as={NavLink}

View File

@@ -28,7 +28,7 @@ const Container = styled.div`
const Loader = styled.div`
width: 100%;
height: 2px;
background-color: ${(props) => props.theme.primary};
background-color: ${(props) => props.theme.accent};
`;
export default LoadingIndicatorBar;

View File

@@ -3,12 +3,12 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import Team from "~/models/Team";
import Scrollable from "~/components/Scrollable";
import SearchPopover from "~/components/SearchPopover";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { IAvatar } from "../Avatar/Avatar";
import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
import HeaderButton from "./components/HeaderButton";
@@ -16,7 +16,7 @@ import Section from "./components/Section";
import DocumentLink from "./components/SharedDocumentLink";
type Props = {
team?: Team;
team?: IAvatar & { name: string };
rootNode: NavigationNode;
shareId: string;
};

View File

@@ -123,7 +123,7 @@ const Input = styled.input`
height: 32px;
&:focus {
outline-color: ${(props) => props.theme.primary};
outline-color: ${(props) => props.theme.accent};
}
`;

View File

@@ -25,7 +25,7 @@ const Anchor = styled.a`
left: 12px;
background: ${(props) => props.theme.background};
color: ${(props) => props.theme.text};
outline-color: ${(props) => props.theme.primary};
outline-color: ${(props) => props.theme.accent};
z-index: ${depths.popover};
width: auto;
height: auto;

View File

@@ -124,11 +124,11 @@ const HiddenInput = styled.input<{ width: number; height: number }>`
}
&:checked + ${Slider} {
background-color: ${(props) => props.theme.primary};
background-color: ${(props) => props.theme.accent};
}
&:focus + ${Slider} {
box-shadow: 0 0 1px ${(props) => props.theme.primary};
box-shadow: 0 0 1px ${(props) => props.theme.accent};
}
&:checked + ${Slider}:before {

View File

@@ -1,26 +1,19 @@
import { observer } from "mobx-react";
import * as React from "react";
import { ThemeProvider } from "styled-components";
import { breakpoints } from "@shared/styles";
import GlobalStyles from "@shared/styles/globals";
import { dark, light, lightMobile, darkMobile } from "@shared/styles/theme";
import { UserPreference } from "@shared/types";
import useMediaQuery from "~/hooks/useMediaQuery";
import { TeamPreference, UserPreference } from "@shared/types";
import useBuildTheme from "~/hooks/useBuildTheme";
import useStores from "~/hooks/useStores";
import { TooltipStyles } from "./Tooltip";
const Theme: React.FC = ({ children }) => {
const { auth, ui } = useStores();
const resolvedTheme = ui.resolvedTheme === "dark" ? dark : light;
const resolvedMobileTheme =
ui.resolvedTheme === "dark" ? darkMobile : lightMobile;
const isMobile = useMediaQuery(`(max-width: ${breakpoints.tablet}px)`);
const isPrinting = useMediaQuery("print");
const theme = isPrinting
? light
: isMobile
? resolvedMobileTheme
: resolvedTheme;
const theme = useBuildTheme(
auth.team?.getPreference(TeamPreference.CustomTheme) ||
auth.config?.customTheme ||
undefined
);
React.useEffect(() => {
window.dispatchEvent(