Desktop support (#4484)
* Remove home link on desktop app * Spellcheck, installation toasts, background styling, … * Add email,slack, auth support * More desktop style tweaks * Move redirect to client * cleanup * Record desktop usage * docs * fix: Selection state in search input when double clicking header
This commit is contained in:
@@ -6,6 +6,7 @@ import styled from "styled-components";
|
||||
import ActionButton, {
|
||||
Props as ActionButtonProps,
|
||||
} from "~/components/ActionButton";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
|
||||
type RealProps = {
|
||||
$fullwidth?: boolean;
|
||||
@@ -33,6 +34,7 @@ const RealButton = styled(ActionButton)<RealProps>`
|
||||
cursor: var(--pointer);
|
||||
user-select: none;
|
||||
appearance: none !important;
|
||||
${undraggableOnDesktop()}
|
||||
|
||||
${(props) =>
|
||||
!props.$borderOnHover &&
|
||||
|
||||
44
app/components/DesktopEventHandler.tsx
Normal file
44
app/components/DesktopEventHandler.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useDesktopTitlebar } from "~/hooks/useDesktopTitlebar";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
|
||||
export default function DesktopEventHandler() {
|
||||
useDesktopTitlebar();
|
||||
const history = useHistory();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
React.useEffect(() => {
|
||||
Desktop.bridge?.redirect((path: string, replace = false) => {
|
||||
if (replace) {
|
||||
history.replace(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
});
|
||||
|
||||
Desktop.bridge?.updateDownloaded(() => {
|
||||
showToast("An update is ready to install.", {
|
||||
type: "info",
|
||||
timeout: Infinity,
|
||||
action: {
|
||||
text: "Install now",
|
||||
onClick: () => {
|
||||
Desktop.bridge?.restartAndInstall();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Desktop.bridge?.focus(() => {
|
||||
window.document.body.classList.remove("backgrounded");
|
||||
});
|
||||
|
||||
Desktop.bridge?.blur(() => {
|
||||
window.document.body.classList.add("backgrounded");
|
||||
});
|
||||
}, [history, showToast]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import Flex from "~/components/Flex";
|
||||
import useEventListener from "~/hooks/useEventListener";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import { supportsPassiveListener } from "~/utils/browser";
|
||||
|
||||
type Props = {
|
||||
@@ -26,6 +28,7 @@ function Header({ left, title, actions, hasSidebar }: Props) {
|
||||
const isMobile = useMobile();
|
||||
|
||||
const hasMobileSidebar = hasSidebar && isMobile;
|
||||
const sidebarCollapsed = ui.isEditing || ui.sidebarCollapsed;
|
||||
|
||||
const passThrough = !actions && !left && !title;
|
||||
|
||||
@@ -50,7 +53,12 @@ function Header({ left, title, actions, hasSidebar }: Props) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Wrapper align="center" shrink={false} $passThrough={passThrough}>
|
||||
<Wrapper
|
||||
align="center"
|
||||
shrink={false}
|
||||
$passThrough={passThrough}
|
||||
$insetTitleAdjust={sidebarCollapsed && Desktop.hasInsetTitlebar()}
|
||||
>
|
||||
{left || hasMobileSidebar ? (
|
||||
<Breadcrumbs>
|
||||
{hasMobileSidebar && (
|
||||
@@ -98,7 +106,12 @@ const Actions = styled(Flex)`
|
||||
`};
|
||||
`;
|
||||
|
||||
const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
|
||||
type WrapperProps = {
|
||||
$passThrough?: boolean;
|
||||
$insetTitleAdjust?: boolean;
|
||||
};
|
||||
|
||||
const Wrapper = styled(Flex)<WrapperProps>`
|
||||
top: 0;
|
||||
z-index: ${depths.header};
|
||||
position: sticky;
|
||||
@@ -120,6 +133,8 @@ const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
|
||||
transform: translate3d(0, 0, 0);
|
||||
min-height: 64px;
|
||||
justify-content: flex-start;
|
||||
${draggableOnDesktop()}
|
||||
${fadeOnDesktopBackgrounded()}
|
||||
|
||||
@supports (backdrop-filter: blur(20px)) {
|
||||
backdrop-filter: blur(20px);
|
||||
@@ -133,7 +148,8 @@ const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
|
||||
${breakpoint("tablet")`
|
||||
padding: 16px;
|
||||
justify-content: center;
|
||||
`};
|
||||
${(props: WrapperProps) => props.$insetTitleAdjust && `padding-left: 64px;`}
|
||||
`};
|
||||
`;
|
||||
|
||||
const Title = styled("div")`
|
||||
|
||||
@@ -5,6 +5,7 @@ import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Flex from "~/components/Flex";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
|
||||
const RealTextarea = styled.textarea<{ hasIcon?: boolean }>`
|
||||
border: 0;
|
||||
@@ -32,6 +33,7 @@ const RealInput = styled.input<{ hasIcon?: boolean }>`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
${undraggableOnDesktop()}
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
@@ -98,6 +100,9 @@ export const Outline = styled(Flex)<{
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.background};
|
||||
|
||||
/* Prevents an issue where input placeholder appears in a selected style when double clicking title bar */
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const LabelText = styled.div`
|
||||
|
||||
@@ -15,6 +15,7 @@ import useMobile from "~/hooks/useMobile";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useUnmount from "~/hooks/useUnmount";
|
||||
import { fadeAndScaleIn } from "~/styles/animations";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
|
||||
let openModals = 0;
|
||||
type Props = {
|
||||
@@ -222,7 +223,7 @@ const Back = styled(NudeButton)`
|
||||
position: absolute;
|
||||
display: none;
|
||||
align-items: center;
|
||||
top: 2rem;
|
||||
top: ${Desktop.hasInsetTitlebar() ? "3rem" : "2rem"};
|
||||
left: 2rem;
|
||||
opacity: 0.75;
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
@@ -14,6 +14,7 @@ import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import OrganizationMenu from "~/menus/OrganizationMenu";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import {
|
||||
homePath,
|
||||
draftsPath,
|
||||
@@ -63,7 +64,16 @@ function AppSidebar() {
|
||||
<HeaderButton
|
||||
{...props}
|
||||
title={team.name}
|
||||
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
|
||||
image={
|
||||
<TeamLogo
|
||||
model={team}
|
||||
size={Desktop.hasInsetTitlebar() ? 24 : 32}
|
||||
alt={t("Logo")}
|
||||
/>
|
||||
}
|
||||
style={
|
||||
Desktop.hasInsetTitlebar() ? { paddingLeft: 8 } : undefined
|
||||
}
|
||||
showDisclosure
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,7 @@ import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import useAuthorizedSettingsConfig from "~/hooks/useAuthorizedSettingsConfig";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import isCloudHosted from "~/utils/isCloudHosted";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Header from "./components/Header";
|
||||
@@ -32,7 +33,7 @@ function SettingsSidebar() {
|
||||
title={t("Return to App")}
|
||||
image={<StyledBackIcon color="currentColor" />}
|
||||
onClick={returnToApp}
|
||||
minHeight={48}
|
||||
minHeight={Desktop.hasInsetTitlebar() ? undefined : 48}
|
||||
/>
|
||||
|
||||
<Flex auto column>
|
||||
|
||||
@@ -11,7 +11,9 @@ import useMenuContext from "~/hooks/useMenuContext";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import AccountMenu from "~/menus/AccountMenu";
|
||||
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
|
||||
import { fadeIn } from "~/styles/animations";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import Avatar from "../Avatar";
|
||||
import HeaderButton, { HeaderButtonProps } from "./components/HeaderButton";
|
||||
import ResizeBorder from "./components/ResizeBorder";
|
||||
@@ -251,6 +253,9 @@ const Container = styled(Flex)<ContainerProps>`
|
||||
z-index: ${depths.sidebar};
|
||||
max-width: 70%;
|
||||
min-width: 280px;
|
||||
padding-top: ${Desktop.hasInsetTitlebar() ? 24 : 0}px;
|
||||
${draggableOnDesktop()}
|
||||
${fadeOnDesktopBackgrounded()}
|
||||
|
||||
${Positioner} {
|
||||
display: none;
|
||||
@@ -265,7 +270,9 @@ const Container = styled(Flex)<ContainerProps>`
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
transform: translateX(${(props: ContainerProps) =>
|
||||
props.$collapsed ? "calc(-100% + 16px)" : 0});
|
||||
props.$collapsed
|
||||
? `calc(-100% + ${Desktop.hasInsetTitlebar() ? 8 : 16}px)`
|
||||
: 0});
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
export type HeaderButtonProps = {
|
||||
export type HeaderButtonProps = React.ComponentProps<typeof Wrapper> & {
|
||||
title: React.ReactNode;
|
||||
image: React.ReactNode;
|
||||
minHeight?: number;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from "styled-components";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
|
||||
const ResizeBorder = styled.div<{ dir?: "left" | "right" }>`
|
||||
position: absolute;
|
||||
@@ -8,6 +9,7 @@ const ResizeBorder = styled.div<{ dir?: "left" | "right" }>`
|
||||
left: ${(props) => (props.dir === "right" ? "-1px" : "auto")};
|
||||
width: 2px;
|
||||
cursor: col-resize;
|
||||
${undraggableOnDesktop()}
|
||||
|
||||
&:hover {
|
||||
transition-delay: 500ms;
|
||||
@@ -22,6 +24,7 @@ const ResizeBorder = styled.div<{ dir?: "left" | "right" }>`
|
||||
bottom: 0;
|
||||
right: -4px;
|
||||
width: 10px;
|
||||
${undraggableOnDesktop()}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import styled, { useTheme, css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import EventBoundary from "~/components/EventBoundary";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
import { NavigationNode } from "~/types";
|
||||
import Disclosure from "./Disclosure";
|
||||
import NavLink, { Props as NavLinkProps } from "./NavLink";
|
||||
@@ -181,6 +182,7 @@ const Link = styled(NavLink)<{
|
||||
font-size: 16px;
|
||||
cursor: var(--pointer);
|
||||
overflow: hidden;
|
||||
${undraggableOnDesktop()}
|
||||
|
||||
${(props) =>
|
||||
props.$disabled &&
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { LabelText } from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
|
||||
type Props = React.HTMLAttributes<HTMLInputElement> & {
|
||||
width?: number;
|
||||
@@ -62,6 +63,7 @@ function Switch({
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-bottom: 8px;
|
||||
${undraggableOnDesktop()}
|
||||
`;
|
||||
|
||||
const InlineLabelText = styled(LabelText)`
|
||||
|
||||
@@ -69,14 +69,14 @@ function Toast({ closeAfterMs = 3000, onRequestClose, toast }: Props) {
|
||||
|
||||
const Action = styled.span`
|
||||
display: inline-block;
|
||||
padding: 10px 12px;
|
||||
height: 100%;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
color: ${(props) => props.theme.toastText};
|
||||
background: ${(props) => darken(0.05, props.theme.toastBackground)};
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
margin-right: -4px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => darken(0.1, props.theme.toastBackground)};
|
||||
|
||||
Reference in New Issue
Block a user