diff --git a/package-lock.json b/package-lock.json index 12cd6a4..6effa22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2472,9 +2472,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2492,9 +2489,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2512,9 +2506,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2532,9 +2523,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2552,9 +2540,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2572,9 +2557,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4713,6 +4695,7 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", + "hasInstallScript": true, "license": "MIT", "dependencies": { "node-addon-api": "^8.0.0" @@ -9383,9 +9366,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9407,9 +9387,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9431,9 +9408,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9455,9 +9429,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12436,6 +12407,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/packages/note-boards/src/board-card.tsx b/packages/note-boards/src/board-card.tsx index 5a3291a..1fffafb 100644 --- a/packages/note-boards/src/board-card.tsx +++ b/packages/note-boards/src/board-card.tsx @@ -2,6 +2,7 @@ import { PopupMenu, useDraggable, usePreventChildDrag } from "@notes/ui"; import { DragEvent, useEffect, useRef, useState } from "react"; import type { IBoardColumn, RichCard } from "./board-format"; +import { cardFilePath } from "./board-format"; const LABEL_COLORS = ["#e2f0fb", "#fde8d8", "#d9f2e8", "#f5e6fb", "#fef9c3"]; @@ -16,6 +17,7 @@ export function BoardCard({ updateCardState, column, onOpenModal, + boardPath, isColumnDragActive = false, }: { card: RichCard; @@ -28,6 +30,7 @@ export function BoardCard({ updateCardState: (card: RichCard) => void; column: IBoardColumn; onOpenModal: (card: RichCard) => void; + boardPath: string; isColumnDragActive?: boolean; }) { const [expanded, setExpanded] = useState(false); @@ -212,13 +215,26 @@ export function BoardCard({ open={menuIsOpen} onClose={() => setMenuIsOpen(false)} menu={ - void handleDeleteCard(card.id)} - > - ๐ Delete - + <> + { + const link = `![[${cardFilePath(boardPath, card.id)}]]`; + void navigator.clipboard?.writeText(link); + setMenuIsOpen(false); + }} + > + ๐ Copy link + + void handleDeleteCard(card.id)} + > + ๐ Delete + + > } > void; handleDeleteCard: (cardId: string) => void; setModalCard: (card: RichCard | null) => void; + boardPath: string; dragRef: React.RefObject<{ cardId: string; fromColumn: string } | null>; }) { const dragHandle = useDraggable( @@ -159,6 +161,7 @@ export function BoardColumn({ handleDeleteCard={handleDeleteCard} column={column} onOpenModal={setModalCard} + boardPath={boardPath} isColumnDragActive={Boolean(draggingColumnName)} /> diff --git a/packages/note-boards/src/board-format.ts b/packages/note-boards/src/board-format.ts index c6a8957..4bf4dc6 100644 --- a/packages/note-boards/src/board-format.ts +++ b/packages/note-boards/src/board-format.ts @@ -5,6 +5,20 @@ import { parseFrontmatter, } from "@notes/web/src/lib/frontmatter"; +/** Returns the companion dot-folder for a board's card files (relative to tome root). */ +export function cardDotFolder(boardPath: string): string { + const lastSlash = boardPath.lastIndexOf("/"); + const dir = lastSlash !== -1 ? boardPath.slice(0, lastSlash + 1) : ""; + const filename = lastSlash !== -1 ? boardPath.slice(lastSlash + 1) : boardPath; + const base = filename.endsWith(".md") ? filename.slice(0, -3) : filename; + return `${dir}.${base}.cards`; +} + +/** Returns the file path for a single card within a board (relative to tome root). */ +export function cardFilePath(boardPath: string, cardId: string): string { + return `${cardDotFolder(boardPath)}/${cardId}.md`; +} + export interface RichCard { id: string; title: string; diff --git a/packages/note-boards/src/board-view.tsx b/packages/note-boards/src/board-view.tsx index d327715..642ebea 100644 --- a/packages/note-boards/src/board-view.tsx +++ b/packages/note-boards/src/board-view.tsx @@ -8,6 +8,7 @@ import { BoardCardModal } from "./board-card-modal"; import { type IBoardColumn, BoardModel, + cardFilePath, parseBoard, type RichCard, serializeBoard, @@ -338,6 +339,13 @@ export function BoardView({ value, onChange, path, onRegisterContextMenu }: Rend const card = cardsRef.current.get(cardId); if (!card) return []; return [ + { + label: "Copy link", + run: () => { + const link = `![[${cardFilePath(path, cardId)}]]`; + void navigator.clipboard?.writeText(link); + }, + }, { label: "Duplicate card", run: () => void duplicateCard(card), @@ -354,7 +362,7 @@ export function BoardView({ value, onChange, path, onRegisterContextMenu }: Rend }; onRegisterContextMenu(builder); return () => onRegisterContextMenu(null); - }, [duplicateCard, handleDeleteCard, onRegisterContextMenu]); + }, [duplicateCard, handleDeleteCard, onRegisterContextMenu, path]); const addColumn = async () => { const values = await openPrompt({ @@ -470,6 +478,7 @@ export function BoardView({ value, onChange, path, onRegisterContextMenu }: Rend updateCardState={updateCardState} handleDeleteCard={handleDeleteCard} setModalCard={setModalCard} + boardPath={path} dragRef={dragRef} /> diff --git a/packages/note-boards/src/styles.css b/packages/note-boards/src/styles.css index e72d3a6..5b857ef 100644 --- a/packages/note-boards/src/styles.css +++ b/packages/note-boards/src/styles.css @@ -109,6 +109,12 @@ color: var(--fg-muted); } +.board-card-menu-item { + background: transparent; + border: none; + border-radius: 4px; +} + .board-card-del { background: transparent; border: none; diff --git a/packages/note-calendar/src/calendar-format.ts b/packages/note-calendar/src/calendar-format.ts index 1452462..6e6ea8f 100644 --- a/packages/note-calendar/src/calendar-format.ts +++ b/packages/note-calendar/src/calendar-format.ts @@ -5,6 +5,20 @@ import { parseFrontmatter, } from "@notes/web/src/lib/frontmatter"; +/** Returns the companion dot-folder for a calendar's event files (relative to tome root). */ +export function eventDotFolder(calendarPath: string): string { + const lastSlash = calendarPath.lastIndexOf("/"); + const dir = lastSlash !== -1 ? calendarPath.slice(0, lastSlash + 1) : ""; + const filename = lastSlash !== -1 ? calendarPath.slice(lastSlash + 1) : calendarPath; + const base = filename.endsWith(".md") ? filename.slice(0, -3) : filename; + return `${dir}.${base}.events`; +} + +/** Returns the file path for a single event within a calendar (relative to tome root). */ +export function eventFilePath(calendarPath: string, eventId: string): string { + return `${eventDotFolder(calendarPath)}/${eventId}.md`; +} + export interface RichEvent { id: string; title: string; diff --git a/packages/note-calendar/src/calendar-view.tsx b/packages/note-calendar/src/calendar-view.tsx index ce4a0c5..3c19acf 100644 --- a/packages/note-calendar/src/calendar-view.tsx +++ b/packages/note-calendar/src/calendar-view.tsx @@ -182,6 +182,7 @@ export function CalendarView({ value, path }: RendererProps) { setSelectedEvent={setSelectedEvent} onUpdateEvent={updateEventState} onDeleteEvent={handleDeleteEvent} + calendarPath={path} /> )} diff --git a/packages/note-calendar/src/event-details.tsx b/packages/note-calendar/src/event-details.tsx index 48a4376..bba3c74 100644 --- a/packages/note-calendar/src/event-details.tsx +++ b/packages/note-calendar/src/event-details.tsx @@ -1,5 +1,6 @@ ๏ปฟimport { RenderedEditor } from "@notes/editor"; import type { RichEvent } from "./calendar-format"; +import { eventFilePath } from "./calendar-format"; const DURATION_OPTIONS = [ { value: 15, label: "15 min" }, @@ -15,16 +16,29 @@ export function EventDetails({ setSelectedEvent, onUpdateEvent, onDeleteEvent, + calendarPath, }: { selectedEvent: RichEvent; setSelectedEvent: (event: RichEvent | null) => void; onUpdateEvent: (event: RichEvent) => void; onDeleteEvent: (eventId: string) => void; + calendarPath: string; }) { return ( Edit Event + { + const link = `![[${eventFilePath(calendarPath, selectedEvent.id)}]]`; + void navigator.clipboard?.writeText(link); + }} + > + ๐ +