Skip to content
Merged
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
16 changes: 0 additions & 16 deletions components/board/BoardCanvas.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -549,22 +549,6 @@
color: var(--secondary-text);
}

/* Hints in corner with low opacity */
.hints {
position: absolute;
bottom: 24px;
left: 24px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
font-size: 12px;
color: var(--secondary-text);
opacity: 0.5;
pointer-events: none;
user-select: none;
}

/* Arrows SVG layer */
.arrows_svg {
position: absolute;
Expand Down
57 changes: 41 additions & 16 deletions components/board/BoardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
if (e.button !== 0) return;
if ((e.target as HTMLElement).closest(`.${styles.card}`)) return;
if ((e.target as HTMLElement).closest(`.${styles.zoom_controls}`)) return;
if ((e.target as HTMLElement).closest(`.${styles.hints}`)) return;
if ((e.target as HTMLElement).closest(`.${styles.context_menu}`)) return;

const container = containerRef.current;
Expand Down Expand Up @@ -448,9 +447,11 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
};
}, [selectionRect !== null]); // eslint-disable-line react-hooks/exhaustive-deps

// Zoom with mouse wheel - centered on cursor
// Zoom with mouse wheel - centered on cursor.
// Attached as a native non-passive listener (see effect below) because React
// registers onWheel as passive, which makes preventDefault() a no-op and warns.
const handleWheel = useCallback(
(e: React.WheelEvent) => {
(e: WheelEvent) => {
e.preventDefault();

const container = containerRef.current;
Expand All @@ -477,6 +478,13 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
[scale, offset],
);

useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener("wheel", handleWheel, { passive: false });
return () => container.removeEventListener("wheel", handleWheel);
}, [handleWheel]);

// Zoom from buttons - centered on viewport
const zoomFromCenter = useCallback(
(zoomIn: boolean) => {
Expand Down Expand Up @@ -508,7 +516,6 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
e.preventDefault();
if ((e.target as HTMLElement).closest(`.${styles.card}`)) return;
if ((e.target as HTMLElement).closest(`.${styles.zoom_controls}`)) return;
if ((e.target as HTMLElement).closest(`.${styles.hints}`)) return;

// Clear selection when creating a new card
setSelectedCardIds(new Set());
Expand Down Expand Up @@ -621,8 +628,8 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
[isReadOnly, projectId, offset, scale, saveCards],
);

// Right-clicking empty canvas opens a menu (record audio). Cards and arrows
// have their own menus, so bail when the click landed on one.
// Right-clicking empty canvas opens a menu (create card / record audio).
// Cards and arrows have their own menus, so bail when the click landed on one.
const handleCanvasContextMenu = useCallback(
(e: React.MouseEvent) => {
if (isReadOnly) return;
Expand All @@ -631,8 +638,7 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
target.closest(`.${styles.card}`) ||
target.closest(`.${styles.arrow_group}`) ||
target.closest(`.${styles.context_menu}`) ||
target.closest(`.${styles.zoom_controls}`) ||
target.closest(`.${styles.hints}`)
target.closest(`.${styles.zoom_controls}`)
)
return;

Expand All @@ -652,6 +658,29 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
[isReadOnly, offset, scale],
);

// Create a text card at the spot the canvas menu was opened.
const handleCreateCard = useCallback(() => {
if (!canvasContextMenu) return;
const { canvasX: x, canvasY: y } = canvasContextMenu;
setCanvasContextMenu(null);
setSelectedCardIds(new Set());

const newCard: BoardCardData = {
id: uuidv7(),
title: "",
description: "",
color: randomCardColor(),
x: isSnapping ? Math.round(x / GRID_SIZE) * GRID_SIZE : x,
y: isSnapping ? Math.round(y / GRID_SIZE) * GRID_SIZE : y,
width: 450,
height: 280,
};

const newCards = [...cardsRef.current, newCard];
setCards(newCards);
saveCards(newCards);
}, [canvasContextMenu, isSnapping, saveCards]);

// Begin recording from the canvas menu; remember where to drop the card.
const handleStartRecording = useCallback(async () => {
if (!canvasContextMenu) return;
Expand Down Expand Up @@ -982,7 +1011,6 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
onMouseDown={handleContainerMouseDown}
onDoubleClick={handleDoubleClick}
onContextMenu={handleCanvasContextMenu}
onWheel={handleWheel}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
Expand Down Expand Up @@ -1220,6 +1248,10 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
left: canvasContextMenu.position.x,
}}
>
<div className={styles.context_menu_item} onClick={handleCreateCard}>
<Plus size={16} />
<p className="unselectable">{t("createCard")}</p>
</div>
<div
className={`${styles.context_menu_item} ${recorder.isSupported ? "" : styles.context_menu_item_disabled}`}
title={recorder.isSupported ? undefined : t("audioUnsupported")}
Expand Down Expand Up @@ -1273,13 +1305,6 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }
<Plus size={14} />
</button>
</div>

