feat: Stop showing collab button and always start with collaboration (#4)

* feat: Stop showing collab button and always start with collaboration

* open export dialog when session closes

* wait for collabApi to be initialized before intiating collab

* fix

* add mobile checks for opening export dialog

* fix

* fix

* fix
This commit is contained in:
Aakansha Doshi
2021-10-15 20:17:58 +05:30
committed by GitHub
parent f181b1c786
commit bb201d77d7
15 changed files with 61 additions and 1019 deletions
+49 -6
View File
@@ -20,7 +20,7 @@ import {
import { ImportedDataState } from "@excalidraw/excalidraw/types/data/types";
import { getCollaborationLinkData } from "./data";
import { ResolvablePromise } from "@excalidraw/excalidraw/types/utils";
import { loadScript, resolvablePromise } from "./utils";
import { isDev, loadScript, resolvablePromise } from "./utils";
import { WEBEX_URL } from "./constants";
const ExcalidrawWrapper = () => {
@@ -46,7 +46,24 @@ const ExcalidrawWrapper = () => {
window.webexInstance = new window.Webex.Application();
const webexApp = window.webexInstance;
if (!collabAPI || !excalidrawAPI) {
return;
}
const initiateCollab = () => {
const roomLinkData = getCollaborationLinkData(window.location.href);
if (!roomLinkData) {
collabAPI?.initializeSocketClient(null);
}
};
// Initial collab session manually as webex onReady will not be triggered in dev mode
if (isDev()) {
initiateCollab();
}
webexApp.onReady().then(() => {
initiateCollab();
const currentTheme = webexApp.theme.toLowerCase();
if (currentTheme !== theme) {
setTheme(currentTheme);
@@ -63,14 +80,41 @@ const ExcalidrawWrapper = () => {
webexApp.on("application:themeChanged", (theme: "LIGHT" | "DARK") => {
setTheme(theme.toLowerCase() as Theme);
});
webexApp.on("application:shareStateChanged", (isShared: boolean) => {
// Open json export modal if sharing turned off
if (!isShared) {
let exportButton = document.querySelector(
'[data-testid="json-export-button"]',
) as HTMLElement;
// This will happen for mobile view
if (exportButton === null) {
// open mobile menu => click on export => close mobile menu
const currentAppState = excalidrawAPI.getAppState();
excalidrawAPI.updateScene({
appState: { ...currentAppState, openMenu: "canvas" },
});
exportButton = document.querySelector(
'[data-testid="json-export-button"]',
) as HTMLElement;
}
exportButton.click();
}
});
});
});
};
loadScript(WEBEX_URL).then(() => {
if (!window.webexInstance) {
loadScript(WEBEX_URL).then(() => {
initializeWebex();
setLoaded(true);
});
} else {
initializeWebex();
setLoaded(true);
});
}, [theme]);
}
}, [theme, excalidrawAPI, collabAPI]);
useEffect(() => {
if (!collabAPI || !excalidrawAPI) {
@@ -107,7 +151,6 @@ const ExcalidrawWrapper = () => {
<Excalidraw
ref={excalidrawRefCallback}
onChange={onChange}
onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating()}
initialData={initialStatePromiseRef.current.promise}
onPointerUpdate={collabAPI?.onPointerUpdate}
+2 -42
View File
@@ -10,7 +10,6 @@ import {
Gesture,
} from "@excalidraw/excalidraw/types/types";
import { EVENT } from "../constants";
import RoomDialog from "./RoomDialog";
import {
getElementMap,
getSceneVersion,
@@ -33,8 +32,6 @@ import {
import { resolvablePromise } from "../utils";
interface CollabState {
modalIsShown: boolean;
activeRoomLink: string;
errorMessage: string;
}
@@ -43,7 +40,6 @@ type CollabInstance = InstanceType<typeof CollabWrapper>;
export interface CollabAPI {
isCollaborating: () => boolean;
initializeSocketClient: CollabInstance["initializeSocketClient"];
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
broadcastElements: CollabInstance["broadcastElements"];
onPointerUpdate: CollabInstance["onPointerUpdate"];
}
@@ -83,8 +79,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
constructor(props: Props) {
super(props);
this.state = {
modalIsShown: false,
activeRoomLink: "",
errorMessage: "",
};
@@ -104,12 +98,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.destroySocketClient({ isUnload: true });
};
onCollabButtonClick = () => {
this.setState({
modalIsShown: true,
});
};
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
if (
getSceneVersion(elements) >
@@ -147,9 +135,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.excalidrawAPI.updateScene({
collaborators: this.collaborators,
});
this.setState({
activeRoomLink: "",
});
window.webexInstance.clearShareUrl();
this.isCollaborating = false;
}
@@ -304,14 +290,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
scenePromise.resolve(null);
});
this.setState(
{
activeRoomLink: window.location.href,
},
() => {
window.webexInstance.setShareUrl(this.state.activeRoomLink);
},
);
window.webexInstance.setShareUrl(window.location.href);
return scenePromise;
};
@@ -383,10 +362,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return newElements as ReconciledElements;
};
openPortal = async () => {
return this.initializeSocketClient(null);
};
closePortal = () => {
window.history.pushState({}, APP_NAME, window.location.origin);
this.destroySocketClient();
@@ -443,28 +418,13 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.contextValue.isCollaborating = () => this.isCollaborating;
this.contextValue.onPointerUpdate = this.onPointerUpdate;
this.contextValue.initializeSocketClient = this.initializeSocketClient;
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
this.contextValue.broadcastElements = this.broadcastElements;
return this.contextValue;
};
render() {
const { activeRoomLink, modalIsShown } = this.state;
return (
<>
{modalIsShown && (
<RoomDialog
handleClose={() => this.setState({ modalIsShown: false })}
activeRoomLink={activeRoomLink}
onRoomCreate={this.openPortal}
onRoomDestroy={this.closePortal}
setErrorMessage={(errorMessage: string) => {
this.setState({ errorMessage });
}}
theme={this.excalidrawAPI.getAppState().theme}
/>
)}
<CollabContextProvider
value={{
api: this.getContextValue(),
-77
View File
@@ -1,77 +0,0 @@
@import "../css/variables.module.scss";
.excalidraw {
.RoomDialog-linkContainer {
display: flex;
margin: 1.5em 0;
}
.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;
height: 2.5rem;
line-height: 2.5rem;
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;
}
}
@include isMobile {
.RoomDialog-usernameLabel {
font-weight: bold;
}
}
.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;
}
height: 2.5rem;
font-size: 1em;
line-height: 1.5;
padding: 0 0.5rem;
}
.RoomDialog-sessionStartButtonContainer {
display: flex;
justify-content: center;
}
.Modal .RoomDialog-stopSession {
background-color: var(--button-destructive-bg-color);
.ToolIcon__label,
.ToolIcon__icon svg {
color: var(--button-destructive-color);
}
}
}
-114
View File
@@ -1,114 +0,0 @@
import { AppState } from "@excalidraw/excalidraw/types/types";
import React, { useRef } from "react";
import { start, stop } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { Dialog } from "../components/Dialog";
import "./RoomDialog.scss";
const RoomDialog = ({
handleClose,
activeRoomLink,
onRoomCreate,
onRoomDestroy,
setErrorMessage,
theme,
}: {
handleClose: () => void;
activeRoomLink: string;
onRoomCreate: () => void;
onRoomDestroy: () => void;
setErrorMessage: (message: string) => void;
theme: AppState["theme"];
}) => {
const roomLinkInput = useRef<HTMLInputElement>(null);
const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target !== document.activeElement) {
event.preventDefault();
(event.target as HTMLInputElement).select();
}
};
const renderRoomDialog = () => {
return (
<div className="RoomDialog-modal">
{!activeRoomLink && (
<>
<p>
You can invite people to your current scene to collaborate with
you
</p>
<p>
🔒 Don't worry, the session uses end-to-end encryption, so
whatever you draw will stay private. Not even our server will be
able to see what you come up with
</p>
<div className="RoomDialog-sessionStartButtonContainer">
<ToolButton
className="RoomDialog-startSession"
type="button"
icon={start}
title="Start Session"
aria-label="Start Session"
showAriaLabel={true}
onClick={onRoomCreate}
/>
</div>
</>
)}
{activeRoomLink && (
<>
<p>Live-collaboration session is now in progress</p>
<p>Share this link with anyone you want to collaborate with:</p>
<div className="RoomDialog-linkContainer">
<input
value={activeRoomLink}
readOnly={true}
className="RoomDialog-link"
ref={roomLinkInput}
onPointerDown={selectInput}
/>
</div>
<p>
<span role="img" aria-hidden="true" className="RoomDialog-emoji">
{"🔒"}
</span>
Don't worry, the session uses end-to-end encryption, so whatever
you draw will stay private. Not even our server will be able to
see what you come up with
</p>
<p>
Stopping the session will disconnect you from the room, but you'll
be able to continue working with the scene, locally. Note that
this won't affect other people, and they'll still be able to
collaborate on their version.
</p>
<div className="RoomDialog-sessionStartButtonContainer">
<ToolButton
className="RoomDialog-stopSession"
type="button"
icon={stop}
title="Stop session"
aria-label="Stop session"
showAriaLabel={true}
onClick={onRoomDestroy}
/>
</div>
</>
)}
</div>
);
};
return (
<Dialog
small
onCloseRequest={handleClose}
title="Live collaboration"
theme={theme}
>
{renderRoomDialog()}
</Dialog>
);
};
export default RoomDialog;
-74
View File
@@ -1,74 +0,0 @@
@import "../css/variables.module";
.excalidraw {
.Dialog {
user-select: text;
cursor: auto;
}
.Dialog__title {
display: grid;
align-items: center;
margin-top: 0;
grid-template-columns: 1fr calc(var(--space-factor) * 7);
grid-gap: var(--metric);
padding: calc(var(--space-factor) * 2);
text-align: center;
font-variant: small-caps;
font-size: 1.2em;
}
.Dialog__titleContent {
flex: 1;
}
.Dialog .Modal__close {
color: var(--icon-fill-color);
margin: 0;
}
.Dialog__content {
padding: 0 16px 16px;
}
@include isMobile {
.Dialog {
--metric: calc(var(--space-factor) * 4);
--inset-left: #{"max(var(--metric), var(--sal))"};
--inset-right: #{"max(var(--metric), var(--sar))"};
}
.Dialog__title {
grid-template-columns: calc(var(--space-factor) * 7) 1fr calc(
var(--space-factor) * 7
);
position: sticky;
top: 0;
padding: calc(var(--space-factor) * 2);
background: var(--island-bg-color);
font-size: 1.25em;
box-sizing: border-box;
border-bottom: 1px solid var(--button-gray-2);
z-index: 1;
}
.Dialog__titleContent {
text-align: center;
}
.Dialog .Island {
width: 100vw;
height: 100%;
box-sizing: border-box;
overflow-y: auto;
padding-left: #{"max(calc(var(--padding) * var(--space-factor)), var(--sal))"};
padding-right: #{"max(calc(var(--padding) * var(--space-factor)), var(--sar))"};
padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), var(--sab))"};
}
.Dialog .Modal__close {
order: -1;
}
}
}
-93
View File
@@ -1,93 +0,0 @@
import { AppState } from "@excalidraw/excalidraw/types/types";
import clsx from "clsx";
import React, { useEffect, useState } from "react";
import { KEYS } from "../keys";
import { Island } from "./Island";
import { Modal } from "./Modal";
import "./Dialog.scss";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { close } from "./icons";
export const Dialog = (props: {
children: React.ReactNode;
className?: string;
small?: boolean;
onCloseRequest(): void;
title: React.ReactNode;
autofocus?: boolean;
theme?: AppState["theme"];
}) => {
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
const [lastActiveElement] = useState(document.activeElement);
useEffect(() => {
if (!islandNode) {
return;
}
const focusableElements = queryFocusableElements(islandNode);
if (focusableElements.length > 0 && props.autofocus !== false) {
// If there's an element other than close, focus it.
(focusableElements[1] || focusableElements[0]).focus();
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.TAB) {
const focusableElements = queryFocusableElements(islandNode);
const { activeElement } = document;
const currentIndex = focusableElements.findIndex(
(element) => element === activeElement,
);
if (currentIndex === 0 && event.shiftKey) {
focusableElements[focusableElements.length - 1].focus();
event.preventDefault();
} else if (
currentIndex === focusableElements.length - 1 &&
!event.shiftKey
) {
focusableElements[0].focus();
event.preventDefault();
}
}
};
islandNode.addEventListener("keydown", handleKeyDown);
return () => islandNode.removeEventListener("keydown", handleKeyDown);
}, [islandNode, props.autofocus]);
const queryFocusableElements = (node: HTMLElement) => {
const focusableElements = node.querySelectorAll<HTMLElement>(
"button, a, input, select, textarea, div[tabindex]",
);
return focusableElements ? Array.from(focusableElements) : [];
};
const onClose = () => {
(lastActiveElement as HTMLElement).focus();
props.onCloseRequest();
};
return (
<Modal
className={clsx("Dialog", props.className)}
labelledBy="dialog-title"
maxWidth={props.small ? 550 : 800}
onCloseRequest={onClose}
>
<Island ref={setIslandNode}>
<h2 className="Dialog__title">
<span className="Dialog__titleContent">{props.title}</span>
<button className="Modal__close" onClick={onClose} aria-label="close">
{close}
</button>
</h2>
<div className="Dialog__content">{props.children}</div>
</Island>
</Modal>
);
};
-15
View File
@@ -1,15 +0,0 @@
.excalidraw {
.Island {
--padding: 0;
background-color: var(--island-bg-color);
box-shadow: var(--shadow-island);
border-radius: 4px;
padding: calc(var(--padding) * var(--space-factor));
position: relative;
transition: box-shadow 0.5s ease-in-out;
&.zen-mode {
box-shadow: none;
}
}
}
-23
View File
@@ -1,23 +0,0 @@
import "./Island.scss";
import React from "react";
import clsx from "clsx";
type IslandProps = {
children: React.ReactNode;
padding?: number;
className?: string | boolean;
style?: object;
};
export const Island = React.forwardRef<HTMLDivElement, IslandProps>(
({ children, padding, className, style }, ref) => (
<div
className={clsx("Island", className)}
style={{ "--padding": padding, ...style }}
ref={ref}
>
{children}
</div>
),
);
-100
View File
@@ -1,100 +0,0 @@
@import "../css/variables.module";
.excalidraw {
&.excalidraw-modal-container {
position: absolute;
z-index: 10;
}
.Modal {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding: calc(var(--space-factor) * 10);
}
.Modal__background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background-color: transparentize($oc-black, 0.3);
}
.Modal__content {
position: relative;
z-index: 2;
width: 100%;
max-width: var(--max-width);
max-height: 100%;
opacity: 0;
transform: translateY(10px);
animation: Modal__content_fade-in 0.1s ease-out 0.05s forwards;
position: relative;
overflow-y: auto;
// for modals, reset blurry bg
background: var(--island-bg-color);
border: 1px solid var(--dialog-border-color);
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
border-radius: 6px;
box-sizing: border-box;
&:focus {
outline: none;
}
@include isMobile {
max-width: 100%;
border: 0;
border-radius: 0;
}
}
@keyframes Modal__content_fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.Modal__close {
width: calc(var(--space-factor) * 7);
height: calc(var(--space-factor) * 7);
display: flex;
align-items: center;
justify-content: center;
svg {
height: calc(var(--space-factor) * 5);
}
}
@include isMobile {
.Modal {
padding: 0;
}
.Modal__content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
}
-68
View File
@@ -1,68 +0,0 @@
import "./Modal.scss";
import React, { useState, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import clsx from "clsx";
import { KEYS } from "../keys";
export const Modal = (props: {
className?: string;
children: React.ReactNode;
maxWidth?: number;
onCloseRequest(): void;
labelledBy: string;
}) => {
const modalRoot = useBodyRoot();
if (!modalRoot) {
return null;
}
const handleKeydown = (event: React.KeyboardEvent) => {
if (event.key === KEYS.ESCAPE) {
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
props.onCloseRequest();
}
};
return createPortal(
<div
className={clsx("Modal", props.className)}
role="dialog"
aria-modal="true"
onKeyDown={handleKeydown}
aria-labelledby={props.labelledBy}
>
<div className="Modal__background" onClick={props.onCloseRequest}></div>
<div
className="Modal__content"
style={{ "--max-width": `${props.maxWidth}px` }}
tabIndex={0}
>
{props.children}
</div>
</div>,
modalRoot,
);
};
const useBodyRoot = () => {
const [div, setDiv] = useState<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const div = document.createElement("div");
div.classList.add("excalidraw", "excalidraw-modal-container");
document.body.appendChild(div);
setDiv(div);
return () => {
document.body.removeChild(div);
};
}, []);
return div;
};
-117
View File
@@ -1,117 +0,0 @@
import "./ToolIcon.scss";
import React from "react";
import clsx from "clsx";
export type ToolButtonSize = "small" | "medium";
type ToolButtonBaseProps = {
icon?: React.ReactNode;
"aria-label": string;
"aria-keyshortcuts"?: string;
"data-testid"?: string;
label?: string;
title?: string;
name?: string;
id?: string;
size?: ToolButtonSize;
keyBindingLabel?: string;
showAriaLabel?: boolean;
hidden?: boolean;
visible?: boolean;
selected?: boolean;
className?: string;
};
type ToolButtonProps =
| (ToolButtonBaseProps & {
type: "button";
children?: React.ReactNode;
onClick?(): void;
})
| (ToolButtonBaseProps & {
type: "icon";
children?: React.ReactNode;
onClick?(): void;
})
| (ToolButtonBaseProps & {
type: "radio";
checked: boolean;
onChange?(): void;
});
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
const innerRef = React.useRef(null);
React.useImperativeHandle(ref, () => innerRef.current);
const sizeCn = `ToolIcon_size_${props.size}`;
if (props.type === "button" || props.type === "icon") {
return (
<button
className={clsx(
"ToolIcon_type_button",
sizeCn,
props.className,
props.visible && !props.hidden
? "ToolIcon_type_button--show"
: "ToolIcon_type_button--hide",
{
ToolIcon: !props.hidden,
"ToolIcon--selected": props.selected,
"ToolIcon--plain": props.type === "icon",
},
)}
data-testid={props["data-testid"]}
hidden={props.hidden}
title={props.title}
aria-label={props["aria-label"]}
type="button"
onClick={props.onClick}
ref={innerRef}
>
{(props.icon || props.label) && (
<div className="ToolIcon__icon" aria-hidden="true">
{props.icon || props.label}
{props.keyBindingLabel && (
<span className="ToolIcon__keybinding">
{props.keyBindingLabel}
</span>
)}
</div>
)}
{props.showAriaLabel && (
<div className="ToolIcon__label">{props["aria-label"]}</div>
)}
{props.children}
</button>
);
}
return (
<label className={clsx("ToolIcon", props.className)} title={props.title}>
<input
className={`ToolIcon_type_radio ${sizeCn}`}
type="radio"
name={props.name}
aria-label={props["aria-label"]}
aria-keyshortcuts={props["aria-keyshortcuts"]}
data-testid={props["data-testid"]}
onChange={props.onChange}
checked={props.checked}
ref={innerRef}
/>
<div className="ToolIcon__icon">
{props.icon}
{props.keyBindingLabel && (
<span className="ToolIcon__keybinding">{props.keyBindingLabel}</span>
)}
</div>
</label>
);
});
ToolButton.defaultProps = {
visible: true,
className: "",
size: "medium",
};
-235
View File
@@ -1,235 +0,0 @@
@import "open-color/open-color.scss";
@import "../css/variables.module.scss";
.excalidraw {
.ToolIcon {
display: inline-flex;
align-items: center;
position: relative;
font-family: Cascadia;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor);
user-select: none;
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
}
.ToolIcon--plain {
background-color: transparent;
.ToolIcon__icon {
width: 2rem;
height: 2rem;
}
}
.ToolIcon__icon {
width: 2.5rem;
height: 2.5rem;
color: var(--icon-fill-color);
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--space-factor);
svg {
position: relative;
height: 1em;
fill: var(--icon-fill-color);
color: var(--icon-fill-color);
}
& + .ToolIcon__label {
margin-inline-start: 0;
}
}
.ToolIcon__label {
color: var(--icon-fill-color);
font-family: var(--ui-font);
margin: 0 0.8em;
text-overflow: ellipsis;
}
.ToolIcon_size_small .ToolIcon__icon {
width: 2rem;
height: 2rem;
font-size: 0.8em;
}
.excalidraw .ToolIcon_type_button,
.Modal .ToolIcon_type_button,
.ToolIcon_type_button {
padding: 0;
border: none;
margin: 0;
font-size: inherit;
&:focus {
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
&.ToolIcon--selected {
background-color: var(--button-gray-2);
&:active {
background-color: var(--button-gray-3);
}
}
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
&--show {
visibility: visible;
}
&--hide {
visibility: hidden;
}
}
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
background-color: var(--button-gray-2);
&:active {
background-color: var(--button-gray-3);
}
}
&:focus + .ToolIcon__icon {
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
&:active + .ToolIcon__icon {
background-color: var(--button-gray-3);
}
}
.ToolIcon_type_floating {
background-color: transparent;
&:hover {
background-color: transparent;
}
&:active {
background-color: transparent;
}
&:focus {
box-shadow: none;
}
.ToolIcon__icon {
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
width: 2rem;
height: 2em;
}
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__keybinding {
position: absolute;
bottom: 2px;
right: 3px;
font-size: 0.5em;
color: var(--keybinding-color);
font-family: var(--ui-font);
user-select: none;
}
// shrink shape icons on small viewports to make them fit
@media (max-width: 425px) {
.Shape .ToolIcon__icon {
width: 2rem;
height: 2rem;
svg {
height: 0.8em;
}
}
}
// move the lock button out of the way on small viewports
// it begins to collide with the GitHub icon before we switch to mobile mode
@media (max-width: 760px) {
.ToolIcon.ToolIcon_type_floating {
display: inline-block;
position: absolute;
right: -8px;
margin-left: 0;
border-radius: 20px 0 0 20px;
z-index: 1;
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-1);
}
&:active {
background-color: var(--button-gray-2);
}
.ToolIcon__icon {
border-radius: inherit;
}
svg {
position: static;
}
}
.ToolIcon.ToolIcon__library {
top: 100px;
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: 0;
top: 60px;
}
}
.unlocked-icon {
:root[dir="ltr"] & {
left: 2px;
}
:root[dir="rtl"] & {
right: 2px;
}
}
}
-55
View File
@@ -1,55 +0,0 @@
//
// All icons are imported from https://fontawesome.com/icons?d=gallery
// Icons are under the license https://fontawesome.com/license
//
// Note: when adding new icons, review https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/RTL_Guidelines
// to determine whether or not the icons should be mirrored in right-to-left languages.
import React from "react";
import clsx from "clsx";
type Opts = {
width?: number;
height?: number;
mirror?: true;
} & React.SVGProps<SVGSVGElement>;
export const createIcon = (
d: string | React.ReactNode,
opts: number | Opts = 512,
) => {
const {
width = 512,
height = width,
mirror,
style,
} = typeof opts === "number" ? ({ width: opts } as Opts) : opts;
return (
<svg
aria-hidden="true"
focusable="false"
role="img"
viewBox={`0 0 ${width} ${height}`}
className={clsx({ "rtl-mirror": mirror })}
style={style}
>
{typeof d === "string" ? <path fill="currentColor" d={d} /> : d}
</svg>
);
};
// not mirrored because it's inspired by a playback control, which is always RTL
export const start = createIcon(
"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm115.7 272l-176 101c-15.8 8.8-35.7-2.5-35.7-21V152c0-18.4 19.8-29.8 35.7-21l176 107c16.4 9.2 16.4 32.9 0 42z",
);
export const stop = createIcon(
"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm96 328c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V176c0-8.8 7.2-16 16-16h160c8.8 0 16 7.2 16 16v160z",
);
export const close = createIcon(
"M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z",
{ width: 352, height: 512 },
);
+4
View File
@@ -5,3 +5,7 @@ export enum EVENT {
export const WEBEX_URL =
"https://binaries.webex.com/static-content-pipeline/webex-embedded-app/v1/webex-embedded-app-sdk.js";
export const ENV = {
DEVELOPMENT: "development",
};
+6
View File
@@ -1,3 +1,5 @@
import { ENV } from "./constants";
export type ResolvablePromise<T> = Promise<T> & {
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
reject: (error: Error) => void;
@@ -25,3 +27,7 @@ export const loadScript = (filePath: string) => {
document.head.append(script);
});
};
export const isDev = () => {
return process.env.NODE_ENV === ENV.DEVELOPMENT;
};