diff --git a/components/board/BoardCanvas.module.css b/components/board/BoardCanvas.module.css index 2417492..193b111 100644 --- a/components/board/BoardCanvas.module.css +++ b/components/board/BoardCanvas.module.css @@ -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; diff --git a/components/board/BoardCanvas.tsx b/components/board/BoardCanvas.tsx index cd187e4..267a504 100644 --- a/components/board/BoardCanvas.tsx +++ b/components/board/BoardCanvas.tsx @@ -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; @@ -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; @@ -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) => { @@ -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()); @@ -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; @@ -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; @@ -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; @@ -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} @@ -1220,6 +1248,10 @@ const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string } left: canvasContextMenu.position.x, }} > +
+ +

{t("createCard")}

+
- -
- {t("hints.pan")} - {t("hints.select")} - {t("hints.create")} - {t("hints.move")} -
); diff --git a/messages/de.json b/messages/de.json index 6d5cdad..f391fad 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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", diff --git a/messages/en.json b/messages/en.json index b1074da..81e3555 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/es.json b/messages/es.json index dc9f87e..7b8224d 100644 --- a/messages/es.json +++ b/messages/es.json @@ -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", diff --git a/messages/fr.json b/messages/fr.json index 7944f2e..fd00873 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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", diff --git a/messages/ja.json b/messages/ja.json index dfd4307..0816f9d 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -473,12 +473,7 @@ "sendToOutline": "アウトラインに送る", "duplicate": "複製", "delete": "削除", - "hints": { - "pan": "ホイールクリックでドラッグ", - "select": "ドラッグでカードを選択", - "create": "ダブルクリックでカードを作成", - "move": "Shiftキーで自由移動" - }, + "createCard": "カードを作成", "untitled": "無題", "titlePlaceholder": "タイトル", "descriptionPlaceholder": "説明", diff --git a/messages/ko.json b/messages/ko.json index 66a4200..921950d 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -473,12 +473,7 @@ "sendToOutline": "아웃라인으로 보내기", "duplicate": "복제", "delete": "삭제", - "hints": { - "pan": "휠 클릭으로 드래그", - "select": "드래그하여 카드 선택", - "create": "더블 클릭하여 카드 생성", - "move": "Shift를 누르고 자유 이동" - }, + "createCard": "카드 생성", "untitled": "제목 없음", "titlePlaceholder": "제목", "descriptionPlaceholder": "설명", diff --git a/messages/pl.json b/messages/pl.json index fa8714c..aeb2023 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -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", diff --git a/messages/zh.json b/messages/zh.json index c2f1eeb..ea69177 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -473,12 +473,7 @@ "sendToOutline": "发送到大纲", "duplicate": "复制", "delete": "删除", - "hints": { - "pan": "中键平移", - "select": "拖动选择卡片", - "create": "双击创建卡片", - "move": "Shift 自由移动" - }, + "createCard": "创建卡片", "untitled": "未命名", "titlePlaceholder": "标题", "descriptionPlaceholder": "描述",