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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
`}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -44,7 +44,7 @@ function DocumentTasks({ document }: Props) {
|
||||
<>
|
||||
{completed === total ? (
|
||||
<Done
|
||||
color={theme.primary}
|
||||
color={theme.accent}
|
||||
size={20}
|
||||
$animated={done && previousDone === false}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
87
app/components/InputColor.tsx
Normal file
87
app/components/InputColor.tsx
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const ListItem = (
|
||||
$border={border}
|
||||
$small={small}
|
||||
activeStyle={{
|
||||
background: theme.primary,
|
||||
background: theme.accent,
|
||||
}}
|
||||
{...rest}
|
||||
as={NavLink}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ const Input = styled.input`
|
||||
height: 32px;
|
||||
|
||||
&:focus {
|
||||
outline-color: ${(props) => props.theme.primary};
|
||||
outline-color: ${(props) => props.theme.accent};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user