Social Annotations in the Atmosphere
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Firefox browser support for sidebar

Firefox cannot open sidebar from content script clicks due to user
gesture preservation issues. Use context menu for annotation instead.
Add useContextMenu and showFloatingButton options for browser-specific UI.

+67 -72
+3
entrypoints/background.ts
··· 1 1 import { BrowserStorageAdapter, ExtensionBackgroundWorker } from '@seams/core'; 2 2 3 3 const BACKEND_URL = import.meta.env.BACKEND_URL || 'http://localhost:8080'; 4 + // Firefox needs context menu since it can't open sidebar from content script click 5 + const isFirefox = import.meta.env.FIREFOX === true; 4 6 5 7 export default defineBackground(() => { 6 8 const storage = new BrowserStorageAdapter(); ··· 8 10 const worker = new ExtensionBackgroundWorker({ 9 11 storage, 10 12 backendUrl: BACKEND_URL, 13 + useContextMenu: isFirefox, 11 14 }); 12 15 13 16 worker.start();
+5
entrypoints/content.ts
··· 1 1 import { BrowserStorageAdapter, ExtensionContentScript, generateSelectors, applyHighlights, clearHighlights } from '@seams/core'; 2 2 3 + // Chrome supports opening sidepanel from content script click via sendMessage 4 + // Firefox does not preserve user gesture through messages 5 + const isChrome = import.meta.env.CHROME === true; 6 + 3 7 export default defineContentScript({ 4 8 matches: ['<all_urls>'], 5 9 main() { ··· 10 14 applyHighlights, 11 15 clearHighlights, 12 16 generateSelectors, 17 + showFloatingButton: isChrome, 13 18 }); 14 19 15 20 content.start();
+24
packages/core/src/background/extension.ts
··· 9 9 export interface ExtensionBackgroundWorkerOptions { 10 10 storage: StorageAdapter; 11 11 backendUrl?: string; 12 + // Firefox needs context menu since it can't open sidebar from content script click 13 + useContextMenu?: boolean; 12 14 } 13 15 14 16 export class ExtensionBackgroundWorker { 15 17 private storage: StorageAdapter; 16 18 private backendUrl: string; 19 + private useContextMenu: boolean; 17 20 18 21 constructor(options: ExtensionBackgroundWorkerOptions) { 19 22 this.storage = options.storage; 20 23 this.backendUrl = options.backendUrl || 'http://localhost:8080'; 24 + this.useContextMenu = options.useContextMenu ?? false; 21 25 } 22 26 23 27 async start(): Promise<void> { ··· 34 38 if (tab.id) { 35 39 if (browser.sidePanel) { 36 40 await browser.sidePanel.open({ tabId: tab.id }); 41 + } else if (browser.sidebarAction) { 42 + await browser.sidebarAction.open(); 43 + } 44 + } 45 + }); 46 + } 47 + 48 + // Firefox: Add context menu for opening sidebar (since floating button doesn't work) 49 + if (this.useContextMenu && browser.menus) { 50 + browser.menus.create({ 51 + id: 'seams-annotate', 52 + title: 'Annotate with Seams', 53 + contexts: ['selection'], 54 + }); 55 + 56 + browser.menus.onClicked.addListener((info: any, tab: any) => { 57 + if (info.menuItemId === 'seams-annotate') { 58 + // Context menu click IS a user action, so sidebarAction.open() works 59 + if (browser.sidebarAction) { 60 + browser.sidebarAction.open(); 37 61 } 38 62 } 39 63 });
+33 -57
packages/core/src/content/extension.ts
··· 1 1 // Extension content script 2 2 import type { StorageAdapter } from '../storage/adapter'; 3 3 import type { Annotation } from '../types'; 4 - import { normalizeUrl } from '../utils'; 5 4 import { BaseContentScript, type ContentScriptAdapter } from './base'; 6 5 import { AnnotationUIManager } from './ui'; 7 6 ··· 12 11 applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 13 12 clearHighlights: () => void; 14 13 generateSelectors: (selection: Selection, root: Element) => any[]; 14 + // Chrome supports opening sidepanel from content script click via message 15 + // Firefox does not - see https://bugzilla.mozilla.org/show_bug.cgi?id=1392624 16 + showFloatingButton?: boolean; 15 17 } 16 18 17 19 export class ExtensionContentScript extends BaseContentScript { 18 - private uiManager: AnnotationUIManager; 20 + private uiManager: AnnotationUIManager | null = null; 19 21 20 22 constructor(options: ExtensionContentScriptOptions) { 21 - // Detect mobile - crude check but works for most cases 22 - const isMobile = window.innerWidth <= 768 || navigator.userAgent.includes('Android') || navigator.userAgent.includes('iPhone'); 23 - 24 - let ui: AnnotationUIManager; // Closure to hold the UI manager instance 23 + const showFloatingButton = options.showFloatingButton ?? true; 24 + let ui: AnnotationUIManager | null = null; 25 25 26 26 const adapter: ContentScriptAdapter = { 27 27 storage: options.storage, ··· 30 30 clearHighlights: options.clearHighlights, 31 31 generateSelectors: options.generateSelectors, 32 32 notifySelectionChange: (selection) => { 33 - // Always update sidepanel state (passive) 33 + // Update sidepanel state (passive) 34 34 browser.runtime.sendMessage({ 35 35 type: 'SELECTION_CHANGED', 36 36 selection ··· 38 38 // Sidepanel might not be open 39 39 }); 40 40 41 - // Handle floating UI 42 - if (selection && selection.text) { 43 - // Calculate selection rect 44 - const domSelection = window.getSelection(); 45 - if (domSelection && domSelection.rangeCount > 0) { 46 - const range = domSelection.getRangeAt(0); 47 - const rect = range.getBoundingClientRect(); 48 - ui.showButton(rect, selection.text, selection.selectors); 49 - } 50 - } else { 51 - ui.removeButton(); 52 - } 41 + // Show/hide floating annotate button (Chrome only) 42 + if (showFloatingButton && ui) { 43 + if (selection && selection.text) { 44 + const domSelection = window.getSelection(); 45 + if (domSelection && domSelection.rangeCount > 0) { 46 + const range = domSelection.getRangeAt(0); 47 + const rect = range.getBoundingClientRect(); 48 + ui.showButton(rect, selection.text, selection.selectors); 49 + } 50 + } else { 51 + ui.removeButton(); 52 + } 53 + } 53 54 } 54 55 }; 55 56 super(adapter); 56 - 57 - this.uiManager = new AnnotationUIManager({ 58 - isMobile, 59 - onAnnotate: (data) => { 60 - if (isMobile) { 61 - // Mobile: Send create message directly (bypass sidepanel UI which might be hidden/different) 62 - // Actually, for extension on mobile, we probably still use the background/sidepanel architecture? 63 - // If it's Firefox Android, sidepanel behavior is different. 64 - 65 - // For now, let's assume we send a message to background to handle creation or open UI. 66 - // But wait, the modal returns the BODY too. 67 - if (data.body) { 68 - // We have the body, so we can create it directly? 69 - // But we need PDS access. Content script shouldn't have PDS access. 70 - // We should send message to background to create it. 71 - browser.runtime.sendMessage({ 72 - type: 'CREATE_ANNOTATION', 73 - payload: data 74 - }); 75 - } 76 - } else { 77 - // Desktop: Open/Focus sidepanel 78 - // Send message to background to open sidepanel, then sidepanel will handle focus via ACTIVATE_ANNOTATION 79 - browser.runtime.sendMessage({ 80 - type: 'ACTIVATE_ANNOTATION', 81 - selection: null // Selection is synced passively 82 - }).catch((e: any) => console.warn('Failed to activate annotation', e)); 83 - } 84 - } 85 - }); 86 - ui = this.uiManager; // Assign to closure 57 + 58 + if (showFloatingButton) { 59 + this.uiManager = new AnnotationUIManager({ 60 + isMobile: false, 61 + onAnnotate: () => { 62 + // Send message to background to open sidepanel 63 + // User gesture is preserved through sendMessage per Chrome sample: 64 + // https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/cookbook.sidepanel-open 65 + browser.runtime.sendMessage({ type: 'ACTIVATE_ANNOTATION' }); 66 + } 67 + }); 68 + ui = this.uiManager; 69 + } 87 70 } 88 - 89 - protected shouldHandleSelection(event: MouseEvent): boolean { 90 - if (event.target instanceof Node && this.uiManager.contains(event.target)) { 91 - return false; 92 - } 93 - return true; 94 - } 95 71 96 72 async start(): Promise<void> { 97 73 await super.start();
+1 -14
packages/core/src/content/mobile.ts
··· 19 19 cursor: 'pointer', 20 20 }); 21 21 22 - // Use mousedown to prevent the document mouseup handler from removing the button 23 - // before the click event fires 24 - btn.addEventListener('mousedown', (e) => { 25 - e.stopPropagation(); 26 - }); 27 - 28 - btn.addEventListener('mouseup', (e) => { 29 - e.stopPropagation(); 30 - }); 31 - 32 - btn.addEventListener('click', (e) => { 33 - e.stopPropagation(); 34 - onClick(); 35 - }); 22 + btn.addEventListener('click', onClick); 36 23 37 24 document.body.appendChild(btn); 38 25 return btn;
+1 -1
wxt.config.ts
··· 30 30 'activeTab', 31 31 'identity', 32 32 'webNavigation', 33 - ...(env.browser === 'chrome' ? ['sidePanel'] : []), 33 + ...(env.browser === 'chrome' ? ['sidePanel'] : ['menus']), 34 34 ], 35 35 host_permissions: ['<all_urls>', 'https://synthes-is.netlify.app/*'], 36 36 content_scripts: [