Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 1266 lines 50 kB view raw
1import { useState, useEffect } from 'react'; 2import { sendMessage } from '@/utils/messaging'; 3import { themeItem, overlayEnabledItem } from '@/utils/storage'; 4import type { MarginSession, Annotation, Bookmark, Highlight, Collection } from '@/utils/types'; 5import { APP_URL } from '@/utils/types'; 6import CollectionIcon from '@/components/CollectionIcon'; 7import TagInput from '@/components/TagInput'; 8 9type Tab = 'page' | 'bookmarks' | 'highlights' | 'collections'; 10type PageFilter = 'all' | 'annotations' | 'highlights'; 11 12const Icons = { 13 settings: ( 14 <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 15 <path d="M12 15a3 3 0 100-6 3 3 0 000 6z" /> 16 <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" /> 17 </svg> 18 ), 19 globe: ( 20 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 21 <circle cx="12" cy="12" r="10" /> 22 <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" /> 23 <path d="M2 12h20" /> 24 </svg> 25 ), 26 bookmark: ( 27 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 28 <path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /> 29 </svg> 30 ), 31 highlighter: ( 32 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 33 <path d="m9 11-6 6v3h9l3-3" /> 34 <path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" /> 35 </svg> 36 ), 37 folder: ( 38 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 39 <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h16z" /> 40 </svg> 41 ), 42 folderPlus: ( 43 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 44 <path d="M12 10v6" /> 45 <path d="M9 13h6" /> 46 <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /> 47 </svg> 48 ), 49 check: ( 50 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 51 <polyline points="20 6 9 17 4 12" /> 52 </svg> 53 ), 54 chevronRight: ( 55 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 56 <polyline points="9 18 15 12 9 6" /> 57 </svg> 58 ), 59 sparkles: ( 60 <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 61 <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" /> 62 </svg> 63 ), 64 externalLink: ( 65 <svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 66 <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /> 67 <polyline points="15 3 21 3 21 9" /> 68 <line x1="10" x2="21" y1="14" y2="3" /> 69 </svg> 70 ), 71 x: ( 72 <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 73 <path d="M18 6 6 18" /> 74 <path d="m6 6 12 12" /> 75 </svg> 76 ), 77}; 78 79export function App() { 80 const [session, setSession] = useState<MarginSession | null>(null); 81 const [loading, setLoading] = useState(true); 82 const [activeTab, setActiveTab] = useState<Tab>('page'); 83 const [pageFilter, setPageFilter] = useState<PageFilter>('all'); 84 const [annotations, setAnnotations] = useState<Annotation[]>([]); 85 const [pageHighlights, setPageHighlights] = useState<Annotation[]>([]); 86 const [bookmarks, setBookmarks] = useState<Bookmark[]>([]); 87 const [highlights, setHighlights] = useState<Highlight[]>([]); 88 const [collections, setCollections] = useState<Collection[]>([]); 89 const [loadingAnnotations, setLoadingAnnotations] = useState(false); 90 const [loadingBookmarks, setLoadingBookmarks] = useState(false); 91 const [loadingHighlights, setLoadingHighlights] = useState(false); 92 const [loadingCollections, setLoadingCollections] = useState(false); 93 const [currentUrl, setCurrentUrl] = useState(''); 94 const [currentTitle, setCurrentTitle] = useState(''); 95 const [text, setText] = useState(''); 96 const [posting, setPosting] = useState(false); 97 const [bookmarking, setBookmarking] = useState(false); 98 const [bookmarked, setBookmarked] = useState(false); 99 const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system'); 100 const [overlayEnabled, setOverlayEnabled] = useState(true); 101 const [showSettings, setShowSettings] = useState(false); 102 const [collectionModalItem, setCollectionModalItem] = useState<string | null>(null); 103 const [addingToCollection, setAddingToCollection] = useState<string | null>(null); 104 const [containingCollections, setContainingCollections] = useState<Set<string>>(new Set()); 105 const [tags, setTags] = useState<string[]>([]); 106 const [tagSuggestions, setTagSuggestions] = useState<string[]>([]); 107 108 useEffect(() => { 109 checkSession(); 110 loadCurrentTab(); 111 loadSettings(); 112 113 browser.tabs.onActivated.addListener(loadCurrentTab); 114 browser.tabs.onUpdated.addListener((_, info) => { 115 if (info.url) loadCurrentTab(); 116 }); 117 118 return () => { 119 browser.tabs.onActivated.removeListener(loadCurrentTab); 120 }; 121 }, []); 122 123 useEffect(() => { 124 if (session?.authenticated && session.did) { 125 Promise.all([ 126 sendMessage('getUserTags', { did: session.did }).catch(() => [] as string[]), 127 sendMessage('getTrendingTags', undefined).catch(() => [] as string[]), 128 ]).then(([userTags, trendingTags]) => { 129 const seen = new Set(userTags); 130 const merged = [...userTags]; 131 for (const t of trendingTags) { 132 if (!seen.has(t)) { 133 merged.push(t); 134 seen.add(t); 135 } 136 } 137 setTagSuggestions(merged); 138 }); 139 } 140 }, [session]); 141 142 useEffect(() => { 143 if (session?.authenticated && currentUrl) { 144 if (activeTab === 'page') loadAnnotations(); 145 else if (activeTab === 'bookmarks') loadBookmarks(); 146 else if (activeTab === 'highlights') loadHighlights(); 147 else if (activeTab === 'collections') loadCollections(); 148 } 149 }, [activeTab, session, currentUrl]); 150 151 async function loadSettings() { 152 const t = await themeItem.getValue(); 153 setTheme(t); 154 applyTheme(t); 155 156 const overlay = await overlayEnabledItem.getValue(); 157 setOverlayEnabled(overlay); 158 159 themeItem.watch((newTheme) => { 160 setTheme(newTheme); 161 applyTheme(newTheme); 162 }); 163 164 overlayEnabledItem.watch((enabled) => { 165 setOverlayEnabled(enabled); 166 }); 167 } 168 169 function applyTheme(t: string) { 170 document.body.classList.remove('light', 'dark'); 171 if (t === 'system') { 172 if (window.matchMedia('(prefers-color-scheme: light)').matches) { 173 document.body.classList.add('light'); 174 } 175 } else { 176 document.body.classList.add(t); 177 } 178 } 179 180 async function checkSession() { 181 try { 182 const result = await sendMessage('checkSession', undefined); 183 setSession(result); 184 } catch (error) { 185 console.error('Session check error:', error); 186 setSession({ authenticated: false }); 187 } finally { 188 setLoading(false); 189 } 190 } 191 192 async function loadCurrentTab() { 193 const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 194 if (tab?.url) { 195 if (isPdfUrl(tab.url) && tab.id) { 196 await sendMessage('activateOnPdf', { tabId: tab.id, url: tab.url }); 197 return; 198 } 199 200 const resolved = extractOriginalUrl(tab.url); 201 setCurrentUrl(resolved); 202 setCurrentTitle(tab.title || ''); 203 } 204 } 205 206 function extractOriginalUrl(url: string): string { 207 if (url.includes('/pdfjs/web/viewer.html')) { 208 try { 209 const fileParam = new URL(url).searchParams.get('file'); 210 if (fileParam) return fileParam; 211 } catch { 212 /* ignore */ 213 } 214 } 215 return url; 216 } 217 218 function isPdfUrl(url: string): boolean { 219 if (!url.startsWith('http://') && !url.startsWith('https://')) return false; 220 try { 221 const { pathname } = new URL(url); 222 return /\.pdf$/i.test(pathname); 223 } catch { 224 return false; 225 } 226 } 227 228 async function loadAnnotations() { 229 if (!currentUrl) return; 230 setLoadingAnnotations(true); 231 try { 232 let result = await sendMessage('getCachedAnnotations', { url: currentUrl }); 233 234 if (!result) { 235 result = await sendMessage('getAnnotations', { url: currentUrl }); 236 } 237 238 const all = result || []; 239 const annots = all.filter( 240 (item: any) => item.type !== 'Bookmark' && item.type !== 'Highlight' 241 ); 242 const hlights = all.filter((item: any) => item.type === 'Highlight'); 243 setAnnotations(annots); 244 setPageHighlights(hlights); 245 246 const isBookmarked = all.some( 247 (item: any) => item.type === 'Bookmark' && item.creator?.did === session?.did 248 ); 249 setBookmarked(isBookmarked); 250 } catch (error) { 251 console.error('Load annotations error:', error); 252 } finally { 253 setLoadingAnnotations(false); 254 } 255 } 256 257 async function loadBookmarks() { 258 if (!session?.did) return; 259 setLoadingBookmarks(true); 260 try { 261 const result = await sendMessage('getUserBookmarks', { did: session.did }); 262 setBookmarks(result || []); 263 } catch (error) { 264 console.error('Load bookmarks error:', error); 265 } finally { 266 setLoadingBookmarks(false); 267 } 268 } 269 270 async function loadHighlights() { 271 if (!session?.did) return; 272 setLoadingHighlights(true); 273 try { 274 const result = await sendMessage('getUserHighlights', { did: session.did }); 275 setHighlights(result || []); 276 } catch (error) { 277 console.error('Load highlights error:', error); 278 } finally { 279 setLoadingHighlights(false); 280 } 281 } 282 283 async function loadCollections() { 284 if (!session?.did) return; 285 setLoadingCollections(true); 286 try { 287 const result = await sendMessage('getUserCollections', { did: session.did }); 288 setCollections(result || []); 289 } catch (error) { 290 console.error('Load collections error:', error); 291 } finally { 292 setLoadingCollections(false); 293 } 294 } 295 296 async function handlePost() { 297 if (!text.trim()) return; 298 setPosting(true); 299 try { 300 const result = await sendMessage('createAnnotation', { 301 url: currentUrl, 302 text: text.trim(), 303 title: currentTitle, 304 tags: tags.length > 0 ? tags : undefined, 305 }); 306 if (result.success) { 307 setText(''); 308 setTags([]); 309 loadAnnotations(); 310 } else { 311 alert('Failed to post annotation'); 312 } 313 } catch (error) { 314 console.error('Post error:', error); 315 alert('Error posting annotation'); 316 } finally { 317 setPosting(false); 318 } 319 } 320 321 async function handleBookmark() { 322 setBookmarking(true); 323 try { 324 const result = await sendMessage('createBookmark', { 325 url: currentUrl, 326 title: currentTitle, 327 }); 328 if (result.success) { 329 setBookmarked(true); 330 } else { 331 alert('Failed to bookmark page'); 332 } 333 } catch (error) { 334 console.error('Bookmark error:', error); 335 alert('Error bookmarking page'); 336 } finally { 337 setBookmarking(false); 338 } 339 } 340 341 async function handleThemeChange(newTheme: 'light' | 'dark' | 'system') { 342 await themeItem.setValue(newTheme); 343 } 344 345 async function handleOverlayToggle() { 346 await overlayEnabledItem.setValue(!overlayEnabled); 347 } 348 349 async function openCollectionModal(itemUri: string) { 350 setCollectionModalItem(itemUri); 351 setContainingCollections(new Set()); 352 353 if (collections.length === 0) { 354 await loadCollections(); 355 } 356 357 try { 358 const itemCollectionUris = await sendMessage('getItemCollections', { 359 annotationUri: itemUri, 360 }); 361 setContainingCollections(new Set(itemCollectionUris)); 362 } catch (error) { 363 console.error('Failed to get item collections:', error); 364 } 365 } 366 367 async function handleAddToCollection(collectionUri: string) { 368 if (!collectionModalItem) return; 369 370 if (containingCollections.has(collectionUri)) { 371 setCollectionModalItem(null); 372 return; 373 } 374 375 setAddingToCollection(collectionUri); 376 try { 377 const result = await sendMessage('addToCollection', { 378 collectionUri, 379 annotationUri: collectionModalItem, 380 }); 381 if (result.success) { 382 setContainingCollections((prev) => new Set([...prev, collectionUri])); 383 } else { 384 alert('Failed to add to collection'); 385 } 386 } catch (error) { 387 console.error('Add to collection error:', error); 388 alert('Error adding to collection'); 389 } finally { 390 setAddingToCollection(null); 391 } 392 } 393 394 function formatDate(dateString?: string) { 395 if (!dateString) return ''; 396 try { 397 return new Date(dateString).toLocaleDateString(); 398 } catch { 399 return dateString; 400 } 401 } 402 403 if (loading) { 404 return ( 405 <div className="flex items-center justify-center h-screen"> 406 <div className="w-5 h-5 border-2 border-[var(--accent)] border-t-transparent rounded-full animate-spin" /> 407 </div> 408 ); 409 } 410 411 if (!session?.authenticated) { 412 return ( 413 <div className="flex flex-col items-center justify-center h-screen p-8 text-center"> 414 <div className="w-14 h-14 rounded-2xl bg-[var(--accent-subtle)] flex items-center justify-center mb-5"> 415 <img src="/icons/logo.svg" alt="Margin" className="w-8 h-8" /> 416 </div> 417 <h2 className="font-display text-xl font-bold tracking-tight mb-2">Welcome to Margin</h2> 418 <p className="text-[var(--text-secondary)] text-sm mb-8 max-w-xs leading-relaxed"> 419 Annotate, highlight, and bookmark the web with your AT Protocol identity. 420 </p> 421 <button 422 onClick={() => browser.tabs.create({ url: `${APP_URL}/login` })} 423 className="px-8 py-2.5 bg-[var(--accent)] text-white rounded-xl text-sm font-semibold hover:bg-[var(--accent-hover)] transition-colors" 424 > 425 Sign In 426 </button> 427 </div> 428 ); 429 } 430 431 return ( 432 <div className="flex flex-col h-screen"> 433 <header className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--border)]"> 434 <div className="flex items-center gap-2"> 435 <img src="/icons/logo.svg" alt="Margin" className="w-5 h-5" /> 436 <span className="font-display font-bold text-sm tracking-tight">Margin</span> 437 </div> 438 <div className="flex items-center gap-1.5"> 439 <button 440 onClick={() => browser.tabs.create({ url: APP_URL })} 441 className="text-[11px] text-[var(--text-tertiary)] hover:text-[var(--accent)] px-2 py-1 rounded-md hover:bg-[var(--bg-hover)] transition-colors" 442 > 443 @{session.handle} 444 </button> 445 <button 446 onClick={() => setShowSettings(!showSettings)} 447 className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-lg transition-colors" 448 title="Settings" 449 > 450 {Icons.settings} 451 </button> 452 </div> 453 </header> 454 455 {showSettings && ( 456 <div className="p-4 border-b border-[var(--border)] space-y-4 animate-slideDown"> 457 <div> 458 <label className="block text-xs font-medium text-[var(--text-secondary)] mb-1.5"> 459 Theme 460 </label> 461 <div className="flex gap-1.5"> 462 {(['light', 'dark', 'system'] as const).map((t) => ( 463 <button 464 key={t} 465 onClick={() => handleThemeChange(t)} 466 className={`flex-1 py-2 px-3 text-xs font-medium rounded-lg transition-colors ${ 467 theme === t 468 ? 'bg-[var(--accent)] text-white' 469 : 'bg-[var(--bg-card)] border border-[var(--border)] hover:bg-[var(--bg-hover)]' 470 }`} 471 > 472 {t.charAt(0).toUpperCase() + t.slice(1)} 473 </button> 474 ))} 475 </div> 476 </div> 477 478 <div className="flex items-center justify-between p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-lg"> 479 <div> 480 <div className="text-sm font-medium">Page Overlay</div> 481 <p className="text-[11px] text-[var(--text-tertiary)] mt-0.5"> 482 Show highlights and annotations on pages 483 </p> 484 </div> 485 <button 486 onClick={handleOverlayToggle} 487 className={`relative w-10 h-[22px] rounded-full transition-colors ${ 488 overlayEnabled 489 ? 'bg-[var(--accent)]' 490 : 'bg-[var(--bg-hover)] border border-[var(--border)]' 491 }`} 492 > 493 <div 494 className={`absolute top-[3px] w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${ 495 overlayEnabled ? 'left-[22px]' : 'left-[3px]' 496 }`} 497 /> 498 </button> 499 </div> 500 501 <button 502 onClick={() => browser.tabs.create({ url: APP_URL })} 503 className="w-full py-2 text-xs font-medium text-[var(--text-tertiary)] hover:text-[var(--accent)] rounded-lg hover:bg-[var(--bg-hover)] transition-colors flex items-center justify-center gap-1.5" 504 > 505 Open Margin {Icons.externalLink} 506 </button> 507 </div> 508 )} 509 510 <div className="flex border-b border-[var(--border)]"> 511 {(['page', 'bookmarks', 'highlights', 'collections'] as Tab[]).map((tab) => { 512 const icons = { 513 page: Icons.globe, 514 bookmarks: Icons.bookmark, 515 highlights: Icons.highlighter, 516 collections: Icons.folder, 517 }; 518 const labels = { 519 page: 'Page', 520 bookmarks: 'Bookmarks', 521 highlights: 'Highlights', 522 collections: 'Collections', 523 }; 524 return ( 525 <button 526 key={tab} 527 onClick={() => setActiveTab(tab)} 528 className={`flex-1 py-2.5 text-[11px] font-medium flex items-center justify-center gap-1 border-b-2 transition-all ${ 529 activeTab === tab 530 ? 'border-[var(--accent)] text-[var(--accent)]' 531 : 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]' 532 }`} 533 > 534 {icons[tab]} 535 <span className="hidden min-[340px]:inline">{labels[tab]}</span> 536 </button> 537 ); 538 })} 539 </div> 540 541 <div className="flex-1 overflow-y-auto"> 542 {activeTab === 'page' && ( 543 <div className="p-4"> 544 <div className="mb-4 p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl"> 545 <div className="flex items-center gap-3"> 546 <div className="w-9 h-9 rounded-lg bg-[var(--bg-hover)] flex items-center justify-center flex-shrink-0 overflow-hidden"> 547 {currentUrl ? ( 548 <img 549 src={`https://www.google.com/s2/favicons?domain=${new URL(currentUrl).hostname}&sz=64`} 550 alt="" 551 className="w-5 h-5" 552 onError={(e) => { 553 (e.target as HTMLImageElement).style.display = 'none'; 554 (e.target as HTMLImageElement).nextElementSibling?.classList.remove( 555 'hidden' 556 ); 557 }} 558 /> 559 ) : null} 560 <span className={`text-[var(--text-tertiary)] ${currentUrl ? 'hidden' : ''}`}> 561 {Icons.globe} 562 </span> 563 </div> 564 <div className="flex-1 min-w-0"> 565 <div className="text-sm font-medium truncate">{currentTitle || 'Untitled'}</div> 566 <div className="text-[11px] text-[var(--text-tertiary)] truncate"> 567 {currentUrl ? new URL(currentUrl).hostname : ''} 568 </div> 569 </div> 570 <button 571 onClick={handleBookmark} 572 disabled={bookmarking || bookmarked} 573 className={`p-1.5 rounded-md transition-colors ${ 574 bookmarked 575 ? 'text-emerald-400' 576 : 'text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--bg-hover)]' 577 }`} 578 title={bookmarked ? 'Bookmarked' : 'Bookmark page'} 579 > 580 {bookmarked ? Icons.check : Icons.bookmark} 581 </button> 582 </div> 583 </div> 584 585 <div className="mb-6"> 586 <textarea 587 value={text} 588 onChange={(e) => setText(e.target.value)} 589 placeholder="Share your thoughts on this page..." 590 className="w-full px-3 py-2.5 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl text-sm resize-none focus:outline-none focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent-subtle)] min-h-[80px] transition-all" 591 /> 592 <div className="mt-2"> 593 <TagInput tags={tags} onChange={setTags} suggestions={tagSuggestions} /> 594 </div> 595 <div className="flex items-center justify-between mt-2"> 596 <span className="text-[11px] text-[var(--text-tertiary)]"> 597 {text.length > 0 ? `${text.length} chars` : ''} 598 </span> 599 <button 600 onClick={handlePost} 601 disabled={posting || !text.trim()} 602 className="px-5 py-1.5 bg-[var(--accent)] text-white text-xs rounded-lg font-medium hover:bg-[var(--accent-hover)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors" 603 > 604 {posting ? 'Posting...' : 'Post'} 605 </button> 606 </div> 607 </div> 608 609 <div className="flex items-center justify-between mb-3"> 610 <div className="flex items-center gap-2"> 611 <span className="text-xs font-semibold text-[var(--text-secondary)]">Activity</span> 612 <span className="text-xs font-semibold bg-[var(--accent-subtle)] text-[var(--accent)] px-2 py-0.5 rounded-full"> 613 {annotations.length + pageHighlights.length} 614 </span> 615 </div> 616 <div className="flex items-center bg-[var(--bg-card)] border border-[var(--border)] rounded-lg p-0.5"> 617 {(['all', 'annotations', 'highlights'] as const).map((key) => ( 618 <button 619 key={key} 620 onClick={() => setPageFilter(key)} 621 className={`px-2 py-1 text-[10px] font-medium rounded-md transition-all ${ 622 pageFilter === key 623 ? 'bg-[var(--accent)] text-white shadow-sm' 624 : 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]' 625 }`} 626 > 627 {key === 'all' ? 'All' : key === 'annotations' ? 'Notes' : 'HL'} 628 </button> 629 ))} 630 </div> 631 </div> 632 633 {loadingAnnotations ? ( 634 <div className="flex items-center justify-center py-16"> 635 <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 636 </div> 637 ) : annotations.length + pageHighlights.length === 0 ? ( 638 <div className="text-center py-16 text-[var(--text-tertiary)]"> 639 <div className="w-12 h-12 rounded-xl bg-[var(--accent-subtle)] flex items-center justify-center mx-auto mb-4"> 640 {Icons.sparkles} 641 </div> 642 <p className="font-medium mb-1">No activity yet</p> 643 <p className="text-xs">Be the first to annotate or highlight</p> 644 </div> 645 ) : ( 646 <div className="space-y-3"> 647 {(pageFilter === 'all' || pageFilter === 'annotations') && 648 annotations.map((item) => ( 649 <AnnotationCard 650 key={item.uri || item.id} 651 item={item} 652 sessionDid={session?.did} 653 formatDate={formatDate} 654 onAddToCollection={() => openCollectionModal(item.uri || item.id || '')} 655 onConverted={loadAnnotations} 656 /> 657 ))} 658 {(pageFilter === 'all' || pageFilter === 'highlights') && 659 pageHighlights.map((item) => ( 660 <AnnotationCard 661 key={item.uri || item.id} 662 item={item} 663 sessionDid={session?.did} 664 formatDate={formatDate} 665 onAddToCollection={() => openCollectionModal(item.uri || item.id || '')} 666 onConverted={loadAnnotations} 667 /> 668 ))} 669 {((pageFilter === 'annotations' && annotations.length === 0) || 670 (pageFilter === 'highlights' && pageHighlights.length === 0)) && ( 671 <div className="text-center py-10 text-[var(--text-tertiary)]"> 672 <p className="text-xs"> 673 No {pageFilter === 'annotations' ? 'annotations' : 'highlights'} on this page 674 </p> 675 </div> 676 )} 677 </div> 678 )} 679 </div> 680 )} 681 682 {activeTab === 'bookmarks' && ( 683 <div className="p-4"> 684 {loadingBookmarks ? ( 685 <div className="flex items-center justify-center py-16"> 686 <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 687 </div> 688 ) : bookmarks.length === 0 ? ( 689 <div className="text-center py-16 text-[var(--text-tertiary)]"> 690 <div className="w-12 h-12 rounded-xl bg-[var(--accent-subtle)] flex items-center justify-center mx-auto mb-4 text-[var(--accent)]"> 691 {Icons.bookmark} 692 </div> 693 <p className="font-medium mb-1">No bookmarks yet</p> 694 <p className="text-xs">Save pages to read later</p> 695 </div> 696 ) : ( 697 <div className="space-y-2"> 698 {bookmarks.map((item) => ( 699 <div 700 key={item.uri || item.id} 701 className="flex items-center gap-3 p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group" 702 > 703 <div className="w-9 h-9 rounded-lg bg-[var(--accent)]/15 flex items-center justify-center flex-shrink-0 text-[var(--accent)]"> 704 {Icons.bookmark} 705 </div> 706 <a 707 href={item.source} 708 target="_blank" 709 rel="noopener noreferrer" 710 className="flex-1 min-w-0" 711 > 712 <div className="text-sm font-medium truncate group-hover:text-[var(--accent)] transition-colors"> 713 {item.title || item.source} 714 </div> 715 <div className="text-xs text-[var(--text-tertiary)] truncate"> 716 {item.source ? new URL(item.source).hostname : ''} 717 </div> 718 </a> 719 <button 720 onClick={(e) => { 721 e.stopPropagation(); 722 if (item.uri) openCollectionModal(item.uri); 723 }} 724 className="p-1.5 rounded-lg text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 725 title="Add to collection" 726 > 727 {Icons.folderPlus} 728 </button> 729 <a 730 href={item.source} 731 target="_blank" 732 rel="noopener noreferrer" 733 className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)]" 734 > 735 {Icons.chevronRight} 736 </a> 737 </div> 738 ))} 739 </div> 740 )} 741 </div> 742 )} 743 744 {activeTab === 'highlights' && ( 745 <div className="p-4"> 746 {loadingHighlights ? ( 747 <div className="flex items-center justify-center py-16"> 748 <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 749 </div> 750 ) : highlights.length === 0 ? ( 751 <div className="text-center py-16 text-[var(--text-tertiary)]"> 752 <div className="w-12 h-12 rounded-xl bg-[var(--accent-subtle)] flex items-center justify-center mx-auto mb-4 text-[var(--accent)]"> 753 {Icons.highlighter} 754 </div> 755 <p className="font-medium mb-1">No highlights yet</p> 756 <p className="text-xs">Select text on any page to highlight</p> 757 </div> 758 ) : ( 759 <div className="space-y-3"> 760 {highlights.map((item) => ( 761 <SidepanelHighlightCard 762 key={item.uri || item.id} 763 item={item} 764 onAddToCollection={() => openCollectionModal(item.uri || item.id || '')} 765 onConverted={loadHighlights} 766 /> 767 ))} 768 </div> 769 )} 770 </div> 771 )} 772 773 {activeTab === 'collections' && ( 774 <div className="p-4"> 775 {loadingCollections ? ( 776 <div className="flex items-center justify-center py-16"> 777 <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 778 </div> 779 ) : collections.length === 0 ? ( 780 <div className="text-center py-16 text-[var(--text-tertiary)]"> 781 <div className="w-12 h-12 rounded-xl bg-[var(--accent-subtle)] flex items-center justify-center mx-auto mb-4 text-[var(--accent)]"> 782 {Icons.folder} 783 </div> 784 <p className="font-medium mb-1">No collections yet</p> 785 <p className="text-xs">Organize your bookmarks into collections</p> 786 </div> 787 ) : ( 788 <div className="space-y-2"> 789 {collections.map((item) => ( 790 <button 791 key={item.uri || item.id} 792 onClick={() => 793 browser.tabs.create({ 794 url: `${APP_URL}/collection/${encodeURIComponent(item.uri || item.id || '')}`, 795 }) 796 } 797 className="w-full text-left p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group flex items-center gap-3" 798 > 799 <div className="w-9 h-9 rounded-lg bg-[var(--accent)]/15 flex items-center justify-center flex-shrink-0 text-[var(--accent)] text-lg"> 800 <CollectionIcon icon={item.icon} size={18} /> 801 </div> 802 <div className="flex-1 min-w-0"> 803 <div className="text-sm font-medium group-hover:text-[var(--accent)] transition-colors"> 804 {item.name} 805 </div> 806 {item.description && ( 807 <div className="text-xs text-[var(--text-tertiary)] truncate"> 808 {item.description} 809 </div> 810 )} 811 </div> 812 <div className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)]"> 813 {Icons.chevronRight} 814 </div> 815 </button> 816 ))} 817 </div> 818 )} 819 </div> 820 )} 821 </div> 822 823 {collectionModalItem && ( 824 <div 825 className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" 826 onClick={() => setCollectionModalItem(null)} 827 > 828 <div 829 className="bg-[var(--bg-primary)] border border-[var(--border)] rounded-2xl w-full max-w-sm max-h-[70vh] overflow-hidden shadow-2xl" 830 onClick={(e) => e.stopPropagation()} 831 > 832 <div className="flex items-center justify-between p-4 border-b border-[var(--border)]"> 833 <h3 className="font-semibold">Add to Collection</h3> 834 <button 835 onClick={() => setCollectionModalItem(null)} 836 className="p-1 rounded-lg hover:bg-[var(--bg-hover)] text-[var(--text-tertiary)]" 837 > 838 {Icons.x} 839 </button> 840 </div> 841 <div className="p-2 max-h-[50vh] overflow-y-auto"> 842 {collections.length === 0 ? ( 843 <div className="text-center py-8 text-[var(--text-tertiary)]"> 844 <p className="text-sm">No collections yet</p> 845 <p className="text-xs mt-1">Create a collection on the web app first</p> 846 </div> 847 ) : ( 848 collections.map((col) => { 849 const colUri = col.uri || col.id || ''; 850 const isAdding = addingToCollection === colUri; 851 const isInCollection = containingCollections.has(colUri); 852 return ( 853 <button 854 key={colUri} 855 onClick={() => !isInCollection && handleAddToCollection(colUri)} 856 disabled={isAdding || isInCollection} 857 className={`w-full text-left p-3 rounded-xl flex items-center gap-3 transition-all ${ 858 isInCollection 859 ? 'bg-emerald-400/10 cursor-default' 860 : 'hover:bg-[var(--bg-hover)]' 861 }`} 862 > 863 <div 864 className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-base ${ 865 isInCollection 866 ? 'bg-emerald-400/15 text-emerald-400' 867 : 'bg-[var(--accent)]/15 text-[var(--accent)]' 868 }`} 869 > 870 <CollectionIcon icon={col.icon} size={16} /> 871 </div> 872 <div className="flex-1 min-w-0"> 873 <div className="text-sm font-medium truncate">{col.name}</div> 874 {col.description && ( 875 <div className="text-xs text-[var(--text-tertiary)] truncate"> 876 {col.description} 877 </div> 878 )} 879 </div> 880 {isAdding ? ( 881 <div className="animate-spin rounded-full h-4 w-4 border-2 border-[var(--accent)] border-t-transparent" /> 882 ) : isInCollection ? ( 883 <span className="text-emerald-400">{Icons.check}</span> 884 ) : ( 885 Icons.folderPlus 886 )} 887 </button> 888 ); 889 }) 890 )} 891 </div> 892 </div> 893 </div> 894 )} 895 </div> 896 ); 897} 898 899function AnnotationCard({ 900 item, 901 sessionDid, 902 formatDate, 903 onAddToCollection, 904 onConverted, 905}: { 906 item: Annotation; 907 sessionDid?: string; 908 formatDate: (d?: string) => string; 909 onAddToCollection?: () => void; 910 onConverted?: () => void; 911}) { 912 const [noteInput, setNoteInput] = useState(''); 913 const [showNoteInput, setShowNoteInput] = useState(false); 914 const [converting, setConverting] = useState(false); 915 916 const author = item.author || item.creator || {}; 917 const handle = author.handle || 'User'; 918 const text = item.body?.value || item.text || ''; 919 const selector = item.target?.selector; 920 const quote = selector?.exact || ''; 921 const isHighlight = (item as any).type === 'Highlight'; 922 const isOwned = sessionDid && author.did === sessionDid; 923 const highlightColor = item.color || (isHighlight ? '#fbbf24' : 'var(--accent)'); 924 925 async function handleConvert() { 926 if (!noteInput.trim()) return; 927 setConverting(true); 928 try { 929 const result = await sendMessage('convertHighlightToAnnotation', { 930 highlightUri: item.uri || item.id || '', 931 url: item.target?.source || '', 932 text: noteInput.trim(), 933 selector: item.target?.selector, 934 }); 935 if (result.success) { 936 setShowNoteInput(false); 937 setNoteInput(''); 938 onConverted?.(); 939 } else { 940 console.error('Convert failed:', result.error); 941 } 942 } catch (error) { 943 console.error('Convert error:', error); 944 } finally { 945 setConverting(false); 946 } 947 } 948 949 return ( 950 <div className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all"> 951 <div className="flex items-start gap-3"> 952 <div className="w-9 h-9 rounded-full bg-gradient-to-br from-[var(--accent)] to-[var(--accent-hover)] flex items-center justify-center text-white text-xs font-bold overflow-hidden flex-shrink-0"> 953 {author.avatar ? ( 954 <img src={author.avatar} alt={handle} className="w-full h-full object-cover" /> 955 ) : ( 956 handle[0]?.toUpperCase() || 'U' 957 )} 958 </div> 959 <div className="flex-1 min-w-0"> 960 <div className="flex items-center gap-2 mb-2"> 961 <span className="text-sm font-semibold">@{handle}</span> 962 <span className="text-xs text-[var(--text-tertiary)]"> 963 {formatDate(item.created || item.createdAt)} 964 </span> 965 {isHighlight && ( 966 <span 967 className="text-[10px] font-semibold px-2 py-0.5 rounded-full flex items-center gap-1" 968 style={{ 969 backgroundColor: `${highlightColor}20`, 970 color: highlightColor, 971 }} 972 > 973 Highlight 974 </span> 975 )} 976 <div className="ml-auto flex items-center gap-0.5"> 977 {isHighlight && isOwned && !showNoteInput && ( 978 <button 979 onClick={(e) => { 980 e.stopPropagation(); 981 setShowNoteInput(true); 982 }} 983 className="p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 984 title="Add note (convert to annotation)" 985 > 986 <svg 987 className="w-3.5 h-3.5" 988 fill="none" 989 stroke="currentColor" 990 strokeWidth="2" 991 viewBox="0 0 24 24" 992 > 993 <path d="M12 19l7-7 3 3-7 7-3-3z" /> 994 <path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" /> 995 <path d="M2 2l7.586 7.586" /> 996 <circle cx="11" cy="11" r="2" /> 997 </svg> 998 </button> 999 )} 1000 {onAddToCollection && ( 1001 <button 1002 onClick={(e) => { 1003 e.stopPropagation(); 1004 onAddToCollection(); 1005 }} 1006 className="p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 1007 title="Add to collection" 1008 > 1009 {Icons.folderPlus} 1010 </button> 1011 )} 1012 </div> 1013 </div> 1014 1015 {quote && ( 1016 <div 1017 className="text-sm text-[var(--text-secondary)] border-l-2 pl-3 mb-2 py-1 rounded-r italic cursor-pointer hover:opacity-80 transition-all" 1018 style={{ 1019 borderColor: highlightColor, 1020 backgroundColor: `${highlightColor}12`, 1021 }} 1022 onClick={async (e) => { 1023 e.stopPropagation(); 1024 const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 1025 if (tab?.id) { 1026 browser.tabs.sendMessage(tab.id, { type: 'SCROLL_TO_TEXT', text: quote }); 1027 } 1028 }} 1029 title="Jump to text on page" 1030 > 1031 "{quote.length > 250 ? quote.slice(0, 250) + '...' : quote}" 1032 </div> 1033 )} 1034 1035 {text && <div className="text-sm leading-relaxed">{text}</div>} 1036 1037 {showNoteInput && ( 1038 <div className="mt-2.5 flex gap-2 items-end"> 1039 <textarea 1040 value={noteInput} 1041 onChange={(e) => setNoteInput(e.target.value)} 1042 placeholder="Add your note..." 1043 autoFocus 1044 onKeyDown={(e) => { 1045 if (e.key === 'Enter' && !e.shiftKey) { 1046 e.preventDefault(); 1047 handleConvert(); 1048 } 1049 if (e.key === 'Escape') { 1050 setShowNoteInput(false); 1051 setNoteInput(''); 1052 } 1053 }} 1054 className="flex-1 p-2.5 bg-[var(--bg-primary)] border border-[var(--border)] rounded-lg text-xs resize-none focus:outline-none focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)]/20 min-h-[60px]" 1055 /> 1056 <div className="flex flex-col gap-1.5"> 1057 <button 1058 onClick={handleConvert} 1059 disabled={converting || !noteInput.trim()} 1060 className="p-2 bg-[var(--accent)] text-white rounded-lg hover:bg-[var(--accent-hover)] disabled:opacity-40 disabled:cursor-not-allowed transition-all" 1061 title="Convert to annotation" 1062 > 1063 {converting ? ( 1064 <div className="animate-spin rounded-full h-3.5 w-3.5 border-2 border-white border-t-transparent" /> 1065 ) : ( 1066 <svg 1067 className="w-3.5 h-3.5" 1068 fill="none" 1069 stroke="currentColor" 1070 strokeWidth="2" 1071 viewBox="0 0 24 24" 1072 > 1073 <line x1="22" x2="11" y1="2" y2="13" /> 1074 <polygon points="22 2 15 22 11 13 2 9 22 2" /> 1075 </svg> 1076 )} 1077 </button> 1078 <button 1079 onClick={() => { 1080 setShowNoteInput(false); 1081 setNoteInput(''); 1082 }} 1083 className="p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-lg transition-all" 1084 title="Cancel" 1085 > 1086 {Icons.x} 1087 </button> 1088 </div> 1089 </div> 1090 )} 1091 </div> 1092 </div> 1093 </div> 1094 ); 1095} 1096 1097function SidepanelHighlightCard({ 1098 item, 1099 onAddToCollection, 1100 onConverted, 1101}: { 1102 item: Highlight; 1103 onAddToCollection?: () => void; 1104 onConverted?: () => void; 1105}) { 1106 const [noteInput, setNoteInput] = useState(''); 1107 const [showNoteInput, setShowNoteInput] = useState(false); 1108 const [converting, setConverting] = useState(false); 1109 1110 const hlItem = item as any; 1111 const selector = hlItem.target?.selector || hlItem.selector; 1112 const source = hlItem.target?.source || hlItem.source || hlItem.url || ''; 1113 const quote = selector?.exact || ''; 1114 const color = hlItem.color || '#fbbf24'; 1115 1116 async function handleConvert() { 1117 if (!noteInput.trim()) return; 1118 setConverting(true); 1119 try { 1120 const result = await sendMessage('convertHighlightToAnnotation', { 1121 highlightUri: hlItem.uri || hlItem.id || '', 1122 url: source, 1123 text: noteInput.trim(), 1124 selector: selector, 1125 }); 1126 if (result.success) { 1127 setShowNoteInput(false); 1128 setNoteInput(''); 1129 onConverted?.(); 1130 } else { 1131 console.error('Convert failed:', result.error); 1132 } 1133 } catch (error) { 1134 console.error('Convert error:', error); 1135 } finally { 1136 setConverting(false); 1137 } 1138 } 1139 1140 return ( 1141 <div className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group"> 1142 {quote && ( 1143 <div 1144 className="text-sm leading-relaxed border-l-3 pl-3 mb-3 cursor-pointer" 1145 style={{ 1146 borderColor: color, 1147 background: `linear-gradient(90deg, ${color}10, transparent)`, 1148 }} 1149 onClick={() => { 1150 if (source) browser.tabs.create({ url: source }); 1151 }} 1152 > 1153 "{quote.length > 150 ? quote.slice(0, 150) + '...' : quote}" 1154 </div> 1155 )} 1156 1157 <div className="flex items-center justify-between text-xs text-[var(--text-tertiary)]"> 1158 <span className="flex items-center gap-1.5"> 1159 {Icons.globe} {source ? new URL(source).hostname : ''} 1160 </span> 1161 <div className="flex items-center gap-1"> 1162 {!showNoteInput && ( 1163 <button 1164 onClick={(e) => { 1165 e.stopPropagation(); 1166 setShowNoteInput(true); 1167 }} 1168 className="p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 1169 title="Add note (convert to annotation)" 1170 > 1171 <svg 1172 className="w-3.5 h-3.5" 1173 fill="none" 1174 stroke="currentColor" 1175 strokeWidth="2" 1176 viewBox="0 0 24 24" 1177 > 1178 <path d="M12 19l7-7 3 3-7 7-3-3z" /> 1179 <path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" /> 1180 <path d="M2 2l7.586 7.586" /> 1181 <circle cx="11" cy="11" r="2" /> 1182 </svg> 1183 </button> 1184 )} 1185 {onAddToCollection && ( 1186 <button 1187 onClick={(e) => { 1188 e.stopPropagation(); 1189 onAddToCollection(); 1190 }} 1191 className="p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 1192 title="Add to collection" 1193 > 1194 {Icons.folderPlus} 1195 </button> 1196 )} 1197 <button 1198 onClick={() => { 1199 if (source) browser.tabs.create({ url: source }); 1200 }} 1201 className="group-hover:text-[var(--accent)]" 1202 > 1203 {Icons.chevronRight} 1204 </button> 1205 </div> 1206 </div> 1207 1208 {showNoteInput && ( 1209 <div className="mt-3 flex gap-2 items-end"> 1210 <textarea 1211 value={noteInput} 1212 onChange={(e) => setNoteInput(e.target.value)} 1213 placeholder="Add your note..." 1214 autoFocus 1215 onKeyDown={(e) => { 1216 if (e.key === 'Enter' && !e.shiftKey) { 1217 e.preventDefault(); 1218 handleConvert(); 1219 } 1220 if (e.key === 'Escape') { 1221 setShowNoteInput(false); 1222 setNoteInput(''); 1223 } 1224 }} 1225 className="flex-1 p-2.5 bg-[var(--bg-primary)] border border-[var(--border)] rounded-lg text-xs resize-none focus:outline-none focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)]/20 min-h-[60px]" 1226 /> 1227 <div className="flex flex-col gap-1.5"> 1228 <button 1229 onClick={handleConvert} 1230 disabled={converting || !noteInput.trim()} 1231 className="p-2 bg-[var(--accent)] text-white rounded-lg hover:bg-[var(--accent-hover)] disabled:opacity-40 disabled:cursor-not-allowed transition-all" 1232 title="Convert to annotation" 1233 > 1234 {converting ? ( 1235 <div className="animate-spin rounded-full h-3.5 w-3.5 border-2 border-white border-t-transparent" /> 1236 ) : ( 1237 <svg 1238 className="w-3.5 h-3.5" 1239 fill="none" 1240 stroke="currentColor" 1241 strokeWidth="2" 1242 viewBox="0 0 24 24" 1243 > 1244 <line x1="22" x2="11" y1="2" y2="13" /> 1245 <polygon points="22 2 15 22 11 13 2 9 22 2" /> 1246 </svg> 1247 )} 1248 </button> 1249 <button 1250 onClick={() => { 1251 setShowNoteInput(false); 1252 setNoteInput(''); 1253 }} 1254 className="p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-lg transition-all" 1255 title="Cancel" 1256 > 1257 {Icons.x} 1258 </button> 1259 </div> 1260 </div> 1261 )} 1262 </div> 1263 ); 1264} 1265 1266export default App;