From 7bf4de5892a1ef7899b0f701ded79ad835b7a1b2 Mon Sep 17 00:00:00 2001 From: Are Date: Wed, 31 May 2023 18:27:29 +0200 Subject: [PATCH] feat: redesign of Live Collaboration dialog (#6635) * feat: redesiged Live Collaboration dialog * fix: address lints * fix: inactive dialog dark mode improvements * fix: follow styleguide with event parameter, add FilledButton size prop * fix: change timer to be imperative * fix: add spacing after emoji * fix: remove unused useEffect * fix: change margin into whitespace * fix: add share button check back --- src/assets/lock.svg | 20 ++ src/components/FilledButton.scss | 95 +++++++ src/components/FilledButton.tsx | 61 +++++ src/components/ImageExportDialog.scss | 42 ---- src/components/ImageExportDialog.tsx | 38 +-- src/components/Switch.tsx | 4 +- src/components/TextField.scss | 118 +++++++++ src/components/TextField.tsx | 57 +++++ src/components/icons.tsx | 28 +++ src/css/theme.scss | 2 + src/excalidraw-app/collab/RoomDialog.scss | 191 +++++++++----- src/excalidraw-app/collab/RoomDialog.tsx | 293 ++++++++++++---------- src/excalidraw-app/index.tsx | 2 +- 13 files changed, 698 insertions(+), 253 deletions(-) create mode 100644 src/assets/lock.svg create mode 100644 src/components/FilledButton.scss create mode 100644 src/components/FilledButton.tsx create mode 100644 src/components/TextField.scss create mode 100644 src/components/TextField.tsx diff --git a/src/assets/lock.svg b/src/assets/lock.svg new file mode 100644 index 00000000..aa9dbf17 --- /dev/null +++ b/src/assets/lock.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/FilledButton.scss b/src/components/FilledButton.scss new file mode 100644 index 00000000..d742e22e --- /dev/null +++ b/src/components/FilledButton.scss @@ -0,0 +1,95 @@ +@import "../css/variables.module"; + +.excalidraw { + .ExcButton { + &--color-primary { + color: var(--input-bg-color); + + --accent-color: var(--color-primary); + --accent-color-hover: var(--color-primary-darker); + --accent-color-active: var(--color-primary-darkest); + } + + &--color-danger { + color: var(--input-bg-color); + + --accent-color: var(--color-danger); + --accent-color-hover: #d65550; + --accent-color-active: #d1413c; + } + + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + flex-wrap: nowrap; + + border-radius: 0.5rem; + + font-family: "Assistant"; + + user-select: none; + + transition: all 150ms ease-out; + + &--size-large { + font-weight: 400; + font-size: 0.875rem; + height: 3rem; + padding: 0.5rem 1.5rem; + gap: 0.75rem; + + letter-spacing: 0.4px; + } + + &--size-medium { + font-weight: 600; + font-size: 0.75rem; + height: 2.5rem; + padding: 0.5rem 1rem; + gap: 0.5rem; + + letter-spacing: normal; + } + + &--variant-filled { + background: var(--accent-color); + border: 1px solid transparent; + + &:hover { + background: var(--accent-color-hover); + } + + &:active { + background: var(--accent-color-active); + } + } + + &--variant-outlined, + &--variant-icon { + border: 1px solid var(--accent-color); + color: var(--accent-color); + background: transparent; + + &:hover { + border: 1px solid var(--accent-color-hover); + color: var(--accent-color-hover); + } + + &:active { + border: 1px solid var(--accent-color-active); + color: var(--accent-color-active); + } + } + + &--variant-icon { + padding: 0.5rem 0.75rem; + width: 3rem; + } + + &__icon { + width: 1.25rem; + height: 1.25rem; + } + } +} diff --git a/src/components/FilledButton.tsx b/src/components/FilledButton.tsx new file mode 100644 index 00000000..0db72421 --- /dev/null +++ b/src/components/FilledButton.tsx @@ -0,0 +1,61 @@ +import React, { forwardRef } from "react"; +import clsx from "clsx"; + +import "./FilledButton.scss"; + +export type ButtonVariant = "filled" | "outlined" | "icon"; +export type ButtonColor = "primary" | "danger"; +export type ButtonSize = "medium" | "large"; + +export type FilledButtonProps = { + label: string; + + children?: React.ReactNode; + onClick?: () => void; + + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + className?: string; + + startIcon?: React.ReactNode; +}; + +export const FilledButton = forwardRef( + ( + { + children, + startIcon, + onClick, + label, + variant = "filled", + color = "primary", + size = "medium", + className, + }, + ref, + ) => { + return ( + + ); + }, +); diff --git a/src/components/ImageExportDialog.scss b/src/components/ImageExportDialog.scss index a74cfdc2..093e1a76 100644 --- a/src/components/ImageExportDialog.scss +++ b/src/components/ImageExportDialog.scss @@ -167,48 +167,6 @@ flex-basis: 100%; justify-content: center; } - - &__button { - box-sizing: border-box; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 8px 16px; - flex-shrink: 0; - width: fit-content; - gap: 8px; - - height: 40px; - border: 0; - border-radius: 8px; - - user-select: none; - font-family: "Assistant"; - font-style: normal; - font-weight: 600; - font-size: 0.75rem; - line-height: 100%; - transition: 150ms ease-out; - transition-property: background, color; - - background: var(--color-primary); - color: var(--color-icon-white); - - &:hover { - background: var(--color-primary-darker); - color: var(--color-icon-white); - } - - &:active { - background: var(--color-primary-darkest); - } - - & > svg { - width: 20px; - height: 20px; - } - } } } } diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index 604f50b4..042d5a3f 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -26,7 +26,6 @@ import { getSelectedElements, isSomeElementSelected } from "../scene"; import { exportToCanvas } from "../packages/utils"; import { copyIcon, downloadIcon, helpIcon } from "./icons"; -import { Button } from "./Button"; import { Dialog } from "./Dialog"; import { RadioGroup } from "./RadioGroup"; import { Switch } from "./Switch"; @@ -34,6 +33,7 @@ import { Tooltip } from "./Tooltip"; import "./ImageExportDialog.scss"; import { useAppProps } from "./App"; +import { FilledButton } from "./FilledButton"; const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; @@ -236,37 +236,37 @@ const ImageExportModal = ({
- - + {t("imageExportDialog.button.exportToSvg")} + {(probablySupportsClipboardBlob || isFirefox) && ( - + {t("imageExportDialog.button.copyPngToClipboard")} + )}
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index dfbf332f..431c644f 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -27,8 +27,8 @@ export const Switch = ({ checked={checked} disabled={disabled} onChange={() => onChange(!checked)} - onKeyDown={(e) => { - if (e.key === " ") { + onKeyDown={(event) => { + if (event.key === " ") { onChange(!checked); } }} diff --git a/src/components/TextField.scss b/src/components/TextField.scss new file mode 100644 index 00000000..57093996 --- /dev/null +++ b/src/components/TextField.scss @@ -0,0 +1,118 @@ +@import "../css/variables.module"; + +.excalidraw { + --ExcTextField--color: var(--color-gray-80); + --ExcTextField--label-color: var(--color-gray-80); + --ExcTextField--background: white; + --ExcTextField--readonly--background: var(--color-gray-10); + --ExcTextField--readonly--color: var(--color-gray-80); + --ExcTextField--border: var(--color-gray-40); + --ExcTextField--border-hover: var(--color-gray-50); + --ExcTextField--placeholder: var(--color-gray-40); + + &.theme--dark { + --ExcTextField--color: var(--color-gray-10); + --ExcTextField--label-color: var(--color-gray-20); + --ExcTextField--background: var(--color-gray-85); + --ExcTextField--readonly--background: var(--color-gray-80); + --ExcTextField--readonly--color: var(--color-gray-40); + --ExcTextField--border: var(--color-gray-70); + --ExcTextField--border-hover: var(--color-gray-60); + --ExcTextField--placeholder: var(--color-gray-80); + } + + .ExcTextField { + &--fullWidth { + width: 100%; + flex-grow: 1; + } + + &__label { + font-family: "Assistant"; + font-style: normal; + font-weight: 600; + font-size: 0.875rem; + line-height: 150%; + + color: var(--ExcTextField--label-color); + + margin-bottom: 0.25rem; + user-select: none; + } + + &__input { + box-sizing: border-box; + + display: flex; + flex-direction: row; + align-items: center; + padding: 0 1rem; + + height: 3rem; + + background: var(--ExcTextField--background); + border: 1px solid var(--ExcTextField--border); + border-radius: 0.5rem; + + &:not(&--readonly) { + &:hover { + border-color: var(--ExcTextField--border-hover); + } + + &:active, + &:focus-within { + border-color: var(--color-primary); + } + } + + & input { + display: flex; + align-items: center; + + border: none; + outline: none; + padding: 0; + margin: 0; + + height: 1.5rem; + + color: var(--ExcTextField--color); + + font-family: "Assistant"; + font-style: normal; + font-weight: 400; + font-size: 1rem; + line-height: 150%; + text-overflow: ellipsis; + + background: transparent; + + width: 100%; + + &::placeholder { + color: var(--ExcTextField--placeholder); + } + + &:not(:focus) { + &:hover { + background-color: initial; + } + } + + &:focus { + outline: initial; + box-shadow: initial; + } + } + + &--readonly { + background: var(--ExcTextField--readonly--background); + border-color: transparent; + + & input { + color: var(--ExcTextField--readonly--color); + } + } + } + } +} diff --git a/src/components/TextField.tsx b/src/components/TextField.tsx new file mode 100644 index 00000000..7f7a41fd --- /dev/null +++ b/src/components/TextField.tsx @@ -0,0 +1,57 @@ +import { forwardRef, useRef, useImperativeHandle, KeyboardEvent } from "react"; +import clsx from "clsx"; + +import "./TextField.scss"; + +export type TextFieldProps = { + value?: string; + + onChange?: (value: string) => void; + onClick?: () => void; + onKeyDown?: (event: KeyboardEvent) => void; + + readonly?: boolean; + fullWidth?: boolean; + + label?: string; + placeholder?: string; +}; + +export const TextField = forwardRef( + ( + { value, onChange, label, fullWidth, placeholder, readonly, onKeyDown }, + ref, + ) => { + const innerRef = useRef(null); + + useImperativeHandle(ref, () => innerRef.current!); + + return ( +
{ + innerRef.current?.focus(); + }} + > +
{label}
+
+ onChange?.(event.target.value)} + onKeyDown={onKeyDown} + /> +
+
+ ); + }, +); diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 3841d030..543248a1 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1579,3 +1579,31 @@ export const helpIcon = createIcon( , tablerIconProps, ); + +export const playerPlayIcon = createIcon( + <> + + + , + tablerIconProps, +); + +export const playerStopFilledIcon = createIcon( + <> + + + , + tablerIconProps, +); + +export const tablerCheckIcon = createIcon( + <> + + + , + tablerIconProps, +); diff --git a/src/css/theme.scss b/src/css/theme.scss index c8abc4ff..92b5989a 100644 --- a/src/css/theme.scss +++ b/src/css/theme.scss @@ -103,6 +103,8 @@ --color-danger: #db6965; --color-promo: #e70078; + --color-success: #268029; + --color-success-lighter: #cafccc; --border-radius-md: 0.375rem; --border-radius-lg: 0.5rem; diff --git a/src/excalidraw-app/collab/RoomDialog.scss b/src/excalidraw-app/collab/RoomDialog.scss index c8bf0dcd..0d1bcad6 100644 --- a/src/excalidraw-app/collab/RoomDialog.scss +++ b/src/excalidraw-app/collab/RoomDialog.scss @@ -1,76 +1,149 @@ @import "../../css/variables.module"; .excalidraw { - .RoomDialog__button { - border: 1px solid var(--default-border-color) !important; - } - - .RoomDialog-linkContainer { + .RoomDialog { display: flex; - margin: 1.5em 0; - } + flex-direction: column; + gap: 1.5rem; - input.RoomDialog-link { - color: var(--text-primary-color); - min-width: 0; - flex: 1 1 auto; - margin-inline-start: 1em; - display: inline-block; - cursor: pointer; - border: none; - padding: 0 0.5rem; - white-space: nowrap; - border-radius: var(--space-factor); - background-color: var(--button-gray-1); - } - - .RoomDialog-emoji { - font-family: sans-serif; - } - - .RoomDialog-usernameContainer { - display: flex; - margin: 1.5em 0; - display: flex; - align-items: center; - justify-content: center; @include isMobile { - flex-direction: column; - align-items: stretch; + height: calc(100vh - 5rem); } - } - @include isMobile { - .RoomDialog-usernameLabel { - font-weight: bold; + &__popover { + @keyframes RoomDialog__popover__scaleIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + box-sizing: border-box; + z-index: 100; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + padding: 0.125rem 0.5rem; + gap: 0.125rem; + + height: 1.125rem; + + border: none; + border-radius: 0.6875rem; + + font-family: "Assistant"; + font-style: normal; + font-weight: 600; + font-size: 0.75rem; + line-height: 110%; + + background: var(--color-success-lighter); + color: var(--color-success); + + & > svg { + width: 0.875rem; + height: 0.875rem; + } + + transform-origin: var(--radix-popover-content-transform-origin); + animation: RoomDialog__popover__scaleIn 150ms ease-out; } - } - .RoomDialog-username { - background-color: var(--input-bg-color); - border-color: var(--input-border-color); - appearance: none; - min-width: 0; - flex: 1 1 auto; - margin-inline-start: 1em; - @include isMobile { - margin-top: 0.5em; - margin-inline-start: 0; + &__inactive { + font-family: "Assistant"; + + &__illustration { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + + & svg { + filter: var(--theme-filter); + } + } + + &__header { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + + font-weight: 700; + font-size: 1.3125rem; + line-height: 130%; + + color: var(--color-primary); + } + + &__description { + font-weight: 400; + font-size: 0.875rem; + line-height: 150%; + + text-align: center; + + color: var(--text-primary-color); + + & strong { + display: block; + font-weight: 700; + } + } + + &__start_session { + display: flex; + + align-items: center; + justify-content: center; + } } - font-size: 1em; - } - .RoomDialog-sessionStartButtonContainer { - display: flex; - justify-content: center; - } + &__active { + &__share { + display: none !important; - .Modal .RoomDialog-stopSession { - background-color: var(--button-destructive-bg-color); + @include isMobile { + display: flex !important; + } + } - .ToolIcon__label, - .ToolIcon__icon svg { - color: var(--button-destructive-color); + &__header { + margin: 0; + } + + &__linkRow { + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 0.75rem; + } + + &__description { + border-top: 1px solid var(--color-gray-20); + + padding: 0.5rem 0.5rem 0; + font-weight: 400; + font-size: 0.75rem; + line-height: 150%; + + & p { + margin: 0; + } + + & p + p { + margin-top: 1em; + } + } + + &__actions { + display: flex; + justify-content: center; + } } } } diff --git a/src/excalidraw-app/collab/RoomDialog.tsx b/src/excalidraw-app/collab/RoomDialog.tsx index 05772774..daec8cfe 100644 --- a/src/excalidraw-app/collab/RoomDialog.tsx +++ b/src/excalidraw-app/collab/RoomDialog.tsx @@ -1,24 +1,29 @@ -import React, { useRef } from "react"; +import { useRef, useState } from "react"; +import * as Popover from "@radix-ui/react-popover"; + import { copyTextToSystemClipboard } from "../../clipboard"; -import { Dialog } from "../../components/Dialog"; -import { - clipboard, - start, - stop, - share, - shareIOS, - shareWindows, -} from "../../components/icons"; -import { ToolButton } from "../../components/ToolButton"; -import "./RoomDialog.scss"; -import Stack from "../../components/Stack"; import { AppState } from "../../types"; import { trackEvent } from "../../analytics"; import { getFrame } from "../../utils"; -import DialogActionButton from "../../components/DialogActionButton"; import { useI18n } from "../../i18n"; import { KEYS } from "../../keys"; +import { Dialog } from "../../components/Dialog"; +import { + copyIcon, + playerPlayIcon, + playerStopFilledIcon, + share, + shareIOS, + shareWindows, + tablerCheckIcon, +} from "../../components/icons"; +import { TextField } from "../../components/TextField"; +import { FilledButton } from "../../components/FilledButton"; + +import { ReactComponent as CollabImage } from "../../assets/lock.svg"; +import "./RoomDialog.scss"; + const getShareIcon = () => { const navigator = window.navigator as any; const isAppleBrowser = /Apple/.test(navigator.vendor); @@ -33,16 +38,7 @@ const getShareIcon = () => { return share; }; -const RoomDialog = ({ - handleClose, - activeRoomLink, - username, - onUsernameChange, - onRoomCreate, - onRoomDestroy, - setErrorMessage, - theme, -}: { +export type RoomModalProps = { handleClose: () => void; activeRoomLink: string; username: string; @@ -51,19 +47,41 @@ const RoomDialog = ({ onRoomDestroy: () => void; setErrorMessage: (message: string) => void; theme: AppState["theme"]; -}) => { +}; + +export const RoomModal = ({ + activeRoomLink, + onRoomCreate, + onRoomDestroy, + setErrorMessage, + username, + onUsernameChange, + handleClose, +}: RoomModalProps) => { const { t } = useI18n(); - const roomLinkInput = useRef(null); + const [justCopied, setJustCopied] = useState(false); + const timerRef = useRef(0); + const ref = useRef(null); + const isShareSupported = "share" in navigator; const copyRoomLink = async () => { try { await copyTextToSystemClipboard(activeRoomLink); + + setJustCopied(true); + + if (timerRef.current) { + window.clearTimeout(timerRef.current); + } + + timerRef.current = window.setTimeout(() => { + setJustCopied(false); + }, 3000); } catch (error: any) { setErrorMessage(error.message); } - if (roomLinkInput.current) { - roomLinkInput.current.select(); - } + + ref.current?.select(); }; const shareRoomLink = async () => { @@ -78,114 +96,129 @@ const RoomDialog = ({ } }; - const selectInput = (event: React.MouseEvent) => { - if (event.target !== document.activeElement) { - event.preventDefault(); - (event.target as HTMLInputElement).select(); - } - }; - - const renderRoomDialog = () => { + if (activeRoomLink) { return ( -
- {!activeRoomLink && ( - <> -

{t("roomDialog.desc_intro")}

-

{`🔒 ${t("roomDialog.desc_privacy")}`}

-
- { - trackEvent("share", "room creation", `ui (${getFrame()})`); - onRoomCreate(); - }} - > - {start} - -
- - )} - {activeRoomLink && ( - <> -

{t("roomDialog.desc_inProgressIntro")}

-

{t("roomDialog.desc_shareLink")}

-
- - {"share" in navigator ? ( - - ) : null} - - - +

+ {t("labels.liveCollaboration")} +

+ event.key === KEYS.ENTER && handleClose()} + /> +
+ + {isShareSupported && ( + + )} + + + -
-
- - onUsernameChange(event.target.value)} - onKeyPress={(event) => - event.key === KEYS.ENTER && handleClose() - } - /> -
-

- - {t("roomDialog.desc_privacy")} -

-

{t("roomDialog.desc_exitSession")}

-
- { - trackEvent("share", "room closed"); - onRoomDestroy(); - }} - > - {stop} - -
- - )} -
+ + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + className="RoomDialog__popover" + side="top" + align="end" + sideOffset={5.5} + > + {tablerCheckIcon} copied + + +
+
+

+ + {t("roomDialog.desc_privacy")} +

+

{t("roomDialog.desc_exitSession")}

+
+ +
+ { + trackEvent("share", "room closed"); + onRoomDestroy(); + }} + /> +
+ ); - }; + } + + return ( + <> +
+ +
+
+ {t("labels.liveCollaboration")} +
+ +
+ {t("roomDialog.desc_intro")} + {t("roomDialog.desc_privacy")} +
+ +
+ { + trackEvent("share", "room creation", `ui (${getFrame()})`); + onRoomCreate(); + }} + /> +
+ + ); +}; + +const RoomDialog = (props: RoomModalProps) => { return ( - {renderRoomDialog()} +
+ +
); }; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 9017bbb8..e1af5480 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -671,8 +671,8 @@ const ExcalidrawWrapper = () => { {t("alerts.collabOfflineWarning")} )} + {excalidrawAPI && } - {excalidrawAPI && } {errorMessage && ( setErrorMessage("")}> {errorMessage}