this repo has no description
6
fork

Configure Feed

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

list view

+444 -82
+6 -5
index.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 7 - <title>woomarks</title> 7 + <title>bluemarks</title> 8 8 <link 9 9 href="https://fonts.googleapis.com/css2?family=Doto&family=Alfa+Slab+One&family=Bebas+Neue&family=Bree+Serif&family=Caveat&family=Courier+Prime&family=Dosis&family=EB+Garamond&family=Permanent+Marker&family=Sedan+SC&family=Ultra&display=swap" 10 10 rel="stylesheet" ··· 53 53 54 54 <div class="topbar"> 55 55 <div style="flex-grow: 1"> 56 - <b><a id="headerTitle" href="">woomarks</a></b> 57 - <a href="./faq.html">FAQ</a> 58 - <span id="connectionStatus" class="connection-status"></span> 56 + <b><a id="headerTitle" href="">bluemarks</a></b> 57 + <img id="userAvatar" class="user-avatar" style="display: none;" /> 58 + <button id="logoutBtn" class="param-btn">Login</button> 59 59 <span id="viewingUser" class="viewing-user" style="display: none;"></span> 60 60 </div> 61 + <img id="searchedUserAvatar" class="searched-user-avatar" style="display: none;" /> 61 62 <input 62 63 type="text" 63 64 id="userSearchInput" ··· 65 66 title="View another user's bookmarks" 66 67 style="display: none; margin-right: 0.5vw;" 67 68 /> 68 - <button id="logoutBtn" class="param-btn" style="display: none;">Logout</button> 69 69 <button id="openEmptyDialogBtn" data-umami-event="Open creation modal" class="param-btn"><span class="btn-text">Add</span> ➕</button> 70 70 71 + <button id="viewToggleBtn" class="param-btn"><span class="btn-text">Grid</span> ⊞</button> 71 72 <button id="sortToggleBtn" data-umami-event="Sort" class="param-btn"><span class="btn-text">Sort</span> ▲</button> 72 73 <input 73 74 type="text"
+289 -62
script.js
··· 35 35 let viewingUserDid = null; 36 36 let viewingUserHandle = null; 37 37 let isViewingOtherUser = false; 38 + let isListView = true; 39 + let currentSearchedUserProfile = null; 38 40 39 41 // ====== DOM Elements ====== 40 42 const loginDialog = document.getElementById("loginDialog"); ··· 42 44 const passwordInput = document.getElementById("passwordInput"); 43 45 const loginBtn = document.getElementById("loginBtn"); 44 46 const logoutBtn = document.getElementById("logoutBtn"); 45 - const connectionStatus = document.getElementById("connectionStatus"); 47 + const userAvatar = document.getElementById("userAvatar"); 48 + const searchedUserAvatar = document.getElementById("searchedUserAvatar"); 46 49 47 50 const dialog = document.getElementById("paramDialog"); 48 51 const titleInput = document.getElementById("paramTitle"); ··· 53 56 const openEmptyDialogBtn = document.getElementById("openEmptyDialogBtn"); 54 57 const searchInput = document.getElementById("searchInput"); 55 58 const sortToggleBtn = document.getElementById("sortToggleBtn"); 59 + const viewToggleBtn = document.getElementById("viewToggleBtn"); 56 60 const userSearchInput = document.getElementById("userSearchInput"); 57 61 const viewingUser = document.getElementById("viewingUser"); 58 62 const guestSearchInput = document.getElementById("guestSearchInput"); ··· 115 119 await atpAgent.resumeSession(JSON.parse(session)); 116 120 userDid = atpAgent.session.did; 117 121 118 - updateConnectionStatus("connected"); 119 - showMainUI(); 122 + await updateUIForLoggedInState(); 120 123 await loadBookmarks(); 121 124 return true; 122 125 } catch (error) { 123 126 console.error("Failed to resume session:", error); 124 127 localStorage.removeItem("atproto_session"); 125 - showLoginDialog(); 128 + updateUIForLoggedOutState(); 126 129 return false; 127 130 } 128 131 } ··· 136 139 137 140 if (!handle || !password) return; 138 141 139 - updateConnectionStatus("connecting"); 140 - 141 142 try { 142 143 atpAgent = new window.AtpAgent({ 143 144 service: "https://bsky.social", ··· 151 152 userDid = atpAgent.session.did; 152 153 localStorage.setItem("atproto_session", JSON.stringify(atpAgent.session)); 153 154 154 - updateConnectionStatus("connected"); 155 155 loginDialog.close(); 156 - showMainUI(); 156 + await updateUIForLoggedInState(); 157 157 await loadBookmarks(); 158 158 } catch (error) { 159 159 console.error("Login failed:", error); 160 - updateConnectionStatus("disconnected"); 161 160 alert("Login failed. Please check your credentials."); 162 161 } 163 162 } 164 163 165 164 /** 165 + * Fetch user profile information 166 + */ 167 + async function fetchUserProfile(did) { 168 + // Try to use the logged-in agent first, fallback to public agent 169 + let agent = atpAgent; 170 + if (!agent) { 171 + agent = new window.AtpAgent({ 172 + service: "https://bsky.social", 173 + }); 174 + } 175 + 176 + try { 177 + const response = await agent.getProfile({ actor: did }); 178 + return response.data; 179 + } catch (error) { 180 + console.error("Failed to fetch user profile:", error); 181 + return null; 182 + } 183 + } 184 + 185 + /** 166 186 * Logout from AT Protocol 167 187 */ 168 188 async function logout() { ··· 178 198 userDid = null; 179 199 bookmarks = []; 180 200 localStorage.removeItem("atproto_session"); 181 - updateConnectionStatus("disconnected"); 182 - showLoginDialog(); 201 + updateUIForLoggedOutState(); 183 202 } 184 203 185 204 /** ··· 199 218 } 200 219 201 220 try { 202 - updateConnectionStatus("connecting"); 203 - 204 221 // First try to describe the repo to see if it exists 205 222 try { 206 223 await agent.com.atproto.repo.describeRepo({ ··· 210 227 console.error("Repo describe failed:", describeError); 211 228 bookmarks = []; 212 229 renderBookmarks(); 213 - updateConnectionStatus("connected"); 214 230 alert("User has no bookmarks or bookmarks are not accessible"); 215 231 return; 216 232 } ··· 227 243 })); 228 244 229 245 renderBookmarks(); 230 - updateConnectionStatus("connected"); 231 246 } catch (error) { 232 247 console.error("Failed to load bookmarks:", error); 233 248 if (error.message?.includes("Could not find repo") || error.message?.includes("not found") || error.message?.includes("RecordNotFound")) { 234 249 bookmarks = []; 235 250 renderBookmarks(); 236 - updateConnectionStatus("connected"); 237 251 alert("User has no bookmarks with this lexicon"); 238 - } else { 239 - updateConnectionStatus("disconnected"); 240 252 } 241 253 } 242 254 } ··· 266 278 } 267 279 268 280 try { 269 - updateConnectionStatus("connecting"); 270 - 271 281 const response = await atpAgent.com.atproto.repo.createRecord({ 272 282 repo: userDid, 273 283 collection: BOOKMARK_LEXICON, ··· 283 293 284 294 renderBookmarks(); 285 295 dialog.close(); 286 - updateConnectionStatus("connected"); 287 296 288 297 // Clear URL params and reload to clean state 289 298 window.history.replaceState({}, document.title, window.location.pathname); 290 299 } catch (error) { 291 300 console.error("Failed to save bookmark:", error); 292 - updateConnectionStatus("disconnected"); 293 301 alert("Failed to save bookmark. Please try again."); 294 302 } 295 303 } ··· 301 309 if (!atpAgent || !userDid) return; 302 310 303 311 try { 304 - updateConnectionStatus("connecting"); 305 - 306 312 console.log("Deleting bookmark with URI:", uri); 307 313 const rkey = uri.split("/").pop(); 308 314 console.log("Extracted rkey:", rkey); ··· 325 331 console.log(`Removed from local array: ${beforeCount} -> ${bookmarks.length}`); 326 332 327 333 renderBookmarks(); 328 - updateConnectionStatus("connected"); 329 334 } catch (error) { 330 335 console.error("Failed to delete bookmark:", error); 331 336 alert("Failed to delete bookmark: " + error.message); 332 - updateConnectionStatus("disconnected"); 333 337 } 334 338 } 335 339 336 340 // ====== UI Functions ====== 337 341 338 - function updateConnectionStatus(status) { 339 - connectionStatus.className = `connection-status ${status}`; 340 - switch (status) { 341 - case "connected": 342 - connectionStatus.textContent = "Connected"; 343 - break; 344 - case "connecting": 345 - connectionStatus.textContent = "Connecting..."; 346 - break; 347 - case "disconnected": 348 - connectionStatus.textContent = "Disconnected"; 349 - break; 342 + async function updateUIForLoggedInState() { 343 + if (!userDid || !atpAgent) return; 344 + 345 + // Fetch and display user avatar 346 + const profile = await fetchUserProfile(userDid); 347 + if (profile && profile.avatar) { 348 + userAvatar.src = profile.avatar; 349 + userAvatar.style.display = "inline-block"; 350 + } else { 351 + userAvatar.style.display = "none"; 350 352 } 353 + 354 + // Update button to show logout 355 + logoutBtn.textContent = "Logout"; 356 + logoutBtn.style.display = "inline-block"; 357 + 358 + showMainUI(); 359 + } 360 + 361 + function updateUIForLoggedOutState() { 362 + // Hide avatar 363 + userAvatar.style.display = "none"; 364 + 365 + // Update button to show login 366 + logoutBtn.textContent = "Login"; 367 + logoutBtn.style.display = "inline-block"; 368 + 369 + showLoginDialog(); 351 370 } 352 371 353 372 function showLoginDialog() { 354 373 loginDialog.showModal(); 355 374 openEmptyDialogBtn.style.display = "none"; 356 375 sortToggleBtn.style.display = "none"; 376 + viewToggleBtn.style.display = "none"; 357 377 searchInput.style.display = "none"; 358 - logoutBtn.style.display = "none"; 359 378 } 360 379 361 380 function showMainUI() { 362 381 openEmptyDialogBtn.style.display = isViewingOtherUser ? "none" : "inline-block"; 363 382 sortToggleBtn.style.display = "inline-block"; 383 + viewToggleBtn.style.display = "inline-block"; 364 384 searchInput.style.display = "inline-block"; 365 385 userSearchInput.style.display = "inline-block"; 366 - logoutBtn.style.display = "inline-block"; 367 386 } 368 387 369 388 function updateViewingUserUI() { 370 389 if (isViewingOtherUser) { 371 - viewingUser.textContent = `Viewing: ${viewingUserHandle}`; 372 - viewingUser.style.display = "inline"; 390 + // Don't show "Viewing: ..." text anymore 391 + viewingUser.style.display = "none"; 373 392 openEmptyDialogBtn.style.display = "none"; 393 + // Show searched user avatar if we have profile data 394 + if (currentSearchedUserProfile && currentSearchedUserProfile.avatar) { 395 + searchedUserAvatar.src = currentSearchedUserProfile.avatar; 396 + searchedUserAvatar.style.display = "inline-block"; 397 + } 374 398 } else { 375 399 viewingUser.style.display = "none"; 376 400 openEmptyDialogBtn.style.display = atpAgent ? "inline-block" : "none"; 401 + searchedUserAvatar.style.display = "none"; // Hide searched user avatar when back to own bookmarks 402 + currentSearchedUserProfile = null; 377 403 } 378 404 } 379 405 ··· 408 434 return fonts[hashString(title) % fonts.length]; 409 435 } 410 436 437 + /** 438 + * Format date as natural language for recent dates, otherwise as regular date 439 + */ 440 + function formatNaturalDate(dateString) { 441 + if (!dateString) return ''; 442 + 443 + const date = new Date(dateString); 444 + const now = new Date(); 445 + const diffTime = now.getTime() - date.getTime(); 446 + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); 447 + 448 + // If it's within the last month (30 days) 449 + if (diffDays < 30) { 450 + if (diffDays === 0) { 451 + return 'today'; 452 + } else if (diffDays === 1) { 453 + return 'yesterday'; 454 + } else { 455 + return `${diffDays} days ago`; 456 + } 457 + } 458 + 459 + // For older dates, show the actual date 460 + return date.toLocaleDateString('en-US', { 461 + year: 'numeric', 462 + month: 'short', 463 + day: 'numeric' 464 + }); 465 + } 466 + 411 467 // ====== Rendering Functions ====== 412 468 413 469 /** 414 - * Renders bookmark containers 470 + * Renders bookmarks in list view 471 + */ 472 + function renderListView() { 473 + const containerWrapper = document.querySelector(".containers"); 474 + containerWrapper.innerHTML = ""; 475 + 476 + const fragment = document.createDocumentFragment(); 477 + const displayBookmarks = reversedOrder ? bookmarks : [...bookmarks].reverse(); 478 + 479 + displayBookmarks.forEach(bookmark => { 480 + const title = bookmark.title || bookmark.subject; 481 + const url = bookmark.subject || bookmark.uri; 482 + const tags = bookmark.tags || []; 483 + const createdAt = bookmark.createdAt; 484 + 485 + if (!url) return; 486 + 487 + const displayTitle = title.replace(/^https?:\/\/(www\.)?/i, ""); 488 + 489 + // Create list item 490 + const listItem = document.createElement("div"); 491 + listItem.className = "bookmark-item"; 492 + 493 + // Content container 494 + const content = document.createElement("div"); 495 + content.className = "bookmark-content"; 496 + 497 + // Link group (title + URL together, but not date) 498 + const linkGroup = document.createElement("div"); 499 + linkGroup.className = "bookmark-link-group"; 500 + 501 + // Title link 502 + const titleLink = document.createElement("a"); 503 + titleLink.className = "bookmark-title"; 504 + titleLink.href = url; 505 + titleLink.target = "_blank"; 506 + titleLink.textContent = displayTitle; 507 + linkGroup.appendChild(titleLink); 508 + 509 + // URL-only container (without date) 510 + const urlContainer = document.createElement("div"); 511 + urlContainer.className = "bookmark-url-container"; 512 + 513 + const urlLink = document.createElement("a"); 514 + urlLink.className = "bookmark-url"; 515 + urlLink.href = url; 516 + urlLink.target = "_blank"; 517 + urlLink.textContent = url; 518 + urlLink.style.textDecoration = "none"; 519 + urlLink.style.color = "#666"; 520 + urlContainer.appendChild(urlLink); 521 + 522 + linkGroup.appendChild(urlContainer); 523 + content.appendChild(linkGroup); 524 + 525 + // Meta row for date and tags (outside hover group) 526 + const metaRow = document.createElement("div"); 527 + metaRow.className = "bookmark-meta-row"; 528 + 529 + // Tags on the left 530 + if (tags.length > 0) { 531 + const tagsDiv = document.createElement("div"); 532 + tagsDiv.className = "bookmark-tags"; 533 + 534 + tags.forEach(tag => { 535 + const tagSpan = document.createElement("span"); 536 + tagSpan.className = "bookmark-tag"; 537 + tagSpan.textContent = `#${tag}`; 538 + tagSpan.addEventListener("click", () => filterByTag(tag)); 539 + tagsDiv.appendChild(tagSpan); 540 + }); 541 + 542 + metaRow.appendChild(tagsDiv); 543 + } 544 + 545 + // Date on the right 546 + if (createdAt) { 547 + const dateDiv = document.createElement("div"); 548 + dateDiv.className = "bookmark-date"; 549 + dateDiv.textContent = formatNaturalDate(createdAt); 550 + metaRow.appendChild(dateDiv); 551 + } 552 + 553 + content.appendChild(metaRow); 554 + 555 + listItem.appendChild(content); 556 + 557 + // Actions (delete button) 558 + if (!isViewingOtherUser) { 559 + const actions = document.createElement("div"); 560 + actions.className = "bookmark-actions"; 561 + 562 + const deleteBtn = document.createElement("button"); 563 + deleteBtn.className = "delete-btn"; 564 + deleteBtn.textContent = "×"; 565 + deleteBtn.title = "Delete this bookmark"; 566 + deleteBtn.addEventListener("click", e => { 567 + e.stopPropagation(); 568 + e.preventDefault(); 569 + if (confirm("Delete this bookmark?")) { 570 + deleteBookmark(bookmark.atUri); 571 + } 572 + }); 573 + 574 + actions.appendChild(deleteBtn); 575 + listItem.appendChild(actions); 576 + } 577 + 578 + fragment.appendChild(listItem); 579 + }); 580 + 581 + containerWrapper.appendChild(fragment); 582 + } 583 + 584 + /** 585 + * Renders bookmarks in grid view (original) 415 586 */ 416 - function renderBookmarks() { 587 + function renderGridView() { 417 588 const containerWrapper = document.querySelector(".containers"); 418 589 containerWrapper.innerHTML = ""; 419 590 ··· 484 655 } 485 656 486 657 /** 658 + * Renders bookmark containers 659 + */ 660 + function renderBookmarks() { 661 + // Toggle body class for CSS styling 662 + document.body.classList.toggle('list-view', isListView); 663 + 664 + if (isListView) { 665 + renderListView(); 666 + } else { 667 + renderGridView(); 668 + } 669 + } 670 + 671 + /** 487 672 * Filter bookmarks by tag 488 673 */ 489 674 function filterByTag(tag) { ··· 559 744 function runSearch(term) { 560 745 const searchTerm = term.toLowerCase(); 561 746 562 - document.querySelectorAll(".container").forEach(container => { 563 - if (searchTerm.startsWith("#")) { 564 - const tagToSearch = searchTerm.slice(1); 565 - const tags = Array.from(container.querySelectorAll(".tags")) 566 - .map(el => el.textContent.toLowerCase().replace("#", "").trim()); 747 + if (isListView) { 748 + document.querySelectorAll(".bookmark-item").forEach(item => { 749 + if (searchTerm.startsWith("#")) { 750 + const tagToSearch = searchTerm.slice(1); 751 + const tags = Array.from(item.querySelectorAll(".bookmark-tag")) 752 + .map(el => el.textContent.toLowerCase().replace("#", "").trim()); 753 + 754 + item.style.display = tags.some(tag => tag.includes(tagToSearch)) ? "flex" : "none"; 755 + } else { 756 + const title = item.querySelector(".bookmark-title")?.textContent.toLowerCase() || ""; 757 + const url = item.querySelector(".bookmark-url")?.textContent.toLowerCase() || ""; 758 + const matches = title.includes(searchTerm) || url.includes(searchTerm); 759 + item.style.display = matches ? "flex" : "none"; 760 + } 761 + }); 762 + } else { 763 + document.querySelectorAll(".container").forEach(container => { 764 + if (searchTerm.startsWith("#")) { 765 + const tagToSearch = searchTerm.slice(1); 766 + const tags = Array.from(container.querySelectorAll(".tags")) 767 + .map(el => el.textContent.toLowerCase().replace("#", "").trim()); 567 768 568 - container.style.display = tags.some(tag => tag.includes(tagToSearch)) ? "block" : "none"; 569 - } else { 570 - const anchor = container.querySelector("a"); 571 - const title = anchor?.innerText.toLowerCase() || ""; 572 - container.style.display = title.includes(searchTerm) ? "block" : "none"; 573 - } 574 - }); 769 + container.style.display = tags.some(tag => tag.includes(tagToSearch)) ? "block" : "none"; 770 + } else { 771 + const anchor = container.querySelector("a"); 772 + const title = anchor?.innerText.toLowerCase() || ""; 773 + container.style.display = title.includes(searchTerm) ? "block" : "none"; 774 + } 775 + }); 776 + } 575 777 } 576 778 577 779 /** ··· 595 797 596 798 // Login/logout 597 799 loginBtn.addEventListener("click", login); 598 - logoutBtn.addEventListener("click", logout); 800 + logoutBtn.addEventListener("click", () => { 801 + if (atpAgent) { 802 + logout(); 803 + } else { 804 + showLoginDialog(); 805 + } 806 + }); 599 807 600 808 // Guest view functionality 601 809 guestViewBtn?.addEventListener("click", async () => { 602 810 const handle = guestSearchInput.value.trim(); 603 811 if (!handle) return; 604 812 605 - updateConnectionStatus("connecting"); 606 813 const result = await resolveHandle(handle); 607 814 if (result) { 608 815 isViewingOtherUser = true; ··· 614 821 updateViewingUserUI(); 615 822 } else { 616 823 alert("User not found"); 617 - updateConnectionStatus("disconnected"); 618 824 } 619 825 }); 620 826 ··· 664 870 } 665 871 }); 666 872 873 + // View toggle 874 + viewToggleBtn?.addEventListener("click", () => { 875 + isListView = !isListView; 876 + renderBookmarks(); 877 + 878 + if (isListView) { 879 + viewToggleBtn.innerHTML = '<span class="btn-text">Grid</span> ⊞'; 880 + } else { 881 + viewToggleBtn.innerHTML = '<span class="btn-text">List</span> ☰'; 882 + } 883 + 884 + // Re-apply current search 885 + const currentSearch = searchInput.value.trim(); 886 + if (currentSearch) { 887 + runSearch(currentSearch); 888 + } 889 + }); 890 + 667 891 // User search 668 892 userSearchInput?.addEventListener("keypress", async (e) => { 669 893 if (e.key === "Enter") { ··· 683 907 isViewingOtherUser = true; 684 908 viewingUserDid = result.did; 685 909 viewingUserHandle = handle; 910 + 911 + // Fetch user profile for avatar 912 + currentSearchedUserProfile = await fetchUserProfile(result.did); 913 + 686 914 await loadBookmarks(result.did, result.pdsUrl); 687 915 updateViewingUserUI(); 688 916 } else { ··· 691 919 } 692 920 }); 693 921 922 + 923 + 694 924 // ====== Initialization ====== 695 925 696 926 document.addEventListener("DOMContentLoaded", async () => { 697 - updateConnectionStatus("disconnected"); 698 - 699 927 // Wait for AtpAgent to be loaded 700 928 let attempts = 0; 701 929 while (!window.AtpAgent && attempts < 50) { ··· 705 933 706 934 if (!window.AtpAgent) { 707 935 console.error("Failed to load AtpAgent"); 708 - updateConnectionStatus("disconnected"); 709 936 return; 710 937 } 711 938
+149 -15
style.css
··· 324 324 } 325 325 } 326 326 327 - /* Connection status styles */ 328 - .connection-status { 329 - font-size: 0.8em; 330 - margin-left: 10px; 331 - padding: 4px 8px; 332 - border-radius: 4px; 327 + /* User avatar styles */ 328 + .user-avatar { 329 + width: 32px; 330 + height: 32px; 331 + border-radius: 50%; 332 + margin-left: 12px; 333 + margin-right: 8px; 334 + object-fit: cover; 335 + border: 2px solid #ddd; 336 + vertical-align: middle; 333 337 } 334 338 335 - .connection-status.connected { 336 - background-color: #2ecc71; 337 - color: white; 339 + .searched-user-avatar { 340 + width: 28px; 341 + height: 28px; 342 + border-radius: 50%; 343 + margin-right: 6px; 344 + object-fit: cover; 345 + border: 2px solid #bbb; 346 + vertical-align: middle; 338 347 } 339 348 340 - .connection-status.disconnected { 341 - background-color: #e74c3c; 342 - color: white; 349 + /* List view styles */ 350 + .list-view .containers { 351 + display: block; 352 + padding: 1rem 3vw; 343 353 } 344 354 345 - .connection-status.connecting { 346 - background-color: #f39c12; 347 - color: white; 355 + .list-view .bookmark-item { 356 + display: flex; 357 + align-items: flex-start; 358 + padding: 0.5rem; 359 + border-bottom: 1px solid #e8e8e8; 360 + background: #fefdfd; 361 + margin-bottom: 0.25rem; 362 + border-radius: 3px; 363 + position: relative; 364 + } 365 + 366 + .list-view .bookmark-item:hover { 367 + background: #f8f8f8; 368 + } 369 + 370 + .list-view .bookmark-content { 371 + flex: 1; 372 + min-width: 0; 373 + } 374 + 375 + .list-view .bookmark-link-group { 376 + cursor: pointer; 377 + } 378 + 379 + .list-view .bookmark-title { 380 + font-family: "Courier", monospace; 381 + font-size: 0.9rem; 382 + font-weight: bold; 383 + color: #333; 384 + text-decoration: none; 385 + display: block; 386 + margin-bottom: 0.1rem; 387 + word-break: break-word; 388 + line-height: 1.3; 389 + transition: color 0.2s ease; 390 + } 391 + 392 + .list-view .bookmark-link-group:hover .bookmark-title { 393 + color: #0066cc; 394 + } 395 + 396 + .list-view .bookmark-url-container { 397 + margin-bottom: 0.1rem; 398 + } 399 + 400 + .list-view .bookmark-meta-row { 401 + display: flex; 402 + justify-content: space-between; 403 + align-items: center; 404 + margin-bottom: 0.25rem; 405 + min-height: 1.2em; 406 + } 407 + 408 + .list-view .bookmark-url { 409 + font-family: "Courier", monospace; 410 + font-size: 0.75rem; 411 + color: #666; 412 + word-break: break-all; 413 + text-decoration: none; 414 + transition: color 0.2s ease, text-decoration 0.2s ease; 415 + display: block; 416 + } 417 + 418 + .list-view .bookmark-link-group:hover .bookmark-url { 419 + color: #0066cc; 420 + text-decoration: underline; 421 + } 422 + 423 + .list-view .bookmark-date { 424 + font-family: "Courier", monospace; 425 + font-size: 0.7rem; 426 + color: #888; 427 + white-space: nowrap; 428 + } 429 + 430 + .list-view .bookmark-tags { 431 + display: flex; 432 + flex-wrap: wrap; 433 + gap: 0.2rem; 434 + flex: 1; 435 + } 436 + 437 + .list-view .bookmark-tag { 438 + font-family: "Courier", monospace; 439 + font-size: 0.7rem; 440 + padding: 1px 4px; 441 + background: #f0f0f0; 442 + color: #555; 443 + border-radius: 2px; 444 + cursor: pointer; 445 + border: 1px solid transparent; 446 + } 447 + 448 + .list-view .bookmark-tag:hover { 449 + border-color: #ccc; 450 + background: #e8e8e8; 451 + } 452 + 453 + .list-view .bookmark-actions { 454 + display: flex; 455 + align-items: center; 456 + margin-left: 1rem; 457 + } 458 + 459 + .list-view .delete-btn { 460 + position: static; 461 + opacity: 1; 462 + padding: 2px 6px; 463 + font-size: 0.8rem; 464 + border: 1px solid transparent; 465 + border-radius: 3px; 466 + background: transparent; 467 + color: #e74c3c; 468 + cursor: pointer; 469 + font-family: "Courier", monospace; 470 + font-weight: bold; 471 + } 472 + 473 + .list-view .delete-btn:hover { 474 + border-color: #e74c3c; 475 + background: #fdf2f2; 476 + color: #c0392b; 477 + } 478 + 479 + /* Hide grid view when in list mode */ 480 + .list-view .container { 481 + display: none; 348 482 } 349 483 350 484