From a3b9d3a340bd9ea77ee67ebf0310cd71d6dce5dc Mon Sep 17 00:00:00 2001 From: Daniel Grossmann-Kavanagh Date: Tue, 20 Jan 2026 17:33:03 -0800 Subject: [PATCH] refactor: Consolidate banner/button logic into Banner class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Banner now automatically detects mobile Obsidian ≥1.11.0 and renders as a header button (short text) vs traditional banner (long text) - Added BannerText type supporting string | { short, long } - Removed setLoginIcon/clearLoginButton from LoggedOutView - Removed setMergeButton/clearMergeButton from LiveView - Removed font-weight bold and text-shadow from desktop banner --- src/LiveViews.ts | 139 +++++++---------------------------------------- src/ui/Banner.ts | 68 ++++++++++++++++++++--- styles.css | 6 +- 3 files changed, 81 insertions(+), 132 deletions(-) diff --git a/src/LiveViews.ts b/src/LiveViews.ts index 5ce0979..2e0c345 100644 --- a/src/LiveViews.ts +++ b/src/LiveViews.ts @@ -124,65 +124,24 @@ export class LoggedOutView implements S3View { this.login = login; } - setLoginIcon(): void { - const viewHeaderElement = - this.view.containerEl.querySelector(".view-header"); - const viewHeaderLeftElement = - this.view.containerEl.querySelector(".view-header-left"); - - if (viewHeaderElement && viewHeaderLeftElement) { - this.clearLoginButton(); - - // Create login button element - const loginButton = document.createElement("button"); - loginButton.className = "view-header-left system3-login-button"; - loginButton.textContent = "Login to enable Live edits"; - loginButton.setAttribute("aria-label", "Login to enable Live edits"); - loginButton.setAttribute("tabindex", "0"); - - // Add click handler - loginButton.addEventListener("click", async () => { - await this.login(); - }); - - // Insert after view-header-left - viewHeaderLeftElement.insertAdjacentElement("afterend", loginButton); - } - } - - clearLoginButton() { - const existingButton = this.view.containerEl.querySelector(".system3-login-button"); - if (existingButton) { - existingButton.remove(); - } - } - attach(): Promise { - // Use header button approach on mobile for Obsidian >=1.11.0 to avoid banner positioning issues - if (Platform.isMobile && requireApiVersion("1.11.0")) { - this.setLoginIcon(); - } else { - this.banner = new Banner( - this.view, - "Login to enable Live edits", - async () => { - return await this.login(); - }, - ); - } + this.banner = new Banner( + this.view, + { short: "Login to Relay", long: "Login to enable Live edits" }, + async () => { + return await this.login(); + }, + ); return Promise.resolve(this); } release() { this.banner?.destroy(); - this.clearLoginButton(); } destroy() { - this.release(); this.banner?.destroy(); this.banner = undefined; - this.clearLoginButton(); this.view = null as any; } } @@ -260,7 +219,7 @@ export class RelayCanvasView implements S3View { if (this.shouldConnect) { const banner = new Banner( this.view, - "You're offline -- click to reconnect", + { short: "Offline", long: "You're offline -- click to reconnect" }, async () => { this._parent.networkStatus.checkStatus(); this.connect(); @@ -475,29 +434,15 @@ export class LiveView } } - setMergeButton(): void { - const viewHeaderElement = - this.view.containerEl.querySelector(".view-header"); - const viewHeaderLeftElement = - this.view.containerEl.querySelector(".view-header-left"); - - if (viewHeaderElement && viewHeaderLeftElement) { - this.clearMergeButton(); - - // Create merge button element - const mergeButton = document.createElement("button"); - mergeButton.className = "view-header-left system3-merge-button"; - mergeButton.textContent = "Merge conflict"; - mergeButton.setAttribute("aria-label", "Merge conflict -- click to resolve"); - mergeButton.setAttribute("tabindex", "0"); - - // Add click handler - mergeButton.addEventListener("click", async () => { + mergeBanner(): () => void { + this._banner = new Banner( + this.view, + { short: "Merge conflict", long: "Merge conflict -- click to resolve" }, + async () => { const diskBuffer = await this.document.diskBuffer(); const stale = await this.document.checkStale(); if (!stale) { - this.clearMergeButton(); - return; + return true; } this._parent.openDiffView({ file1: this.document, @@ -505,7 +450,7 @@ export class LiveView showMergeOption: true, onResolve: async () => { this.document.clearDiskBuffer(); - this.clearMergeButton(); + this._banner?.destroy(); // Force view to sync to CRDT state after differ resolution if ( this._plugin && @@ -515,53 +460,9 @@ export class LiveView } }, }); - }); - - // Insert after view-header-left - viewHeaderLeftElement.insertAdjacentElement("afterend", mergeButton); - } - } - - clearMergeButton() { - const existingButton = this.view.containerEl.querySelector(".system3-merge-button"); - if (existingButton) { - existingButton.remove(); - } - } - - mergeBanner(): () => void { - // Use header button approach on mobile for Obsidian >=1.11.0 to avoid banner positioning issues - if (Platform.isMobile && requireApiVersion("1.11.0")) { - this.setMergeButton(); - } else { - this._banner = new Banner( - this.view, - "Merge conflict -- click to resolve", - async () => { - const diskBuffer = await this.document.diskBuffer(); - const stale = await this.document.checkStale(); - if (!stale) { - return true; - } - this._parent.openDiffView({ - file1: this.document, - file2: diskBuffer, - showMergeOption: true, - onResolve: async () => { - this.document.clearDiskBuffer(); - // Force view to sync to CRDT state after differ resolution - if ( - this._plugin && - typeof this._plugin.syncViewToCRDT === "function" - ) { - await this._plugin.syncViewToCRDT(); - } - }, - }); - return true; - }, - ); - } + return true; + }, + ); return () => {}; } @@ -569,7 +470,7 @@ export class LiveView if (this.shouldConnect) { const banner = new Banner( this.view, - "You're offline -- click to reconnect", + { short: "Offline", long: "You're offline -- click to reconnect" }, async () => { this._parent.networkStatus.checkStatus(); this.connect(); @@ -725,7 +626,6 @@ export class LiveView this._viewActions = undefined; this._banner?.destroy(); this._banner = undefined; - this.clearMergeButton(); if (this.offConnectionStatusSubscription) { this.offConnectionStatusSubscription(); this.offConnectionStatusSubscription = undefined; @@ -741,7 +641,6 @@ export class LiveView destroy() { this.release(); this.clearViewActions(); - this.clearMergeButton(); (this.view.leaf as any).rebuildView?.(); this._parent = null as any; this.view = null as any; diff --git a/src/ui/Banner.ts b/src/ui/Banner.ts index 6dbb9b3..c1cf966 100644 --- a/src/ui/Banner.ts +++ b/src/ui/Banner.ts @@ -1,32 +1,50 @@ "use strict"; -import { TextFileView } from "obsidian"; +import { Platform, requireApiVersion, TextFileView } from "obsidian"; import type { CanvasView } from "src/CanvasView"; +export type BannerText = string | { short: string; long: string }; + export class Banner { view: TextFileView | CanvasView; - text: string; + text: BannerText; onClick: () => Promise; + private useHeaderButton: boolean; constructor( view: TextFileView | CanvasView, - text: string, + text: BannerText, onClick: () => Promise, ) { this.view = view; this.text = text; this.onClick = onClick; + // Use header button approach on mobile for Obsidian >=1.11.0 to avoid banner positioning issues + this.useHeaderButton = Platform.isMobile && requireApiVersion("1.11.0"); this.display(); } + private get shortText(): string { + return typeof this.text === "string" ? this.text : this.text.short; + } + + private get longText(): string { + return typeof this.text === "string" ? this.text : this.text.long; + } + display() { if (!this.view) return true; const leafContentEl = this.view.containerEl; - const contentEl = this.view.containerEl.querySelector(".view-content"); if (!leafContentEl) { return; } + if (this.useHeaderButton) { + return this.displayHeaderButton(); + } + + const contentEl = this.view.containerEl.querySelector(".view-content"); + // container to enable easy removal of the banner let bannerBox = leafContentEl.querySelector(".system3-banner-box"); if (!bannerBox) { @@ -40,7 +58,7 @@ export class Banner { banner = document.createElement("div"); banner.classList.add("system3-banner"); const span = banner.createSpan(); - span.setText(this.text); + span.setText(this.longText); banner.appendChild(span); bannerBox.appendChild(banner); const onClick = async () => { @@ -54,14 +72,48 @@ export class Banner { return true; } + private displayHeaderButton() { + const leafContentEl = this.view.containerEl; + const viewHeaderLeftElement = + leafContentEl.querySelector(".view-header-left"); + + if (!viewHeaderLeftElement) { + return; + } + + // Remove existing button if any + leafContentEl.querySelector(".system3-header-button")?.remove(); + + const button = document.createElement("button"); + button.className = "view-header-left system3-header-button"; + button.textContent = this.shortText; + button.setAttribute("aria-label", this.longText); + button.setAttribute("tabindex", "0"); + + button.addEventListener("click", async () => { + const destroy = await this.onClick(); + if (destroy) { + this.destroy(); + } + }); + + viewHeaderLeftElement.insertAdjacentElement("afterend", button); + return true; + } + destroy() { const leafContentEl = this.view.containerEl; if (!leafContentEl) { return; } - const bannerBox = leafContentEl.querySelector(".system3-banner-box"); - if (bannerBox) { - bannerBox.replaceChildren(); + + if (this.useHeaderButton) { + leafContentEl.querySelector(".system3-header-button")?.remove(); + } else { + const bannerBox = leafContentEl.querySelector(".system3-banner-box"); + if (bannerBox) { + bannerBox.replaceChildren(); + } } this.onClick = async () => true; return true; diff --git a/styles.css b/styles.css index aed8e5a..0720f00 100644 --- a/styles.css +++ b/styles.css @@ -123,17 +123,15 @@ } .system3-banner > span { - font-weight: bold; display: flex; flex-grow: 1; font-size: var(--font-ui-medium); color: var(--text-on-accent); font: var(--font-interface-theme); - text-shadow: var(--input-shadow); } -.system3-login-button, -.system3-merge-button { +.system3-header-button, +.system3-login-button { background: var(--interactive-accent) !important; color: var(--text-on-accent) !important; border: var(--modal-border-width) solid var(--pill-border-color-hover) !important;