<div className={styles.hints}>
<span>{t("hints.pan")}</span>
<span>{t("hints.select")}</span>
<span>{t("hints.create")}</span>
<span>{t("hints.move")}</span>
</div>
</div>
</div>
);
Expand Down
7 changes: 1 addition & 6 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,7 @@
"sendToOutline": "An Gliederung senden",
"duplicate": "Duplizieren",
"delete": "Löschen",
"hints": {
"pan": "Mittelklick zum Schwenken",
"select": "Ziehen, um Karten auszuwählen",
"create": "Doppelklick zum Erstellen einer Karte",
"move": "Umschalt zum freien Bewegen halten"
},
"createCard": "Karte erstellen",
"untitled": "Unbenannt",
"titlePlaceholder": "Titel",
"descriptionPlaceholder": "Beschreibung",
Expand Down
7 changes: 1 addition & 6 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,7 @@
"sendToOutline": "Send to outline",
"duplicate": "Duplicate",
"delete": "Delete",
"hints": {
"pan": "Middle-click to pan",
"select": "Drag to select cards",
"create": "Double-click to create card",
"move": "Hold Shift to move freely"
},
"createCard": "Create card",
"untitled": "Untitled",
"titlePlaceholder": "Title",
"descriptionPlaceholder": "Description",
Expand Down
7 changes: 1 addition & 6 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,7 @@
"sendToOutline": "Enviar al esquema",
"duplicate": "Duplicar",
"delete": "Eliminar",
"hints": {
"pan": "Clic central para desplazar",
"select": "Arrastrar para seleccionar tarjetas",
"create": "Doble clic para crear tarjeta",
"move": "Mantener Shift para mover libremente"
},
"createCard": "Crear tarjeta",
"untitled": "Sin título",
"titlePlaceholder": "Título",
"descriptionPlaceholder": "Descripción",
Expand Down
7 changes: 1 addition & 6 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,7 @@
"sendToOutline": "Envoyer vers le séquencier",
"duplicate": "Dupliquer",
"delete": "Supprimer",
"hints": {
"pan": "Clic-milieu pour déplacer",
"select": "Glissez pour sélectionner des cartes",
"create": "Double-cliquez pour créer une carte",
"move": "Maintenez Maj pour vous déplacer librement"
},
"createCard": "Créer une carte",
"untitled": "Sans titre",
"titlePlaceholder": "Titre",
"descriptionPlaceholder": "Description",
Expand Down
7 changes: 1 addition & 6 deletions messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,7 @@
"sendToOutline": "アウトラインに送る",
"duplicate": "複製",
"delete": "削除",
"hints": {
"pan": "ホイールクリックでドラッグ",
"select": "ドラッグでカードを選択",
"create": "ダブルクリックでカードを作成",
"move": "Shiftキーで自由移動"
},
"createCard": "カードを作成",
"untitled": "無題",
"titlePlaceholder": "タイトル",
"descriptionPlaceholder": "説明",
Expand Down
7 changes: 1 addition & 6 deletions messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,7 @@
"sendToOutline": "아웃라인으로 보내기",
"duplicate": "복제",
"delete": "삭제",
"hints": {
"pan": "휠 클릭으로 드래그",
"select": "드래그하여 카드 선택",
"create": "더블 클릭하여 카드 생성",
"move": "Shift를 누르고 자유 이동"
},
"createCard": "카드 생성",
"untitled": "제목 없음",
"titlePlaceholder": "제목",
"descriptionPlaceholder": "설명",
Expand Down
7 changes: 1 addition & 6 deletions messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,7 @@
"sendToOutline": "Wyślij do konspektu",
"duplicate": "Powiel",
"delete": "Usuń",
"hints": {
"pan": "Środkowy przycisk, aby przesuwać",
"select": "Przeciągnij, aby zaznaczyć karty",
"create": "Kliknij dwukrotnie, aby utworzyć kartę",
"move": "Przytrzymaj Shift, aby swobodnie przesuwać"
},
"createCard": "Utwórz kartę",
"untitled": "Bez tytułu",
"titlePlaceholder": "Tytuł",
"descriptionPlaceholder": "Opis",
Expand Down
7 changes: 1 addition & 6 deletions messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,7 @@
"sendToOutline": "发送到大纲",
"duplicate": "复制",
"delete": "删除",
"hints": {
"pan": "中键平移",
"select": "拖动选择卡片",
"create": "双击创建卡片",
"move": "Shift 自由移动"
},
"createCard": "创建卡片",
"untitled": "未命名",
"titlePlaceholder": "标题",
"descriptionPlaceholder": "描述",
Expand Down
Loading