diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f794f9..d9ee91e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@blocknote/core": "^0.47.1", + "@blocknote/mantine": "^0.47.1", "@blocknote/react": "^0.47.1", "axios": "^1.7.9", "pixi-filters": "^6.1.5", @@ -266,6 +267,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -370,6 +381,24 @@ } } }, + "node_modules/@blocknote/mantine": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@blocknote/mantine/-/mantine-0.47.1.tgz", + "integrity": "sha512-lcmHo/o35lVrCoY6qhTLn3Rs+tiZTktG2fYffe8LK4/qqs1hId4Xp2pbARLOGnDEREKvCL1PUZ9JKifgzA0DhQ==", + "license": "MPL-2.0", + "dependencies": { + "@blocknote/core": "0.47.1", + "@blocknote/react": "0.47.1", + "react-icons": "^5.5.0" + }, + "peerDependencies": { + "@mantine/core": "^8.3.11", + "@mantine/hooks": "^8.3.11", + "@mantine/utils": "^6.0.22", + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, "node_modules/@blocknote/react": { "version": "0.47.1", "resolved": "https://registry.npmjs.org/@blocknote/react/-/react-0.47.1.tgz", @@ -962,6 +991,46 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mantine/core": { + "version": "8.3.16", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.16.tgz", + "integrity": "sha512-YgmoYrb0I9UUDYfs9OVLvBUTgkA5vn5KZTfxlOaOSFxzhyYjrtwMQK+P7fvEIkenNQbbikhelj/IRcC2yNQyFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.3.16", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.3.16", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.16.tgz", + "integrity": "sha512-DthR8/KDCLIip3KoXyn370HEiKxiOwH+ZeXs1UoSv/e58xR88OE85Slyb5b5pRsDa4ndYnES+j9wyRyrRXLWfg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/utils": { + "version": "6.0.22", + "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.22.tgz", + "integrity": "sha512-RSKlNZvxhMCkOFZ6slbYvZYbWjHUM+PxDQnupIOxIdsTZQQjx/BFfrfJ7kQFOP+g7MtpOds8weAetEs5obwMOQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@mixmark-io/domino": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", @@ -2013,6 +2082,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2102,6 +2181,13 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT", + "peer": true + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2449,6 +2535,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4500,6 +4596,17 @@ "react": ">=18" } }, + "node_modules/react-number-format": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", + "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4510,6 +4617,55 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.30.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", @@ -4542,6 +4698,47 @@ "react-dom": ">=16.8" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/rehype-format": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", @@ -4909,6 +5106,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, "node_modules/turndown": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", @@ -4918,6 +5122,19 @@ "@mixmark-io/domino": "^2.2.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -5070,6 +5287,99 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 58a99ed..0f06a3b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@blocknote/core": "^0.47.1", + "@blocknote/mantine": "^0.47.1", "@blocknote/react": "^0.47.1", "axios": "^1.7.9", "pixi-filters": "^6.1.5", diff --git a/frontend/src/canvas/MarkdownOverlay.ts b/frontend/src/canvas/MarkdownOverlay.ts index 62671a4..9839100 100644 --- a/frontend/src/canvas/MarkdownOverlay.ts +++ b/frontend/src/canvas/MarkdownOverlay.ts @@ -21,6 +21,8 @@ interface CardEntry { contentMount: HTMLDivElement; /** Current item ID. */ id: string; + /** ResizeObserver for auto-height updates. */ + resizeObserver: ResizeObserver; } export class MarkdownOverlay { @@ -33,8 +35,8 @@ export class MarkdownOverlay { /** Callback when a card's height changes (from DOM measurement). */ onHeightChange: ((id: string, newHeight: number) => void) | null = null; - /** Callback to enter edit mode for a card (wired by Editor.tsx). */ - onRequestEdit: ((id: string) => void) | null = null; + /** Callback to enter/exit edit mode for a card (wired by Editor.tsx). */ + onRequestEdit: ((id: string | null) => void) | null = null; /** Callback when checkbox is toggled in read mode. */ onCheckboxToggle: ((id: string, newContent: string) => void) | null = null; @@ -64,6 +66,8 @@ export class MarkdownOverlay { for (const [cardId, entry] of this._cards) { entry.el.style.pointerEvents = cardId === id ? 'auto' : 'none'; } + // Notify React to open/close the side panel editor + this.onRequestEdit?.(id); } get editingId(): string | null { @@ -171,8 +175,14 @@ export class MarkdownOverlay { const contentMount = document.createElement('div'); el.appendChild(contentMount); + // Auto-measure height whenever DOM content changes size + const resizeObserver = new ResizeObserver(() => { + this._syncHeight(item.id); + }); + resizeObserver.observe(el); + this._container.appendChild(el); - this._cards.set(item.id, { el, contentMount, id: item.id }); + this._cards.set(item.id, { el, contentMount, id: item.id, resizeObserver }); this.onChange?.(); } @@ -191,9 +201,26 @@ export class MarkdownOverlay { entry.el.style.transform = `scale(${zoom})`; } + /** Sync data.h from DOM measurement — called by ResizeObserver. */ + private _syncHeight(id: string): void { + const entry = this._cards.get(id); + const item = this._scene.getById(id); + if (!entry || !item || item.type !== 'markdown') return; + + const h = entry.el.offsetHeight; + if (h > 0 && Math.abs(h - item.data.h) > 1) { + item.data.h = h; + if (item.displayObject instanceof MarkdownSprite) { + item.displayObject.updateFromData(item.data as MarkdownObject); + } + this.onHeightChange?.(id, h); + } + } + private _removeCard(id: string): void { const entry = this._cards.get(id); if (!entry) return; + entry.resizeObserver.disconnect(); entry.el.remove(); this._cards.delete(id); this.onChange?.(); diff --git a/frontend/src/canvas/image-drop.ts b/frontend/src/canvas/image-drop.ts index 3fdaff5..145a780 100644 --- a/frontend/src/canvas/image-drop.ts +++ b/frontend/src/canvas/image-drop.ts @@ -330,6 +330,10 @@ export function setupPaste( }, ): () => void { async function onPaste(e: ClipboardEvent) { + // If focus is inside a contentEditable (e.g. BlockNote editor), let native paste through + const active = document.activeElement; + if (active instanceof HTMLElement && (active.isContentEditable || active.closest('[contenteditable]'))) return; + const items = e.clipboardData?.items; if (!items) return; diff --git a/frontend/src/canvas/operations.ts b/frontend/src/canvas/operations.ts index d6fba90..06a3f36 100644 --- a/frontend/src/canvas/operations.ts +++ b/frontend/src/canvas/operations.ts @@ -11,6 +11,11 @@ import { applyImageDisplayTransform, getImageDisplayTransform } from './imageTra // ─── Helpers ─── +/** Items with fixed dimensions — scale must stay at 1. */ +function isFixedSize(item: SceneItem): boolean { + return item.type === 'markdown' || item.type === 'sticky'; +} + function scaledW(item: SceneItem): number { return item.data.w * item.data.sx; } @@ -182,9 +187,11 @@ export function distributeVertical(objects: SceneItem[]) { export function normalizeSize(objects: SceneItem[]) { if (objects.length < 2) return; - const areas = objects.map((item) => scaledW(item) * scaledH(item)); + const scalable = objects.filter(i => !isFixedSize(i)); + if (scalable.length < 2) return; + const areas = scalable.map((item) => scaledW(item) * scaledH(item)); const avgArea = areas.reduce((a, b) => a + b, 0) / areas.length; - objects.forEach((item) => { + scalable.forEach((item) => { const currentArea = scaledW(item) * scaledH(item); if (currentArea <= 0) return; const ratio = Math.sqrt(avgArea / currentArea); @@ -196,9 +203,11 @@ export function normalizeSize(objects: SceneItem[]) { export function normalizeScale(objects: SceneItem[]) { if (objects.length < 2) return; - const avgSX = objects.reduce((s, item) => s + item.data.sx, 0) / objects.length; - const avgSY = objects.reduce((s, item) => s + item.data.sy, 0) / objects.length; - objects.forEach((item) => { + const scalable = objects.filter(i => !isFixedSize(i)); + if (scalable.length < 2) return; + const avgSX = scalable.reduce((s, item) => s + item.data.sx, 0) / scalable.length; + const avgSY = scalable.reduce((s, item) => s + item.data.sy, 0) / scalable.length; + scalable.forEach((item) => { item.data.sx = avgSX; item.data.sy = avgSY; syncScale(item); @@ -207,8 +216,10 @@ export function normalizeScale(objects: SceneItem[]) { export function normalizeHeight(objects: SceneItem[]) { if (objects.length < 2) return; - const avgH = objects.reduce((s, item) => s + scaledH(item), 0) / objects.length; - objects.forEach((item) => { + const scalable = objects.filter(i => !isFixedSize(i)); + if (scalable.length < 2) return; + const avgH = scalable.reduce((s, item) => s + scaledH(item), 0) / scalable.length; + scalable.forEach((item) => { const h = scaledH(item); if (h <= 0) return; const ratio = avgH / h; @@ -220,8 +231,10 @@ export function normalizeHeight(objects: SceneItem[]) { export function normalizeWidth(objects: SceneItem[]) { if (objects.length < 2) return; - const avgW = objects.reduce((s, item) => s + scaledW(item), 0) / objects.length; - objects.forEach((item) => { + const scalable = objects.filter(i => !isFixedSize(i)); + if (scalable.length < 2) return; + const avgW = scalable.reduce((s, item) => s + scaledW(item), 0) / scalable.length; + scalable.forEach((item) => { const w = scaledW(item); if (w <= 0) return; const ratio = avgW / w; @@ -367,6 +380,7 @@ function layoutAsGrid(sorted: SceneItem[], anchor: { x: number; y: number }) { export function flipHorizontal(objects: SceneItem[]) { objects.forEach((item) => { + if (isFixedSize(item)) return; item.data.flipX = !item.data.flipX; if (item.type === 'image') { applyImageDisplayTransform(item.displayObject, item.data as ImageObject); @@ -378,6 +392,7 @@ export function flipHorizontal(objects: SceneItem[]) { export function flipVertical(objects: SceneItem[]) { objects.forEach((item) => { + if (isFixedSize(item)) return; item.data.flipY = !item.data.flipY; if (item.type === 'image') { applyImageDisplayTransform(item.displayObject, item.data as ImageObject); diff --git a/frontend/src/components/MarkdownEditView.tsx b/frontend/src/components/MarkdownEditView.tsx index 0a08698..26c3e78 100644 --- a/frontend/src/components/MarkdownEditView.tsx +++ b/frontend/src/components/MarkdownEditView.tsx @@ -1,12 +1,12 @@ /** - * MarkdownEditView — BlockNote editor wrapper for markdown card edit mode. + * MarkdownEditView — BlockNote editor in a side panel. * Lazy-loaded on first double-click via dynamic import(). */ import React, { useCallback, useEffect, useRef } from 'react'; import { useCreateBlockNote } from '@blocknote/react'; -import { BlockNoteViewRaw } from '@blocknote/react'; -import '@blocknote/react/style.css'; +import { BlockNoteView } from '@blocknote/mantine'; +import '@blocknote/mantine/style.css'; import { MD_BLOCKNOTE_DARK_CSS } from '../canvas/markdownStyles'; interface MarkdownEditViewProps { @@ -16,11 +16,6 @@ interface MarkdownEditViewProps { onCancel: () => void; } -// Markdown conversion is built into the BlockNote editor instance: -// - editor.tryParseMarkdownToBlocks(md) — markdown → blocks -// - editor.blocksToMarkdownLossy(blocks) — blocks → markdown -// No separate @blocknote/xl-markdown package needed. - export default function MarkdownEditView(props: MarkdownEditViewProps) { const { initialContent, accentColor, onSave, onCancel } = props; const savedRef = useRef(false); @@ -74,70 +69,67 @@ export default function MarkdownEditView(props: MarkdownEditViewProps) { doCancel(); } }; - document.addEventListener('keydown', onKeyDown, true); // capture phase + document.addEventListener('keydown', onKeyDown, true); return () => document.removeEventListener('keydown', onKeyDown, true); }, [doSave, doCancel]); - // Click outside to save - useEffect(() => { - const onClick = (e: MouseEvent) => { - const target = e.target as HTMLElement; - if (!target.closest('[data-markdown-edit]')) { - doSave(); - } - }; - const timer = setTimeout(() => { - document.addEventListener('pointerdown', onClick); - }, 200); - return () => { - clearTimeout(timer); - document.removeEventListener('pointerdown', onClick); - }; - }, [doSave]); - - // Header with Done/Cancel controls - const headerStyle: React.CSSProperties = { - padding: '7px 14px', - background: '#2a2a42', - borderBottom: '1px solid #333', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - fontSize: '11px', - }; - return (