Social Annotations in the Atmosphere
15
fork

Configure Feed

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

refactor(content): unify selection logic and message handling

Standardize on SELECTION_CHANGED (sync) and ACTIVATE_ANNOTATION (open/focus) across proxy/extension.
Remove legacy SEAMS_TEXT_SELECTED and mobile modal; consolidate flow to sidebar.
Improve selection tracking (add touch/keyboard support) and fix button positioning.
Fix ProxyContentScript constructor bug.

+263 -356
+2
entrypoints/sidepanel/main.ts
··· 33 33 browser.runtime.onMessage.addListener((message) => { 34 34 if (message.type === 'SELECTION_CHANGED') { 35 35 sidebar.setSelection(message.selection); 36 + } else if (message.type === 'ACTIVATE_ANNOTATION') { 37 + sidebar.handleActivateAnnotation(); 36 38 } 37 39 }); 38 40
+31 -57
entrypoints/via-client/main.ts
··· 1 1 // Via proxy client - injects sidebar iframe and handles page interaction 2 - import { WebStorageAdapter, BackgroundWorker, ContentScript, applyHighlights, clearHighlights, generateSelectors, createMobileAnnotateButton, createMobileSidebarToggle, createMobileAnnotationModal, AnnotationUIManager } from '@seams/core'; 2 + import { WebStorageAdapter, BackgroundWorker, ProxyContentScript, applyHighlights, clearHighlights, generateSelectors, createMobileAnnotateButton, createMobileSidebarToggle, createMobileAnnotationModal } from '@seams/core'; 3 3 import type { Annotation } from '@seams/core'; 4 4 5 5 console.log('🔬 Seams via client loaded!'); ··· 11 11 const SIDEBAR_WIDTH = 400; 12 12 const IS_MOBILE = window.innerWidth <= 768; 13 13 14 - // Initialize UI Manager 15 - const uiManager = new AnnotationUIManager({ 16 - isMobile: IS_MOBILE, 17 - onAnnotate: (data) => { 18 - const iframe = document.getElementById(SIDEBAR_ID) as HTMLIFrameElement; 19 - 20 - if (IS_MOBILE) { 21 - // Mobile: Show Modal (already handled by UIManager internally calls this callback with body) 22 - if (data.body && iframe && iframe.contentWindow) { 23 - iframe.contentWindow.postMessage({ 24 - type: 'SEAMS_CREATE_ANNOTATION', 25 - payload: { 26 - text: data.text, 27 - selectors: data.selectors, 28 - body: data.body 29 - } 30 - }, '*'); 31 - } 32 - } else { 33 - // Desktop: Send to sidebar 34 - if (iframe && iframe.contentWindow) { 35 - iframe.contentWindow.postMessage({ 36 - type: 'SEAMS_TEXT_SELECTED', 37 - payload: { 38 - text: data.text, 39 - selectors: data.selectors 40 - } 41 - }, '*'); 42 - } 43 - } 44 - } 45 - }); 46 - 47 14 // Initialize content script only (sidebar handles fetching via BackgroundWorker) 48 - const contentScript = new ContentScript({ 15 + const contentScript = new ProxyContentScript({ 49 16 storage, 50 17 getCurrentUrl: getActualUrl, 51 18 applyHighlights, 52 19 clearHighlights, 53 20 generateSelectors, 54 - onSelectionChange: (selection) => { 55 - const iframe = document.getElementById(SIDEBAR_ID) as HTMLIFrameElement; 56 - 57 - if (selection && selection.text) { 58 - // Calculate selection rect 59 - const domSelection = window.getSelection(); 60 - if (domSelection && domSelection.rangeCount > 0) { 61 - const range = domSelection.getRangeAt(0); 62 - const rect = range.getBoundingClientRect(); 63 - 64 - uiManager.showButton(rect, selection.text, selection.selectors); 65 - } 66 - } else { 67 - uiManager.removeButton(); 68 - // Clear sidebar selection 69 - if (iframe && iframe.contentWindow) { 70 - iframe.contentWindow.postMessage({ 71 - type: 'SEAMS_TEXT_SELECTED', 72 - payload: null 73 - }, '*'); 74 - } 21 + onAnnotate: (data) => { 22 + const iframe = document.getElementById(SIDEBAR_ID) as HTMLIFrameElement; 23 + if (!iframe || !iframe.contentWindow) return; 24 + 25 + // Always use desktop flow: Activate form 26 + iframe.contentWindow.postMessage({ 27 + type: 'ACTIVATE_ANNOTATION', 28 + payload: null 29 + }, '*'); 30 + 31 + // Mobile handling: If sidebar is hidden, show it 32 + if (IS_MOBILE) { 33 + const isHidden = iframe.style.display === 'none'; 34 + if (isHidden) { 35 + iframe.style.display = 'block'; 36 + // We should also update the toggle button text if we could access it, 37 + // but it's inside a closure in injectSidebar. 38 + // Ideally we should trigger the toggle logic. 39 + } 75 40 } 76 - } 41 + }, 42 + onSelectionChange: (selection) => { 43 + const iframe = document.getElementById(SIDEBAR_ID) as HTMLIFrameElement; 44 + if (iframe && iframe.contentWindow) { 45 + iframe.contentWindow.postMessage({ 46 + type: 'SELECTION_CHANGED', 47 + payload: selection 48 + }, '*'); 49 + } 50 + } 77 51 }); 78 52 79 53 function injectSidebar() {
+4 -19
entrypoints/via-client/sidebar.ts
··· 47 47 console.log('[seams-sidebar] Received page URL:', url); 48 48 sidebar.setCurrentUrl(url); 49 49 backgroundWorker.setCurrentUrl(url); 50 - } else if (event.data.type === 'SEAMS_TEXT_SELECTED') { 50 + } else if (event.data.type === 'SELECTION_CHANGED') { 51 51 console.log('[seams-sidebar] Received selection update'); 52 52 sidebar.setSelection(event.data.payload); 53 - } else if (event.data.type === 'SEAMS_CREATE_ANNOTATION') { 54 - console.log('[seams-sidebar] Received create annotation request'); 55 - const { text, selectors, body } = event.data.payload; 56 - // Use the current URL of the sidebar (which should match the page) 57 - // But we can also use the source URL from the payload if we sent it, 58 - // currently sidebar uses its own this.currentUrl 59 - 60 - // We need to make sure sidebar has the correct URL set 61 - // The sidebar tracks currentUrl via SEAMS_PAGE_URL messages 62 - 63 - sidebar.createAnnotation({ 64 - source: sidebar.getCurrentUrl(), // Assuming public getter or we can just rely on internal state 65 - selectors 66 - }, body).then(() => { 67 - console.log('[seams-sidebar] Annotation created from mobile request'); 68 - }).catch(err => { 69 - console.error('[seams-sidebar] Failed to create annotation from mobile request:', err); 70 - }); 53 + } else if (event.data.type === 'ACTIVATE_ANNOTATION') { 54 + console.log('[seams-sidebar] Received activation request'); 55 + sidebar.handleActivateAnnotation(); 71 56 } 72 57 }); 73 58
+18 -14
history/REFACTOR_PLAN.md
··· 40 40 - [x] **Integrate into Landing** (`landing/landing.js`) 41 41 - Import and use the web component, removing duplicate rendering code. 42 42 43 - ## Phase 3: Content Script & Proxy Refactoring 43 + ## Phase 3: Content Script & Proxy Refactoring (Completed) 44 44 **Goal:** Clean up proxy client architecture and share logic. 45 - - [ ] **Rename Proxy Script** (`packages/core/src/content/script.ts`) 46 - - Rename `ContentScript` to `ProxyContentScript`. 47 - - [ ] **Consolidate Selection Logic** 48 - - Move `handleSelection` from `main.ts` to shared content script. 49 - - [ ] **Fix Selection Notification** 50 - - Implement `notifySelectionChange` in proxy script. 51 - - [ ] **Cleanup Event Handling** 52 - - Fix logic for detecting clicks inside/outside UI. 45 + - [x] **Rename Proxy Script** (`packages/core/src/content/script.ts`) 46 + - Renamed `ContentScript` to `ProxyContentScript` and moved to `proxy.ts`. 47 + - [x] **Consolidate Selection Logic** 48 + - Moved selection handling logic into `ProxyContentScript` using `AnnotationUIManager`. 49 + - [x] **Fix Selection Notification** 50 + - Implemented `notifySelectionChange` and `onClearSelection` in `ProxyContentScript`. 51 + - [x] **Cleanup Event Handling** 52 + - Added `shouldHandleSelection` to prevent UI clicks from re-triggering selection logic. 53 53 54 54 ## Phase 4: OAuth Flow Modernization 55 - **Goal:** Improve authentication experience by removing popups. 56 - - [ ] **Switch to Same-Tab OAuth** (`packages/core/src/oauth/launchers.ts`) 57 - - Modify `WebOAuthLauncher` to use `window.location.assign`. 58 - - [ ] **Handle OAuth Redirects** (`entrypoints/via-client/oauth-callback.ts`) 59 - - Update callback to redirect back to origin using stored state. 55 + **Goal:** Fix Oauth implementation 56 + - [ ] Improve authentication experience by removing popups. 57 + - [ ] **Switch to Same-Tab OAuth** (`packages/core/src/oauth/launchers.ts`) 58 + - Modify `WebOAuthLauncher` to use `window.location.assign`. 59 + - [ ] **Handle OAuth Redirects** (`entrypoints/via-client/oauth-callback.ts`) 60 + - Update callback to redirect back to previously viewed page using query params. 61 + - [ ] Ensure oauth development environment is working with 127.0.0.1 for both extension and the proxy 60 62 61 63 ## Phase 5: Optimization & Cleanup 62 64 **Goal:** Performance improvements and dead code removal. ··· 66 68 - Use `Set.has()` for merging annotations. 67 69 - [ ] **Add search from proxy HTML to landing.html 68 70 - Cleanup unused static proxy HTML files. (`proxy/static/via-landing.html`) 71 + - [ ] Document messages types, and define them in @seams/core 72 + - We have multiple messages that aren't defined concretely. We need to make these explicit
+10
packages/core/src/background/extension.ts
··· 87 87 } 88 88 }); 89 89 sendResponse({ success: true }); 90 + } else if (message.type === 'ACTIVATE_ANNOTATION') { 91 + console.log('[background] ACTIVATE_ANNOTATION received'); 92 + if (sender.tab?.id) { 93 + // Open sidepanel 94 + if (browser.sidePanel && browser.sidePanel.open) { 95 + browser.sidePanel.open({ tabId: sender.tab.id }).catch((err: any) => { 96 + console.error('[background] Failed to open sidepanel:', err); 97 + }); 98 + } 99 + } 90 100 } 91 101 }); 92 102 }
+32 -30
packages/core/src/content/base.ts
··· 76 76 } 77 77 } 78 78 79 + protected shouldHandleSelection(event: MouseEvent): boolean { 80 + return true; 81 + } 82 + 79 83 private setupSelectionTracking() { 80 - document.addEventListener('mouseup', (e) => { 81 - // AMPDO: The below comment is nonesense, we need to sort it out 84 + ['mouseup', 'touchend', 'keyup'].forEach(event => { 85 + document.addEventListener(event, (e) => { 86 + // Small delay for touchend/keyup to let selection settle 87 + setTimeout(() => { 88 + if (!this.shouldHandleSelection(e as MouseEvent)) { 89 + return; 90 + } 82 91 83 - // Don't handle if clicking inside mobile UI elements (handled by subclass or UI script) 84 - // But we can't easily know that here without tighter coupling. 85 - // For now, we assume the adapter or subclass handles UI specific checks if needed, 86 - // or we just let the selection logic run and it notifies the adapter. 87 - 88 - // Actually, the Proxy client has specific logic to ignore clicks in its own UI. 89 - // We might want to allow the subclass to override or intercept this. 90 - // But standard selection API usually works fine. 91 - 92 - const selection = window.getSelection(); 93 - if (selection && selection.toString().trim().length > 0) { 94 - const text = selection.toString().trim(); 95 - // Heuristic for root element - could be configurable 96 - const root = document.querySelector('main') || document.querySelector('article') || document.body; 92 + const selection = window.getSelection(); 93 + if (selection && selection.toString().trim().length > 0) { 94 + const text = selection.toString().trim(); 95 + // Heuristic for root element - could be configurable 96 + const root = document.querySelector('main') || document.querySelector('article') || document.body; 97 97 98 - try { 99 - const selectors = this.adapter.generateSelectors(selection, root); 100 - this.currentSelection = { text, selectors }; 101 - console.log('[content] Text selected:', text); 102 - this.adapter.notifySelectionChange(this.currentSelection); 103 - } catch (err) { 104 - console.error('[content] Failed to generate selectors:', err); 105 - } 106 - } else { 107 - if (this.currentSelection) { 108 - this.currentSelection = null; 109 - this.adapter.notifySelectionChange(null); 110 - } 111 - } 98 + try { 99 + const selectors = this.adapter.generateSelectors(selection, root); 100 + this.currentSelection = { text, selectors }; 101 + console.log('[content] Text selected:', text); 102 + this.adapter.notifySelectionChange(this.currentSelection); 103 + } catch (err) { 104 + console.error('[content] Failed to generate selectors:', err); 105 + } 106 + } else { 107 + if (this.currentSelection) { 108 + this.currentSelection = null; 109 + this.adapter.notifySelectionChange(null); 110 + } 111 + } 112 + }, 10); 113 + }); 112 114 }); 113 115 } 114 116
+11 -8
packages/core/src/content/extension.ts
··· 75 75 } 76 76 } else { 77 77 // Desktop: Open/Focus sidepanel 78 - // We can't programmatically open sidepanel in all browsers/contexts easily without user action on the extension icon, 79 - // BUT chrome.sidePanel.open is available in newer APIs with user gesture. 80 - // The click on floating button IS a user gesture. 81 - 82 - // We'll send a message to background to try opening sidepanel or just focus it. 78 + // Send message to background to open sidepanel, then sidepanel will handle focus via ACTIVATE_ANNOTATION 83 79 browser.runtime.sendMessage({ 84 - type: 'OPEN_SIDEPANEL', 85 - selection: { text: data.text, selectors: data.selectors } 86 - }).catch((e: any) => console.warn('Failed to open sidepanel', e)); 80 + type: 'ACTIVATE_ANNOTATION', 81 + selection: null // Selection is synced passively 82 + }).catch((e: any) => console.warn('Failed to activate annotation', e)); 87 83 } 88 84 } 89 85 }); 90 86 ui = this.uiManager; // Assign to closure 91 87 } 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 + } 92 95 93 96 async start(): Promise<void> { 94 97 await super.start();
+2 -2
packages/core/src/content/index.ts
··· 1 - export { ContentScript } from './script'; 2 - export type { ContentScriptOptions } from './script'; 1 + export { ProxyContentScript } from './proxy'; 2 + export type { ProxyContentScriptOptions } from './proxy'; 3 3 export { ExtensionContentScript } from './extension'; 4 4 export type { ExtensionContentScriptOptions } from './extension'; 5 5 export { BaseContentScript } from './base';
+63 -176
packages/core/src/content/mobile.ts
··· 1 1 export function createMobileAnnotateButton(x: number, y: number, onClick: () => void): HTMLElement { 2 - const btn = document.createElement('button'); 3 - btn.textContent = 'Annotate'; 4 - btn.className = 'seams-mobile-annotate-btn'; 5 - Object.assign(btn.style, { 6 - position: 'fixed', 7 - top: `${y + 20}px`, 8 - left: `${x}px`, 9 - transform: 'translateX(-50%)', 10 - zIndex: '2147483647', 11 - padding: '8px 16px', 12 - background: '#2d5016', // Forest green 13 - color: 'white', 14 - border: 'none', 15 - borderRadius: '20px', 16 - boxShadow: '0 2px 8px rgba(0,0,0,0.2)', 17 - fontSize: '14px', 18 - fontWeight: '600', 19 - cursor: 'pointer', 20 - }); 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 - }); 2 + const btn = document.createElement('button'); 3 + btn.textContent = 'Annotate'; 4 + btn.className = 'seams-mobile-annotate-btn'; 5 + Object.assign(btn.style, { 6 + position: 'fixed', 7 + top: `${y + 8}px`, 8 + left: `${x}px`, 9 + transform: 'translateX(-50%)', 10 + zIndex: '2147483647', 11 + padding: '8px 16px', 12 + background: '#2d5016', // Forest green 13 + color: 'white', 14 + border: 'none', 15 + borderRadius: '20px', 16 + boxShadow: '0 2px 8px rgba(0,0,0,0.2)', 17 + fontSize: '14px', 18 + fontWeight: '600', 19 + cursor: 'pointer', 20 + }); 31 21 32 - btn.addEventListener('click', (e) => { 33 - e.stopPropagation(); 34 - onClick(); 35 - }); 36 - 37 - document.body.appendChild(btn); 38 - return btn; 39 - } 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 + }); 40 27 41 - export function createMobileSidebarToggle(onClick: () => void): HTMLElement { 42 - const btn = document.createElement('button'); 43 - btn.className = 'seams-mobile-toggle-btn'; 44 - btn.textContent = '<<'; // Default state (closed -> open) 45 - Object.assign(btn.style, { 46 - position: 'fixed', 47 - top: '10px', 48 - right: '10px', 49 - zIndex: '2147483647', 50 - width: '40px', 51 - height: '40px', 52 - background: 'white', 53 - color: '#2d5016', // Forest green text 54 - border: '1px solid #ddd', 55 - borderRadius: '50%', 56 - boxShadow: '0 2px 8px rgba(0,0,0,0.1)', 57 - display: 'flex', 58 - alignItems: 'center', 59 - justifyContent: 'center', 60 - fontSize: '14px', 61 - fontWeight: 'bold', 62 - cursor: 'pointer', 63 - }); 28 + btn.addEventListener('mouseup', (e) => { 29 + e.stopPropagation(); 30 + }); 64 31 65 - btn.addEventListener('click', (e) => { 66 - e.stopPropagation(); 67 - onClick(); 68 - }); 32 + btn.addEventListener('click', (e) => { 33 + e.stopPropagation(); 34 + onClick(); 35 + }); 69 36 70 - document.body.appendChild(btn); 71 - return btn; 37 + document.body.appendChild(btn); 38 + return btn; 72 39 } 73 40 74 - export function createMobileAnnotationModal( 75 - text: string, 76 - onSave: (body: string) => void, 77 - onCancel: () => void 78 - ): HTMLElement { 79 - const overlay = document.createElement('div'); 80 - Object.assign(overlay.style, { 81 - position: 'fixed', 82 - top: '0', 83 - left: '0', 84 - width: '100%', 85 - height: '100%', 86 - background: 'rgba(0,0,0,0.5)', 87 - zIndex: '2147483647', 88 - display: 'flex', 89 - alignItems: 'center', 90 - justifyContent: 'center', 91 - padding: '20px', 92 - boxSizing: 'border-box', 93 - }); 94 41 95 - const modal = document.createElement('div'); 96 - Object.assign(modal.style, { 97 - background: 'white', 98 - borderRadius: '12px', 99 - width: '100%', 100 - maxWidth: '400px', 101 - padding: '20px', 102 - boxShadow: '0 4px 12px rgba(0,0,0,0.2)', 103 - display: 'flex', 104 - flexDirection: 'column', 105 - gap: '12px', 106 - }); 42 + export function createMobileSidebarToggle(onClick: () => void): HTMLElement { 43 + const btn = document.createElement('button'); 44 + btn.className = 'seams-mobile-toggle-btn'; 45 + btn.textContent = '<<'; // Default state (closed -> open) 46 + Object.assign(btn.style, { 47 + position: 'fixed', 48 + top: '10px', 49 + right: '10px', 50 + zIndex: '2147483647', 51 + width: '40px', 52 + height: '40px', 53 + background: 'white', 54 + color: '#2d5016', // Forest green text 55 + border: '1px solid #ddd', 56 + borderRadius: '50%', 57 + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', 58 + display: 'flex', 59 + alignItems: 'center', 60 + justifyContent: 'center', 61 + fontSize: '14px', 62 + fontWeight: 'bold', 63 + cursor: 'pointer', 64 + }); 107 65 108 - const quote = document.createElement('blockquote'); 109 - quote.textContent = text; 110 - Object.assign(quote.style, { 111 - borderLeft: '3px solid #2d5016', // Forest green 112 - margin: '0', 113 - paddingLeft: '10px', 114 - color: '#666', 115 - fontSize: '14px', 116 - maxHeight: '100px', 117 - overflowY: 'auto', 118 - }); 66 + btn.addEventListener('click', (e) => { 67 + e.stopPropagation(); 68 + onClick(); 69 + }); 119 70 120 - const textarea = document.createElement('textarea'); 121 - textarea.placeholder = 'Add your note...'; 122 - Object.assign(textarea.style, { 123 - width: '100%', 124 - height: '100px', 125 - padding: '10px', 126 - border: '1px solid #ddd', 127 - borderRadius: '8px', 128 - resize: 'none', 129 - fontFamily: 'inherit', 130 - boxSizing: 'border-box', 131 - }); 71 + document.body.appendChild(btn); 72 + return btn; 73 + } 132 74 133 - const buttons = document.createElement('div'); 134 - Object.assign(buttons.style, { 135 - display: 'flex', 136 - justifyContent: 'flex-end', 137 - gap: '8px', 138 - }); 139 - 140 - const cancelBtn = document.createElement('button'); 141 - cancelBtn.textContent = 'Cancel'; 142 - Object.assign(cancelBtn.style, { 143 - padding: '8px 16px', 144 - background: 'transparent', 145 - border: '1px solid #ddd', 146 - borderRadius: '6px', 147 - cursor: 'pointer', 148 - color: '#666', 149 - }); 150 - cancelBtn.onclick = () => { 151 - document.body.removeChild(overlay); 152 - onCancel(); 153 - }; 154 - 155 - const saveBtn = document.createElement('button'); 156 - saveBtn.textContent = 'Save'; 157 - Object.assign(saveBtn.style, { 158 - padding: '8px 16px', 159 - background: '#2d5016', // Forest green 160 - color: 'white', 161 - border: 'none', 162 - borderRadius: '6px', 163 - cursor: 'pointer', 164 - }); 165 - saveBtn.onclick = () => { 166 - const body = textarea.value.trim(); 167 - if (body) { 168 - onSave(body); 169 - document.body.removeChild(overlay); 170 - } 171 - }; 172 - 173 - buttons.appendChild(cancelBtn); 174 - buttons.appendChild(saveBtn); 175 - 176 - modal.appendChild(quote); 177 - modal.appendChild(textarea); 178 - modal.appendChild(buttons); 179 - overlay.appendChild(modal); 180 - 181 - document.body.appendChild(overlay); 182 - 183 - // Focus textarea 184 - setTimeout(() => textarea.focus(), 50); 185 - 186 - return overlay; 187 - }
+65
packages/core/src/content/proxy.ts
··· 1 + // Proxy content script 2 + import type { StorageAdapter } from '../storage/adapter'; 3 + import type { Annotation } from '../types'; 4 + import { normalizeUrl } from '../utils'; 5 + import { BaseContentScript, type ContentScriptAdapter } from './base'; 6 + import { AnnotationUIManager } from './ui'; 7 + 8 + export interface ProxyContentScriptOptions { 9 + storage: StorageAdapter; 10 + getCurrentUrl: () => string; 11 + applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 12 + clearHighlights: () => void; 13 + generateSelectors: (selection: Selection, root: Element) => any[]; 14 + onAnnotate: (data: { text: string; selectors: any[] }) => void; 15 + onSelectionChange?: (selection: { text: string; selectors: any[] } | null) => void; 16 + } 17 + 18 + export class ProxyContentScript extends BaseContentScript { 19 + private uiManager: AnnotationUIManager; 20 + 21 + constructor(options: ProxyContentScriptOptions) { 22 + // Detect mobile - consistent with extension 23 + const isMobile = window.innerWidth <= 768 || navigator.userAgent.includes('Android') || navigator.userAgent.includes('iPhone'); 24 + 25 + const adapter: ContentScriptAdapter = { 26 + storage: options.storage, 27 + getCurrentUrl: options.getCurrentUrl, 28 + applyHighlights: options.applyHighlights, 29 + clearHighlights: options.clearHighlights, 30 + generateSelectors: options.generateSelectors || ((_s, _r) => []), 31 + notifySelectionChange: (selection) => { 32 + // Notify listener (sidebar) 33 + if (options.onSelectionChange) { 34 + options.onSelectionChange(selection); 35 + } 36 + 37 + // Handle floating UI 38 + if (selection && selection.text) { 39 + // Calculate selection rect 40 + const domSelection = window.getSelection(); 41 + if (domSelection && domSelection.rangeCount > 0) { 42 + const range = domSelection.getRangeAt(0); 43 + const rect = range.getBoundingClientRect(); 44 + this.uiManager.showButton(rect, selection.text, selection.selectors); 45 + } 46 + } else { 47 + this.uiManager.removeButton(); 48 + } 49 + } 50 + }; 51 + super(adapter); 52 + 53 + this.uiManager = new AnnotationUIManager({ 54 + isMobile, 55 + onAnnotate: options.onAnnotate 56 + }); 57 + } 58 + 59 + protected shouldHandleSelection(event: MouseEvent): boolean { 60 + if (event.target instanceof Node && this.uiManager.contains(event.target)) { 61 + return false; 62 + } 63 + return true; 64 + } 65 + }
-33
packages/core/src/content/script.ts
··· 1 - // Proxy content script 2 - import type { StorageAdapter } from '../storage/adapter'; 3 - import type { Annotation } from '../types'; 4 - import { normalizeUrl } from '../utils'; 5 - import { BaseContentScript, type ContentScriptAdapter } from './base'; 6 - 7 - export interface ContentScriptOptions { 8 - storage: StorageAdapter; 9 - getCurrentUrl: () => string; 10 - applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 11 - clearHighlights: () => void; 12 - generateSelectors: (selection: Selection, root: Element) => any[]; 13 - onSelectionChange?: (selection: { text: string; selectors: any[] } | null) => void; 14 - } 15 - 16 - // AMPDO: Call this `ProxyContentScript` 17 - export class ContentScript extends BaseContentScript { 18 - constructor(options: ContentScriptOptions) { 19 - const adapter: ContentScriptAdapter = { 20 - storage: options.storage, 21 - getCurrentUrl: options.getCurrentUrl, 22 - applyHighlights: options.applyHighlights, 23 - clearHighlights: options.clearHighlights, 24 - generateSelectors: options.generateSelectors || ((_s, _r) => []), 25 - notifySelectionChange: (selection) => { 26 - if (options.onSelectionChange) { 27 - options.onSelectionChange(selection); 28 - } 29 - } 30 - }; 31 - super(adapter); 32 - } 33 - }
+9 -17
packages/core/src/content/ui.ts
··· 1 - import { createMobileAnnotateButton, createMobileAnnotationModal } from './mobile'; 1 + import { createMobileAnnotateButton } from './mobile'; 2 2 3 3 export interface AnnotationUIOptions { 4 - onAnnotate: (data: { text: string; selectors: any[]; body?: string }) => void; 4 + onAnnotate: (data: { text: string; selectors: any[] }) => void; 5 5 isMobile: boolean; 6 6 } 7 7 ··· 17 17 setTimeout(() => { 18 18 this.activeBtn = createMobileAnnotateButton( 19 19 rect.left + rect.width / 2, 20 - rect.top, 20 + rect.bottom, 21 21 () => { 22 - if (this.options.isMobile) { 23 - createMobileAnnotationModal( 24 - text, 25 - (body) => { 26 - this.options.onAnnotate({ text, selectors, body }); 27 - }, 28 - () => { 29 - // Cancelled 30 - } 31 - ); 32 - } else { 33 - // Desktop - trigger annotate (opens sidebar or focuses it) 34 - this.options.onAnnotate({ text, selectors }); 35 - } 22 + // Trigger annotate (opens sidebar or focuses it) 23 + this.options.onAnnotate({ text, selectors }); 36 24 this.removeButton(); 37 25 } 38 26 ); ··· 44 32 this.activeBtn.remove(); 45 33 this.activeBtn = null; 46 34 } 35 + } 36 + 37 + contains(element: Node): boolean { 38 + return this.activeBtn ? this.activeBtn.contains(element) : false; 47 39 } 48 40 }
+16
packages/core/src/sidebar/index.ts
··· 257 257 } 258 258 } 259 259 260 + handleActivateAnnotation() { 261 + if (this.currentSelection) { 262 + // Ensure the form is visible 263 + this.updateSelectionUI(); 264 + 265 + // Focus the textarea 266 + const annotationTextarea = this.container.querySelector('#annotation-text') as HTMLTextAreaElement; 267 + if (annotationTextarea) { 268 + annotationTextarea.focus(); 269 + } 270 + } else { 271 + // If no selection, maybe just focus the container or show a "Select text first" hint? 272 + // For now, do nothing as per requirement "Activate the text area" which implies it exists. 273 + } 274 + } 275 + 260 276 async createAnnotation(target: { source: string, selectors: any[] }, body: string) { 261 277 try { 262 278 await this.pds.createAnnotation({