From 1dfb901cba647e17652cbcdce269a0ba8270651c Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 7 Aug 2025 15:22:10 -0400 Subject: [PATCH 1/8] current state --- apps/obsidian/src/components/TldrawView.tsx | 39 +++- .../src/components/TldrawViewComponent.tsx | 30 ++- apps/obsidian/src/utils/asset-store.ts | 193 ++++++++++++++++++ 3 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 apps/obsidian/src/utils/asset-store.ts diff --git a/apps/obsidian/src/components/TldrawView.tsx b/apps/obsidian/src/components/TldrawView.tsx index 775daef2a..bbb6a3e38 100644 --- a/apps/obsidian/src/components/TldrawView.tsx +++ b/apps/obsidian/src/components/TldrawView.tsx @@ -6,11 +6,13 @@ import { TLStore } from "tldraw"; import React from "react"; import DiscourseGraphPlugin from "~/index"; import { processInitialData, TLData } from "~/utils/tldraw"; +import { ObsidianTLAssetStore } from "~/utils/asset-store"; export class TldrawView extends TextFileView { plugin: DiscourseGraphPlugin; private reactRoot?: Root; private store?: TLStore; + private assetStore?: ObsidianTLAssetStore; private onUnloadCallbacks: (() => void)[] = []; constructor(leaf: WorkspaceLeaf, plugin: DiscourseGraphPlugin) { @@ -73,6 +75,16 @@ export class TldrawView extends TextFileView { return; } + // Create asset store + this.assetStore = new ObsidianTLAssetStore(`tldraw-${file.path}`, { + app: this.app, + file, + }); + + // Set the asset store on the TLStore + // store.props.assets = {}; + // store.props.assets = this.assetStore; + this.setStore(store); } @@ -106,12 +118,20 @@ export class TldrawView extends TextFileView { const root = createRoot(entryPoint); if (!this.file) return; + if (!this.assetStore) { + console.error("Asset store is not set"); + return; + } + + console.log("assetStore", this.assetStore); + root.render( , ); @@ -129,11 +149,11 @@ export class TldrawView extends TextFileView { this.store = store; if (this.tldrawContainer) { - this.refreshView(); + void this.refreshView(); } } - private refreshView() { + private async refreshView() { if (!this.store) return; if (this.reactRoot) { @@ -151,6 +171,7 @@ export class TldrawView extends TextFileView { const container = this.tldrawContainer; if (container) { this.reactRoot = this.createReactRoot(container, this.store); + await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for React to render } } @@ -163,6 +184,12 @@ export class TldrawView extends TextFileView { this.onUnloadCallbacks = []; callbacks.forEach((cb) => cb()); + // Clean up asset store + if (this.assetStore) { + this.assetStore.dispose(); + this.assetStore = undefined; + } + return super.onUnloadFile(file); } @@ -189,5 +216,11 @@ export class TldrawView extends TextFileView { } this.store = undefined; } + + // Clean up asset store + if (this.assetStore) { + this.assetStore.dispose(); + this.assetStore = undefined; + } } -} +} \ No newline at end of file diff --git a/apps/obsidian/src/components/TldrawViewComponent.tsx b/apps/obsidian/src/components/TldrawViewComponent.tsx index e37111c55..57460f602 100644 --- a/apps/obsidian/src/components/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/TldrawViewComponent.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { Editor, ErrorBoundary, Tldraw, TLStore } from "tldraw"; +import { Editor, ErrorBoundary, TLAssetStore, Tldraw, TLStore } from "tldraw"; import "tldraw/tldraw.css"; import { getTLDataTemplate, @@ -20,12 +20,14 @@ interface TldrawPreviewProps { store: TLStore; plugin: DiscourseGraphPlugin; file: TFile; + assetStore: TLAssetStore; } export const TldrawPreviewComponent = ({ store, plugin, file, + assetStore, }: TldrawPreviewProps) => { const containerRef = useRef(null); const [currentStore, setCurrentStore] = useState(store); @@ -91,6 +93,13 @@ export const TldrawPreviewComponent = ({ if (match?.[1]) { const data = JSON.parse(match[1]) as TLData; const { store: newStore } = processInitialData(data); + + // // Restore asset store if it exists + // if (assetStore) { + // newStore.props.assetUrls = {}; + // newStore.props.assets = assetStore; + // } + setCurrentStore(newStore); } } @@ -118,10 +127,14 @@ export const TldrawPreviewComponent = ({ }; }, [currentStore, saveChanges]); - const handleMount = useCallback((editor: Editor) => { - editorRef.current = editor; - editor.setCurrentTool("select"); - }, []); + const handleMount = useCallback( + (editor: Editor) => { + editorRef.current = editor; + editor.setCurrentTool("select"); + currentStore.props.assets = assetStore; + }, + [currentStore, assetStore], + ); return (
Error in Tldraw component: {JSON.stringify(error)}
)} > - + ) : (
Loading Tldraw...
diff --git a/apps/obsidian/src/utils/asset-store.ts b/apps/obsidian/src/utils/asset-store.ts new file mode 100644 index 000000000..79db01668 --- /dev/null +++ b/apps/obsidian/src/utils/asset-store.ts @@ -0,0 +1,193 @@ +import { App, TFile } from "obsidian" +import { TLAsset, TLAssetStore, TLAssetId, TLAssetContext } from "tldraw" +import { JsonObject } from "@tldraw/utils" + +const ASSET_PREFIX = "obsidian.blockref." +type BlockRefAssetId = `${typeof ASSET_PREFIX}${string}` +type AssetDataUrl = string + +interface AssetStoreOptions { + app: App + file: TFile +} + +/** + * Proxy class that handles Obsidian-specific file operations for the TLDraw asset store + */ +class ObsidianMarkdownFileTLAssetStoreProxy { + #resolvedAssetDataCache = new Map() + #app: App + #file: TFile + + constructor(options: AssetStoreOptions) { + this.#app = options.app + this.#file = options.file + } + + async storeAsset(asset: TLAsset, file: File): Promise { + // Generate unique block reference ID + const blockRefId = crypto.randomUUID() + + // Create sanitized file name + const objectName = `${blockRefId}-${file.name}`.replace(/\W/g, '-') + const ext = file.type.split('/').at(1) + const fileName = !ext ? objectName : `${objectName}.${ext}` + + console.log("fileName", fileName) + + // Get the attachment folder path + let attachmentFolder = this.#app.vault.getFolderByPath("attachments") + if (!attachmentFolder) { + attachmentFolder = await this.#app.vault.createFolder("attachments") + } + const filePath = `${attachmentFolder.path}/${fileName}` + + // Store file in vault + const arrayBuffer = await file.arrayBuffer() + console.log("arrayBuffer", arrayBuffer) + const assetFile = await this.#app.vault.createBinary(filePath, arrayBuffer) + console.log("assetFile", assetFile) + + // Create markdown link and block reference + const internalLink = this.#app.fileManager.generateMarkdownLink(assetFile, this.#file.path) + const linkBlock = `${internalLink}\n^${blockRefId}` + + // Add to top of file after frontmatter + await this.#addToTopOfFile(linkBlock) + + // Cache the asset URL + const assetDataUri = URL.createObjectURL(file) + const assetId = `${ASSET_PREFIX}${blockRefId}` as BlockRefAssetId + this.#resolvedAssetDataCache.set(assetId, assetDataUri) + + return assetId + } + + async getCached(blockRefAssetId: BlockRefAssetId): Promise { + try { + // Check cache first + const cached = this.#resolvedAssetDataCache.get(blockRefAssetId) + if (cached) return cached + + // Load and cache if needed + const assetData = await this.#getAssetData(blockRefAssetId) + if (!assetData) return null + + const uri = URL.createObjectURL(new Blob([assetData])) + this.#resolvedAssetDataCache.set(blockRefAssetId, uri) + return uri + } catch (error) { + console.error("Error getting cached asset:", error) + return null + } + } + + dispose() { + // Revoke all cached URLs + for (const url of this.#resolvedAssetDataCache.values()) { + URL.revokeObjectURL(url) + } + this.#resolvedAssetDataCache.clear() + } + + // Private helper methods + async #addToTopOfFile(content: string) { + const fileContent = await this.#app.vault.read(this.#file) + const fileCache = this.#app.metadataCache.getFileCache(this.#file) + + if (fileCache?.frontmatter?.position) { + const position = fileCache.frontmatter.position + const before = fileContent.slice(0, position.end.offset) + const after = fileContent.slice(position.end.offset) + const newContent = `${before}\n${content}${after}` + await this.#app.vault.modify(this.#file, newContent) + } else { + // No frontmatter, just add to top + const newContent = `${content}\n\n${fileContent}` + await this.#app.vault.modify(this.#file, newContent) + } + } + + async #getAssetData(blockRefAssetId: BlockRefAssetId): Promise { + try { + const blockRef = blockRefAssetId.slice(ASSET_PREFIX.length) + if (!blockRef) return null + + // Get block from metadata cache + const fileCache = this.#app.metadataCache.getFileCache(this.#file) + if (!fileCache?.blocks?.[blockRef]) return null + + const block = fileCache.blocks[blockRef] + const fileContent = await this.#app.vault.read(this.#file) + const blockContent = fileContent.substring(block.position.start.offset, block.position.end.offset) + + // Extract link from block content + const match = blockContent.match(/\[\[(.*?)\]\]/) + if (!match?.[1]) return null + + // Resolve link to actual file + const linkPath = match[1] + const linkedFile = this.#app.metadataCache.getFirstLinkpathDest(linkPath, this.#file.path) + if (!linkedFile) return null + + // Read the binary data + return await this.#app.vault.readBinary(linkedFile) + } catch (error) { + console.error("Error getting asset data:", error) + return null + } + } +} + +/** + * TLDraw asset store implementation for Obsidian + */ +export class ObsidianTLAssetStore implements TLAssetStore { + #proxy: ObsidianMarkdownFileTLAssetStoreProxy; + + constructor( + public readonly persistenceKey: string, + options: AssetStoreOptions, + ) { + this.#proxy = new ObsidianMarkdownFileTLAssetStoreProxy(options); + } + + async upload( + asset: TLAsset, + file: File, + ): Promise<{ src: string; meta?: JsonObject }> { + try { + const blockRefAssetId = await this.#proxy.storeAsset(asset, file); + return { + src: `asset:${blockRefAssetId}`, + }; + } catch (error) { + console.error("Error uploading asset:", error); + throw error; + } + } + + async resolve(asset: TLAsset, ctx: TLAssetContext): Promise { + try { + const assetSrc = asset.props.src; + if (!assetSrc?.startsWith("asset:")) return assetSrc ?? null; + + const assetId = assetSrc.split(":")[1] as BlockRefAssetId; + if (!assetId) return null; + + return await this.#proxy.getCached(assetId); + } catch (error) { + console.error("Error resolving asset:", error); + return null; + } + } + + async remove(assetIds: TLAssetId[]): Promise { + // No-op for now as we don't want to delete files from the vault + // The files will remain in the vault and can be managed by the user + } + + dispose() { + this.#proxy.dispose(); + } +} \ No newline at end of file From 57c8ebdc8519d7be2fc973a1e718798afe7cb8f0 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 7 Aug 2025 15:54:30 -0400 Subject: [PATCH 2/8] works now --- .../src/components/TldrawViewComponent.tsx | 9 +- apps/obsidian/src/utils/asset-store.ts | 166 +++++++++--------- 2 files changed, 89 insertions(+), 86 deletions(-) diff --git a/apps/obsidian/src/components/TldrawViewComponent.tsx b/apps/obsidian/src/components/TldrawViewComponent.tsx index 57460f602..8a5ef5de9 100644 --- a/apps/obsidian/src/components/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/TldrawViewComponent.tsx @@ -20,7 +20,7 @@ interface TldrawPreviewProps { store: TLStore; plugin: DiscourseGraphPlugin; file: TFile; - assetStore: TLAssetStore; + assetStore: Required; } export const TldrawPreviewComponent = ({ @@ -148,12 +148,7 @@ export const TldrawPreviewComponent = ({
Error in Tldraw component: {JSON.stringify(error)}
)} > - + ) : (
Loading Tldraw...
diff --git a/apps/obsidian/src/utils/asset-store.ts b/apps/obsidian/src/utils/asset-store.ts index 79db01668..486833d38 100644 --- a/apps/obsidian/src/utils/asset-store.ts +++ b/apps/obsidian/src/utils/asset-store.ts @@ -1,140 +1,148 @@ -import { App, TFile } from "obsidian" -import { TLAsset, TLAssetStore, TLAssetId, TLAssetContext } from "tldraw" -import { JsonObject } from "@tldraw/utils" +import { App, TFile } from "obsidian"; +import { TLAsset, TLAssetStore, TLAssetId, TLAssetContext } from "tldraw"; +import { JsonObject } from "@tldraw/utils"; -const ASSET_PREFIX = "obsidian.blockref." -type BlockRefAssetId = `${typeof ASSET_PREFIX}${string}` -type AssetDataUrl = string +const ASSET_PREFIX = "obsidian.blockref."; +type BlockRefAssetId = `${typeof ASSET_PREFIX}${string}`; +type AssetDataUrl = string; interface AssetStoreOptions { - app: App - file: TFile + app: App; + file: TFile; } /** * Proxy class that handles Obsidian-specific file operations for the TLDraw asset store */ class ObsidianMarkdownFileTLAssetStoreProxy { - #resolvedAssetDataCache = new Map() - #app: App - #file: TFile + #resolvedAssetDataCache = new Map(); + #app: App; + #file: TFile; constructor(options: AssetStoreOptions) { - this.#app = options.app - this.#file = options.file + this.#app = options.app; + this.#file = options.file; } async storeAsset(asset: TLAsset, file: File): Promise { // Generate unique block reference ID - const blockRefId = crypto.randomUUID() - + const blockRefId = crypto.randomUUID(); + // Create sanitized file name - const objectName = `${blockRefId}-${file.name}`.replace(/\W/g, '-') - const ext = file.type.split('/').at(1) - const fileName = !ext ? objectName : `${objectName}.${ext}` + const objectName = `${blockRefId}-${file.name}`.replace(/\W/g, "-"); + const ext = file.type.split("/").at(1); + const fileName = !ext ? objectName : `${objectName}.${ext}`; - console.log("fileName", fileName) + console.log("fileName", fileName); // Get the attachment folder path - let attachmentFolder = this.#app.vault.getFolderByPath("attachments") + let attachmentFolder = this.#app.vault.getFolderByPath("attachments"); if (!attachmentFolder) { - attachmentFolder = await this.#app.vault.createFolder("attachments") + attachmentFolder = await this.#app.vault.createFolder("attachments"); } - const filePath = `${attachmentFolder.path}/${fileName}` + const filePath = `${attachmentFolder.path}/${fileName}`; // Store file in vault - const arrayBuffer = await file.arrayBuffer() - console.log("arrayBuffer", arrayBuffer) - const assetFile = await this.#app.vault.createBinary(filePath, arrayBuffer) - console.log("assetFile", assetFile) + const arrayBuffer = await file.arrayBuffer(); + console.log("arrayBuffer", arrayBuffer); + const assetFile = await this.#app.vault.createBinary(filePath, arrayBuffer); + console.log("assetFile", assetFile); // Create markdown link and block reference - const internalLink = this.#app.fileManager.generateMarkdownLink(assetFile, this.#file.path) - const linkBlock = `${internalLink}\n^${blockRefId}` + const internalLink = this.#app.fileManager.generateMarkdownLink( + assetFile, + this.#file.path, + ); + const linkBlock = `${internalLink}\n^${blockRefId}`; - // Add to top of file after frontmatter - await this.#addToTopOfFile(linkBlock) + await this.#addToTopOfFile(linkBlock); - // Cache the asset URL - const assetDataUri = URL.createObjectURL(file) - const assetId = `${ASSET_PREFIX}${blockRefId}` as BlockRefAssetId - this.#resolvedAssetDataCache.set(assetId, assetDataUri) + const assetDataUri = URL.createObjectURL(file); + const assetId = `${ASSET_PREFIX}${blockRefId}` as BlockRefAssetId; + this.#resolvedAssetDataCache.set(assetId, assetDataUri); - return assetId + return assetId; } - async getCached(blockRefAssetId: BlockRefAssetId): Promise { + async getCached( + blockRefAssetId: BlockRefAssetId, + ): Promise { try { // Check cache first - const cached = this.#resolvedAssetDataCache.get(blockRefAssetId) - if (cached) return cached + const cached = this.#resolvedAssetDataCache.get(blockRefAssetId); + if (cached) return cached; // Load and cache if needed - const assetData = await this.#getAssetData(blockRefAssetId) - if (!assetData) return null + const assetData = await this.#getAssetData(blockRefAssetId); + if (!assetData) return null; - const uri = URL.createObjectURL(new Blob([assetData])) - this.#resolvedAssetDataCache.set(blockRefAssetId, uri) - return uri + const uri = URL.createObjectURL(new Blob([assetData])); + this.#resolvedAssetDataCache.set(blockRefAssetId, uri); + return uri; } catch (error) { - console.error("Error getting cached asset:", error) - return null + console.error("Error getting cached asset:", error); + return null; } } dispose() { // Revoke all cached URLs for (const url of this.#resolvedAssetDataCache.values()) { - URL.revokeObjectURL(url) + URL.revokeObjectURL(url); } - this.#resolvedAssetDataCache.clear() + this.#resolvedAssetDataCache.clear(); } // Private helper methods async #addToTopOfFile(content: string) { - const fileContent = await this.#app.vault.read(this.#file) - const fileCache = this.#app.metadataCache.getFileCache(this.#file) - - if (fileCache?.frontmatter?.position) { - const position = fileCache.frontmatter.position - const before = fileContent.slice(0, position.end.offset) - const after = fileContent.slice(position.end.offset) - const newContent = `${before}\n${content}${after}` - await this.#app.vault.modify(this.#file, newContent) - } else { - // No frontmatter, just add to top - const newContent = `${content}\n\n${fileContent}` - await this.#app.vault.modify(this.#file, newContent) - } + await this.#app.vault.process(this.#file, (data: string) => { + const fileCache = this.#app.metadataCache.getFileCache(this.#file); + const { start, end } = fileCache?.frontmatterPosition ?? { + start: { offset: 0 }, + end: { offset: 0 }, + }; + + const frontmatter = data.slice(start.offset, end.offset); + const rest = data.slice(end.offset); + return `${frontmatter}\n${content}\n${rest}`; + }); } - async #getAssetData(blockRefAssetId: BlockRefAssetId): Promise { + async #getAssetData( + blockRefAssetId: BlockRefAssetId, + ): Promise { try { - const blockRef = blockRefAssetId.slice(ASSET_PREFIX.length) - if (!blockRef) return null + const blockRef = blockRefAssetId.slice(ASSET_PREFIX.length); + if (!blockRef) return null; // Get block from metadata cache - const fileCache = this.#app.metadataCache.getFileCache(this.#file) - if (!fileCache?.blocks?.[blockRef]) return null + const fileCache = this.#app.metadataCache.getFileCache(this.#file); + if (!fileCache?.blocks?.[blockRef]) return null; - const block = fileCache.blocks[blockRef] - const fileContent = await this.#app.vault.read(this.#file) - const blockContent = fileContent.substring(block.position.start.offset, block.position.end.offset) + const block = fileCache.blocks[blockRef]; + const fileContent = await this.#app.vault.read(this.#file); + const blockContent = fileContent.substring( + block.position.start.offset, + block.position.end.offset, + ); // Extract link from block content - const match = blockContent.match(/\[\[(.*?)\]\]/) - if (!match?.[1]) return null + const match = blockContent.match(/\[\[(.*?)\]\]/); + if (!match?.[1]) return null; // Resolve link to actual file - const linkPath = match[1] - const linkedFile = this.#app.metadataCache.getFirstLinkpathDest(linkPath, this.#file.path) - if (!linkedFile) return null + const linkPath = match[1]; + const linkedFile = this.#app.metadataCache.getFirstLinkpathDest( + linkPath, + this.#file.path, + ); + if (!linkedFile) return null; // Read the binary data - return await this.#app.vault.readBinary(linkedFile) + return await this.#app.vault.readBinary(linkedFile); } catch (error) { - console.error("Error getting asset data:", error) - return null + console.error("Error getting asset data:", error); + return null; } } } @@ -142,7 +150,7 @@ class ObsidianMarkdownFileTLAssetStoreProxy { /** * TLDraw asset store implementation for Obsidian */ -export class ObsidianTLAssetStore implements TLAssetStore { +export class ObsidianTLAssetStore implements Required { #proxy: ObsidianMarkdownFileTLAssetStoreProxy; constructor( @@ -190,4 +198,4 @@ export class ObsidianTLAssetStore implements TLAssetStore { dispose() { this.#proxy.dispose(); } -} \ No newline at end of file +} From 9fb95e7ec3295e5a0c7455c4f6e6db6d484c6c43 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 8 Aug 2025 16:01:57 -0400 Subject: [PATCH 3/8] clean up --- apps/obsidian/src/components/TldrawView.tsx | 7 ---- .../src/components/TldrawViewComponent.tsx | 7 ---- apps/obsidian/src/utils/asset-store.ts | 39 ++++++++++--------- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/apps/obsidian/src/components/TldrawView.tsx b/apps/obsidian/src/components/TldrawView.tsx index bbb6a3e38..4effe6b0a 100644 --- a/apps/obsidian/src/components/TldrawView.tsx +++ b/apps/obsidian/src/components/TldrawView.tsx @@ -75,16 +75,11 @@ export class TldrawView extends TextFileView { return; } - // Create asset store this.assetStore = new ObsidianTLAssetStore(`tldraw-${file.path}`, { app: this.app, file, }); - // Set the asset store on the TLStore - // store.props.assets = {}; - // store.props.assets = this.assetStore; - this.setStore(store); } @@ -184,7 +179,6 @@ export class TldrawView extends TextFileView { this.onUnloadCallbacks = []; callbacks.forEach((cb) => cb()); - // Clean up asset store if (this.assetStore) { this.assetStore.dispose(); this.assetStore = undefined; @@ -217,7 +211,6 @@ export class TldrawView extends TextFileView { this.store = undefined; } - // Clean up asset store if (this.assetStore) { this.assetStore.dispose(); this.assetStore = undefined; diff --git a/apps/obsidian/src/components/TldrawViewComponent.tsx b/apps/obsidian/src/components/TldrawViewComponent.tsx index 8a5ef5de9..1add48b3f 100644 --- a/apps/obsidian/src/components/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/TldrawViewComponent.tsx @@ -93,13 +93,6 @@ export const TldrawPreviewComponent = ({ if (match?.[1]) { const data = JSON.parse(match[1]) as TLData; const { store: newStore } = processInitialData(data); - - // // Restore asset store if it exists - // if (assetStore) { - // newStore.props.assetUrls = {}; - // newStore.props.assets = assetStore; - // } - setCurrentStore(newStore); } } diff --git a/apps/obsidian/src/utils/asset-store.ts b/apps/obsidian/src/utils/asset-store.ts index 486833d38..0e96a5167 100644 --- a/apps/obsidian/src/utils/asset-store.ts +++ b/apps/obsidian/src/utils/asset-store.ts @@ -12,7 +12,7 @@ interface AssetStoreOptions { } /** - * Proxy class that handles Obsidian-specific file operations for the TLDraw asset store + * Proxy class that handles Obsidian-specific file operations for the TLAssetStore */ class ObsidianMarkdownFileTLAssetStoreProxy { #resolvedAssetDataCache = new Map(); @@ -25,30 +25,22 @@ class ObsidianMarkdownFileTLAssetStoreProxy { } async storeAsset(asset: TLAsset, file: File): Promise { - // Generate unique block reference ID const blockRefId = crypto.randomUUID(); - // Create sanitized file name const objectName = `${blockRefId}-${file.name}`.replace(/\W/g, "-"); const ext = file.type.split("/").at(1); const fileName = !ext ? objectName : `${objectName}.${ext}`; - console.log("fileName", fileName); - - // Get the attachment folder path + // TODO: in the future, get this from the user's settings let attachmentFolder = this.#app.vault.getFolderByPath("attachments"); if (!attachmentFolder) { attachmentFolder = await this.#app.vault.createFolder("attachments"); } const filePath = `${attachmentFolder.path}/${fileName}`; - // Store file in vault const arrayBuffer = await file.arrayBuffer(); - console.log("arrayBuffer", arrayBuffer); const assetFile = await this.#app.vault.createBinary(filePath, arrayBuffer); - console.log("assetFile", assetFile); - // Create markdown link and block reference const internalLink = this.#app.fileManager.generateMarkdownLink( assetFile, this.#file.path, @@ -86,14 +78,12 @@ class ObsidianMarkdownFileTLAssetStoreProxy { } dispose() { - // Revoke all cached URLs for (const url of this.#resolvedAssetDataCache.values()) { URL.revokeObjectURL(url); } this.#resolvedAssetDataCache.clear(); } - // Private helper methods async #addToTopOfFile(content: string) { await this.#app.vault.process(this.#file, (data: string) => { const fileCache = this.#app.metadataCache.getFileCache(this.#file); @@ -115,7 +105,6 @@ class ObsidianMarkdownFileTLAssetStoreProxy { const blockRef = blockRefAssetId.slice(ASSET_PREFIX.length); if (!blockRef) return null; - // Get block from metadata cache const fileCache = this.#app.metadataCache.getFileCache(this.#file); if (!fileCache?.blocks?.[blockRef]) return null; @@ -126,19 +115,17 @@ class ObsidianMarkdownFileTLAssetStoreProxy { block.position.end.offset, ); - // Extract link from block content const match = blockContent.match(/\[\[(.*?)\]\]/); if (!match?.[1]) return null; - // Resolve link to actual file const linkPath = match[1]; const linkedFile = this.#app.metadataCache.getFirstLinkpathDest( linkPath, this.#file.path, ); - if (!linkedFile) return null; - // Read the binary data + if (!linkedFile) return null; + // TODO: handle other file types too return await this.#app.vault.readBinary(linkedFile); } catch (error) { console.error("Error getting asset data:", error); @@ -148,7 +135,7 @@ class ObsidianMarkdownFileTLAssetStoreProxy { } /** - * TLDraw asset store implementation for Obsidian + * TLAssetStore implementation for Obsidian */ export class ObsidianTLAssetStore implements Required { #proxy: ObsidianMarkdownFileTLAssetStoreProxy; @@ -168,6 +155,9 @@ export class ObsidianTLAssetStore implements Required { const blockRefAssetId = await this.#proxy.storeAsset(asset, file); return { src: `asset:${blockRefAssetId}`, + meta: { + mimeType: file.type, + }, }; } catch (error) { console.error("Error uploading asset:", error); @@ -183,7 +173,18 @@ export class ObsidianTLAssetStore implements Required { const assetId = assetSrc.split(":")[1] as BlockRefAssetId; if (!assetId) return null; - return await this.#proxy.getCached(assetId); + const mimeType = + (asset.props as unknown as { mimeType?: string })?.mimeType ?? + (asset.meta as unknown as { mimeType?: string })?.mimeType ?? + ""; + + if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) { + return await this.#proxy.getCached(assetId); + } + + // Non-media (e.g., text/markdown, application/*): let custom shapes decide. + // Return null so default media shapes won't attempt to render it. + return null; } catch (error) { console.error("Error resolving asset:", error); return null; From 2da79fb7c8a3d5f72a05a846c5de13c4e281eba5 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 8 Aug 2025 16:33:10 -0400 Subject: [PATCH 4/8] address PR comments --- apps/obsidian/src/components/TldrawView.tsx | 21 +++++++++++---------- apps/obsidian/src/utils/asset-store.ts | 19 +++++++++++++++++-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/obsidian/src/components/TldrawView.tsx b/apps/obsidian/src/components/TldrawView.tsx index 4effe6b0a..50c0e6108 100644 --- a/apps/obsidian/src/components/TldrawView.tsx +++ b/apps/obsidian/src/components/TldrawView.tsx @@ -75,12 +75,15 @@ export class TldrawView extends TextFileView { return; } - this.assetStore = new ObsidianTLAssetStore(`tldraw-${file.path}`, { - app: this.app, - file, - }); + this.assetStore = new ObsidianTLAssetStore( + `tldraw-${encodeURIComponent(file.path)}`, + { + app: this.app, + file, + }, + ); - this.setStore(store); + await this.setStore(store); } private createStore(fileData: string): TLStore | undefined { @@ -114,12 +117,10 @@ export class TldrawView extends TextFileView { if (!this.file) return; if (!this.assetStore) { - console.error("Asset store is not set"); + console.warn("Asset store is not set"); return; } - console.log("assetStore", this.assetStore); - root.render( Date: Mon, 11 Aug 2025 11:06:40 -0400 Subject: [PATCH 5/8] address PR reviews --- apps/obsidian/src/components/TldrawView.tsx | 56 +++++---- .../src/components/TldrawViewComponent.tsx | 19 +-- .../utils/{asset-store.ts => assetStore.ts} | 109 ++++++++++-------- apps/obsidian/src/utils/tldraw.ts | 4 + 4 files changed, 99 insertions(+), 89 deletions(-) rename apps/obsidian/src/utils/{asset-store.ts => assetStore.ts} (63%) diff --git a/apps/obsidian/src/components/TldrawView.tsx b/apps/obsidian/src/components/TldrawView.tsx index 50c0e6108..b22711839 100644 --- a/apps/obsidian/src/components/TldrawView.tsx +++ b/apps/obsidian/src/components/TldrawView.tsx @@ -6,13 +6,13 @@ import { TLStore } from "tldraw"; import React from "react"; import DiscourseGraphPlugin from "~/index"; import { processInitialData, TLData } from "~/utils/tldraw"; -import { ObsidianTLAssetStore } from "~/utils/asset-store"; +import { ObsidianTLAssetStore } from "~/utils/assetStore"; export class TldrawView extends TextFileView { plugin: DiscourseGraphPlugin; private reactRoot?: Root; - private store?: TLStore; - private assetStore?: ObsidianTLAssetStore; + private store: TLStore | null = null; + private assetStore: ObsidianTLAssetStore | null = null; private onUnloadCallbacks: (() => void)[] = []; constructor(leaf: WorkspaceLeaf, plugin: DiscourseGraphPlugin) { @@ -33,7 +33,7 @@ export class TldrawView extends TextFileView { return this.data; } - setViewData(data: string, clear: boolean): void { + setViewData(data: string, _clear: boolean): void { this.data = data; } @@ -68,25 +68,28 @@ export class TldrawView extends TextFileView { const fileData = await this.app.vault.read(file); - const store = this.createStore(fileData); - - if (!store) { - console.warn("No tldraw data found in file"); - return; - } - - this.assetStore = new ObsidianTLAssetStore( + const assetStore = new ObsidianTLAssetStore( `tldraw-${encodeURIComponent(file.path)}`, { app: this.app, file, }, ); + const store = this.createStore(fileData, assetStore); + + if (!store) { + console.warn("No tldraw data found in file"); + return; + } + this.assetStore = assetStore; await this.setStore(store); } - private createStore(fileData: string): TLStore | undefined { + private createStore( + fileData: string, + assetStore: ObsidianTLAssetStore, + ): TLStore | undefined { try { const match = fileData.match( /```json !!!_START_OF_TLDRAW_DG_DATA__DO_NOT_CHANGE_THIS_PHRASE_!!!([\s\S]*?)!!!_END_OF_TLDRAW_DG_DATA__DO_NOT_CHANGE_THIS_PHRASE_!!!\n```/, @@ -103,7 +106,7 @@ export class TldrawView extends TextFileView { return; } - const { store } = processInitialData(data); + const { store } = processInitialData(data, assetStore); return store; } catch (e) { @@ -112,22 +115,25 @@ export class TldrawView extends TextFileView { } } + private assertInitialized(): void { + if (!this.file) throw new Error("TldrawView not initialized: missing file"); + if (!this.assetStore) + throw new Error("TldrawView not initialized: missing assetStore"); + if (!this.store) + throw new Error("TldrawView not initialized: missing store"); + } + private createReactRoot(entryPoint: Element, store: TLStore) { const root = createRoot(entryPoint); - if (!this.file) return; - - if (!this.assetStore) { - console.warn("Asset store is not set"); - return; - } + this.assertInitialized(); root.render( , ); @@ -182,7 +188,7 @@ export class TldrawView extends TextFileView { if (this.assetStore) { this.assetStore.dispose(); - this.assetStore = undefined; + this.assetStore = null; } return super.onUnloadFile(file); @@ -209,12 +215,12 @@ export class TldrawView extends TextFileView { } catch (e) { console.error("Failed to dispose store", e); } - this.store = undefined; + this.store = null; } if (this.assetStore) { this.assetStore.dispose(); - this.assetStore = undefined; + this.assetStore = null; } } } \ No newline at end of file diff --git a/apps/obsidian/src/components/TldrawViewComponent.tsx b/apps/obsidian/src/components/TldrawViewComponent.tsx index 1add48b3f..e0a1bf2ef 100644 --- a/apps/obsidian/src/components/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/TldrawViewComponent.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { Editor, ErrorBoundary, TLAssetStore, Tldraw, TLStore } from "tldraw"; +import { ErrorBoundary, Tldraw, TLStore } from "tldraw"; import "tldraw/tldraw.css"; import { getTLDataTemplate, @@ -15,12 +15,13 @@ import { TLDATA_DELIMITER_START, } from "~/constants"; import { TFile } from "obsidian"; +import { ObsidianTLAssetStore } from "~/utils/assetStore"; interface TldrawPreviewProps { store: TLStore; plugin: DiscourseGraphPlugin; file: TFile; - assetStore: Required; + assetStore: ObsidianTLAssetStore; } export const TldrawPreviewComponent = ({ @@ -32,7 +33,6 @@ export const TldrawPreviewComponent = ({ const containerRef = useRef(null); const [currentStore, setCurrentStore] = useState(store); const [isReady, setIsReady] = useState(false); - const editorRef = useRef(null); const saveTimeoutRef = useRef(); const lastSavedDataRef = useRef(""); @@ -92,7 +92,7 @@ export const TldrawPreviewComponent = ({ ); if (match?.[1]) { const data = JSON.parse(match[1]) as TLData; - const { store: newStore } = processInitialData(data); + const { store: newStore } = processInitialData(data, assetStore); setCurrentStore(newStore); } } @@ -120,15 +120,6 @@ export const TldrawPreviewComponent = ({ }; }, [currentStore, saveChanges]); - const handleMount = useCallback( - (editor: Editor) => { - editorRef.current = editor; - editor.setCurrentTool("select"); - currentStore.props.assets = assetStore; - }, - [currentStore, assetStore], - ); - return (
Error in Tldraw component: {JSON.stringify(error)}
)} > - + ) : (
Loading Tldraw...
diff --git a/apps/obsidian/src/utils/asset-store.ts b/apps/obsidian/src/utils/assetStore.ts similarity index 63% rename from apps/obsidian/src/utils/asset-store.ts rename to apps/obsidian/src/utils/assetStore.ts index f738dd601..72231bdf0 100644 --- a/apps/obsidian/src/utils/asset-store.ts +++ b/apps/obsidian/src/utils/assetStore.ts @@ -6,24 +6,24 @@ const ASSET_PREFIX = "obsidian.blockref."; type BlockRefAssetId = `${typeof ASSET_PREFIX}${string}`; type AssetDataUrl = string; -interface AssetStoreOptions { +type AssetStoreOptions = { app: App; file: TFile; -} +}; /** * Proxy class that handles Obsidian-specific file operations for the TLAssetStore */ class ObsidianMarkdownFileTLAssetStoreProxy { - #resolvedAssetDataCache = new Map(); - #app: App; - #file: TFile; + private resolvedAssetDataCache = new Map(); + private app: App; + private file: TFile; /** * Safely set a cached Blob URL for an asset id, revoking any previous URL to avoid leaks */ - #setCachedUrl(blockRefAssetId: BlockRefAssetId, url: AssetDataUrl) { - const previousUrl = this.#resolvedAssetDataCache.get(blockRefAssetId); + private setCachedUrl(blockRefAssetId: BlockRefAssetId, url: AssetDataUrl) { + const previousUrl = this.resolvedAssetDataCache.get(blockRefAssetId); if (previousUrl && previousUrl !== url) { try { URL.revokeObjectURL(previousUrl); @@ -31,15 +31,18 @@ class ObsidianMarkdownFileTLAssetStoreProxy { console.warn("Failed to revoke previous object URL", err); } } - this.#resolvedAssetDataCache.set(blockRefAssetId, url); + this.resolvedAssetDataCache.set(blockRefAssetId, url); } constructor(options: AssetStoreOptions) { - this.#app = options.app; - this.#file = options.file; + this.app = options.app; + this.file = options.file; } - async storeAsset(asset: TLAsset, file: File): Promise { + storeAsset = async ( + _asset: TLAsset, + file: File, + ): Promise => { const blockRefId = crypto.randomUUID(); const objectName = `${blockRefId}-${file.name}`.replace(/\W/g, "-"); @@ -47,44 +50,47 @@ class ObsidianMarkdownFileTLAssetStoreProxy { const fileName = !ext ? objectName : `${objectName}.${ext}`; // TODO: in the future, get this from the user's settings - let attachmentFolder = this.#app.vault.getFolderByPath("attachments"); + let attachmentFolder = this.app.vault.getFolderByPath("attachments"); if (!attachmentFolder) { - attachmentFolder = await this.#app.vault.createFolder("attachments"); + attachmentFolder = await this.app.vault.createFolder("attachments"); } const filePath = `${attachmentFolder.path}/${fileName}`; const arrayBuffer = await file.arrayBuffer(); - const assetFile = await this.#app.vault.createBinary(filePath, arrayBuffer); + const assetFile = await this.app.vault.createBinary(filePath, arrayBuffer); - const internalLink = this.#app.fileManager.generateMarkdownLink( + // Generate a plain wikilink (without image embed bang) using Obsidian API + const linkText = this.app.metadataCache.fileToLinktext( assetFile, - this.#file.path, + this.file.path, ); + const internalLink = `[[${linkText}]]`; + console.log("internalLink", internalLink); const linkBlock = `${internalLink}\n^${blockRefId}`; - await this.#addToTopOfFile(linkBlock); + await this.addToTopOfFile(linkBlock); const assetDataUri = URL.createObjectURL(file); const assetId = `${ASSET_PREFIX}${blockRefId}` as BlockRefAssetId; - this.#setCachedUrl(assetId, assetDataUri); + this.setCachedUrl(assetId, assetDataUri); return assetId; } - async getCached( + getCached = async ( blockRefAssetId: BlockRefAssetId, - ): Promise { + ): Promise => { try { // Check cache first - const cached = this.#resolvedAssetDataCache.get(blockRefAssetId); + const cached = this.resolvedAssetDataCache.get(blockRefAssetId); if (cached) return cached; // Load and cache if needed - const assetData = await this.#getAssetData(blockRefAssetId); + const assetData = await this.getAssetData(blockRefAssetId); if (!assetData) return null; const uri = URL.createObjectURL(new Blob([assetData])); - this.#setCachedUrl(blockRefAssetId, uri); + this.setCachedUrl(blockRefAssetId, uri); return uri; } catch (error) { console.error("Error getting cached asset:", error); @@ -92,16 +98,16 @@ class ObsidianMarkdownFileTLAssetStoreProxy { } } - dispose() { - for (const url of this.#resolvedAssetDataCache.values()) { + dispose = () => { + for (const url of this.resolvedAssetDataCache.values()) { URL.revokeObjectURL(url); } - this.#resolvedAssetDataCache.clear(); + this.resolvedAssetDataCache.clear(); } - async #addToTopOfFile(content: string) { - await this.#app.vault.process(this.#file, (data: string) => { - const fileCache = this.#app.metadataCache.getFileCache(this.#file); + private addToTopOfFile = async (content: string) => { + await this.app.vault.process(this.file, (data: string) => { + const fileCache = this.app.metadataCache.getFileCache(this.file); const { start, end } = fileCache?.frontmatterPosition ?? { start: { offset: 0 }, end: { offset: 0 }, @@ -113,18 +119,18 @@ class ObsidianMarkdownFileTLAssetStoreProxy { }); } - async #getAssetData( + private getAssetData = async ( blockRefAssetId: BlockRefAssetId, - ): Promise { + ): Promise => { try { const blockRef = blockRefAssetId.slice(ASSET_PREFIX.length); if (!blockRef) return null; - const fileCache = this.#app.metadataCache.getFileCache(this.#file); + const fileCache = this.app.metadataCache.getFileCache(this.file); if (!fileCache?.blocks?.[blockRef]) return null; const block = fileCache.blocks[blockRef]; - const fileContent = await this.#app.vault.read(this.#file); + const fileContent = await this.app.vault.read(this.file); const blockContent = fileContent.substring( block.position.start.offset, block.position.end.offset, @@ -134,14 +140,14 @@ class ObsidianMarkdownFileTLAssetStoreProxy { if (!match?.[1]) return null; const linkPath = match[1]; - const linkedFile = this.#app.metadataCache.getFirstLinkpathDest( + const linkedFile = this.app.metadataCache.getFirstLinkpathDest( linkPath, - this.#file.path, + this.file.path, ); if (!linkedFile) return null; // TODO: handle other file types too - return await this.#app.vault.readBinary(linkedFile); + return await this.app.vault.readBinary(linkedFile); } catch (error) { console.error("Error getting asset data:", error); return null; @@ -153,21 +159,21 @@ class ObsidianMarkdownFileTLAssetStoreProxy { * TLAssetStore implementation for Obsidian */ export class ObsidianTLAssetStore implements Required { - #proxy: ObsidianMarkdownFileTLAssetStoreProxy; + private proxy: ObsidianMarkdownFileTLAssetStoreProxy; constructor( public readonly persistenceKey: string, options: AssetStoreOptions, ) { - this.#proxy = new ObsidianMarkdownFileTLAssetStoreProxy(options); + this.proxy = new ObsidianMarkdownFileTLAssetStoreProxy(options); } - async upload( + upload = async ( asset: TLAsset, file: File, - ): Promise<{ src: string; meta?: JsonObject }> { + ): Promise<{ src: string; meta?: JsonObject }> => { try { - const blockRefAssetId = await this.#proxy.storeAsset(asset, file); + const blockRefAssetId = await this.proxy.storeAsset(asset, file); return { src: `asset:${blockRefAssetId}`, meta: { @@ -178,9 +184,12 @@ export class ObsidianTLAssetStore implements Required { console.error("Error uploading asset:", error); throw error; } - } + }; - async resolve(asset: TLAsset, ctx: TLAssetContext): Promise { + resolve = async ( + asset: TLAsset, + _ctx: TLAssetContext, + ): Promise => { try { const assetSrc = asset.props.src; if (!assetSrc?.startsWith("asset:")) return assetSrc ?? null; @@ -194,7 +203,7 @@ export class ObsidianTLAssetStore implements Required { ""; if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) { - return await this.#proxy.getCached(assetId); + return await this.proxy.getCached(assetId); } // Non-media (e.g., text/markdown, application/*): let custom shapes decide. @@ -204,14 +213,14 @@ export class ObsidianTLAssetStore implements Required { console.error("Error resolving asset:", error); return null; } - } + }; - async remove(assetIds: TLAssetId[]): Promise { + remove = async (_assetIds: TLAssetId[]): Promise => { // No-op for now as we don't want to delete files from the vault // The files will remain in the vault and can be managed by the user - } + }; - dispose() { - this.#proxy.dispose(); - } + dispose = () => { + this.proxy.dispose(); + }; } diff --git a/apps/obsidian/src/utils/tldraw.ts b/apps/obsidian/src/utils/tldraw.ts index 6b868d811..fb2656773 100644 --- a/apps/obsidian/src/utils/tldraw.ts +++ b/apps/obsidian/src/utils/tldraw.ts @@ -9,6 +9,7 @@ import DiscourseGraphPlugin from "~/index"; import { checkAndCreateFolder, getNewUniqueFilepath } from "./file"; import { Notice } from "obsidian"; import { format } from "date-fns"; +import { ObsidianTLAssetStore } from "./assetStore"; export type TldrawPluginMetaData = { "plugin-version": string; @@ -29,6 +30,7 @@ export type TLData = { export const processInitialData = ( data: TLData, + assetStore: ObsidianTLAssetStore, ): { meta: TldrawPluginMetaData; store: TLStore } => { const recordsData = Array.isArray(data.raw.records) ? data.raw.records.reduce( @@ -47,10 +49,12 @@ export const processInitialData = ( store = createTLStore({ shapeUtils: defaultShapeUtils, initialData: recordsData, + assets: assetStore, }); } else { store = createTLStore({ shapeUtils: defaultShapeUtils, + assets: assetStore, }); } From 19fe14faf839ef89a7345483526108462437c090 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 11 Aug 2025 11:09:00 -0400 Subject: [PATCH 6/8] cleanup --- apps/obsidian/src/components/TldrawViewComponent.tsx | 2 +- apps/obsidian/src/utils/assetStore.ts | 2 -- apps/obsidian/src/utils/tldraw.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/obsidian/src/components/TldrawViewComponent.tsx b/apps/obsidian/src/components/TldrawViewComponent.tsx index e0a1bf2ef..cb7cb479b 100644 --- a/apps/obsidian/src/components/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/TldrawViewComponent.tsx @@ -96,7 +96,7 @@ export const TldrawPreviewComponent = ({ setCurrentStore(newStore); } } - }, [file, plugin, currentStore]); + }, [file, plugin, currentStore, assetStore]); useEffect(() => { const unsubscribe = currentStore.listen( diff --git a/apps/obsidian/src/utils/assetStore.ts b/apps/obsidian/src/utils/assetStore.ts index 72231bdf0..30f25b0f1 100644 --- a/apps/obsidian/src/utils/assetStore.ts +++ b/apps/obsidian/src/utils/assetStore.ts @@ -59,13 +59,11 @@ class ObsidianMarkdownFileTLAssetStoreProxy { const arrayBuffer = await file.arrayBuffer(); const assetFile = await this.app.vault.createBinary(filePath, arrayBuffer); - // Generate a plain wikilink (without image embed bang) using Obsidian API const linkText = this.app.metadataCache.fileToLinktext( assetFile, this.file.path, ); const internalLink = `[[${linkText}]]`; - console.log("internalLink", internalLink); const linkBlock = `${internalLink}\n^${blockRefId}`; await this.addToTopOfFile(linkBlock); diff --git a/apps/obsidian/src/utils/tldraw.ts b/apps/obsidian/src/utils/tldraw.ts index fb2656773..a34d140c6 100644 --- a/apps/obsidian/src/utils/tldraw.ts +++ b/apps/obsidian/src/utils/tldraw.ts @@ -4,7 +4,7 @@ import { TLDATA_DELIMITER_END, TLDATA_DELIMITER_START, TLDRAW_VERSION, -} from "../constants"; +} from "~/constants"; import DiscourseGraphPlugin from "~/index"; import { checkAndCreateFolder, getNewUniqueFilepath } from "./file"; import { Notice } from "obsidian"; From 6fb54068dd05814682d0942d42d8c299d3f11e14 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 12 Aug 2025 13:02:54 -0400 Subject: [PATCH 7/8] fix styling issues --- apps/obsidian/src/components/TldrawViewComponent.tsx | 8 ++------ apps/obsidian/styles.css | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/obsidian/src/components/TldrawViewComponent.tsx b/apps/obsidian/src/components/TldrawViewComponent.tsx index cb7cb479b..1b4414e17 100644 --- a/apps/obsidian/src/components/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/TldrawViewComponent.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { ErrorBoundary, Tldraw, TLStore } from "tldraw"; +import { Editor, ErrorBoundary, Tldraw, TLStore } from "tldraw"; import "tldraw/tldraw.css"; import { getTLDataTemplate, @@ -121,11 +121,7 @@ export const TldrawPreviewComponent = ({ }, [currentStore, saveChanges]); return ( -
e.stopPropagation()} - > +
{isReady ? ( ( diff --git a/apps/obsidian/styles.css b/apps/obsidian/styles.css index 295645138..565152a06 100644 --- a/apps/obsidian/styles.css +++ b/apps/obsidian/styles.css @@ -17,3 +17,9 @@ .dg-h4 { @apply text-lg font-bold mb-2; } + +/* Neutralize host button styling inside our editor */ +.tldraw__editor button { + background: transparent !important; + color: inherit; +} From 4ba931eb47a0d62dbb082f283751e64b495488c6 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 15 Aug 2025 14:07:39 -0400 Subject: [PATCH 8/8] address PR comments --- apps/obsidian/src/components/TldrawView.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/obsidian/src/components/TldrawView.tsx b/apps/obsidian/src/components/TldrawView.tsx index b22711839..44fd45528 100644 --- a/apps/obsidian/src/components/TldrawView.tsx +++ b/apps/obsidian/src/components/TldrawView.tsx @@ -125,15 +125,19 @@ export class TldrawView extends TextFileView { private createReactRoot(entryPoint: Element, store: TLStore) { const root = createRoot(entryPoint); - this.assertInitialized(); + if (!this.file) throw new Error("TldrawView not initialized: missing file"); + if (!this.assetStore) + throw new Error("TldrawView not initialized: missing assetStore"); + if (!this.store) + throw new Error("TldrawView not initialized: missing store"); root.render( , );