feat: follow mode (#6848)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
88a2b286c7
commit
aad8ab0123
@ -15,11 +15,14 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000;
|
|||||||
export const WS_EVENTS = {
|
export const WS_EVENTS = {
|
||||||
SERVER_VOLATILE: "server-volatile-broadcast",
|
SERVER_VOLATILE: "server-volatile-broadcast",
|
||||||
SERVER: "server-broadcast",
|
SERVER: "server-broadcast",
|
||||||
};
|
USER_FOLLOW_CHANGE: "user-follow",
|
||||||
|
USER_FOLLOW_ROOM_CHANGE: "user-follow-room-change",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export enum WS_SCENE_EVENT_TYPES {
|
export enum WS_SUBTYPES {
|
||||||
INIT = "SCENE_INIT",
|
INIT = "SCENE_INIT",
|
||||||
UPDATE = "SCENE_UPDATE",
|
UPDATE = "SCENE_UPDATE",
|
||||||
|
USER_VIEWPORT_BOUNDS = "USER_VIEWPORT_BOUNDS",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FIREBASE_STORAGE_PREFIXES = {
|
export const FIREBASE_STORAGE_PREFIXES = {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { PureComponent } from "react";
|
import { PureComponent } from "react";
|
||||||
import { ExcalidrawImperativeAPI } from "../../packages/excalidraw/types";
|
import {
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
SocketId,
|
||||||
|
} from "../../packages/excalidraw/types";
|
||||||
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
||||||
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
@ -11,11 +14,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
|
zoomToFitBounds,
|
||||||
} from "../../packages/excalidraw/index";
|
} from "../../packages/excalidraw/index";
|
||||||
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
|
throttleRAF,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
} from "../../packages/excalidraw/utils";
|
} from "../../packages/excalidraw/utils";
|
||||||
import {
|
import {
|
||||||
@ -24,8 +30,9 @@ import {
|
|||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||||
LOAD_IMAGES_TIMEOUT,
|
LOAD_IMAGES_TIMEOUT,
|
||||||
WS_SCENE_EVENT_TYPES,
|
WS_SUBTYPES,
|
||||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
|
WS_EVENTS,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
@ -74,6 +81,7 @@ import { resetBrowserStateVersions } from "../data/tabSync";
|
|||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { appJotaiStore } from "../app-jotai";
|
import { appJotaiStore } from "../app-jotai";
|
||||||
|
import { Mutable } from "../../packages/excalidraw/utility-types";
|
||||||
|
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const collabDialogShownAtom = atom(false);
|
export const collabDialogShownAtom = atom(false);
|
||||||
@ -154,12 +162,28 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
this.idleTimeoutId = null;
|
this.idleTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onUmmount: (() => void) | null = null;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||||
window.addEventListener("online", this.onOfflineStatusToggle);
|
window.addEventListener("online", this.onOfflineStatusToggle);
|
||||||
window.addEventListener("offline", this.onOfflineStatusToggle);
|
window.addEventListener("offline", this.onOfflineStatusToggle);
|
||||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||||
|
|
||||||
|
const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => {
|
||||||
|
this.portal.socket && this.portal.broadcastUserFollowed(payload);
|
||||||
|
});
|
||||||
|
const throttledRelayUserViewportBounds = throttleRAF(
|
||||||
|
this.relayUserViewportBounds,
|
||||||
|
);
|
||||||
|
const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() =>
|
||||||
|
throttledRelayUserViewportBounds(),
|
||||||
|
);
|
||||||
|
this.onUmmount = () => {
|
||||||
|
unsubOnUserFollow();
|
||||||
|
unsubOnScrollChange();
|
||||||
|
};
|
||||||
|
|
||||||
this.onOfflineStatusToggle();
|
this.onOfflineStatusToggle();
|
||||||
|
|
||||||
const collabAPI: CollabAPI = {
|
const collabAPI: CollabAPI = {
|
||||||
@ -207,6 +231,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
window.clearTimeout(this.idleTimeoutId);
|
window.clearTimeout(this.idleTimeoutId);
|
||||||
this.idleTimeoutId = null;
|
this.idleTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
this.onUmmount?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||||
@ -489,7 +514,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
switch (decryptedData.type) {
|
switch (decryptedData.type) {
|
||||||
case "INVALID_RESPONSE":
|
case "INVALID_RESPONSE":
|
||||||
return;
|
return;
|
||||||
case WS_SCENE_EVENT_TYPES.INIT: {
|
case WS_SUBTYPES.INIT: {
|
||||||
if (!this.portal.socketInitialized) {
|
if (!this.portal.socketInitialized) {
|
||||||
this.initializeRoom({ fetchScene: false });
|
this.initializeRoom({ fetchScene: false });
|
||||||
const remoteElements = decryptedData.payload.elements;
|
const remoteElements = decryptedData.payload.elements;
|
||||||
@ -505,7 +530,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WS_SCENE_EVENT_TYPES.UPDATE:
|
case WS_SUBTYPES.UPDATE:
|
||||||
this.handleRemoteSceneUpdate(
|
this.handleRemoteSceneUpdate(
|
||||||
this.reconcileElements(decryptedData.payload.elements),
|
this.reconcileElements(decryptedData.payload.elements),
|
||||||
);
|
);
|
||||||
@ -513,31 +538,61 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
case "MOUSE_LOCATION": {
|
case "MOUSE_LOCATION": {
|
||||||
const { pointer, button, username, selectedElementIds } =
|
const { pointer, button, username, selectedElementIds } =
|
||||||
decryptedData.payload;
|
decryptedData.payload;
|
||||||
|
|
||||||
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
||||||
decryptedData.payload.socketId ||
|
decryptedData.payload.socketId ||
|
||||||
// @ts-ignore legacy, see #2094 (#2097)
|
// @ts-ignore legacy, see #2094 (#2097)
|
||||||
decryptedData.payload.socketID;
|
decryptedData.payload.socketID;
|
||||||
|
|
||||||
const collaborators = new Map(this.collaborators);
|
this.updateCollaborator(socketId, {
|
||||||
const user = collaborators.get(socketId) || {}!;
|
pointer,
|
||||||
user.pointer = pointer;
|
button,
|
||||||
user.button = button;
|
selectedElementIds,
|
||||||
user.selectedElementIds = selectedElementIds;
|
username,
|
||||||
user.username = username;
|
|
||||||
collaborators.set(socketId, user);
|
|
||||||
this.excalidrawAPI.updateScene({
|
|
||||||
collaborators,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case WS_SUBTYPES.USER_VIEWPORT_BOUNDS: {
|
||||||
|
const { bounds, socketId } = decryptedData.payload;
|
||||||
|
|
||||||
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
|
|
||||||
|
// we're not following the user
|
||||||
|
// (shouldn't happen, but could be late message or bug upstream)
|
||||||
|
if (appState.userToFollow?.socketId !== socketId) {
|
||||||
|
console.warn(
|
||||||
|
`receiving remote client's (from ${socketId}) viewport bounds even though we're not subscribed to it!`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross-follow case, ignore updates in this case
|
||||||
|
if (
|
||||||
|
appState.userToFollow &&
|
||||||
|
appState.followedBy.has(appState.userToFollow.socketId)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
appState: zoomToFitBounds({
|
||||||
|
appState,
|
||||||
|
bounds,
|
||||||
|
fitToViewport: true,
|
||||||
|
viewportZoomFactor: 1,
|
||||||
|
}).appState,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "IDLE_STATUS": {
|
case "IDLE_STATUS": {
|
||||||
const { userState, socketId, username } = decryptedData.payload;
|
const { userState, socketId, username } = decryptedData.payload;
|
||||||
const collaborators = new Map(this.collaborators);
|
this.updateCollaborator(socketId, {
|
||||||
const user = collaborators.get(socketId) || {}!;
|
userState,
|
||||||
user.userState = userState;
|
username,
|
||||||
user.username = username;
|
|
||||||
this.excalidrawAPI.updateScene({
|
|
||||||
collaborators,
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -556,6 +611,17 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
scenePromise.resolve(sceneData);
|
scenePromise.resolve(sceneData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.portal.socket.on(
|
||||||
|
WS_EVENTS.USER_FOLLOW_ROOM_CHANGE,
|
||||||
|
(followedBy: string[]) => {
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
appState: { followedBy: new Set(followedBy) },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.relayUserViewportBounds({ shouldPerform: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.initializeIdleDetector();
|
this.initializeIdleDetector();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -738,6 +804,24 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
this.excalidrawAPI.updateScene({ collaborators });
|
this.excalidrawAPI.updateScene({ collaborators });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateCollaborator = (
|
||||||
|
socketId: SocketId,
|
||||||
|
updates: Partial<Collaborator>,
|
||||||
|
) => {
|
||||||
|
const collaborators = new Map(this.collaborators);
|
||||||
|
const user: Mutable<Collaborator> = Object.assign(
|
||||||
|
{},
|
||||||
|
collaborators.get(socketId),
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
collaborators.set(socketId, user);
|
||||||
|
this.collaborators = collaborators;
|
||||||
|
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
collaborators,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = version;
|
this.lastBroadcastedOrReceivedSceneVersion = version;
|
||||||
};
|
};
|
||||||
@ -763,6 +847,30 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
CURSOR_SYNC_TIMEOUT,
|
CURSOR_SYNC_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
relayUserViewportBounds = (props?: { shouldPerform: boolean }) => {
|
||||||
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.portal.socket &&
|
||||||
|
(appState.followedBy.size > 0 || props?.shouldPerform)
|
||||||
|
) {
|
||||||
|
const { x: x1, y: y1 } = viewportCoordsToSceneCoords(
|
||||||
|
{ clientX: 0, clientY: 0 },
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { x: x2, y: y2 } = viewportCoordsToSceneCoords(
|
||||||
|
{ clientX: appState.width, clientY: appState.height },
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.portal.broadcastUserViewportBounds(
|
||||||
|
{ bounds: [x1, y1, x2, y2] },
|
||||||
|
`follow_${this.portal.socket.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onIdleStateChange = (userState: UserIdleState) => {
|
onIdleStateChange = (userState: UserIdleState) => {
|
||||||
this.portal.broadcastIdleChange(userState);
|
this.portal.broadcastIdleChange(userState);
|
||||||
};
|
};
|
||||||
@ -772,7 +880,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
getSceneVersion(elements) >
|
getSceneVersion(elements) >
|
||||||
this.getLastBroadcastedOrReceivedSceneVersion()
|
this.getLastBroadcastedOrReceivedSceneVersion()
|
||||||
) {
|
) {
|
||||||
this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false);
|
this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false);
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
||||||
this.queueBroadcastAllElements();
|
this.queueBroadcastAllElements();
|
||||||
}
|
}
|
||||||
@ -785,7 +893,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
queueBroadcastAllElements = throttle(() => {
|
queueBroadcastAllElements = throttle(() => {
|
||||||
this.portal.broadcastScene(
|
this.portal.broadcastScene(
|
||||||
WS_SCENE_EVENT_TYPES.UPDATE,
|
WS_SUBTYPES.UPDATE,
|
||||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
@ -7,12 +7,11 @@ import {
|
|||||||
import { TCollabClass } from "./Collab";
|
import { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||||
|
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||||
import {
|
import {
|
||||||
WS_EVENTS,
|
OnUserFollowedPayload,
|
||||||
FILE_UPLOAD_TIMEOUT,
|
UserIdleState,
|
||||||
WS_SCENE_EVENT_TYPES,
|
} from "../../packages/excalidraw/types";
|
||||||
} from "../app_constants";
|
|
||||||
import { UserIdleState } from "../../packages/excalidraw/types";
|
|
||||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
@ -46,7 +45,7 @@ class Portal {
|
|||||||
});
|
});
|
||||||
this.socket.on("new-user", async (_socketId: string) => {
|
this.socket.on("new-user", async (_socketId: string) => {
|
||||||
this.broadcastScene(
|
this.broadcastScene(
|
||||||
WS_SCENE_EVENT_TYPES.INIT,
|
WS_SUBTYPES.INIT,
|
||||||
this.collab.getSceneElementsIncludingDeleted(),
|
this.collab.getSceneElementsIncludingDeleted(),
|
||||||
/* syncAll */ true,
|
/* syncAll */ true,
|
||||||
);
|
);
|
||||||
@ -83,6 +82,7 @@ class Portal {
|
|||||||
async _broadcastSocketData(
|
async _broadcastSocketData(
|
||||||
data: SocketUpdateData,
|
data: SocketUpdateData,
|
||||||
volatile: boolean = false,
|
volatile: boolean = false,
|
||||||
|
roomId?: string,
|
||||||
) {
|
) {
|
||||||
if (this.isOpen()) {
|
if (this.isOpen()) {
|
||||||
const json = JSON.stringify(data);
|
const json = JSON.stringify(data);
|
||||||
@ -91,7 +91,7 @@ class Portal {
|
|||||||
|
|
||||||
this.socket?.emit(
|
this.socket?.emit(
|
||||||
volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER,
|
volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER,
|
||||||
this.roomId,
|
roomId ?? this.roomId,
|
||||||
encryptedBuffer,
|
encryptedBuffer,
|
||||||
iv,
|
iv,
|
||||||
);
|
);
|
||||||
@ -130,11 +130,11 @@ class Portal {
|
|||||||
}, FILE_UPLOAD_TIMEOUT);
|
}, FILE_UPLOAD_TIMEOUT);
|
||||||
|
|
||||||
broadcastScene = async (
|
broadcastScene = async (
|
||||||
updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE,
|
updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
|
||||||
allElements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
syncAll: boolean,
|
syncAll: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (updateType === WS_SCENE_EVENT_TYPES.INIT && !syncAll) {
|
if (updateType === WS_SUBTYPES.INIT && !syncAll) {
|
||||||
throw new Error("syncAll must be true when sending SCENE.INIT");
|
throw new Error("syncAll must be true when sending SCENE.INIT");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,12 +213,43 @@ class Portal {
|
|||||||
username: this.collab.state.username,
|
username: this.collab.state.username,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._broadcastSocketData(
|
return this._broadcastSocketData(
|
||||||
data as SocketUpdateData,
|
data as SocketUpdateData,
|
||||||
true, // volatile
|
true, // volatile
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
broadcastUserViewportBounds = (
|
||||||
|
payload: {
|
||||||
|
bounds: [number, number, number, number];
|
||||||
|
},
|
||||||
|
roomId: string,
|
||||||
|
) => {
|
||||||
|
if (this.socket?.id) {
|
||||||
|
const data: SocketUpdateDataSource["USER_VIEWPORT_BOUNDS"] = {
|
||||||
|
type: WS_SUBTYPES.USER_VIEWPORT_BOUNDS,
|
||||||
|
payload: {
|
||||||
|
socketId: this.socket.id,
|
||||||
|
username: this.collab.state.username,
|
||||||
|
bounds: payload.bounds,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._broadcastSocketData(
|
||||||
|
data as SocketUpdateData,
|
||||||
|
true, // volatile
|
||||||
|
roomId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
broadcastUserFollowed = (payload: OnUserFollowedPayload) => {
|
||||||
|
if (this.socket?.id) {
|
||||||
|
this.socket.emit(WS_EVENTS.USER_FOLLOW_CHANGE, payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Portal;
|
export default Portal;
|
||||||
|
@ -119,6 +119,14 @@ export type SocketUpdateDataSource = {
|
|||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
USER_VIEWPORT_BOUNDS: {
|
||||||
|
type: "USER_VIEWPORT_BOUNDS";
|
||||||
|
payload: {
|
||||||
|
socketId: string;
|
||||||
|
username: string;
|
||||||
|
bounds: [number, number, number, number];
|
||||||
|
};
|
||||||
|
};
|
||||||
IDLE_STATUS: {
|
IDLE_STATUS: {
|
||||||
type: "IDLE_STATUS";
|
type: "IDLE_STATUS";
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -109,6 +109,7 @@ export const actionZoomIn = register({
|
|||||||
},
|
},
|
||||||
appState,
|
appState,
|
||||||
),
|
),
|
||||||
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -146,6 +147,7 @@ export const actionZoomOut = register({
|
|||||||
},
|
},
|
||||||
appState,
|
appState,
|
||||||
),
|
),
|
||||||
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -183,6 +185,7 @@ export const actionResetZoom = register({
|
|||||||
},
|
},
|
||||||
appState,
|
appState,
|
||||||
),
|
),
|
||||||
|
userToFollow: null,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
@ -226,22 +229,20 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFit = ({
|
export const zoomToFitBounds = ({
|
||||||
targetElements,
|
bounds,
|
||||||
appState,
|
appState,
|
||||||
fitToViewport = false,
|
fitToViewport = false,
|
||||||
viewportZoomFactor = 0.7,
|
viewportZoomFactor = 0.7,
|
||||||
}: {
|
}: {
|
||||||
targetElements: readonly ExcalidrawElement[];
|
bounds: readonly [number, number, number, number];
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
fitToViewport: boolean;
|
fitToViewport: boolean;
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
const [x1, y1, x2, y2] = bounds;
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = commonBounds;
|
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
|
||||||
@ -282,7 +283,7 @@ export const zoomToFit = ({
|
|||||||
scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX;
|
scrollX = (appStateWidth / 2) * (1 / newZoomValue) - centerX;
|
||||||
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
||||||
} else {
|
} else {
|
||||||
newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
|
newZoomValue = zoomValueToFitBoundsOnViewport(bounds, {
|
||||||
width: appState.width,
|
width: appState.width,
|
||||||
height: appState.height,
|
height: appState.height,
|
||||||
});
|
});
|
||||||
@ -311,6 +312,29 @@ export const zoomToFit = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const zoomToFit = ({
|
||||||
|
targetElements,
|
||||||
|
appState,
|
||||||
|
fitToViewport,
|
||||||
|
viewportZoomFactor,
|
||||||
|
}: {
|
||||||
|
targetElements: readonly ExcalidrawElement[];
|
||||||
|
appState: Readonly<AppState>;
|
||||||
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
|
fitToViewport: boolean;
|
||||||
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
|
viewportZoomFactor?: number;
|
||||||
|
}) => {
|
||||||
|
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
||||||
|
|
||||||
|
return zoomToFitBounds({
|
||||||
|
bounds: commonBounds,
|
||||||
|
appState,
|
||||||
|
fitToViewport,
|
||||||
|
viewportZoomFactor,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Note, this action differs from actionZoomToFitSelection in that it doesn't
|
// Note, this action differs from actionZoomToFitSelection in that it doesn't
|
||||||
// zoom beyond 100%. In other words, if the content is smaller than viewport
|
// zoom beyond 100%. In other words, if the content is smaller than viewport
|
||||||
// size, it won't be zoomed in.
|
// size, it won't be zoomed in.
|
||||||
@ -321,7 +345,10 @@ export const actionZoomToFitSelectionInViewport = register({
|
|||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
return zoomToFit({
|
return zoomToFit({
|
||||||
targetElements: selectedElements.length ? selectedElements : elements,
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
appState,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
userToFollow: null,
|
||||||
|
},
|
||||||
fitToViewport: false,
|
fitToViewport: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -341,7 +368,10 @@ export const actionZoomToFitSelection = register({
|
|||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
return zoomToFit({
|
return zoomToFit({
|
||||||
targetElements: selectedElements.length ? selectedElements : elements,
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
appState,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
userToFollow: null,
|
||||||
|
},
|
||||||
fitToViewport: true,
|
fitToViewport: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -358,7 +388,14 @@ export const actionZoomToFit = register({
|
|||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState) =>
|
||||||
zoomToFit({ targetElements: elements, appState, fitToViewport: false }),
|
zoomToFit({
|
||||||
|
targetElements: elements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
userToFollow: null,
|
||||||
|
},
|
||||||
|
fitToViewport: false,
|
||||||
|
}),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { getClientColor } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
|
||||||
import { Collaborator } from "../types";
|
import { Collaborator } from "../types";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@ -9,39 +8,68 @@ export const actionGoToCollaborator = register({
|
|||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "collab" },
|
trackEvent: { category: "collab" },
|
||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value) => {
|
||||||
const point = value as Collaborator["pointer"];
|
const _value = value as Collaborator;
|
||||||
|
const point = _value.pointer;
|
||||||
|
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return { appState, commitToHistory: false };
|
return { appState, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appState.userToFollow?.socketId === _value.socketId) {
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
userToFollow: null,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...centerScrollOn({
|
userToFollow: {
|
||||||
scenePoint: point,
|
socketId: _value.socketId!,
|
||||||
viewportDimensions: {
|
username: _value.username || "",
|
||||||
width: appState.width,
|
},
|
||||||
height: appState.height,
|
|
||||||
},
|
|
||||||
zoom: appState.zoom,
|
|
||||||
}),
|
|
||||||
// Close mobile menu
|
// Close mobile menu
|
||||||
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ updateData, data }) => {
|
PanelComponent: ({ updateData, data, appState }) => {
|
||||||
const [clientId, collaborator] = data as [string, Collaborator];
|
const [clientId, collaborator, withName] = data as [
|
||||||
|
string,
|
||||||
|
Collaborator,
|
||||||
|
boolean,
|
||||||
|
];
|
||||||
|
|
||||||
const background = getClientColor(clientId);
|
const background = getClientColor(clientId);
|
||||||
|
|
||||||
return (
|
return withName ? (
|
||||||
|
<div
|
||||||
|
className="dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
onClick={() => updateData({ ...collaborator, clientId })}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
color={background}
|
||||||
|
onClick={() => {}}
|
||||||
|
name={collaborator.username || ""}
|
||||||
|
src={collaborator.avatarUrl}
|
||||||
|
isBeingFollowed={appState.userToFollow?.socketId === clientId}
|
||||||
|
/>
|
||||||
|
{collaborator.username}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
color={background}
|
color={background}
|
||||||
onClick={() => updateData(collaborator.pointer)}
|
onClick={() => {
|
||||||
|
updateData({ ...collaborator, clientId });
|
||||||
|
}}
|
||||||
name={collaborator.username || ""}
|
name={collaborator.username || ""}
|
||||||
src={collaborator.avatarUrl}
|
src={collaborator.avatarUrl}
|
||||||
|
isBeingFollowed={appState.userToFollow?.socketId === clientId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -105,6 +105,8 @@ export const getDefaultAppState = (): Omit<
|
|||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
objectsSnapModeEnabled: false,
|
objectsSnapModeEnabled: false,
|
||||||
|
userToFollow: null,
|
||||||
|
followedBy: new Set(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -215,6 +217,8 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
snapLines: { browser: false, export: false, server: false },
|
snapLines: { browser: false, export: false, server: false },
|
||||||
originSnapOffset: { browser: false, export: false, server: false },
|
originSnapOffset: { browser: false, export: false, server: false },
|
||||||
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
||||||
|
userToFollow: { browser: false, export: false, server: false },
|
||||||
|
followedBy: { browser: false, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
@ -244,6 +244,7 @@ import {
|
|||||||
KeyboardModifiersObject,
|
KeyboardModifiersObject,
|
||||||
CollaboratorPointer,
|
CollaboratorPointer,
|
||||||
ToolType,
|
ToolType,
|
||||||
|
OnUserFollowedPayload,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
@ -396,6 +397,7 @@ import { COLOR_PALETTE } from "../colors";
|
|||||||
import { ElementCanvasButton } from "./MagicButton";
|
import { ElementCanvasButton } from "./MagicButton";
|
||||||
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
||||||
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
||||||
|
import FollowMode from "./FollowMode/FollowMode";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@ -551,6 +553,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
]
|
]
|
||||||
>();
|
>();
|
||||||
|
onUserFollowEmitter = new Emitter<[payload: OnUserFollowedPayload]>();
|
||||||
|
onScrollChangeEmitter = new Emitter<
|
||||||
|
[scrollX: number, scrollY: number, zoom: AppState["zoom"]]
|
||||||
|
>();
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -620,6 +626,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onChange: (cb) => this.onChangeEmitter.on(cb),
|
onChange: (cb) => this.onChangeEmitter.on(cb),
|
||||||
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
|
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
|
||||||
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
|
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
|
||||||
|
onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb),
|
||||||
|
onUserFollow: (cb) => this.onUserFollowEmitter.on(cb),
|
||||||
} as const;
|
} as const;
|
||||||
if (typeof excalidrawAPI === "function") {
|
if (typeof excalidrawAPI === "function") {
|
||||||
excalidrawAPI(api);
|
excalidrawAPI(api);
|
||||||
@ -1582,6 +1590,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onPointerDown={this.handleCanvasPointerDown}
|
onPointerDown={this.handleCanvasPointerDown}
|
||||||
onDoubleClick={this.handleCanvasDoubleClick}
|
onDoubleClick={this.handleCanvasDoubleClick}
|
||||||
/>
|
/>
|
||||||
|
{this.state.userToFollow && (
|
||||||
|
<FollowMode
|
||||||
|
width={this.state.width}
|
||||||
|
height={this.state.height}
|
||||||
|
userToFollow={this.state.userToFollow}
|
||||||
|
onDisconnect={this.maybeUnfollowRemoteUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{this.renderFrameNames()}
|
{this.renderFrameNames()}
|
||||||
</ExcalidrawActionManagerContext.Provider>
|
</ExcalidrawActionManagerContext.Provider>
|
||||||
{this.renderEmbeddables()}
|
{this.renderEmbeddables()}
|
||||||
@ -2531,11 +2547,45 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.refreshEditorBreakpoints();
|
this.refreshEditorBreakpoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFollowedPersonLeft =
|
||||||
|
prevState.userToFollow &&
|
||||||
|
!this.state.collaborators.has(prevState.userToFollow.socketId);
|
||||||
|
|
||||||
|
if (hasFollowedPersonLeft) {
|
||||||
|
this.maybeUnfollowRemoteUser();
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
prevState.zoom.value !== this.state.zoom.value ||
|
||||||
prevState.scrollX !== this.state.scrollX ||
|
prevState.scrollX !== this.state.scrollX ||
|
||||||
prevState.scrollY !== this.state.scrollY
|
prevState.scrollY !== this.state.scrollY
|
||||||
) {
|
) {
|
||||||
this.props?.onScrollChange?.(this.state.scrollX, this.state.scrollY);
|
this.props?.onScrollChange?.(
|
||||||
|
this.state.scrollX,
|
||||||
|
this.state.scrollY,
|
||||||
|
this.state.zoom,
|
||||||
|
);
|
||||||
|
this.onScrollChangeEmitter.trigger(
|
||||||
|
this.state.scrollX,
|
||||||
|
this.state.scrollY,
|
||||||
|
this.state.zoom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevState.userToFollow !== this.state.userToFollow) {
|
||||||
|
if (prevState.userToFollow) {
|
||||||
|
this.onUserFollowEmitter.trigger({
|
||||||
|
userToFollow: prevState.userToFollow,
|
||||||
|
action: "UNFOLLOW",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.userToFollow) {
|
||||||
|
this.onUserFollowEmitter.trigger({
|
||||||
|
userToFollow: this.state.userToFollow,
|
||||||
|
action: "FOLLOW",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -3421,11 +3471,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private maybeUnfollowRemoteUser = () => {
|
||||||
|
if (this.state.userToFollow) {
|
||||||
|
this.setState({ userToFollow: null });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** use when changing scrollX/scrollY/zoom based on user interaction */
|
/** use when changing scrollX/scrollY/zoom based on user interaction */
|
||||||
private translateCanvas: React.Component<any, AppState>["setState"] = (
|
private translateCanvas: React.Component<any, AppState>["setState"] = (
|
||||||
state,
|
state,
|
||||||
) => {
|
) => {
|
||||||
this.cancelInProgresAnimation?.();
|
this.cancelInProgresAnimation?.();
|
||||||
|
this.maybeUnfollowRemoteUser();
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -5154,6 +5211,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private handleCanvasPointerDown = (
|
private handleCanvasPointerDown = (
|
||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
) => {
|
) => {
|
||||||
|
this.maybeUnfollowRemoteUser();
|
||||||
|
|
||||||
// since contextMenu options are potentially evaluated on each render,
|
// since contextMenu options are potentially evaluated on each render,
|
||||||
// and an contextMenu action may depend on selection state, we must
|
// and an contextMenu action may depend on selection state, we must
|
||||||
// close the contextMenu before we update the selection on pointerDown
|
// close the contextMenu before we update the selection on pointerDown
|
||||||
|
@ -2,34 +2,6 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Avatar {
|
.Avatar {
|
||||||
width: 1.25rem;
|
@include avatarStyles;
|
||||||
height: 1.25rem;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 100%;
|
|
||||||
outline-offset: 2px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -3px;
|
|
||||||
right: -3px;
|
|
||||||
bottom: -3px;
|
|
||||||
left: -3px;
|
|
||||||
border: 1px solid var(--avatar-border-color);
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,33 @@ import "./Avatar.scss";
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { getNameInitial } from "../clients";
|
import { getNameInitial } from "../clients";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
type AvatarProps = {
|
type AvatarProps = {
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
color: string;
|
color: string;
|
||||||
name: string;
|
name: string;
|
||||||
src?: string;
|
src?: string;
|
||||||
|
isBeingFollowed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
|
export const Avatar = ({
|
||||||
|
color,
|
||||||
|
onClick,
|
||||||
|
name,
|
||||||
|
src,
|
||||||
|
isBeingFollowed,
|
||||||
|
}: AvatarProps) => {
|
||||||
const shortName = getNameInitial(name);
|
const shortName = getNameInitial(name);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const loadImg = !error && src;
|
const loadImg = !error && src;
|
||||||
const style = loadImg ? undefined : { background: color };
|
const style = loadImg ? undefined : { background: color };
|
||||||
return (
|
return (
|
||||||
<div className="Avatar" style={style} onClick={onClick}>
|
<div
|
||||||
|
className={clsx("Avatar", { "Avatar--is-followed": isBeingFollowed })}
|
||||||
|
style={style}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
{loadImg ? (
|
{loadImg ? (
|
||||||
<img
|
<img
|
||||||
className="Avatar-img"
|
className="Avatar-img"
|
||||||
|
59
packages/excalidraw/components/FollowMode/FollowMode.scss
Normal file
59
packages/excalidraw/components/FollowMode/FollowMode.scss
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
.excalidraw {
|
||||||
|
.follow-mode {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 2px solid var(--color-primary-hover);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
color: var(--color-primary-light);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
pointer-events: all;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
display: flex;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__username {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__disconnect-btn {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
packages/excalidraw/components/FollowMode/FollowMode.tsx
Normal file
43
packages/excalidraw/components/FollowMode/FollowMode.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { UserToFollow } from "../../types";
|
||||||
|
import { CloseIcon } from "../icons";
|
||||||
|
import "./FollowMode.scss";
|
||||||
|
|
||||||
|
interface FollowModeProps {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
userToFollow: UserToFollow;
|
||||||
|
onDisconnect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FollowMode = ({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
userToFollow,
|
||||||
|
onDisconnect,
|
||||||
|
}: FollowModeProps) => {
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<div className="follow-mode" style={{ width, height }}>
|
||||||
|
<div className="follow-mode__badge">
|
||||||
|
<div className="follow-mode__badge__label">
|
||||||
|
Following{" "}
|
||||||
|
<span
|
||||||
|
className="follow-mode__badge__username"
|
||||||
|
title={userToFollow.username}
|
||||||
|
>
|
||||||
|
{userToFollow.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onDisconnect}
|
||||||
|
className="follow-mode__disconnect-btn"
|
||||||
|
>
|
||||||
|
{CloseIcon}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FollowMode;
|
@ -21,6 +21,10 @@
|
|||||||
width: var(--lg-icon-size);
|
width: var(--lg-icon-size);
|
||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__label-element {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-sidebar-trigger .sidebar-trigger__label {
|
.default-sidebar-trigger .sidebar-trigger__label {
|
||||||
|
@ -19,7 +19,7 @@ export const SidebarTrigger = ({
|
|||||||
const appState = useUIAppState();
|
const appState = useUIAppState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label title={title}>
|
<label title={title} className="sidebar-trigger__label-element">
|
||||||
<input
|
<input
|
||||||
className="ToolIcon_type_checkbox"
|
className="ToolIcon_type_checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.UserList {
|
.UserList {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -14,11 +16,13 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// can fit max 5 avatars in a column
|
box-sizing: border-box;
|
||||||
max-height: 140px;
|
|
||||||
|
|
||||||
// can fit max 10 avatars in a row when there's enough space
|
// can fit max 4 avatars (3 avatars + show more) in a column
|
||||||
max-width: 290px;
|
max-height: 120px;
|
||||||
|
|
||||||
|
// can fit max 4 avatars (3 avatars + show more) when there's enough space
|
||||||
|
max-width: 120px;
|
||||||
|
|
||||||
// Tweak in 30px increments to fit more/fewer avatars in a row/column ^^
|
// Tweak in 30px increments to fit more/fewer avatars in a row/column ^^
|
||||||
|
|
||||||
@ -33,5 +37,93 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
justify-content: normal;
|
justify-content: normal;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList__more {
|
||||||
|
@include avatarStyles;
|
||||||
|
background-color: var(--color-gray-20);
|
||||||
|
border: 0 !important;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
--userlist-hint-bg-color: var(--color-gray-10);
|
||||||
|
--userlist-hint-heading-color: var(--color-gray-80);
|
||||||
|
--userlist-hint-text-color: var(--color-gray-60);
|
||||||
|
--userlist-collaborators-border-color: var(--color-gray-20);
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--userlist-hint-bg-color: var(--color-gray-90);
|
||||||
|
--userlist-hint-heading-color: var(--color-gray-30);
|
||||||
|
--userlist-hint-text-color: var(--color-gray-40);
|
||||||
|
--userlist-collaborators-border-color: var(--color-gray-80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList__collaborators {
|
||||||
|
position: static;
|
||||||
|
top: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
max-height: 12rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-top: 1px solid var(--userlist-collaborators-border-color);
|
||||||
|
border-bottom: 1px solid var(--userlist-collaborators-border-color);
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
color: var(--color-gray-60);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 150%;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList__hint {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--userlist-hint-text-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList__search-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: 0.75rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList__search {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding-left: 2.5rem !important;
|
||||||
|
padding-right: 0.75rem !important;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,51 +2,234 @@ import "./UserList.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { AppState, Collaborator } from "../types";
|
import { Collaborator, SocketId } from "../types";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { useExcalidrawActionManager } from "./App";
|
import { useExcalidrawActionManager } from "./App";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
export const UserList: React.FC<{
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
className?: string;
|
import { Island } from "./Island";
|
||||||
mobile?: boolean;
|
import { searchIcon } from "./icons";
|
||||||
collaborators: AppState["collaborators"];
|
import { t } from "../i18n";
|
||||||
}> = ({ className, mobile, collaborators }) => {
|
import { isShallowEqual } from "../utils";
|
||||||
const actionManager = useExcalidrawActionManager();
|
|
||||||
|
|
||||||
const uniqueCollaborators = new Map<string, Collaborator>();
|
const FIRST_N_AVATARS = 3;
|
||||||
collaborators.forEach((collaborator, socketId) => {
|
const SHOW_COLLABORATORS_FILTER_AT = 8;
|
||||||
uniqueCollaborators.set(
|
|
||||||
// filter on user id, else fall back on unique socketId
|
|
||||||
collaborator.id || socketId,
|
|
||||||
collaborator,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const avatars =
|
const ConditionalTooltipWrapper = ({
|
||||||
uniqueCollaborators.size > 0 &&
|
shouldWrap,
|
||||||
Array.from(uniqueCollaborators)
|
children,
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
clientId,
|
||||||
.map(([clientId, collaborator]) => {
|
username,
|
||||||
const avatarJSX = actionManager.renderAction("goToCollaborator", [
|
}: {
|
||||||
clientId,
|
shouldWrap: boolean;
|
||||||
collaborator,
|
children: React.ReactNode;
|
||||||
]);
|
username?: string | null;
|
||||||
|
clientId: string;
|
||||||
|
}) =>
|
||||||
|
shouldWrap ? (
|
||||||
|
<Tooltip label={username || "Unknown user"} key={clientId}>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<React.Fragment key={clientId}>{children}</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
return mobile ? (
|
const renderCollaborator = ({
|
||||||
<Tooltip
|
actionManager,
|
||||||
label={collaborator.username || "Unknown user"}
|
collaborator,
|
||||||
key={clientId}
|
clientId,
|
||||||
>
|
withName = false,
|
||||||
{avatarJSX}
|
shouldWrapWithTooltip = false,
|
||||||
</Tooltip>
|
}: {
|
||||||
) : (
|
actionManager: ActionManager;
|
||||||
<React.Fragment key={clientId}>{avatarJSX}</React.Fragment>
|
collaborator: Collaborator;
|
||||||
);
|
clientId: string;
|
||||||
});
|
withName?: boolean;
|
||||||
|
shouldWrapWithTooltip?: boolean;
|
||||||
|
}) => {
|
||||||
|
const avatarJSX = actionManager.renderAction("goToCollaborator", [
|
||||||
|
clientId,
|
||||||
|
collaborator,
|
||||||
|
withName,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("UserList", className, { UserList_mobile: mobile })}>
|
<ConditionalTooltipWrapper
|
||||||
{avatars}
|
key={clientId}
|
||||||
</div>
|
clientId={clientId}
|
||||||
|
username={collaborator.username}
|
||||||
|
shouldWrap={shouldWrapWithTooltip}
|
||||||
|
>
|
||||||
|
{avatarJSX}
|
||||||
|
</ConditionalTooltipWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UserListUserObject = Pick<
|
||||||
|
Collaborator,
|
||||||
|
"avatarUrl" | "id" | "socketId" | "username"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type UserListProps = {
|
||||||
|
className?: string;
|
||||||
|
mobile?: boolean;
|
||||||
|
collaborators: Map<SocketId, UserListUserObject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const collaboratorComparatorKeys = [
|
||||||
|
"avatarUrl",
|
||||||
|
"id",
|
||||||
|
"socketId",
|
||||||
|
"username",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const UserList = React.memo(
|
||||||
|
({ className, mobile, collaborators }: UserListProps) => {
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
const uniqueCollaboratorsMap = new Map<string, Collaborator>();
|
||||||
|
|
||||||
|
collaborators.forEach((collaborator, socketId) => {
|
||||||
|
uniqueCollaboratorsMap.set(
|
||||||
|
// filter on user id, else fall back on unique socketId
|
||||||
|
collaborator.id || socketId,
|
||||||
|
{ ...collaborator, socketId },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// const uniqueCollaboratorsMap = sampleCollaborators;
|
||||||
|
const uniqueCollaboratorsArray = Array.from(uniqueCollaboratorsMap).filter(
|
||||||
|
([_, collaborator]) => Object.keys(collaborator).length !== 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = React.useState("");
|
||||||
|
|
||||||
|
if (uniqueCollaboratorsArray.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTermNormalized = searchTerm.trim().toLowerCase();
|
||||||
|
|
||||||
|
const filteredCollaborators = searchTermNormalized
|
||||||
|
? uniqueCollaboratorsArray.filter(([, collaborator]) =>
|
||||||
|
collaborator.username?.toLowerCase().includes(searchTerm),
|
||||||
|
)
|
||||||
|
: uniqueCollaboratorsArray;
|
||||||
|
|
||||||
|
const firstNCollaborators = uniqueCollaboratorsArray.slice(
|
||||||
|
0,
|
||||||
|
FIRST_N_AVATARS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstNAvatarsJSX = firstNCollaborators.map(
|
||||||
|
([clientId, collaborator]) =>
|
||||||
|
renderCollaborator({
|
||||||
|
actionManager,
|
||||||
|
collaborator,
|
||||||
|
clientId,
|
||||||
|
shouldWrapWithTooltip: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return mobile ? (
|
||||||
|
<div className={clsx("UserList UserList_mobile", className)}>
|
||||||
|
{uniqueCollaboratorsArray.map(([clientId, collaborator]) =>
|
||||||
|
renderCollaborator({
|
||||||
|
actionManager,
|
||||||
|
collaborator,
|
||||||
|
clientId,
|
||||||
|
shouldWrapWithTooltip: true,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={clsx("UserList", className)}>
|
||||||
|
{firstNAvatarsJSX}
|
||||||
|
|
||||||
|
{uniqueCollaboratorsArray.length > FIRST_N_AVATARS && (
|
||||||
|
<Popover.Root
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSearchTerm("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover.Trigger className="UserList__more">
|
||||||
|
+{uniqueCollaboratorsArray.length - FIRST_N_AVATARS}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content
|
||||||
|
style={{
|
||||||
|
zIndex: 2,
|
||||||
|
width: "12rem",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
align="end"
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
<Island style={{ overflow: "hidden" }}>
|
||||||
|
{uniqueCollaboratorsArray.length >=
|
||||||
|
SHOW_COLLABORATORS_FILTER_AT && (
|
||||||
|
<div className="UserList__search-wrapper">
|
||||||
|
{searchIcon}
|
||||||
|
<input
|
||||||
|
className="UserList__search"
|
||||||
|
type="text"
|
||||||
|
placeholder={t("userList.search.placeholder")}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="dropdown-menu UserList__collaborators">
|
||||||
|
{filteredCollaborators.length === 0 && (
|
||||||
|
<div className="UserList__collaborators__empty">
|
||||||
|
{t("userList.search.empty")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="UserList__hint">
|
||||||
|
{t("userList.hint.text")}
|
||||||
|
</div>
|
||||||
|
{filteredCollaborators.map(([clientId, collaborator]) =>
|
||||||
|
renderCollaborator({
|
||||||
|
actionManager,
|
||||||
|
collaborator,
|
||||||
|
clientId,
|
||||||
|
withName: true,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Island>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prev, next) => {
|
||||||
|
if (
|
||||||
|
prev.collaborators.size !== next.collaborators.size ||
|
||||||
|
prev.mobile !== next.mobile ||
|
||||||
|
prev.className !== next.className
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [socketId, collaborator] of prev.collaborators) {
|
||||||
|
const nextCollaborator = next.collaborators.get(socketId);
|
||||||
|
if (
|
||||||
|
!nextCollaborator ||
|
||||||
|
!isShallowEqual(
|
||||||
|
collaborator,
|
||||||
|
nextCollaborator,
|
||||||
|
collaboratorComparatorKeys,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -1823,3 +1823,12 @@ export const brainIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const searchIcon = createIcon(
|
||||||
|
<g strokeWidth={1.5}>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
|
||||||
|
<path d="M21 21l-6 -6" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
@ -84,6 +84,7 @@
|
|||||||
--color-primary-darkest: #4a47b1;
|
--color-primary-darkest: #4a47b1;
|
||||||
--color-primary-light: #e3e2fe;
|
--color-primary-light: #e3e2fe;
|
||||||
--color-primary-light-darker: #d7d5ff;
|
--color-primary-light-darker: #d7d5ff;
|
||||||
|
--color-primary-hover: #5753d0;
|
||||||
|
|
||||||
--color-gray-10: #f5f5f5;
|
--color-gray-10: #f5f5f5;
|
||||||
--color-gray-20: #ebebeb;
|
--color-gray-20: #ebebeb;
|
||||||
@ -205,6 +206,7 @@
|
|||||||
--color-primary-darkest: #beb9ff;
|
--color-primary-darkest: #beb9ff;
|
||||||
--color-primary-light: #4f4d6f;
|
--color-primary-light: #4f4d6f;
|
||||||
--color-primary-light-darker: #43415e;
|
--color-primary-light-darker: #43415e;
|
||||||
|
--color-primary-hover: #bbb8ff;
|
||||||
|
|
||||||
--color-text-warning: var(--color-gray-80);
|
--color-text-warning: var(--color-gray-80);
|
||||||
|
|
||||||
|
@ -114,6 +114,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin avatarStyles {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 100%;
|
||||||
|
outline-offset: 2px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-gray-90);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
&-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
right: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
left: -3px;
|
||||||
|
border: 1px solid var(--avatar-border-color);
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--is-followed::before {
|
||||||
|
border-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin filledButtonOnCanvas {
|
@mixin filledButtonOnCanvas {
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 0 0 1px var(--color-surface-lowest);
|
box-shadow: 0 0 0 1px var(--color-surface-lowest);
|
||||||
|
@ -247,6 +247,7 @@ export { TTDDialog } from "./components/TTDDialog/TTDDialog";
|
|||||||
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
|
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
|
||||||
|
|
||||||
export { normalizeLink } from "./data/url";
|
export { normalizeLink } from "./data/url";
|
||||||
|
export { zoomToFitBounds } from "./actions/actionCanvas";
|
||||||
export { convertToExcalidrawElements } from "./data/transform";
|
export { convertToExcalidrawElements } from "./data/transform";
|
||||||
export { getCommonBounds } from "./element/bounds";
|
export { getCommonBounds } from "./element/bounds";
|
||||||
|
|
||||||
|
@ -521,5 +521,14 @@
|
|||||||
"description": "Currently only <flowchartLink>Flowchart</flowchartLink>,<sequenceLink> Sequence, </sequenceLink> and <classLink>Class </classLink>Diagrams are supported. The other types will be rendered as image in Excalidraw.",
|
"description": "Currently only <flowchartLink>Flowchart</flowchartLink>,<sequenceLink> Sequence, </sequenceLink> and <classLink>Class </classLink>Diagrams are supported. The other types will be rendered as image in Excalidraw.",
|
||||||
"syntax": "Mermaid Syntax",
|
"syntax": "Mermaid Syntax",
|
||||||
"preview": "Preview"
|
"preview": "Preview"
|
||||||
|
},
|
||||||
|
"userList": {
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Quick search",
|
||||||
|
"empty": "No users found"
|
||||||
|
},
|
||||||
|
"hint": {
|
||||||
|
"text": "Click on user to follow"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -312,6 +312,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -370,6 +371,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -511,6 +513,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -565,6 +568,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"toast": {
|
"toast": {
|
||||||
"message": "Added to library",
|
"message": "Added to library",
|
||||||
},
|
},
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -713,6 +717,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -765,6 +770,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -1089,6 +1095,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -1141,6 +1148,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -1465,6 +1473,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -1519,6 +1528,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"toast": {
|
"toast": {
|
||||||
"message": "Copied styles.",
|
"message": "Copied styles.",
|
||||||
},
|
},
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -1667,6 +1677,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -1717,6 +1728,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -1906,6 +1918,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -1958,6 +1971,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -2210,6 +2224,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -2267,6 +2282,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -2602,6 +2618,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -2656,6 +2673,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"toast": {
|
"toast": {
|
||||||
"message": "Copied styles.",
|
"message": "Copied styles.",
|
||||||
},
|
},
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -3412,6 +3430,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -3464,6 +3483,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -3788,6 +3808,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -3840,6 +3861,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -4164,6 +4186,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -4219,6 +4242,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -4896,6 +4920,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -4951,6 +4976,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -5476,6 +5502,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -5533,6 +5560,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -5995,6 +6023,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6048,6 +6077,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -6394,6 +6424,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6446,6 +6477,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
@ -6768,6 +6800,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6823,6 +6856,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 200,
|
"width": 200,
|
||||||
|
@ -39,6 +39,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -98,6 +99,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -495,6 +497,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -556,6 +559,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -953,6 +957,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -1005,6 +1010,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -1784,6 +1790,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -1838,6 +1845,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -1997,6 +2005,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -2054,6 +2063,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -2451,6 +2461,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -2505,6 +2516,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -2693,6 +2705,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -2745,6 +2758,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -2861,6 +2875,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -2915,6 +2930,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -3305,6 +3321,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -3357,6 +3374,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -3602,6 +3620,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -3656,6 +3675,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -3847,6 +3867,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -3901,6 +3922,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -4103,6 +4125,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -4157,6 +4180,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -4345,6 +4369,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -4400,6 +4425,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -4689,6 +4715,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -4743,6 +4770,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -5192,6 +5220,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -5273,6 +5302,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -5489,6 +5519,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -5542,6 +5573,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -5758,6 +5790,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -5838,6 +5871,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -5954,6 +5988,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6006,6 +6041,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -6122,6 +6158,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6174,6 +6211,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -6574,6 +6612,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6630,6 +6669,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -6891,6 +6931,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -6941,6 +6982,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -8958,6 +9000,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -9013,6 +9056,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -9302,6 +9346,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -9357,6 +9402,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -9545,6 +9591,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -9599,6 +9646,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -9744,6 +9792,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -9798,6 +9847,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -10015,6 +10065,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -10067,6 +10118,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -10183,6 +10235,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -10235,6 +10288,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -10351,6 +10405,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -10403,6 +10458,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -10519,6 +10575,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -10594,6 +10651,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -10725,6 +10783,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -10800,6 +10859,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -10931,6 +10991,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -10981,6 +11042,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -11117,6 +11179,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -11192,6 +11255,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -11323,6 +11387,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -11375,6 +11440,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -11491,6 +11557,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -11566,6 +11633,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -11697,6 +11765,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -11749,6 +11818,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -11865,6 +11935,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -11915,6 +11986,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -12051,6 +12123,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -12103,6 +12176,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -12219,6 +12293,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -12279,6 +12354,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -12883,6 +12959,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -12937,6 +13014,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -13125,6 +13203,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -13175,6 +13254,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -13248,6 +13328,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -13300,6 +13381,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -13416,6 +13498,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -13472,6 +13555,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -13733,6 +13817,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -13791,6 +13876,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -14294,6 +14380,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -14356,6 +14443,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -15150,6 +15238,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -15203,6 +15292,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -15276,6 +15366,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -15330,6 +15421,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -16091,6 +16183,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -16174,6 +16267,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -16491,6 +16585,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -16573,6 +16668,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -16761,6 +16857,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -16811,6 +16908,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -16884,6 +16982,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -16936,6 +17035,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -17367,6 +17467,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -17417,6 +17518,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@ -17490,6 +17592,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -17543,6 +17646,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
|
@ -9,6 +9,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"amIBeingFollowed": false,
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
@ -82,6 +83,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
|
"shouldDisconnectFollowModeOnCanvasInteraction": true,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
@ -90,6 +92,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"zenModeEnabled": false,
|
"zenModeEnabled": false,
|
||||||
|
@ -41,7 +41,9 @@ import { Merge, ValueOf } from "./utility-types";
|
|||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
export type Collaborator = {
|
export type SocketId = string;
|
||||||
|
|
||||||
|
export type Collaborator = Readonly<{
|
||||||
pointer?: CollaboratorPointer;
|
pointer?: CollaboratorPointer;
|
||||||
button?: "up" | "down";
|
button?: "up" | "down";
|
||||||
selectedElementIds?: AppState["selectedElementIds"];
|
selectedElementIds?: AppState["selectedElementIds"];
|
||||||
@ -56,7 +58,8 @@ export type Collaborator = {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
// user id. If supplied, we'll filter out duplicates when rendering user avatars.
|
// user id. If supplied, we'll filter out duplicates when rendering user avatars.
|
||||||
id?: string;
|
id?: string;
|
||||||
};
|
socketId?: SocketId;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type CollaboratorPointer = {
|
export type CollaboratorPointer = {
|
||||||
x: number;
|
x: number;
|
||||||
@ -123,6 +126,11 @@ export type ActiveTool =
|
|||||||
export type SidebarName = string;
|
export type SidebarName = string;
|
||||||
export type SidebarTabName = string;
|
export type SidebarTabName = string;
|
||||||
|
|
||||||
|
export type UserToFollow = {
|
||||||
|
socketId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
type _CommonCanvasAppState = {
|
type _CommonCanvasAppState = {
|
||||||
zoom: AppState["zoom"];
|
zoom: AppState["zoom"];
|
||||||
scrollX: AppState["scrollX"];
|
scrollX: AppState["scrollX"];
|
||||||
@ -303,13 +311,16 @@ export interface AppState {
|
|||||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
selectedLinearElement: LinearElementEditor | null;
|
selectedLinearElement: LinearElementEditor | null;
|
||||||
|
|
||||||
snapLines: readonly SnapLine[];
|
snapLines: readonly SnapLine[];
|
||||||
originSnapOffset: {
|
originSnapOffset: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
} | null;
|
} | null;
|
||||||
objectsSnapModeEnabled: boolean;
|
objectsSnapModeEnabled: boolean;
|
||||||
|
/** the user's clientId & username who is being followed on the canvas */
|
||||||
|
userToFollow: UserToFollow | null;
|
||||||
|
/** the clientIds of the users following the current user */
|
||||||
|
followedBy: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
@ -385,6 +396,11 @@ export type ExcalidrawInitialDataState = Merge<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type OnUserFollowedPayload = {
|
||||||
|
userToFollow: UserToFollow;
|
||||||
|
action: "FOLLOW" | "UNFOLLOW";
|
||||||
|
};
|
||||||
|
|
||||||
export interface ExcalidrawProps {
|
export interface ExcalidrawProps {
|
||||||
onChange?: (
|
onChange?: (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@ -438,7 +454,8 @@ export interface ExcalidrawProps {
|
|||||||
activeTool: AppState["activeTool"],
|
activeTool: AppState["activeTool"],
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
) => void;
|
) => void;
|
||||||
onScrollChange?: (scrollX: number, scrollY: number) => void;
|
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
||||||
|
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
validateEmbeddable?:
|
validateEmbeddable?:
|
||||||
| boolean
|
| boolean
|
||||||
@ -675,6 +692,12 @@ export type ExcalidrawImperativeAPI = {
|
|||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
) => void,
|
) => void,
|
||||||
) => UnsubscribeCallback;
|
) => UnsubscribeCallback;
|
||||||
|
onScrollChange: (
|
||||||
|
callback: (scrollX: number, scrollY: number, zoom: Zoom) => void,
|
||||||
|
) => UnsubscribeCallback;
|
||||||
|
onUserFollow: (
|
||||||
|
callback: (payload: OnUserFollowedPayload) => void,
|
||||||
|
) => UnsubscribeCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Device = Readonly<{
|
export type Device = Readonly<{
|
||||||
|
@ -771,11 +771,21 @@ export const queryFocusableElements = (container: HTMLElement | null) => {
|
|||||||
|
|
||||||
export const isShallowEqual = <
|
export const isShallowEqual = <
|
||||||
T extends Record<string, any>,
|
T extends Record<string, any>,
|
||||||
I extends keyof T,
|
K extends readonly unknown[],
|
||||||
>(
|
>(
|
||||||
objA: T,
|
objA: T,
|
||||||
objB: T,
|
objB: T,
|
||||||
comparators?: Record<I, (a: T[I], b: T[I]) => boolean>,
|
comparators?:
|
||||||
|
| { [key in keyof T]?: (a: T[key], b: T[key]) => boolean }
|
||||||
|
| (keyof T extends K[number]
|
||||||
|
? K extends readonly (keyof T)[]
|
||||||
|
? K
|
||||||
|
: {
|
||||||
|
_error: "keys are either missing or include keys not in compared obj";
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
_error: "keys are either missing or include keys not in compared obj";
|
||||||
|
}),
|
||||||
debug = false,
|
debug = false,
|
||||||
) => {
|
) => {
|
||||||
const aKeys = Object.keys(objA);
|
const aKeys = Object.keys(objA);
|
||||||
@ -783,8 +793,29 @@ export const isShallowEqual = <
|
|||||||
if (aKeys.length !== bKeys.length) {
|
if (aKeys.length !== bKeys.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (comparators && Array.isArray(comparators)) {
|
||||||
|
for (const key of comparators) {
|
||||||
|
const ret = objA[key] === objB[key];
|
||||||
|
if (!ret) {
|
||||||
|
if (debug) {
|
||||||
|
console.info(
|
||||||
|
`%cisShallowEqual: ${key} not equal ->`,
|
||||||
|
"color: #8B4000",
|
||||||
|
objA[key],
|
||||||
|
objB[key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return aKeys.every((key) => {
|
return aKeys.every((key) => {
|
||||||
const comparator = comparators?.[key as I];
|
const comparator = (
|
||||||
|
comparators as { [key in keyof T]?: (a: T[key], b: T[key]) => boolean }
|
||||||
|
)?.[key as keyof T];
|
||||||
const ret = comparator
|
const ret = comparator
|
||||||
? comparator(objA[key], objB[key])
|
? comparator(objA[key], objB[key])
|
||||||
: objA[key] === objB[key];
|
: objA[key] === objB[key];
|
||||||
|
@ -40,6 +40,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -90,6 +91,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"zenModeEnabled": false,
|
"zenModeEnabled": false,
|
||||||
|
@ -40,6 +40,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"followedBy": Set {},
|
||||||
"frameRendering": {
|
"frameRendering": {
|
||||||
"clip": true,
|
"clip": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -90,6 +91,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
"userToFollow": null,
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
"viewModeEnabled": false,
|
"viewModeEnabled": false,
|
||||||
"zenModeEnabled": false,
|
"zenModeEnabled": false,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user