Compare commits

..

2 Commits

Author SHA1 Message Date
dwelle e8d4b0a8df feat: tweak sidebar promo style 2026-04-15 20:59:32 +02:00
Tom Louveau 1caec99b29 docs: change twitter label by X (#11158)
Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
2026-04-13 10:30:58 +02:00
15 changed files with 129 additions and 262 deletions
+2 -2
View File
@@ -97,8 +97,8 @@ const config = {
href: "https://discord.gg/UexuTaE",
},
{
label: "Twitter",
href: "https://twitter.com/excalidraw",
label: "𝕏",
href: "https://x.com/excalidraw",
},
{
label: "Linkedin",
-2
View File
@@ -136,7 +136,6 @@ import { useHandleAppTheme } from "./useHandleAppTheme";
import { getPreferredLanguage } from "./app-language/language-detector";
import { useAppLangCode } from "./app-language/language-state";
import DebugCanvas, {
ConsoleLogger,
debugRenderer,
isVisualDebuggerEnabled,
loadSavedDebugState,
@@ -1262,7 +1261,6 @@ const ExcalidrawWrapper = () => {
ref={debugCanvasRef}
/>
)}
{isVisualDebuggerEnabled() && <ConsoleLogger />}
</Excalidraw>
</div>
);
-1
View File
@@ -42,7 +42,6 @@ export const STORAGE_KEYS = {
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
LOCAL_STORAGE_THEME: "excalidraw-theme",
LOCAL_STORAGE_DEBUG: "excalidraw-debug",
LOCAL_STORAGE_DEBUG_CONSOLE: "excalidraw-debug-console",
VERSION_DATA_STATE: "version-dataState",
VERSION_FILES: "version-files",
+3 -32
View File
@@ -4,8 +4,7 @@ import {
eyeIcon,
} from "@excalidraw/excalidraw/components/icons";
import { MainMenu } from "@excalidraw/excalidraw/index";
import DropdownMenuItemCheckbox from "@excalidraw/excalidraw/components/dropdownMenu/DropdownMenuItemCheckbox";
import React, { useState } from "react";
import React from "react";
import { isDevEnv } from "@excalidraw/common";
@@ -14,29 +13,7 @@ import type { Theme } from "@excalidraw/element/types";
import { LanguageList } from "../app-language/LanguageList";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import {
isVisualDebuggerEnabled,
loadConsoleLoggerState,
saveDebugState,
setConsoleLoggerEnabled,
} from "./DebugCanvas";
const ConsoleLoggerToggle = () => {
const [checked, setChecked] = useState(() => loadConsoleLoggerState());
return (
<DropdownMenuItemCheckbox
checked={checked}
onSelect={(event) => {
const next = !checked;
setChecked(next);
setConsoleLoggerEnabled(next);
event.preventDefault();
}}
>
Show console log overlay
</DropdownMenuItemCheckbox>
);
};
import { saveDebugState } from "./DebugCanvas";
export const AppMainMenu: React.FC<{
onCollabDialogOpen: () => any;
@@ -100,13 +77,7 @@ export const AppMainMenu: React.FC<{
</MainMenu.Item>
)}
<MainMenu.Separator />
<MainMenu.DefaultItems.Preferences
additionalItems={
isDevEnv() && isVisualDebuggerEnabled() ? (
<ConsoleLoggerToggle />
) : null
}
/>
<MainMenu.DefaultItems.Preferences />
<MainMenu.DefaultItems.ToggleTheme
allowSystemTheme
theme={props.theme}
+59 -3
View File
@@ -4,21 +4,32 @@
display: flex;
flex-direction: column;
text-align: center;
gap: 1rem;
gap: 1.5rem;
flex: 1 1 auto;
.link-button {
margin: 0 auto !important;
width: 100%;
button {
width: 100%;
}
}
}
.app-sidebar-promo-image {
margin: 1rem 0;
height: 16.25rem;
height: 295px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
background-size: calc(
var(--right-sidebar-width, 100%) - var(--space-factor) * 2
);
background-image: radial-gradient(
circle,
transparent 60%,
transparent 0%,
var(--sidebar-bg-color) 100%
),
var(--image-source);
@@ -33,4 +44,49 @@
.link-button {
margin: 0 auto;
}
.app-sidebar-promo-text {
padding: 0 0.5rem;
font-size: 0.875rem;
font-weight: 700;
line-height: 1.4;
}
.app-sidebar-promo-illustration {
position: relative;
min-height: 7rem;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 0.125rem;
.app-sidebar-promo-heart {
width: 46px;
transform: translateX(-14px);
height: auto;
}
.app-sidebar-promo-trial-note {
position: absolute;
top: 2.75rem;
left: 50%;
transform: translateX(-56%) rotate(-7deg);
font-size: 1rem;
line-height: 1.15;
color: var(--color-primary);
white-space: nowrap;
}
.app-sidebar-promo-trial-arrow {
position: absolute;
top: 3.35rem;
left: 55%;
width: 4.5rem;
height: 3rem;
transform: translateX(1.75rem) rotate(10deg);
color: var(--color-primary);
overflow: visible;
}
}
}
+64 -12
View File
@@ -8,6 +8,63 @@ import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
import "./AppSidebar.scss";
type SidebarPromoCopyProps = {
text: string;
};
const SidebarPromoCopy = (props: SidebarPromoCopyProps) => {
return (
<div className="app-sidebar-promo-copy">
<div className="app-sidebar-promo-illustration" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 250"
className="app-sidebar-promo-heart"
>
<path
d="M 145 75
C 110 35, 60 55, 65 120
C 70 180, 140 190, 215 200
C 225 180, 260 110, 235 55
C 210 -5, 140 20, 160 105"
fill="none"
stroke="#D06B64"
strokeWidth="16"
strokeLinecap="round"
/>
</svg>
<div className="app-sidebar-promo-trial-note excalifont">
14 days of
<br />
free trial
</div>
<svg
className="app-sidebar-promo-trial-arrow"
viewBox="0 0 72 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 6C23 1 50 8 48 32"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M42 26L48 32L54 26"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div className="app-sidebar-promo-text">{props.text}</div>
</div>
);
};
export const AppSidebar = () => {
const { theme, openSidebar } = useUIAppState();
@@ -32,15 +89,13 @@ export const AppSidebar = () => {
<div
className="app-sidebar-promo-image"
style={{
["--image-source" as any]: `url(/oss_promo_comments_${
["--image-source" as any]: `url(/sidebar-comments-promo-${
theme === THEME.DARK ? "dark" : "light"
}.jpg)`,
opacity: 0.7,
opacity: 0.9,
}}
/>
<div className="app-sidebar-promo-text">
Make comments with Excalidraw+
</div>
<SidebarPromoCopy text="Make comments with Excalidraw+" />
<LinkButton
href={`${
import.meta.env.VITE_APP_PLUS_LP
@@ -55,16 +110,13 @@ export const AppSidebar = () => {
<div
className="app-sidebar-promo-image"
style={{
["--image-source" as any]: `url(/oss_promo_presentations_${
["--image-source" as any]: `url(/sidebar-presentation-promo-${
theme === THEME.DARK ? "dark" : "light"
}.svg)`,
backgroundSize: "60%",
opacity: 0.4,
}.jpg)`,
opacity: 0.7,
}}
/>
<div className="app-sidebar-promo-text">
Create presentations with Excalidraw+
</div>
<SidebarPromoCopy text="Create presentation with Excalidraw+" />
<LinkButton
href={`${
import.meta.env.VITE_APP_PLUS_LP
+1 -208
View File
@@ -9,7 +9,7 @@ import {
} from "@excalidraw/excalidraw/renderer/helpers";
import { type AppState } from "@excalidraw/excalidraw/types";
import { arrayToMap, throttleRAF } from "@excalidraw/common";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback } from "react";
import {
getGlobalFixedPointForBindableElement,
@@ -435,34 +435,6 @@ export const loadSavedDebugState = () => {
export const isVisualDebuggerEnabled = () =>
Array.isArray(window.visualDebug?.data);
export const loadConsoleLoggerState = (): boolean => {
try {
const raw = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_DEBUG_CONSOLE);
if (raw !== null) {
return JSON.parse(raw) === true;
}
} catch {}
return false;
};
export const saveConsoleLoggerState = (enabled: boolean) => {
try {
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_DEBUG_CONSOLE,
JSON.stringify(enabled),
);
} catch {}
};
const CONSOLE_LOGGER_TOGGLE_EVENT = "excalidraw-debug-console-toggle";
export const setConsoleLoggerEnabled = (enabled: boolean) => {
saveConsoleLoggerState(enabled);
window.dispatchEvent(
new CustomEvent<boolean>(CONSOLE_LOGGER_TOGGLE_EVENT, { detail: enabled }),
);
};
export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
const moveForward = useCallback(() => {
if (
@@ -487,7 +459,6 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
}, [onChange]);
const reset = useCallback(() => {
window.visualDebug!.currentFrame = undefined;
_clearLogsCallback?.();
onChange();
}, [onChange]);
const trashFrames = useCallback(() => {
@@ -495,7 +466,6 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
window.visualDebug.currentFrame = undefined;
window.visualDebug.data = [];
}
_clearLogsCallback?.();
onChange();
}, [onChange]);
@@ -593,181 +563,4 @@ const DebugCanvas = React.forwardRef<HTMLCanvasElement, DebugCanvasProps>(
},
);
type LogLevel = "log" | "info" | "warn" | "error";
interface LogEntry {
id: number;
level: LogLevel;
message: string;
timestamp: number;
}
const LOG_COLORS: Record<LogLevel, string> = {
log: "rgba(220,220,220,0.9)",
info: "rgba(100,180,255,0.9)",
warn: "rgba(255,200,60,0.9)",
error: "rgba(255,90,90,0.9)",
};
const MAX_LOGS = 500;
let logIdCounter = 0;
let _clearLogsCallback: (() => void) | null = null;
export const ConsoleLogger = () => {
const [enabled, setEnabled] = useState(() => loadConsoleLoggerState());
const [logs, setLogs] = useState<LogEntry[]>([]);
const logsRef = useRef<LogEntry[]>([]);
const scrollRef = useRef<HTMLDivElement>(null);
const dragState = useRef<{ startY: number; startScrollTop: number } | null>(
null,
);
const isDragging = useRef(false);
useEffect(() => {
const handler = (e: Event) => {
setEnabled((e as CustomEvent<boolean>).detail);
};
window.addEventListener(CONSOLE_LOGGER_TOGGLE_EVENT, handler);
return () => {
window.removeEventListener(CONSOLE_LOGGER_TOGGLE_EVENT, handler);
};
}, []);
useEffect(() => {
_clearLogsCallback = () => {
logsRef.current = [];
setLogs([]);
};
return () => {
_clearLogsCallback = null;
};
}, []);
useEffect(() => {
const originals: Record<LogLevel, (...args: unknown[]) => void> = {
// eslint-disable-next-line no-console
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
const patch = (level: LogLevel) => {
// eslint-disable-next-line no-console
console[level] = (...args: unknown[]) => {
originals[level](...args);
const message = args
.map((a) =>
typeof a === "object" ? JSON.stringify(a, null, 0) : String(a),
)
.join(" ");
const entry: LogEntry = {
id: ++logIdCounter,
level,
message,
timestamp: Date.now(),
};
logsRef.current = [...logsRef.current, entry].slice(-MAX_LOGS);
setLogs([...logsRef.current]);
if (!isDragging.current && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
};
};
(["log", "info", "warn", "error"] as LogLevel[]).forEach(patch);
return () => {
(["log", "info", "warn", "error"] as LogLevel[]).forEach((level) => {
// eslint-disable-next-line no-console
console[level] = originals[level];
});
};
}, []);
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
if (!scrollRef.current) {
return;
}
dragState.current = {
startY: e.clientY,
startScrollTop: scrollRef.current.scrollTop,
};
scrollRef.current.setPointerCapture(e.pointerId);
isDragging.current = true;
e.preventDefault();
}, []);
const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
if (!dragState.current || !scrollRef.current) {
return;
}
const delta = dragState.current.startY - e.clientY;
scrollRef.current.scrollTop = dragState.current.startScrollTop + delta;
e.preventDefault();
}, []);
const onPointerUp = useCallback(() => {
dragState.current = null;
isDragging.current = false;
}, []);
if (!enabled || logs.length === 0) {
return null;
}
return (
<div
ref={scrollRef}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
style={{
position: "fixed",
top: 8,
right: 8,
zIndex: 9999,
maxWidth: 420,
maxHeight: "60vh",
overflowY: "auto",
display: "flex",
flexDirection: "column",
gap: 2,
pointerEvents: "all",
cursor: "grab",
userSelect: "none",
scrollbarWidth: "none",
}}
>
{logs.map((entry) => (
<div
key={entry.id}
style={{
background: "rgba(18,18,20,0.55)",
backdropFilter: "blur(8px) saturate(1.4)",
WebkitBackdropFilter: "blur(8px) saturate(1.4)",
borderLeft: `3px solid ${LOG_COLORS[entry.level]}`,
borderRadius: 4,
padding: "2px 8px",
fontFamily: "monospace",
fontSize: 11,
lineHeight: 1.5,
color: LOG_COLORS[entry.level],
wordBreak: "break-all",
whiteSpace: "pre-wrap",
opacity: 0.95,
boxShadow: "0 1px 4px rgba(0,0,0,0.35)",
}}
>
<span style={{ opacity: 0.5, marginRight: 6 }}>
{entry.level.toUpperCase()}
</span>
{entry.message}
</div>
))}
</div>
);
};
export default DebugCanvas;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB