mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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
This commit is contained in:
parent
253c5c7866
commit
7bf4de5892
13 changed files with 698 additions and 253 deletions
95
src/components/FilledButton.scss
Normal file
95
src/components/FilledButton.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
61
src/components/FilledButton.tsx
Normal file
61
src/components/FilledButton.tsx
Normal file
|
@ -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<HTMLButtonElement, FilledButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
startIcon,
|
||||
onClick,
|
||||
label,
|
||||
variant = "filled",
|
||||
color = "primary",
|
||||
size = "medium",
|
||||
className,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"ExcButton",
|
||||
`ExcButton--color-${color}`,
|
||||
`ExcButton--variant-${variant}`,
|
||||
`ExcButton--size-${size}`,
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
ref={ref}
|
||||
>
|
||||
{startIcon && (
|
||||
<div className="ExcButton__icon" aria-hidden>
|
||||
{startIcon}
|
||||
</div>
|
||||
)}
|
||||
{variant !== "icon" && (children ?? label)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = ({
|
|||
</ExportSetting>
|
||||
|
||||
<div className="ImageExportModal__settings__buttons">
|
||||
<Button
|
||||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
title={t("imageExportDialog.title.exportToPng")}
|
||||
aria-label={t("imageExportDialog.title.exportToPng")}
|
||||
onSelect={() =>
|
||||
label={t("imageExportDialog.title.exportToPng")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
||||
}
|
||||
startIcon={downloadIcon}
|
||||
>
|
||||
{downloadIcon} {t("imageExportDialog.button.exportToPng")}
|
||||
</Button>
|
||||
<Button
|
||||
{t("imageExportDialog.button.exportToPng")}
|
||||
</FilledButton>
|
||||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
title={t("imageExportDialog.title.exportToSvg")}
|
||||
aria-label={t("imageExportDialog.title.exportToSvg")}
|
||||
onSelect={() =>
|
||||
label={t("imageExportDialog.title.exportToSvg")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
||||
}
|
||||
startIcon={downloadIcon}
|
||||
>
|
||||
{downloadIcon} {t("imageExportDialog.button.exportToSvg")}
|
||||
</Button>
|
||||
{t("imageExportDialog.button.exportToSvg")}
|
||||
</FilledButton>
|
||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||
<Button
|
||||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
title={t("imageExportDialog.title.copyPngToClipboard")}
|
||||
aria-label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||
onSelect={() =>
|
||||
label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
||||
}
|
||||
startIcon={copyIcon}
|
||||
>
|
||||
{copyIcon} {t("imageExportDialog.button.copyPngToClipboard")}
|
||||
</Button>
|
||||
{t("imageExportDialog.button.copyPngToClipboard")}
|
||||
</FilledButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}}
|
||||
|
|
118
src/components/TextField.scss
Normal file
118
src/components/TextField.scss
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/components/TextField.tsx
Normal file
57
src/components/TextField.tsx
Normal file
|
@ -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<HTMLInputElement>) => void;
|
||||
|
||||
readonly?: boolean;
|
||||
fullWidth?: boolean;
|
||||
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
(
|
||||
{ value, onChange, label, fullWidth, placeholder, readonly, onKeyDown },
|
||||
ref,
|
||||
) => {
|
||||
const innerRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => innerRef.current!);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("ExcTextField", {
|
||||
"ExcTextField--fullWidth": fullWidth,
|
||||
})}
|
||||
onClick={() => {
|
||||
innerRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<div className="ExcTextField__label">{label}</div>
|
||||
<div
|
||||
className={clsx("ExcTextField__input", {
|
||||
"ExcTextField__input--readonly": readonly,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
readOnly={readonly}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
ref={innerRef}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -1579,3 +1579,31 @@ export const helpIcon = createIcon(
|
|||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const playerPlayIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M7 4v16l13 -8z"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const playerStopFilledIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M17 4h-10a3 3 0 0 0 -3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3 -3v-10a3 3 0 0 0 -3 -3z"
|
||||
strokeWidth="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const tablerCheckIcon = createIcon(
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M5 12l5 5l10 -10"></path>
|
||||
</>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue