fix(markdown): polish editor UX — side panel, overlay sync, paste segregation
- Move markdown editor from canvas overlay to side panel (70% width) for better performance and editing experience - Add @blocknote/mantine for full BlockNoteView with default UI components - Fix socket reconnect loop caused by unstable pasteOpts object reference (memoize with useMemo) - Add ResizeObserver to markdown overlay cards for automatic height sync - Call mdOverlay.refreshAll() on every canvas change so overlays track pack/grid/arrange/save operations - Paste goes to BlockNote editor when contentEditable is focused - Simplify toolbar: single color picker (accent + auto-derived bg), title/name field, width presets S/M/L - Guard normalize/flip operations to skip markdown and sticky items - Fix title not updating on card preview (pass name prop, bump revision) - Fix preview not refreshing after editor save (revision counter + overlay refresh)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user