Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ body {
position: absolute;
top: 8px;
right: 8px;
z-index: 100;
z-index: 10000;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s ease;
}
Expand All @@ -119,8 +121,9 @@ body {
opacity: 1;
}

/* Fullscreen button */
.app-button {
/* Toolbar buttons (shared) */
.app-button,
.mute-btn {
display: flex;
align-items: center;
justify-content: center;
Expand All @@ -139,12 +142,43 @@ body {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.app-button:hover {
.app-button:hover,
.mute-btn:hover {
color: rgba(0, 0, 0, 0.7);
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}

/* Reposition toolbar in fullscreen to sit beside Excalidraw's top-right UI */
.main.fullscreen .toolbar {
right: 100px;
top: 12px;
opacity: 1;
}

.main.fullscreen .mute-btn {
width: 36px;
height: 36px;
background: var(--island-bg-color, #ffffff);
color: var(--color-on-surface, #1b1b1f);
border: 1px solid var(--default-border-color, #e4e4eb);
border-radius: 8px;
box-shadow: var(--shadow-island, 0 1px 4px rgba(0, 0, 0, 0.08));
backdrop-filter: none;
-webkit-backdrop-filter: none;
}

.main.fullscreen .mute-btn:hover {
background: var(--color-surface-mid, #f1f0ff);
color: var(--color-on-surface, #1b1b1f);
box-shadow: var(--shadow-island, 0 1px 4px rgba(0, 0, 0, 0.08));
}

.main.fullscreen .mute-btn svg {
width: 18px;
height: 18px;
}

.excalidraw-container {
width: 100%;
position: relative;
Expand Down
71 changes: 48 additions & 23 deletions src/mcp-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { App } from "@modelcontextprotocol/ext-apps";
import { Excalidraw, exportToSvg, convertToExcalidrawElements, restore, CaptureUpdateAction, FONT_FAMILY, serializeAsJSON, MainMenu } from "@excalidraw/excalidraw";
import morphdom from "morphdom";
import { useCallback, useEffect, useRef, useState } from "react";
import { initPencilAudio, playStroke } from "./pencil-audio";
import { initPencilAudio, playStroke, isMuted, toggleMute } from "./pencil-audio";
import { captureInitialElements, onEditorChange, setStorageKey, loadPersistedElements, getLatestEditedElements, setCheckpointId } from "./edit-context";
import "./global.css";

Expand Down Expand Up @@ -105,6 +105,22 @@ function extractViewportAndElements(elements: any[]): {
return { viewport, drawElements: processedDraw, restoreId, deleteIds };
}

const SoundOnIcon = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" fill="currentColor" />
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
</svg>
);

const SoundOffIcon = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" fill="currentColor" />
<line x1="23" y1="9" x2="17" y2="15" />
<line x1="17" y1="9" x2="23" y2="15" />
</svg>
);

const ExpandIcon = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8.5 1.5H12.5V5.5" />
Expand Down Expand Up @@ -664,6 +680,7 @@ export function ExcalidrawAppCore({ app }: { app: App }) {
const [editorReady, setEditorReady] = useState(false);
const [excalidrawApi, setExcalidrawApi] = useState<any>(null);
const [editorSettled, setEditorSettled] = useState(false);
const [soundMuted, setSoundMuted] = useState(isMuted);
const appRef = useRef<App | null>(null);
const svgViewportRef = useRef<ViewportRect | null>(null);
const elementsRef = useRef<any[]>([]);
Expand Down Expand Up @@ -863,28 +880,36 @@ export function ExcalidrawAppCore({ app }: { app: App }) {

return (
<main className={`main${displayMode === "fullscreen" ? " fullscreen" : ""}`} style={displayMode === "fullscreen" && containerHeight ? { height: containerHeight } : undefined}>
{displayMode === "inline" && (
<div className="toolbar">
<ShareButton
onConfirm={async () => {
await shareToExcalidraw({
elements,
appState: {},
files: {}
}, app);
}}
/>

<button
className="app-button"
onClick={toggleFullscreen}
title="Enter fullscreen"
>
<span>Edit</span>
<ExpandIcon />
</button>
</div>
)}
<div className="toolbar">
<button
className="mute-btn"
onClick={() => setSoundMuted(toggleMute())}
title={soundMuted ? "Unmute sounds" : "Mute sounds"}
>
{soundMuted ? <SoundOffIcon /> : <SoundOnIcon />}
</button>
{displayMode === "inline" && (
<>
<ShareButton
onConfirm={async () => {
await shareToExcalidraw({
elements,
appState: {},
files: {}
}, app);
}}
/>
<button
className="app-button"
onClick={toggleFullscreen}
title="Enter fullscreen"
>
<span>Edit</span>
<ExpandIcon />
</button>
</>
)}
</div>
{/* Editor: mount hidden when ready, reveal after viewport is set */}
{mountEditor && (
<div style={{
Expand Down
21 changes: 20 additions & 1 deletion src/pencil-audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ let softBuffer: AudioBuffer | null = null;
let initialized = false;
let initPromise: Promise<void> | null = null;

const MUTED_KEY = "excalidraw:muted";

let muted: boolean = (() => {
try {
const stored = localStorage.getItem(MUTED_KEY);
return stored === null ? true : stored === "true";
} catch { return true; }
})();

export function isMuted(): boolean {
return muted;
}

export function toggleMute(): boolean {
muted = !muted;
try { localStorage.setItem(MUTED_KEY, String(muted)); } catch {}
return muted;
}

function getAudioContext(): AudioContext {
if (!audioCtx) {
audioCtx = new AudioContext();
Expand Down Expand Up @@ -46,7 +65,7 @@ export async function initPencilAudio(): Promise<void> {

/** Play a pencil stroke sound for a given element type. */
export function playStroke(elementType: string): void {
if (!initialized || !audioCtx) return;
if (muted || !initialized || !audioCtx) return;

// Use soft stroke for all element types
const isLine = elementType === "arrow" || elementType === "line";
Expand Down