From eae1fdea5efcbdb65170a4be0ec6dc01e587519a Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Apr 2026 18:03:51 +0530 Subject: [PATCH 1/7] ENG-1553 Embed canvas in block --- .../src/components/canvas/CanvasEmbed.tsx | 98 ++++++++++++++ .../components/canvas/CanvasEmbedDialog.tsx | 123 ++++++++++++++++++ apps/roam/src/components/canvas/Tldraw.tsx | 2 +- .../src/components/canvas/tldrawStyles.ts | 10 +- .../utils/initializeObserversAndListeners.ts | 7 + .../utils/registerCommandPaletteCommands.ts | 17 +++ 6 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 apps/roam/src/components/canvas/CanvasEmbed.tsx create mode 100644 apps/roam/src/components/canvas/CanvasEmbedDialog.tsx diff --git a/apps/roam/src/components/canvas/CanvasEmbed.tsx b/apps/roam/src/components/canvas/CanvasEmbed.tsx new file mode 100644 index 000000000..1b713039f --- /dev/null +++ b/apps/roam/src/components/canvas/CanvasEmbed.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; +import { OnloadArgs } from "roamjs-components/types"; +import renderWithUnmount from "roamjs-components/util/renderWithUnmount"; +import { getPageTitleValueByHtmlElement } from "roamjs-components/dom"; +import getBlockUidFromTarget from "roamjs-components/dom/getBlockUidFromTarget"; +import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import { TldrawCanvas } from "./Tldraw"; + +const BLOCK_TEXT_REGEX = /\{\{dg-canvas:\s*\[\[(.+?)\]\]\s*\}\}/i; + +const extractCanvasTitle = (button: HTMLElement): string | null => { + const blockUid = getBlockUidFromTarget(button); + if (!blockUid) return null; + const blockText = getTextByBlockUid(blockUid); + if (!blockText) return null; + const match = blockText.match(BLOCK_TEXT_REGEX); + if (!match) return null; + return match[1].trim(); +}; + +const getCurrentPageTitle = (el: HTMLElement): string | null => { + try { + return getPageTitleValueByHtmlElement(el); + } catch { + return null; + } +}; + +const CanvasEmbedPlaceholder = ({ message }: { message: string }) => ( +
+ {message} +
+); + +export const renderCanvasEmbed = ( + button: HTMLElement, + onloadArgs: OnloadArgs, +) => { + button.style.display = "none"; + + if (!button.parentElement) return; + + const title = extractCanvasTitle(button); + if (!title) return; + + const currentPageTitle = getCurrentPageTitle(button); + if (currentPageTitle === title) { + const wrapper = document.createElement("div"); + button.parentElement.appendChild(wrapper); + renderWithUnmount( + , + wrapper, + ); + return; + } + + const pageUid = getPageUidByPageTitle(title); + if (!pageUid) { + const wrapper = document.createElement("div"); + button.parentElement.appendChild(wrapper); + renderWithUnmount( + , + wrapper, + ); + return; + } + + button.parentElement.onmousedown = (e: MouseEvent) => e.stopPropagation(); + + const wrapper = document.createElement("div"); + wrapper.className = "dg-canvas-embed"; + wrapper.style.height = "400px"; + wrapper.style.width = "100%"; + wrapper.style.overflow = "hidden"; + wrapper.style.borderRadius = "6px"; + wrapper.style.margin = "8px 0"; + button.parentElement.appendChild(wrapper); + + renderWithUnmount( + + + , + wrapper, + ); +}; diff --git a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx new file mode 100644 index 000000000..a67d076a1 --- /dev/null +++ b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx @@ -0,0 +1,123 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { Dialog, InputGroup, Menu, MenuItem } from "@blueprintjs/core"; +import renderOverlay, { + RoamOverlayProps, +} from "roamjs-components/util/renderOverlay"; +import { DEFAULT_CANVAS_PAGE_FORMAT } from "~/index"; +import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; + +type CanvasEmbedDialogProps = { + onSelect: (title: string) => void; +}; + +const getCanvasPages = (): { title: string; uid: string }[] => { + const { canvasPageFormat } = getFormattedConfigTree(); + const format = canvasPageFormat.value || DEFAULT_CANVAS_PAGE_FORMAT; + const regexSource = `^${format.replace(/\*/g, ".+")}$`; + const escaped = regexSource.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + + try { + const results = window.roamAlphaAPI.q(`[ + :find (pull ?node [:node/title :block/uid]) + :where + [(re-pattern "${escaped}") ?regex] + [?node :node/title ?title] + [(re-find ?regex ?title)] + ]`) as [{ title: string; uid: string }][]; + + return results + .map(([r]) => ({ title: r.title, uid: r.uid })) + .sort((a, b) => a.title.localeCompare(b.title)); + } catch { + return []; + } +}; + +const CanvasEmbedDialog = ({ + isOpen, + onClose, + onSelect, +}: RoamOverlayProps) => { + const [filter, setFilter] = useState(""); + const [activeIndex, setActiveIndex] = useState(0); + const canvasPages = useMemo(getCanvasPages, []); + + const filtered = useMemo(() => { + if (!filter) return canvasPages; + const lower = filter.toLowerCase(); + return canvasPages.filter((p) => p.title.toLowerCase().includes(lower)); + }, [filter, canvasPages]); + + const handleSelect = useCallback( + (title: string) => { + onSelect(title); + onClose(); + }, + [onSelect, onClose], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setActiveIndex((i) => Math.min(i + 1, filtered.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIndex((i) => Math.max(i - 1, 0)); + } else if (e.key === "Enter" && filtered.length > 0) { + e.preventDefault(); + handleSelect(filtered[activeIndex].title); + } + }, + [filtered, activeIndex, handleSelect], + ); + + return ( + +
+ { + setFilter(e.target.value); + setActiveIndex(0); + }} + autoFocus + onKeyDown={handleKeyDown} + /> + + {filtered.length === 0 ? ( + + ) : ( + filtered.map((page, i) => ( + handleSelect(page.title)} + /> + )) + )} + +
+
+ ); +}; + +export const renderCanvasEmbedDialog = (props: CanvasEmbedDialogProps) => + renderOverlay({ + // eslint-disable-next-line @typescript-eslint/naming-convention + Overlay: CanvasEmbedDialog, + props, + }); diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 703d11288..e8f1404f8 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -189,7 +189,7 @@ export const isPageUid = (uid: string) => ":node/title" ]; -const TldrawCanvas = ({ title }: { title: string }) => { +export const TldrawCanvas = ({ title }: { title: string }) => { // In Roam, canvas identity is currently keyed by the page UID. // Room sync is graph/page encoded as an opaque base64url token. const pageUid = useMemo(() => getPageUidByPageTitle(title), [title]); diff --git a/apps/roam/src/components/canvas/tldrawStyles.ts b/apps/roam/src/components/canvas/tldrawStyles.ts index 946af91b2..03a5265e7 100644 --- a/apps/roam/src/components/canvas/tldrawStyles.ts +++ b/apps/roam/src/components/canvas/tldrawStyles.ts @@ -1,12 +1,12 @@ // tldrawStyles.ts because some of these styles need to be inlined export default /* css */ ` - /* Hide Roam Blocks only when a canvas is present under the root */ - .roam-article:has(.roamjs-tldraw-canvas-container) .rm-block-children { + /* Hide Roam Blocks only when a full-page canvas is present (not embedded) */ + .roam-article:has(.roamjs-tldraw-canvas-container:not(.dg-canvas-embed *)) .rm-block-children { display: none; } - - /* Hide Roam Blocks in sidebar when a canvas is present */ - .rm-sidebar-outline:has(.roamjs-tldraw-canvas-container) .rm-block-children { + + /* Hide Roam Blocks in sidebar when a full-page canvas is present (not embedded) */ + .rm-sidebar-outline:has(.roamjs-tldraw-canvas-container:not(.dg-canvas-embed *)) .rm-block-children { display: none; } diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index f2fba602d..d6418ddba 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -57,6 +57,7 @@ import { getFeatureFlag } from "~/components/settings/utils/accessors"; import { getCleanTagText } from "~/components/settings/NodeConfig"; import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors"; import { renderPossibleDuplicates } from "~/components/VectorDuplicateMatches"; +import { renderCanvasEmbed } from "~/components/canvas/CanvasEmbed"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import findDiscourseNode from "./findDiscourseNode"; @@ -131,6 +132,11 @@ export const initObservers = async ({ render: (b) => renderQueryBlock(b, onloadArgs), }); + const canvasEmbedObserver = createButtonObserver({ + attribute: "dg-canvas", + render: (b) => renderCanvasEmbed(b, onloadArgs), + }); + const nodeTagPopupButtonObserver = createHTMLObserver({ className: "rm-page-ref--tag", tag: "SPAN", @@ -394,6 +400,7 @@ export const initObservers = async ({ observers: [ pageTitleObserver, queryBlockObserver, + canvasEmbedObserver, configPageObserver, graphOverviewExportObserver, nodeTagPopupButtonObserver, diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 26b780084..e0bbb7252 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -1,4 +1,5 @@ import { openQueryDrawer } from "~/components/QueryDrawer"; +import { renderCanvasEmbedDialog } from "~/components/canvas/CanvasEmbedDialog"; import { render as exportRender } from "~/components/Export"; import { render as renderToast } from "roamjs-components/components/Toast"; import { createBlock, updateBlock } from "roamjs-components/writes"; @@ -314,6 +315,22 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }; // Roam organizes commands alphabetically + window.roamAlphaAPI.ui.slashCommand.addCommand({ + label: "DG: Embed canvas", + callback: (context) => { + const uid = context["block-uid"]; + if (!uid) return null; + renderCanvasEmbedDialog({ + onSelect: (title: string) => { + void updateBlock({ + uid, + text: `{{dg-canvas: [[${title}]]}}`, + }); + }, + }); + return null; + }, + }); void addCommand("DG: Create/Insert discourse node", () => createDiscourseNodeFromCommand(extensionAPI), ); From e2ee3870bd6e75be80e4da9b88748b3e78aec829 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Apr 2026 21:23:51 +0530 Subject: [PATCH 2/7] Fix tsc and eslint errors for slashCommand registration Cast window.roamAlphaAPI.ui to access untyped slashCommand API with proper type annotation instead of relying on any. Fix void callback returning null. --- .../src/utils/registerCommandPaletteCommands.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index e0bbb7252..85fcf308b 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -315,11 +315,21 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }; // Roam organizes commands alphabetically - window.roamAlphaAPI.ui.slashCommand.addCommand({ + ( + window.roamAlphaAPI.ui as unknown as { + slashCommand: { + addCommand: (cmd: { + label: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + callback: (context: { "block-uid": string }) => void; + }) => void; + }; + } + ).slashCommand.addCommand({ label: "DG: Embed canvas", callback: (context) => { const uid = context["block-uid"]; - if (!uid) return null; + if (!uid) return; renderCanvasEmbedDialog({ onSelect: (title: string) => { void updateBlock({ @@ -328,7 +338,6 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }); }, }); - return null; }, }); void addCommand("DG: Create/Insert discourse node", () => From 08161b063bf71a373056db1f80479cf8f06aecbe Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 15:40:24 +0530 Subject: [PATCH 3/7] ENG-1553 Address PR review comments - Move slash command out of registerCommandPaletteCommands into new registerSlashCommands.ts; wire teardown into index.ts unload - Replace hand-rolled InputGroup+Menu+keyboard logic in CanvasEmbedDialog with roamjs-components AutocompleteInput; add loading + empty states - Use data.async.fast.q (not the deprecated sync .q) for canvas page lookup - Extract canvas-page regex into shared helper in isCanvasPage.ts; escape regex special chars in user-provided canvasPageFormat - Convert inline styles to Tailwind in CanvasEmbed and CanvasEmbedDialog Pending follow-ups (not in this commit): - Upstream slashCommand type to roamjs-components so the `unknown` cast in registerSlashCommands.ts can be dropped - Dedupe getCanvasPageTargets (conditionToDatalog) and checkForCanvasPage (Export.tsx) onto the shared helper --- .../src/components/canvas/CanvasEmbed.tsx | 23 +--- .../components/canvas/CanvasEmbedDialog.tsx | 120 +++++------------- apps/roam/src/index.ts | 3 + apps/roam/src/utils/isCanvasPage.ts | 29 ++++- .../utils/registerCommandPaletteCommands.ts | 26 ---- apps/roam/src/utils/registerSlashCommands.ts | 48 +++++++ 6 files changed, 114 insertions(+), 135 deletions(-) create mode 100644 apps/roam/src/utils/registerSlashCommands.ts diff --git a/apps/roam/src/components/canvas/CanvasEmbed.tsx b/apps/roam/src/components/canvas/CanvasEmbed.tsx index 1b713039f..6b7b6f4cc 100644 --- a/apps/roam/src/components/canvas/CanvasEmbed.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbed.tsx @@ -29,18 +29,7 @@ const getCurrentPageTitle = (el: HTMLElement): string | null => { }; const CanvasEmbedPlaceholder = ({ message }: { message: string }) => ( -
+
{message}
); @@ -49,7 +38,7 @@ export const renderCanvasEmbed = ( button: HTMLElement, onloadArgs: OnloadArgs, ) => { - button.style.display = "none"; + button.classList.add("hidden"); if (!button.parentElement) return; @@ -81,12 +70,8 @@ export const renderCanvasEmbed = ( button.parentElement.onmousedown = (e: MouseEvent) => e.stopPropagation(); const wrapper = document.createElement("div"); - wrapper.className = "dg-canvas-embed"; - wrapper.style.height = "400px"; - wrapper.style.width = "100%"; - wrapper.style.overflow = "hidden"; - wrapper.style.borderRadius = "6px"; - wrapper.style.margin = "8px 0"; + wrapper.className = + "dg-canvas-embed h-[400px] w-full overflow-hidden rounded-md my-2"; button.parentElement.appendChild(wrapper); renderWithUnmount( diff --git a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx index a67d076a1..27633a289 100644 --- a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx @@ -1,116 +1,62 @@ -import React, { useState, useMemo, useCallback } from "react"; -import { Dialog, InputGroup, Menu, MenuItem } from "@blueprintjs/core"; +import React, { useCallback, useEffect, useState } from "react"; +import { Dialog } from "@blueprintjs/core"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import renderOverlay, { RoamOverlayProps, } from "roamjs-components/util/renderOverlay"; -import { DEFAULT_CANVAS_PAGE_FORMAT } from "~/index"; -import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; +import { getCanvasPageTitles } from "~/utils/isCanvasPage"; type CanvasEmbedDialogProps = { onSelect: (title: string) => void; }; -const getCanvasPages = (): { title: string; uid: string }[] => { - const { canvasPageFormat } = getFormattedConfigTree(); - const format = canvasPageFormat.value || DEFAULT_CANVAS_PAGE_FORMAT; - const regexSource = `^${format.replace(/\*/g, ".+")}$`; - const escaped = regexSource.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); - - try { - const results = window.roamAlphaAPI.q(`[ - :find (pull ?node [:node/title :block/uid]) - :where - [(re-pattern "${escaped}") ?regex] - [?node :node/title ?title] - [(re-find ?regex ?title)] - ]`) as [{ title: string; uid: string }][]; - - return results - .map(([r]) => ({ title: r.title, uid: r.uid })) - .sort((a, b) => a.title.localeCompare(b.title)); - } catch { - return []; - } -}; - const CanvasEmbedDialog = ({ isOpen, onClose, onSelect, }: RoamOverlayProps) => { - const [filter, setFilter] = useState(""); - const [activeIndex, setActiveIndex] = useState(0); - const canvasPages = useMemo(getCanvasPages, []); + const [canvasPages, setCanvasPages] = useState(null); - const filtered = useMemo(() => { - if (!filter) return canvasPages; - const lower = filter.toLowerCase(); - return canvasPages.filter((p) => p.title.toLowerCase().includes(lower)); - }, [filter, canvasPages]); + useEffect(() => { + void getCanvasPageTitles().then(setCanvasPages); + }, []); - const handleSelect = useCallback( + const handleSetValue = useCallback( (title: string) => { - onSelect(title); - onClose(); - }, - [onSelect, onClose], - ); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "ArrowDown") { - e.preventDefault(); - setActiveIndex((i) => Math.min(i + 1, filtered.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setActiveIndex((i) => Math.max(i - 1, 0)); - } else if (e.key === "Enter" && filtered.length > 0) { - e.preventDefault(); - handleSelect(filtered[activeIndex].title); + if (canvasPages?.includes(title)) { + onSelect(title); + onClose(); } }, - [filtered, activeIndex, handleSelect], + [canvasPages, onSelect, onClose], ); + const renderContent = () => { + if (canvasPages === null) + return ( +
Loading canvas pages...
+ ); + if (canvasPages.length === 0) + return
No canvas pages found
; + return ( + + ); + }; + return ( -
- { - setFilter(e.target.value); - setActiveIndex(0); - }} - autoFocus - onKeyDown={handleKeyDown} - /> - - {filtered.length === 0 ? ( - - ) : ( - filtered.map((page, i) => ( - handleSelect(page.title)} - /> - )) - )} - -
+
{renderContent()}
); }; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index ab74d4b9c..2113014ee 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -8,6 +8,7 @@ import { fireQuerySync } from "./utils/fireQuery"; import parseQuery from "./utils/parseQuery"; import refreshConfigTree from "./utils/refreshConfigTree"; import { registerCommandPaletteCommands } from "./utils/registerCommandPaletteCommands"; +import { registerSlashCommands } from "./utils/registerSlashCommands"; import { createSettingsPanel } from "~/utils/createSettingsPanel"; import { listActiveQueries } from "./utils/listActiveQueries"; import { registerSmartBlock } from "./utils/registerSmartBlock"; @@ -78,6 +79,7 @@ export default runExtension(async (onloadArgs) => { addGraphViewNodeStyling(); registerCommandPaletteCommands(onloadArgs); + const unregisterSlashCommands = registerSlashCommands(); createSettingsPanel(onloadArgs); registerSmartBlock(onloadArgs); setQueryPages(onloadArgs); @@ -157,6 +159,7 @@ export default runExtension(async (onloadArgs) => { observers: observers, unload: () => { setSyncActivity(false); + unregisterSlashCommands(); window.roamjs.extension?.smartblocks?.unregisterCommand("QUERYBUILDER"); // @ts-expect-error - tldraw throws a warning on multiple loads delete window[Symbol.for("__signia__")]; diff --git a/apps/roam/src/utils/isCanvasPage.ts b/apps/roam/src/utils/isCanvasPage.ts index 591ccea5c..26eee68de 100644 --- a/apps/roam/src/utils/isCanvasPage.ts +++ b/apps/roam/src/utils/isCanvasPage.ts @@ -1,11 +1,17 @@ import { DEFAULT_CANVAS_PAGE_FORMAT } from ".."; import { getFormattedConfigTree } from "./discourseConfigRef"; -export const isCanvasPage = ({ title }: { title: string }) => { +const getCanvasPageRegex = (): RegExp => { const { canvasPageFormat } = getFormattedConfigTree(); const format = canvasPageFormat.value || DEFAULT_CANVAS_PAGE_FORMAT; - const canvasRegex = new RegExp(`^${format}$`.replace(/\*/g, ".+")); - return canvasRegex.test(title); + const escaped = format + .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + .replace(/\\\*/g, ".+"); + return new RegExp(`^${escaped}$`); +}; + +export const isCanvasPage = ({ title }: { title: string }) => { + return getCanvasPageRegex().test(title); }; export const isCurrentPageCanvas = ({ @@ -27,3 +33,20 @@ export const isSidebarCanvas = ({ }) => { return isCanvasPage({ title }) && !!h1.closest(".rm-sidebar-outline"); }; + +export const getCanvasPageTitles = async (): Promise => { + const regex = getCanvasPageRegex(); + const escaped = regex.source.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + try { + const results = (await window.roamAlphaAPI.data.async.fast.q(`[ + :find ?title + :where + [(re-pattern "${escaped}") ?regex] + [?node :node/title ?title] + [(re-find ?regex ?title)] + ]`)) as [string][]; + return results.map(([title]) => title).sort((a, b) => a.localeCompare(b)); + } catch { + return []; + } +}; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 85fcf308b..26b780084 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -1,5 +1,4 @@ import { openQueryDrawer } from "~/components/QueryDrawer"; -import { renderCanvasEmbedDialog } from "~/components/canvas/CanvasEmbedDialog"; import { render as exportRender } from "~/components/Export"; import { render as renderToast } from "roamjs-components/components/Toast"; import { createBlock, updateBlock } from "roamjs-components/writes"; @@ -315,31 +314,6 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }; // Roam organizes commands alphabetically - ( - window.roamAlphaAPI.ui as unknown as { - slashCommand: { - addCommand: (cmd: { - label: string; - // eslint-disable-next-line @typescript-eslint/naming-convention - callback: (context: { "block-uid": string }) => void; - }) => void; - }; - } - ).slashCommand.addCommand({ - label: "DG: Embed canvas", - callback: (context) => { - const uid = context["block-uid"]; - if (!uid) return; - renderCanvasEmbedDialog({ - onSelect: (title: string) => { - void updateBlock({ - uid, - text: `{{dg-canvas: [[${title}]]}}`, - }); - }, - }); - }, - }); void addCommand("DG: Create/Insert discourse node", () => createDiscourseNodeFromCommand(extensionAPI), ); diff --git a/apps/roam/src/utils/registerSlashCommands.ts b/apps/roam/src/utils/registerSlashCommands.ts new file mode 100644 index 000000000..a234a0ad7 --- /dev/null +++ b/apps/roam/src/utils/registerSlashCommands.ts @@ -0,0 +1,48 @@ +import { updateBlock } from "roamjs-components/writes"; +import { renderCanvasEmbedDialog } from "~/components/canvas/CanvasEmbedDialog"; + +type SlashCommandContext = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "block-uid": string; +}; + +type SlashCommandApi = { + addCommand: (cmd: { + label: string; + callback: (context: SlashCommandContext) => void; + }) => void; + removeCommand: (cmd: { label: string }) => void; +}; + +const getSlashCommandApi = (): SlashCommandApi => + (window.roamAlphaAPI.ui as unknown as { slashCommand: SlashCommandApi }) + .slashCommand; + +const SLASH_COMMANDS: { + label: string; + callback: (context: SlashCommandContext) => void; +}[] = [ + { + label: "DG: Embed canvas", + callback: (context) => { + const uid = context["block-uid"]; + if (!uid) return; + renderCanvasEmbedDialog({ + onSelect: (title: string) => { + void updateBlock({ + uid, + text: `{{dg-canvas: [[${title}]]}}`, + }); + }, + }); + }, + }, +]; + +export const registerSlashCommands = (): (() => void) => { + const api = getSlashCommandApi(); + for (const cmd of SLASH_COMMANDS) api.addCommand(cmd); + return () => { + for (const { label } of SLASH_COMMANDS) api.removeCommand({ label }); + }; +}; From 56a67d65bdab99f43f333ab659599f81854ca68e Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 20 May 2026 14:05:38 +0530 Subject: [PATCH 4/7] ENG-1553 Replace Tailwind utilities with CSS classes in styles.css Tailwind isn't compiled in the roam build (config has empty content, no @tailwind directives in styles/*.css, no PostCSS step in scripts/compile.ts), so the utility classes added in 08161b06 produced no CSS and the canvas embed wrapper collapsed to 0x0. Move the styles to dedicated classes in styles.css (.dg-canvas-embed, .dg-canvas-embed-placeholder, .dg-canvas-embed-dialog*, .dg-canvas-embed-source-hidden) to match the existing pattern in that file. Uses descendant selectors and proper specificity to override Blueprint dialog defaults and to size the AutocompleteInput inside the dialog -- neither possible from inline styles. --- .../src/components/canvas/CanvasEmbed.tsx | 9 ++-- .../components/canvas/CanvasEmbedDialog.tsx | 14 ++++-- apps/roam/src/styles/styles.css | 48 +++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/apps/roam/src/components/canvas/CanvasEmbed.tsx b/apps/roam/src/components/canvas/CanvasEmbed.tsx index 6b7b6f4cc..e06d28204 100644 --- a/apps/roam/src/components/canvas/CanvasEmbed.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbed.tsx @@ -29,16 +29,14 @@ const getCurrentPageTitle = (el: HTMLElement): string | null => { }; const CanvasEmbedPlaceholder = ({ message }: { message: string }) => ( -
- {message} -
+
{message}
); export const renderCanvasEmbed = ( button: HTMLElement, onloadArgs: OnloadArgs, ) => { - button.classList.add("hidden"); + button.classList.add("dg-canvas-embed-source-hidden"); if (!button.parentElement) return; @@ -70,8 +68,7 @@ export const renderCanvasEmbed = ( button.parentElement.onmousedown = (e: MouseEvent) => e.stopPropagation(); const wrapper = document.createElement("div"); - wrapper.className = - "dg-canvas-embed h-[400px] w-full overflow-hidden rounded-md my-2"; + wrapper.className = "dg-canvas-embed"; button.parentElement.appendChild(wrapper); renderWithUnmount( diff --git a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx index 27633a289..8088efffd 100644 --- a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx @@ -34,10 +34,16 @@ const CanvasEmbedDialog = ({ const renderContent = () => { if (canvasPages === null) return ( -
Loading canvas pages...
+
+ Loading canvas pages... +
); if (canvasPages.length === 0) - return
No canvas pages found
; + return ( +
+ No canvas pages found +
+ ); return ( -
{renderContent()}
+
{renderContent()}
); }; diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index 4675b34e0..d9cd140da 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -177,3 +177,51 @@ #dg-left-sidebar-root .bp3-dark .bp3-button:not([class*="bp3-intent-"]):hover { color: #f5f8fa; } + +.dg-canvas-embed-source-hidden { + display: none; +} + +.dg-canvas-embed { + height: 400px; + width: 100%; + overflow: hidden; + border-radius: 6px; + margin: 8px 0; +} + +.dg-canvas-embed > .roamjs-tldraw-canvas-container { + height: 100%; + width: 100%; +} + +.dg-canvas-embed-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: #8a9ba8; + font-size: 14px; + border: 1px dashed #d1d5db; + border-radius: 6px; +} + +.dg-canvas-embed-dialog.bp3-dialog { + width: 400px; + padding-bottom: 0; +} + +.dg-canvas-embed-dialog-body { + padding: 16px; +} + +.dg-canvas-embed-dialog-message { + color: #5c7080; + font-size: 14px; +} + +.dg-canvas-embed-dialog .roamjs-autocomplete-input-target, +.dg-canvas-embed-dialog .roamjs-autocomplete-input-target .bp3-input-group { + display: block; + width: 100%; +} From e918a8a50b5c988ba736cbf2014521e5268d575f Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 20 May 2026 17:33:27 +0530 Subject: [PATCH 5/7] ENG-1553 Attach canvas embed mousedown handler to wrapper Moves the stopPropagation handler from button.parentElement to the wrapper div. Attaching to the parent overwrote any existing onmousedown Roam had on the block element and was never cleaned up; attaching to the wrapper (which renderWithUnmount owns) scopes the handler to the canvas and removes it on unmount. --- apps/roam/src/components/canvas/CanvasEmbed.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/roam/src/components/canvas/CanvasEmbed.tsx b/apps/roam/src/components/canvas/CanvasEmbed.tsx index e06d28204..0ef498355 100644 --- a/apps/roam/src/components/canvas/CanvasEmbed.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbed.tsx @@ -65,10 +65,9 @@ export const renderCanvasEmbed = ( return; } - button.parentElement.onmousedown = (e: MouseEvent) => e.stopPropagation(); - const wrapper = document.createElement("div"); wrapper.className = "dg-canvas-embed"; + wrapper.onmousedown = (e: MouseEvent) => e.stopPropagation(); button.parentElement.appendChild(wrapper); renderWithUnmount( From 01fc0cf4a4f55d3460e24bac15b12f0fe8d3bfe0 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 20 May 2026 17:58:06 +0530 Subject: [PATCH 6/7] ENG-1553 Drop unreachable self-embed guard The check guarded against a {{dg-canvas: [[X]]}} block living on canvas page X. But canvas pages hide their block outline (tldrawStyles.ts:4-6), so users can't author the block via the normal flow on a canvas page. Removing the dead guard and the getCurrentPageTitle helper that only served it. --- .../src/components/canvas/CanvasEmbed.tsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/apps/roam/src/components/canvas/CanvasEmbed.tsx b/apps/roam/src/components/canvas/CanvasEmbed.tsx index 0ef498355..8293ee9d0 100644 --- a/apps/roam/src/components/canvas/CanvasEmbed.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbed.tsx @@ -2,7 +2,6 @@ import React from "react"; import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; import { OnloadArgs } from "roamjs-components/types"; import renderWithUnmount from "roamjs-components/util/renderWithUnmount"; -import { getPageTitleValueByHtmlElement } from "roamjs-components/dom"; import getBlockUidFromTarget from "roamjs-components/dom/getBlockUidFromTarget"; import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; @@ -20,14 +19,6 @@ const extractCanvasTitle = (button: HTMLElement): string | null => { return match[1].trim(); }; -const getCurrentPageTitle = (el: HTMLElement): string | null => { - try { - return getPageTitleValueByHtmlElement(el); - } catch { - return null; - } -}; - const CanvasEmbedPlaceholder = ({ message }: { message: string }) => (
{message}
); @@ -43,17 +34,6 @@ export const renderCanvasEmbed = ( const title = extractCanvasTitle(button); if (!title) return; - const currentPageTitle = getCurrentPageTitle(button); - if (currentPageTitle === title) { - const wrapper = document.createElement("div"); - button.parentElement.appendChild(wrapper); - renderWithUnmount( - , - wrapper, - ); - return; - } - const pageUid = getPageUidByPageTitle(title); if (!pageUid) { const wrapper = document.createElement("div"); From 383136d668221ae84aebaba0bac58c5967df6cdb Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 21 May 2026 16:14:44 +0530 Subject: [PATCH 7/7] Minimize canvas embed CSS --- .../src/components/canvas/CanvasEmbed.tsx | 8 +++--- .../components/canvas/CanvasEmbedDialog.tsx | 12 +++------ apps/roam/src/styles/styles.css | 25 ------------------- 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/apps/roam/src/components/canvas/CanvasEmbed.tsx b/apps/roam/src/components/canvas/CanvasEmbed.tsx index 8293ee9d0..2b0fc9c0b 100644 --- a/apps/roam/src/components/canvas/CanvasEmbed.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbed.tsx @@ -20,14 +20,16 @@ const extractCanvasTitle = (button: HTMLElement): string | null => { }; const CanvasEmbedPlaceholder = ({ message }: { message: string }) => ( -
{message}
+
+ {message} +
); export const renderCanvasEmbed = ( button: HTMLElement, onloadArgs: OnloadArgs, ) => { - button.classList.add("dg-canvas-embed-source-hidden"); + button.hidden = true; if (!button.parentElement) return; @@ -46,7 +48,7 @@ export const renderCanvasEmbed = ( } const wrapper = document.createElement("div"); - wrapper.className = "dg-canvas-embed"; + wrapper.className = "dg-canvas-embed my-2 w-full overflow-hidden rounded-md"; wrapper.onmousedown = (e: MouseEvent) => e.stopPropagation(); button.parentElement.appendChild(wrapper); diff --git a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx index 8088efffd..308dc8132 100644 --- a/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx +++ b/apps/roam/src/components/canvas/CanvasEmbedDialog.tsx @@ -34,15 +34,11 @@ const CanvasEmbedDialog = ({ const renderContent = () => { if (canvasPages === null) return ( -
- Loading canvas pages... -
+
Loading canvas pages...
); if (canvasPages.length === 0) return ( -
- No canvas pages found -
+
No canvas pages found
); return ( -
{renderContent()}
+
{renderContent()}
); }; diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index fb1a00074..096345e95 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -192,16 +192,8 @@ color: #f5f8fa; } -.dg-canvas-embed-source-hidden { - display: none; -} - .dg-canvas-embed { height: 400px; - width: 100%; - overflow: hidden; - border-radius: 6px; - margin: 8px 0; } .dg-canvas-embed > .roamjs-tldraw-canvas-container { @@ -210,28 +202,11 @@ } .dg-canvas-embed-placeholder { - display: flex; - align-items: center; - justify-content: center; height: 100px; - color: #8a9ba8; - font-size: 14px; - border: 1px dashed #d1d5db; - border-radius: 6px; } .dg-canvas-embed-dialog.bp3-dialog { width: 400px; - padding-bottom: 0; -} - -.dg-canvas-embed-dialog-body { - padding: 16px; -} - -.dg-canvas-embed-dialog-message { - color: #5c7080; - font-size: 14px; } .dg-canvas-embed-dialog .roamjs-autocomplete-input-target,