Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
111
fork

Configure Feed

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

v0.1.13 - UI redesign - Extension now shows annotations, highlights, and more, on-site. - Various bug fixes

+5398 -4408
+1 -1
.github/workflows/docker-publish.yml
··· 2 2 3 3 on: 4 4 push: 5 - branches: [ "main" ] 5 + branches: ["main"] 6 6 workflow_dispatch: 7 7 8 8 env:
+3 -3
.github/workflows/release-extension.yml
··· 3 3 on: 4 4 push: 5 5 tags: 6 - - 'v*' 6 + - "v*" 7 7 8 8 jobs: 9 9 release: ··· 19 19 run: | 20 20 VERSION=${GITHUB_REF_NAME#v} 21 21 echo "Updating manifests to version $VERSION" 22 - 22 + 23 23 cd extension 24 24 for manifest in manifest.json manifest.chrome.json manifest.firefox.json; do 25 25 if [ -f "$manifest" ]; then ··· 36 36 cp manifest.chrome.json manifest.json 37 37 zip -r ../margin-extension-chrome.zip . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json" 38 38 cd .. 39 - 39 + 40 40 - name: Build Extension (Firefox) 41 41 run: | 42 42 cd extension
+1 -1
README.md
··· 1 1 # Margin 2 2 3 - *Write in the margins of the web* 3 + _Write in the margins of the web_ 4 4 5 5 A web comments layer built on [AT Protocol](https://atproto.com) that lets you annotate any URL on the internet. 6 6
+2
backend/cmd/server/main.go
··· 101 101 r.Get("/{handle}/highlight/{rkey}", ogHandler.HandleAnnotationPage) 102 102 r.Get("/{handle}/bookmark/{rkey}", ogHandler.HandleAnnotationPage) 103 103 104 + r.Get("/api/tags/trending", handler.HandleGetTrendingTags) 105 + 104 106 r.Get("/collection/{uri}", ogHandler.HandleCollectionPage) 105 107 r.Get("/{handle}/collection/{rkey}", ogHandler.HandleCollectionPage) 106 108
+25
backend/internal/api/tags.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + "strconv" 7 + ) 8 + 9 + func (h *Handler) HandleGetTrendingTags(w http.ResponseWriter, r *http.Request) { 10 + limit := 10 11 + if l := r.URL.Query().Get("limit"); l != "" { 12 + if val, err := strconv.Atoi(l); err == nil && val > 0 && val <= 50 { 13 + limit = val 14 + } 15 + } 16 + 17 + tags, err := h.db.GetTrendingTags(limit) 18 + if err != nil { 19 + http.Error(w, `{"error": "Failed to fetch trending tags: `+err.Error()+`"}`, http.StatusInternalServerError) 20 + return 21 + } 22 + 23 + w.Header().Set("Content-Type", "application/json") 24 + json.NewEncoder(w).Encode(tags) 25 + }
+46
backend/internal/db/tags.go
··· 1 + package db 2 + 3 + type TrendingTag struct { 4 + Tag string `json:"tag"` 5 + Count int `json:"count"` 6 + } 7 + 8 + func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) { 9 + query := ` 10 + SELECT 11 + json_each.value as tag, 12 + COUNT(*) as count 13 + FROM annotations, json_each(annotations.tags_json) 14 + WHERE tags_json IS NOT NULL 15 + AND tags_json != '' 16 + AND tags_json != '[]' 17 + GROUP BY tag 18 + ORDER BY count DESC 19 + LIMIT ? 20 + ` 21 + 22 + rows, err := db.Query(db.Rebind(query), limit) 23 + if err != nil { 24 + return nil, err 25 + } 26 + defer rows.Close() 27 + 28 + var tags []TrendingTag 29 + for rows.Next() { 30 + var t TrendingTag 31 + if err := rows.Scan(&t.Tag, &t.Count); err != nil { 32 + return nil, err 33 + } 34 + tags = append(tags, t) 35 + } 36 + 37 + if err = rows.Err(); err != nil { 38 + return nil, err 39 + } 40 + 41 + if tags == nil { 42 + return []TrendingTag{}, nil 43 + } 44 + 45 + return tags, nil 46 + }
+25 -2
extension/background/service-worker.js
··· 334 334 } 335 335 336 336 case "GET_ANNOTATIONS": { 337 + const stored = await chrome.storage.local.get(["apiUrl"]); 338 + const currentApiUrl = stored.apiUrl 339 + ? stored.apiUrl.replace(/\/$/, "") 340 + : API_BASE; 341 + 337 342 const pageUrl = request.data.url; 338 343 const res = await fetch( 339 - `${API_BASE}/api/targets?source=${encodeURIComponent(pageUrl)}`, 344 + `${currentApiUrl}/api/targets?source=${encodeURIComponent(pageUrl)}`, 340 345 ); 341 346 const data = await res.json(); 342 347 ··· 422 427 return; 423 428 } 424 429 const { url, selector } = request.data; 425 - 426 430 let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(url)}`; 427 431 if (selector) { 428 432 composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`; ··· 430 434 chrome.tabs.create({ url: composeUrl }); 431 435 break; 432 436 } 437 + 438 + case "OPEN_APP_URL": { 439 + if (!WEB_BASE) { 440 + chrome.runtime.openOptionsPage(); 441 + return; 442 + } 443 + const path = request.data.path; 444 + const safePath = path.startsWith("/") ? path : `/${path}`; 445 + chrome.tabs.create({ url: `${WEB_BASE}${safePath}` }); 446 + break; 447 + } 448 + 449 + case "OPEN_SIDE_PANEL": 450 + if (sender.tab && sender.tab.windowId) { 451 + chrome.sidePanel 452 + .open({ windowId: sender.tab.windowId }) 453 + .catch((err) => console.error("Failed to open side panel", err)); 454 + } 455 + break; 433 456 434 457 case "CREATE_BOOKMARK": { 435 458 if (!API_BASE) {
+632 -249
extension/content/content.js
··· 1 1 (() => { 2 - function buildTextQuoteSelector(selection) { 3 - const exact = selection.toString().trim(); 4 - if (!exact) return null; 2 + let sidebarHost = null; 3 + let sidebarShadow = null; 4 + let popoverEl = null; 5 5 6 - const range = selection.getRangeAt(0); 7 - const contextLength = 32; 6 + let activeItems = []; 7 + 8 + let hoveredItems = []; 9 + let tooltipEl = null; 10 + let hideTimer = null; 8 11 9 - let prefix = ""; 10 - try { 11 - const preRange = document.createRange(); 12 - preRange.selectNodeContents(document.body); 13 - preRange.setEnd(range.startContainer, range.startOffset); 14 - const preText = preRange.toString(); 15 - prefix = preText.slice(-contextLength).trim(); 16 - } catch (e) { 17 - console.warn("Could not get prefix:", e); 12 + const OVERLAY_STYLES = ` 13 + :host { all: initial; } 14 + .margin-overlay { 15 + position: absolute; 16 + top: 0; 17 + left: 0; 18 + width: 100%; 19 + height: 100%; 20 + pointer-events: none; 21 + } 22 + .margin-badge { 23 + position: absolute; 24 + background: #6366f1; 25 + color: white; 26 + padding: 4px 10px; 27 + border-radius: 99px; 28 + font-size: 11px; 29 + font-weight: 600; 30 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 31 + cursor: pointer; 32 + pointer-events: auto; 33 + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); 34 + display: flex; 35 + align-items: center; 36 + gap: 6px; 37 + transform: translateY(-120%); 38 + white-space: nowrap; 39 + transition: transform 0.15s, background-color 0.15s; 40 + z-index: 2147483647; 41 + } 42 + .margin-badge:hover { 43 + transform: translateY(-125%) scale(1.05); 44 + background: #4f46e5; 45 + z-index: 2147483647; 46 + } 47 + .margin-badge-avatar { 48 + width: 16px; 49 + height: 16px; 50 + border-radius: 50%; 51 + background: rgba(255,255,255,0.2); 52 + display: flex; 53 + align-items: center; 54 + justify-content: center; 55 + font-size: 9px; 56 + object-fit: cover; 57 + } 58 + .margin-badge-stack { 59 + display: flex; 60 + align-items: center; 61 + } 62 + .margin-badge-stack .margin-badge-avatar { 63 + margin-left: -6px; 64 + border: 1px solid #6366f1; 65 + } 66 + .margin-badge-stack .margin-badge-avatar:first-child { 67 + margin-left: 0; 68 + } 69 + .margin-badge-stem { 70 + position: absolute; 71 + left: 14px; 72 + bottom: -6px; 73 + width: 2px; 74 + height: 6px; 75 + background: #6366f1; 76 + border-radius: 2px; 18 77 } 19 78 20 - let suffix = ""; 21 - try { 22 - const postRange = document.createRange(); 23 - postRange.selectNodeContents(document.body); 24 - postRange.setStart(range.endContainer, range.endOffset); 25 - const postText = postRange.toString(); 26 - suffix = postText.slice(0, contextLength).trim(); 27 - } catch (e) { 28 - console.warn("Could not get suffix:", e); 79 + .margin-popover { 80 + position: absolute; 81 + width: 320px; 82 + background: #09090b; 83 + border: 1px solid #27272a; 84 + border-radius: 12px; 85 + padding: 0; 86 + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); 87 + display: flex; 88 + flex-direction: column; 89 + pointer-events: auto; 90 + z-index: 2147483647; 91 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 92 + color: #e4e4e7; 93 + opacity: 0; 94 + transform: scale(0.95); 95 + animation: popover-in 0.15s forwards; 96 + max-height: 480px; 97 + overflow: hidden; 98 + } 99 + @keyframes popover-in { to { opacity: 1; transform: scale(1); } } 100 + .popover-header { 101 + padding: 12px 16px; 102 + border-bottom: 1px solid #27272a; 103 + display: flex; 104 + justify-content: space-between; 105 + align-items: center; 106 + background: #0f0f12; 107 + border-radius: 12px 12px 0 0; 108 + font-weight: 600; 109 + font-size: 13px; 110 + } 111 + .popover-scroll-area { 112 + overflow-y: auto; 113 + max-height: 400px; 114 + } 115 + .popover-item-block { 116 + border-bottom: 1px solid #27272a; 117 + margin-bottom: 0; 118 + animation: fade-in 0.2s; 119 + } 120 + .popover-item-block:last-child { 121 + border-bottom: none; 122 + } 123 + .popover-item-header { 124 + padding: 12px 16px 4px; 125 + display: flex; 126 + align-items: center; 127 + gap: 8px; 128 + } 129 + .popover-avatar { 130 + width: 24px; height: 24px; border-radius: 50%; background: #27272a; 131 + display: flex; align-items: center; justify-content: center; 132 + font-size: 10px; color: #a1a1aa; 133 + } 134 + .popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; } 135 + .popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; } 136 + .popover-close:hover { color: #e4e4e7; } 137 + .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; } 138 + .popover-quote { 139 + margin-top: 8px; padding: 6px 10px; background: #18181b; 140 + border-left: 2px solid #6366f1; border-radius: 4px; 141 + font-size: 11px; color: #a1a1aa; font-style: italic; 142 + } 143 + .popover-actions { 144 + padding: 8px 16px; 145 + display: flex; justify-content: flex-end; gap: 8px; 29 146 } 147 + .btn-action { 148 + background: none; border: 1px solid #27272a; border-radius: 4px; 149 + padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; 150 + } 151 + .btn-action:hover { background: #27272a; color: #e4e4e7; } 152 + `; 30 153 31 - return { 32 - type: "TextQuoteSelector", 33 - exact: exact, 34 - prefix: prefix || undefined, 35 - suffix: suffix || undefined, 36 - }; 37 - } 154 + class DOMTextMatcher { 155 + constructor() { 156 + this.textNodes = []; 157 + this.corpus = ""; 158 + this.indices = []; 159 + this.buildMap(); 160 + } 38 161 39 - function findAndScrollToText(selector) { 40 - if (!selector || !selector.exact) return false; 162 + buildMap() { 163 + const walker = document.createTreeWalker( 164 + document.body, 165 + NodeFilter.SHOW_TEXT, 166 + { 167 + acceptNode: (node) => { 168 + if (!node.parentNode) return NodeFilter.FILTER_REJECT; 169 + const tag = node.parentNode.tagName; 170 + if ( 171 + ["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag) 172 + ) 173 + return NodeFilter.FILTER_REJECT; 174 + if (node.textContent.trim().length === 0) 175 + return NodeFilter.FILTER_SKIP; 41 176 42 - const searchText = selector.exact.trim(); 43 - const normalizedSearch = searchText.replace(/\s+/g, " "); 177 + if (node.parentNode.offsetParent === null) 178 + return NodeFilter.FILTER_REJECT; 44 179 45 - const treeWalker = document.createTreeWalker( 46 - document.body, 47 - NodeFilter.SHOW_TEXT, 48 - null, 49 - false, 50 - ); 180 + return NodeFilter.FILTER_ACCEPT; 181 + }, 182 + }, 183 + ); 51 184 52 - let currentNode; 53 - while ((currentNode = treeWalker.nextNode())) { 54 - const nodeText = currentNode.textContent; 55 - const normalizedNode = nodeText.replace(/\s+/g, " "); 56 - 57 - let index = nodeText.indexOf(searchText); 58 - 59 - if (index === -1) { 60 - const normIndex = normalizedNode.indexOf(normalizedSearch); 61 - if (normIndex !== -1) { 62 - index = nodeText.indexOf(searchText.substring(0, 20)); 63 - if (index === -1) index = 0; 64 - } 185 + let currentNode; 186 + let index = 0; 187 + while ((currentNode = walker.nextNode())) { 188 + const text = currentNode.textContent; 189 + this.textNodes.push(currentNode); 190 + this.corpus += text; 191 + this.indices.push({ 192 + start: index, 193 + node: currentNode, 194 + length: text.length, 195 + }); 196 + index += text.length; 65 197 } 198 + } 66 199 67 - if (index !== -1 && nodeText.trim().length > 0) { 68 - try { 69 - const range = document.createRange(); 70 - const endIndex = Math.min(index + searchText.length, nodeText.length); 71 - range.setStart(currentNode, index); 72 - range.setEnd(currentNode, endIndex); 200 + findRange(searchText) { 201 + if (!searchText) return null; 73 202 74 - if (typeof CSS !== "undefined" && CSS.highlights) { 75 - const highlight = new Highlight(range); 76 - CSS.highlights.set("margin-scroll-highlight", highlight); 203 + let matchIndex = this.corpus.indexOf(searchText); 77 204 78 - setTimeout(() => { 79 - CSS.highlights.delete("margin-scroll-highlight"); 80 - }, 3000); 81 - } 205 + if (matchIndex === -1) { 206 + const cleaned = searchText.replace(/\s+/g, " "); 207 + return null; 208 + } 82 209 83 - const rect = range.getBoundingClientRect(); 84 - window.scrollTo({ 85 - top: window.scrollY + rect.top - window.innerHeight / 3, 86 - behavior: "smooth", 87 - }); 210 + const start = this.mapIndexToPoint(matchIndex); 211 + const end = this.mapIndexToPoint(matchIndex + searchText.length); 88 212 89 - window.scrollTo({ 90 - top: window.scrollY + rect.top - window.innerHeight / 3, 91 - behavior: "smooth", 92 - }); 93 - 94 - return true; 95 - } catch (e) { 96 - console.warn("Could not create range:", e); 97 - } 213 + if (start && end) { 214 + const range = document.createRange(); 215 + range.setStart(start.node, start.offset); 216 + range.setEnd(end.node, end.offset); 217 + return range; 98 218 } 219 + return null; 99 220 } 100 221 101 - if (window.find) { 102 - window.getSelection()?.removeAllRanges(); 103 - const found = window.find(searchText, false, false, true, false); 104 - if (found) { 105 - const selection = window.getSelection(); 106 - if (selection && selection.rangeCount > 0) { 107 - const range = selection.getRangeAt(0); 108 - const rect = range.getBoundingClientRect(); 109 - window.scrollTo({ 110 - top: window.scrollY + rect.top - window.innerHeight / 3, 111 - behavior: "smooth", 112 - }); 222 + mapIndexToPoint(corpusIndex) { 223 + for (const info of this.indices) { 224 + if ( 225 + corpusIndex >= info.start && 226 + corpusIndex < info.start + info.length 227 + ) { 228 + return { node: info.node, offset: corpusIndex - info.start }; 229 + } 230 + } 231 + if (this.indices.length > 0) { 232 + const last = this.indices[this.indices.length - 1]; 233 + if (corpusIndex === last.start + last.length) { 234 + return { node: last.node, offset: last.length }; 113 235 } 114 - return true; 115 236 } 237 + return null; 116 238 } 117 - 118 - return false; 119 239 } 120 240 121 - function renderPageHighlights(highlights) { 122 - if (!highlights || !Array.isArray(highlights) || !CSS.highlights) return; 241 + function initOverlay() { 242 + sidebarHost = document.createElement("div"); 243 + sidebarHost.id = "margin-overlay-host"; 244 + const getScrollHeight = () => { 245 + const bodyH = document.body?.scrollHeight || 0; 246 + const docH = document.documentElement?.scrollHeight || 0; 247 + return Math.max(bodyH, docH); 248 + }; 123 249 124 - const ranges = []; 250 + sidebarHost.style.cssText = ` 251 + position: absolute; top: 0; left: 0; width: 100%; 252 + height: ${getScrollHeight()}px; 253 + pointer-events: none; z-index: 2147483647; 254 + `; 255 + document.body?.appendChild(sidebarHost) || 256 + document.documentElement.appendChild(sidebarHost); 125 257 126 - highlights.forEach((item) => { 127 - const selector = item.target?.selector; 128 - if (!selector?.exact) return; 258 + sidebarShadow = sidebarHost.attachShadow({ mode: "open" }); 259 + const styleEl = document.createElement("style"); 260 + styleEl.textContent = OVERLAY_STYLES; 261 + sidebarShadow.appendChild(styleEl); 129 262 130 - const searchText = selector.exact; 131 - const treeWalker = document.createTreeWalker( 132 - document.body, 133 - NodeFilter.SHOW_TEXT, 134 - null, 135 - false, 136 - ); 263 + const container = document.createElement("div"); 264 + container.className = "margin-overlay"; 265 + container.id = "margin-overlay-container"; 266 + sidebarShadow.appendChild(container); 267 + 268 + createTooltip(container); 269 + 270 + const observer = new ResizeObserver(() => { 271 + sidebarHost.style.height = `${getScrollHeight()}px`; 272 + }); 273 + if (document.body) observer.observe(document.body); 274 + if (document.documentElement) observer.observe(document.documentElement); 275 + 276 + fetchAnnotations(); 277 + 278 + document.addEventListener("mousemove", handleMouseMove); 279 + document.addEventListener("click", handleDocumentClick); 280 + } 137 281 138 - let currentNode; 139 - while ((currentNode = treeWalker.nextNode())) { 140 - const nodeText = currentNode.textContent; 141 - const index = nodeText.indexOf(searchText); 282 + function createTooltip(container) { 283 + tooltipEl = document.createElement("div"); 284 + tooltipEl.className = "margin-badge"; 285 + tooltipEl.style.opacity = "0"; 286 + tooltipEl.style.transition = "opacity 0.1s, transform 0.1s"; 287 + tooltipEl.style.pointerEvents = "auto"; 142 288 143 - if (index !== -1) { 144 - try { 145 - const range = document.createRange(); 146 - range.setStart(currentNode, index); 147 - range.setEnd(currentNode, index + searchText.length); 148 - ranges.push(range); 149 - } catch (e) { 150 - console.warn("Could not create range for highlight:", e); 289 + tooltipEl.addEventListener("click", (e) => { 290 + e.stopPropagation(); 291 + if (hoveredItems.length > 0) { 292 + const firstItem = hoveredItems[0]; 293 + const rect = activeItems 294 + .find((x) => x.item === firstItem) 295 + ?.range.getBoundingClientRect(); 296 + if (rect) { 297 + const top = rect.top + window.scrollY; 298 + const left = rect.left + window.scrollX; 299 + showPopover(hoveredItems, top, left); 300 + } 301 + } 302 + }); 303 + container.appendChild(tooltipEl); 304 + } 305 + 306 + function handleMouseMove(e) { 307 + const x = e.clientX; 308 + const y = e.clientY; 309 + 310 + let foundItems = []; 311 + 312 + for (const { range, item } of activeItems) { 313 + const rects = range.getClientRects(); 314 + for (const rect of rects) { 315 + const padding = 5; 316 + if ( 317 + x >= rect.left - padding && 318 + x <= rect.right + padding && 319 + y >= rect.top - padding && 320 + y <= rect.bottom + padding 321 + ) { 322 + if (!foundItems.includes(item)) { 323 + foundItems.push(item); 151 324 } 152 325 break; 153 326 } 154 327 } 155 - }); 328 + } 156 329 157 - if (ranges.length > 0) { 158 - const highlight = new Highlight(...ranges); 159 - CSS.highlights.set("margin-page-highlights", highlight); 330 + let isOverTooltip = false; 331 + if (tooltipEl && tooltipEl.style.opacity === "1") { 332 + const rect = tooltipEl.getBoundingClientRect(); 333 + if ( 334 + x >= rect.left && 335 + x <= rect.right && 336 + y >= rect.top && 337 + y <= rect.bottom 338 + ) { 339 + isOverTooltip = true; 340 + } 160 341 } 161 - } 162 342 163 - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 164 - if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { 165 - const selection = window.getSelection(); 166 - if (!selection || selection.toString().trim().length === 0) { 167 - sendResponse({ selector: null }); 168 - return true; 343 + if (foundItems.length > 0 || isOverTooltip) { 344 + if (hideTimer) { 345 + clearTimeout(hideTimer); 346 + hideTimer = null; 169 347 } 348 + if (foundItems.length > 0) { 349 + const currentIds = hoveredItems 350 + .map((i) => i.id || i.cid) 351 + .sort() 352 + .join(","); 353 + const newIds = foundItems 354 + .map((i) => i.id || i.cid) 355 + .sort() 356 + .join(","); 170 357 171 - const selector = buildTextQuoteSelector(selection); 172 - sendResponse({ selector: selector }); 173 - return true; 358 + if (currentIds !== newIds) { 359 + hoveredItems = foundItems; 360 + updateTooltip(); 361 + } 362 + } 363 + } else { 364 + if (!hideTimer && hoveredItems.length > 0) { 365 + hideTimer = setTimeout(() => { 366 + hoveredItems = []; 367 + updateTooltip(); 368 + hideTimer = null; 369 + }, 300); 370 + } 174 371 } 372 + } 175 373 176 - if (request.type === "GET_SELECTOR_FOR_ANNOTATE") { 177 - const selection = window.getSelection(); 178 - if (!selection || selection.toString().trim().length === 0) { 179 - return; 180 - } 374 + function updateTooltip() { 375 + if (!tooltipEl) return; 181 376 182 - const selector = buildTextQuoteSelector(selection); 183 - if (selector) { 184 - chrome.runtime.sendMessage({ 185 - type: "OPEN_COMPOSE", 186 - data: { 187 - url: window.location.href, 188 - selector: selector, 189 - }, 190 - }); 191 - } 377 + if (hoveredItems.length === 0) { 378 + tooltipEl.style.opacity = "0"; 379 + tooltipEl.style.transform = "translateY(-105%) scale(0.9)"; 380 + tooltipEl.style.pointerEvents = "none"; 381 + return; 192 382 } 193 383 194 - if (request.type === "GET_SELECTOR_FOR_HIGHLIGHT") { 195 - const selection = window.getSelection(); 196 - if (!selection || selection.toString().trim().length === 0) { 197 - sendResponse({ success: false, error: "No text selected" }); 198 - return true; 384 + tooltipEl.style.pointerEvents = "auto"; 385 + 386 + const authorsMap = new Map(); 387 + hoveredItems.forEach((item) => { 388 + const author = item.author || item.creator || {}; 389 + const id = author.did || author.handle; 390 + if (id && !authorsMap.has(id)) { 391 + authorsMap.set(id, author); 199 392 } 393 + }); 200 394 201 - const selector = buildTextQuoteSelector(selection); 202 - if (selector) { 203 - chrome.runtime 204 - .sendMessage({ 205 - type: "CREATE_HIGHLIGHT", 206 - data: { 207 - url: window.location.href, 208 - title: document.title, 209 - selector: selector, 210 - }, 211 - }) 212 - .then((response) => { 213 - if (response?.success) { 214 - showNotification("Text highlighted!", "success"); 395 + const uniqueAuthors = Array.from(authorsMap.values()); 396 + let contentHtml = ""; 215 397 216 - if (CSS.highlights) { 217 - try { 218 - const range = selection.getRangeAt(0); 219 - const highlight = new Highlight(range); 220 - CSS.highlights.set("margin-highlight-preview", highlight); 221 - } catch (e) { 222 - console.warn("Could not visually highlight:", e); 223 - } 224 - } 398 + if (uniqueAuthors.length === 1) { 399 + const author = uniqueAuthors[0] || {}; 400 + const handle = author.handle || "User"; 401 + const avatar = author.avatar; 402 + const count = hoveredItems.length; 225 403 226 - window.getSelection().removeAllRanges(); 227 - } else { 228 - showNotification( 229 - "Failed to highlight: " + (response?.error || "Unknown error"), 230 - "error", 231 - ); 232 - } 233 - sendResponse(response); 234 - }) 235 - .catch((err) => { 236 - console.error("Highlight error:", err); 237 - showNotification("Error creating highlight", "error"); 238 - sendResponse({ success: false, error: err.message }); 239 - }); 240 - return true; 404 + let avatarHtml = `<div class="margin-badge-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 405 + if (avatar) { 406 + avatarHtml = `<img src="${avatar}" class="margin-badge-avatar">`; 241 407 } 242 - sendResponse({ success: false, error: "Could not build selector" }); 243 - return true; 408 + 409 + contentHtml = `${avatarHtml}<span>${handle}${count > 1 ? ` (${count})` : ""}</span>`; 410 + } else { 411 + let stackHtml = `<div class="margin-badge-stack">`; 412 + const displayAuthors = uniqueAuthors.slice(0, 3); 413 + displayAuthors.forEach((author) => { 414 + const handle = author.handle || "U"; 415 + const avatar = author.avatar; 416 + if (avatar) { 417 + stackHtml += `<img src="${avatar}" class="margin-badge-avatar">`; 418 + } else { 419 + stackHtml += `<div class="margin-badge-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 420 + } 421 + }); 422 + stackHtml += `</div>`; 423 + 424 + contentHtml = `${stackHtml}<span>${uniqueAuthors.length} people</span>`; 244 425 } 245 426 246 - if (request.type === "SCROLL_TO_TEXT") { 247 - const found = findAndScrollToText(request.selector); 248 - if (!found) { 249 - showNotification("Could not find text on page", "error"); 427 + tooltipEl.innerHTML = ` 428 + ${contentHtml} 429 + <div class="margin-badge-stem"></div> 430 + `; 431 + 432 + const firstItem = hoveredItems[0]; 433 + const match = activeItems.find((x) => x.item === firstItem); 434 + if (match) { 435 + const rects = match.range.getClientRects(); 436 + if (rects && rects.length > 0) { 437 + const rect = rects[0]; 438 + const top = rect.top + window.scrollY; 439 + const left = rect.left + window.scrollX; 440 + 441 + tooltipEl.style.top = `${top - 36}px`; 442 + tooltipEl.style.left = `${left}px`; 443 + tooltipEl.style.opacity = "1"; 444 + tooltipEl.style.transform = "translateY(0) scale(1)"; 250 445 } 251 446 } 447 + } 252 448 253 - if (request.type === "RENDER_HIGHLIGHTS") { 254 - renderPageHighlights(request.highlights); 449 + function handleDocumentClick(e) { 450 + if (hoveredItems.length > 0) { 451 + e.preventDefault(); 452 + e.stopPropagation(); 453 + 454 + const item = hoveredItems[0]; 455 + const match = activeItems.find((x) => x.item === item); 456 + if (match) { 457 + const rects = match.range.getClientRects(); 458 + if (rects.length > 0) { 459 + const rect = rects[0]; 460 + const top = rect.top + window.scrollY; 461 + const left = rect.left + window.scrollX; 462 + showPopover(hoveredItems, top, left); 463 + } 464 + } 255 465 } 466 + } 256 467 257 - return true; 258 - }); 468 + function refreshPositions() {} 259 469 260 - function showNotification(message, type = "info") { 261 - const existing = document.querySelector(".margin-notification"); 262 - if (existing) existing.remove(); 470 + function renderBadges(annotations) { 471 + if (!sidebarShadow) return; 263 472 264 - const notification = document.createElement("div"); 265 - notification.className = "margin-notification"; 266 - notification.textContent = message; 473 + const itemsToRender = annotations || []; 474 + activeItems = []; 475 + const rangesByColor = {}; 476 + 477 + const matcher = new DOMTextMatcher(); 267 478 268 - const bgColor = 269 - type === "success" ? "#10b981" : type === "error" ? "#ef4444" : "#6366f1"; 270 - notification.style.cssText = ` 271 - position: fixed; 272 - bottom: 24px; 273 - right: 24px; 274 - padding: 12px 20px; 275 - background: ${bgColor}; 276 - color: white; 277 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 278 - font-size: 14px; 279 - font-weight: 500; 280 - border-radius: 8px; 281 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); 282 - z-index: 999999; 283 - animation: margin-slide-in 0.2s ease; 284 - `; 479 + itemsToRender.forEach((item) => { 480 + const selector = item.target?.selector || item.selector; 481 + if (!selector?.exact) return; 482 + 483 + const range = matcher.findRange(selector.exact); 484 + if (range) { 485 + activeItems.push({ range, item }); 285 486 286 - document.body.appendChild(notification); 487 + const color = item.color || "#c084fc"; 488 + if (!rangesByColor[color]) rangesByColor[color] = []; 489 + rangesByColor[color].push(range); 490 + } 491 + }); 287 492 288 - setTimeout(() => { 289 - notification.style.animation = "margin-slide-out 0.2s ease forwards"; 290 - setTimeout(() => notification.remove(), 200); 291 - }, 3000); 493 + if (CSS.highlights) { 494 + CSS.highlights.clear(); 495 + for (const [color, ranges] of Object.entries(rangesByColor)) { 496 + const highlight = new Highlight(...ranges); 497 + const safeColor = color.replace(/[^a-zA-Z0-9]/g, ""); 498 + const name = `margin-hl-${safeColor}`; 499 + CSS.highlights.set(name, highlight); 500 + injectHighlightStyle(name, color); 501 + } 502 + } 292 503 } 293 504 294 - const style = document.createElement("style"); 295 - style.textContent = ` 296 - @keyframes margin-slide-in { 297 - from { opacity: 0; transform: translateY(10px); } 298 - to { opacity: 1; transform: translateY(0); } 505 + const injectedStyles = new Set(); 506 + function injectHighlightStyle(name, color) { 507 + if (injectedStyles.has(name)) return; 508 + const style = document.createElement("style"); 509 + style.textContent = ` 510 + ::highlight(${name}) { 511 + background-color: ${color}66; 512 + color: inherit; 513 + cursor: pointer; 299 514 } 300 - @keyframes margin-slide-out { 301 - from { opacity: 1; transform: translateY(0); } 302 - to { opacity: 0; transform: translateY(10px); } 515 + `; 516 + document.head.appendChild(style); 517 + injectedStyles.add(name); 518 + } 519 + 520 + function showPopover(items, top, left) { 521 + if (popoverEl) popoverEl.remove(); 522 + const container = sidebarShadow.getElementById("margin-overlay-container"); 523 + popoverEl = document.createElement("div"); 524 + popoverEl.className = "margin-popover"; 525 + 526 + const popWidth = 320; 527 + const screenWidth = window.innerWidth; 528 + let finalLeft = left; 529 + if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; 530 + 531 + popoverEl.style.top = `${top + 20}px`; 532 + popoverEl.style.left = `${finalLeft}px`; 533 + 534 + const title = 535 + items.length > 1 ? `${items.length} Annotations` : "Annotation"; 536 + 537 + let contentHtml = items 538 + .map((item) => { 539 + const author = item.author || item.creator || {}; 540 + const handle = author.handle || "User"; 541 + const avatar = author.avatar; 542 + const text = item.body?.value || item.text || ""; 543 + const quote = 544 + item.target?.selector?.exact || item.selector?.exact || ""; 545 + const id = item.id || item.uri; 546 + 547 + let avatarHtml = `<div class="popover-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 548 + if (avatar) { 549 + avatarHtml = `<img src="${avatar}" class="popover-avatar" style="object-fit: cover;">`; 303 550 } 304 - ::highlight(margin-highlight-preview) { 305 - background-color: rgba(168, 85, 247, 0.3); 306 - color: inherit; 551 + 552 + const isHighlight = item.type === "Highlight"; 553 + 554 + let bodyHtml = ""; 555 + if (isHighlight) { 556 + bodyHtml = `<div class="popover-text" style="font-style: italic; color: #a1a1aa;">"${quote}"</div>`; 557 + } else { 558 + bodyHtml = `<div class="popover-text">${text}</div>`; 559 + if (quote) { 560 + bodyHtml += `<div class="popover-quote">"${quote}"</div>`; 561 + } 307 562 } 308 - ::highlight(margin-scroll-highlight) { 309 - background-color: rgba(99, 102, 241, 0.4); 310 - color: inherit; 563 + 564 + return ` 565 + <div class="popover-item-block"> 566 + <div class="popover-item-header"> 567 + <div class="popover-author"> 568 + ${avatarHtml} 569 + <span class="popover-handle">@${handle}</span> 570 + </div> 571 + </div> 572 + <div class="popover-content"> 573 + ${bodyHtml} 574 + </div> 575 + <div class="popover-actions"> 576 + ${!isHighlight ? `<button class="btn-action btn-reply" data-id="${id}">Reply</button>` : ""} 577 + <button class="btn-action btn-share" data-id="${id}" data-text="${text}" data-quote="${quote}">Share</button> 578 + </div> 579 + </div> 580 + `; 581 + }) 582 + .join(""); 583 + 584 + popoverEl.innerHTML = ` 585 + <div class="popover-header"> 586 + <span>${title}</span> 587 + <button class="popover-close">✕</button> 588 + </div> 589 + <div class="popover-scroll-area"> 590 + ${contentHtml} 591 + </div> 592 + `; 593 + 594 + popoverEl.querySelector(".popover-close").addEventListener("click", () => { 595 + popoverEl.remove(); 596 + popoverEl = null; 597 + }); 598 + 599 + const replyBtns = popoverEl.querySelectorAll(".btn-reply"); 600 + replyBtns.forEach((btn) => { 601 + btn.addEventListener("click", () => { 602 + const id = btn.getAttribute("data-id"); 603 + if (id) { 604 + chrome.runtime.sendMessage({ 605 + type: "OPEN_APP_URL", 606 + data: { path: `/annotation/${encodeURIComponent(id)}` }, 607 + }); 311 608 } 312 - ::highlight(margin-page-highlights) { 313 - background-color: rgba(252, 211, 77, 0.3); 314 - color: inherit; 609 + }); 610 + }); 611 + 612 + const shareBtns = popoverEl.querySelectorAll(".btn-share"); 613 + shareBtns.forEach((btn) => { 614 + btn.addEventListener("click", async () => { 615 + const id = btn.getAttribute("data-id"); 616 + const text = btn.getAttribute("data-text"); 617 + const quote = btn.getAttribute("data-quote"); 618 + const u = `https://margin.at/annotation/${encodeURIComponent(id)}`; 619 + const shareText = `${text ? text + "\n" : ""}${quote ? `"${quote}"\n` : ""}${u}`; 620 + 621 + try { 622 + await navigator.clipboard.writeText(shareText); 623 + const originalText = btn.innerText; 624 + btn.innerText = "Copied!"; 625 + setTimeout(() => (btn.innerText = originalText), 2000); 626 + } catch (e) { 627 + console.error("Failed to copy", e); 315 628 } 316 - `; 317 - document.head.appendChild(style); 629 + }); 630 + }); 631 + 632 + container.appendChild(popoverEl); 633 + 634 + setTimeout(() => { 635 + document.addEventListener("click", closePopoverOutside); 636 + }, 0); 637 + } 638 + 639 + function closePopoverOutside(e) { 640 + if (popoverEl) { 641 + popoverEl.remove(); 642 + popoverEl = null; 643 + document.removeEventListener("click", closePopoverOutside); 644 + } 645 + } 646 + 647 + function fetchAnnotations() { 648 + if (typeof chrome !== "undefined" && chrome.runtime) { 649 + chrome.runtime.sendMessage( 650 + { 651 + type: "GET_ANNOTATIONS", 652 + data: { url: window.location.href }, 653 + }, 654 + (res) => { 655 + if (res && res.success) { 656 + renderBadges(res.data); 657 + } 658 + }, 659 + ); 660 + } 661 + } 662 + 663 + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 664 + if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { 665 + const sel = window.getSelection(); 666 + if (!sel || !sel.toString()) { 667 + sendResponse({ selector: null }); 668 + return true; 669 + } 670 + const exact = sel.toString().trim(); 671 + sendResponse({ selector: { type: "TextQuoteSelector", exact } }); 672 + return true; 673 + } 674 + 675 + if (request.type === "SCROLL_TO_TEXT") { 676 + const selector = request.selector; 677 + if (selector?.exact) { 678 + const matcher = new DOMTextMatcher(); 679 + const range = matcher.findRange(selector.exact); 680 + if (range) { 681 + const rect = range.getBoundingClientRect(); 682 + window.scrollTo({ 683 + top: window.scrollY + rect.top - window.innerHeight / 3, 684 + behavior: "smooth", 685 + }); 686 + const highlight = new Highlight(range); 687 + CSS.highlights.set("margin-scroll-flash", highlight); 688 + injectHighlightStyle("margin-scroll-flash", "#8b5cf6"); 689 + setTimeout(() => CSS.highlights.delete("margin-scroll-flash"), 2000); 690 + } 691 + } 692 + } 693 + return true; 694 + }); 695 + 696 + if (document.readyState === "loading") { 697 + document.addEventListener("DOMContentLoaded", initOverlay); 698 + } else { 699 + initOverlay(); 700 + } 318 701 })();
+19 -1
extension/icons/site.webmanifest
··· 1 - {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} 1 + { 2 + "name": "Margin", 3 + "short_name": "Margin", 4 + "icons": [ 5 + { 6 + "src": "/android-chrome-192x192.png", 7 + "sizes": "192x192", 8 + "type": "image/png" 9 + }, 10 + { 11 + "src": "/android-chrome-512x512.png", 12 + "sizes": "512x512", 13 + "type": "image/png" 14 + } 15 + ], 16 + "theme_color": "#ffffff", 17 + "background_color": "#ffffff", 18 + "display": "standalone" 19 + }
+24 -20
extension/popup/popup.css
··· 1 1 :root { 2 - --bg-primary: #0c0a14; 3 - --bg-secondary: #14111f; 4 - --bg-tertiary: #1a1528; 5 - --bg-card: #14111f; 6 - --bg-hover: #1e1932; 7 - 8 - --text-primary: #f4f0ff; 9 - --text-secondary: #a89ec8; 10 - --text-tertiary: #6b5f8a; 11 - 12 - --accent: #a855f7; 13 - --accent-hover: #c084fc; 14 - --accent-subtle: rgba(168, 85, 247, 0.15); 2 + --bg-primary: #09090b; 3 + --bg-secondary: #0f0f12; 4 + --bg-tertiary: #18181b; 5 + --bg-card: #09090b; 6 + --bg-elevated: #18181b; 7 + --bg-hover: #27272a; 15 8 16 - --border: #2d2640; 17 - --border-hover: #3d3560; 9 + --text-primary: #e4e4e7; 10 + --text-secondary: #a1a1aa; 11 + --text-tertiary: #71717a; 12 + --border: #27272a; 13 + --border-hover: #3f3f46; 18 14 19 - --success: #22c55e; 20 - --danger: #ef4444; 15 + --accent: #6366f1; 16 + --accent-hover: #4f46e5; 17 + --accent-subtle: rgba(99, 102, 241, 0.1); 18 + --accent-text: #818cf8; 19 + --success: #10b981; 20 + --error: #ef4444; 21 21 --warning: #f59e0b; 22 22 23 - --radius-sm: 6px; 24 - --radius-md: 10px; 25 - --radius-lg: 16px; 23 + --radius-sm: 4px; 24 + --radius-md: 6px; 25 + --radius-lg: 8px; 26 + --radius-full: 9999px; 27 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 28 + --shadow-md: 29 + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 26 30 } 27 31 28 32 * {
+4 -1
extension/popup/popup.js
··· 358 358 const res = await sendMessage({ type: "CHECK_SESSION" }); 359 359 360 360 if (res.success && res.data?.authenticated) { 361 - if (els.userHandle) els.userHandle.textContent = "@" + res.data.handle; 361 + if (els.userHandle) { 362 + const handle = res.data.handle || res.data.email || "User"; 363 + els.userHandle.textContent = "@" + handle; 364 + } 362 365 els.userInfo.style.display = "flex"; 363 366 currentUserDid = res.data.did; 364 367 showView("main");
+170 -20
extension/sidepanel/sidepanel.css
··· 1 1 :root { 2 - --bg-primary: #0c0a14; 3 - --bg-secondary: #110e1c; 4 - --bg-tertiary: #1a1528; 5 - --bg-card: #14111f; 6 - --bg-hover: #1e1932; 7 - --bg-elevated: #1a1528; 2 + --bg-primary: #09090b; 3 + --bg-secondary: #0f0f12; 4 + --bg-tertiary: #18181b; 5 + --bg-card: #09090b; 6 + --bg-hover: #18181b; 7 + --bg-elevated: #18181b; 8 8 9 - --text-primary: #f4f0ff; 10 - --text-secondary: #a89ec8; 11 - --text-tertiary: #6b5f8a; 9 + --text-primary: #e4e4e7; 10 + --text-secondary: #a1a1aa; 11 + --text-tertiary: #71717a; 12 12 13 - --accent: #a855f7; 14 - --accent-hover: #c084fc; 15 - --accent-subtle: rgba(168, 85, 247, 0.15); 13 + --accent: #6366f1; 14 + --accent-hover: #4f46e5; 15 + --accent-subtle: rgba(99, 102, 241, 0.1); 16 + --accent-text: #818cf8; 16 17 17 - --border: #2d2640; 18 - --border-hover: #3d3560; 18 + --border: #27272a; 19 + --border-hover: #3f3f46; 19 20 20 - --success: #22c55e; 21 + --success: #10b981; 21 22 --error: #ef4444; 22 23 --warning: #f59e0b; 23 24 24 - --radius-sm: 6px; 25 - --radius-md: 10px; 26 - --radius-lg: 16px; 25 + --radius-sm: 4px; 26 + --radius-md: 6px; 27 + --radius-lg: 8px; 27 28 --radius-full: 9999px; 28 29 29 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 30 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 30 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 31 + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 32 + } 33 + 34 + * { 35 + margin: 0; 36 + padding: 0; 37 + box-sizing: border-box; 38 + } 39 + 40 + body { 41 + font-family: 42 + "Inter", 43 + -apple-system, 44 + BlinkMacSystemFont, 45 + "Segoe UI", 46 + sans-serif; 47 + background: var(--bg-primary); 48 + color: var(--text-primary); 49 + min-height: 100vh; 50 + -webkit-font-smoothing: antialiased; 51 + } 52 + 53 + .sidebar { 54 + display: flex; 55 + flex-direction: column; 56 + height: 100vh; 57 + background: var(--bg-primary); 58 + } 59 + 60 + .sidebar-header { 61 + display: flex; 62 + align-items: center; 63 + justify-content: space-between; 64 + padding: 14px 16px; 65 + border-bottom: 1px solid var(--border); 66 + background: var(--bg-primary); 67 + } 68 + 69 + .user-handle { 70 + font-size: 12px; 71 + color: var(--text-secondary); 72 + background: var(--bg-tertiary); 73 + padding: 4px 8px; 74 + border-radius: var(--radius-sm); 75 + } 76 + 77 + .current-page-info { 78 + display: flex; 79 + align-items: center; 80 + gap: 8px; 81 + padding: 10px 16px; 82 + background: var(--bg-primary); 83 + border-bottom: 1px solid var(--border); 84 + } 85 + 86 + .tabs { 87 + display: flex; 88 + border-bottom: 1px solid var(--border); 89 + background: var(--bg-primary); 90 + padding: 4px; 91 + gap: 4px; 92 + margin: 0; 93 + } 94 + 95 + .tab-btn { 96 + flex: 1; 97 + padding: 10px 8px; 98 + background: transparent; 99 + border: none; 100 + font-size: 12px; 101 + font-weight: 500; 102 + color: var(--text-secondary); 103 + cursor: pointer; 104 + border-radius: var(--radius-sm); 105 + transition: all 0.15s; 106 + } 107 + 108 + .tab-btn:hover { 109 + color: var(--text-primary); 110 + background: var(--bg-hover); 111 + } 112 + 113 + .tab-btn.active { 114 + color: var(--text-primary); 115 + background: var(--bg-tertiary); 116 + box-shadow: none; 117 + } 118 + 119 + .quick-actions { 120 + display: flex; 121 + gap: 8px; 122 + padding: 12px 16px; 123 + border-bottom: 1px solid var(--border); 124 + background: var(--bg-primary); 125 + } 126 + 127 + .create-form { 128 + padding: 16px; 129 + border-bottom: 1px solid var(--border); 130 + background: var(--bg-primary); 131 + } 132 + 133 + .section-header { 134 + display: flex; 135 + justify-content: space-between; 136 + align-items: center; 137 + padding: 14px 16px; 138 + background: var(--bg-primary); 139 + border-bottom: 1px solid var(--border); 140 + } 141 + 142 + .annotation-item { 143 + border: 1px solid var(--border); 144 + border-radius: var(--radius-md); 145 + padding: 12px; 146 + background: var(--bg-primary); 147 + transition: border-color 0.15s; 148 + } 149 + 150 + .annotation-item:hover { 151 + border-color: var(--border-hover); 152 + background: var(--bg-hover); 153 + } 154 + 155 + .sidebar-footer { 156 + display: flex; 157 + align-items: center; 158 + justify-content: space-between; 159 + padding: 12px 16px; 160 + border-top: 1px solid var(--border); 161 + background: var(--bg-primary); 162 + } 163 + 164 + ::-webkit-scrollbar { 165 + width: 10px; 166 + height: 10px; 167 + } 168 + 169 + ::-webkit-scrollbar-track { 170 + background: transparent; 171 + } 172 + 173 + ::-webkit-scrollbar-thumb { 174 + background: var(--border); 175 + border-radius: 5px; 176 + border: 2px solid var(--bg-primary); 177 + } 178 + 179 + ::-webkit-scrollbar-thumb:hover { 180 + background: var(--border-hover); 31 181 } 32 182 33 183 * {
+9 -28
lexicons/at/margin/annotation.json
··· 10 10 "key": "tid", 11 11 "record": { 12 12 "type": "object", 13 - "required": [ 14 - "target", 15 - "createdAt" 16 - ], 13 + "required": ["target", "createdAt"], 17 14 "properties": { 18 15 "motivation": { 19 16 "type": "string", ··· 87 84 "target": { 88 85 "type": "object", 89 86 "description": "W3C SpecificResource - the target with optional selector", 90 - "required": [ 91 - "source" 92 - ], 87 + "required": ["source"], 93 88 "properties": { 94 89 "source": { 95 90 "type": "string", ··· 127 122 "textQuoteSelector": { 128 123 "type": "object", 129 124 "description": "W3C TextQuoteSelector - select text by quoting it with context", 130 - "required": [ 131 - "exact" 132 - ], 125 + "required": ["exact"], 133 126 "properties": { 134 127 "type": { 135 128 "type": "string", ··· 158 151 "textPositionSelector": { 159 152 "type": "object", 160 153 "description": "W3C TextPositionSelector - select by character offsets", 161 - "required": [ 162 - "start", 163 - "end" 164 - ], 154 + "required": ["start", "end"], 165 155 "properties": { 166 156 "type": { 167 157 "type": "string", ··· 182 172 "cssSelector": { 183 173 "type": "object", 184 174 "description": "W3C CssSelector - select DOM elements by CSS selector", 185 - "required": [ 186 - "value" 187 - ], 175 + "required": ["value"], 188 176 "properties": { 189 177 "type": { 190 178 "type": "string", ··· 200 188 "xpathSelector": { 201 189 "type": "object", 202 190 "description": "W3C XPathSelector - select by XPath expression", 203 - "required": [ 204 - "value" 205 - ], 191 + "required": ["value"], 206 192 "properties": { 207 193 "type": { 208 194 "type": "string", ··· 218 204 "fragmentSelector": { 219 205 "type": "object", 220 206 "description": "W3C FragmentSelector - select by URI fragment", 221 - "required": [ 222 - "value" 223 - ], 207 + "required": ["value"], 224 208 "properties": { 225 209 "type": { 226 210 "type": "string", ··· 241 225 "rangeSelector": { 242 226 "type": "object", 243 227 "description": "W3C RangeSelector - select range between two selectors", 244 - "required": [ 245 - "startSelector", 246 - "endSelector" 247 - ], 228 + "required": ["startSelector", "endSelector"], 248 229 "properties": { 249 230 "type": { 250 231 "type": "string", ··· 289 270 } 290 271 } 291 272 } 292 - } 273 + }
+49 -52
lexicons/at/margin/bookmark.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.bookmark", 4 - "description": "A bookmark record - save URL for later", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A bookmarked URL (motivation: bookmarking)", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "source", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "source": { 18 - "type": "string", 19 - "format": "uri", 20 - "description": "The bookmarked URL" 21 - }, 22 - "sourceHash": { 23 - "type": "string", 24 - "description": "SHA256 hash of normalized URL for indexing" 25 - }, 26 - "title": { 27 - "type": "string", 28 - "maxLength": 500, 29 - "description": "Page title" 30 - }, 31 - "description": { 32 - "type": "string", 33 - "maxLength": 1000, 34 - "maxGraphemes": 300, 35 - "description": "Optional description/note" 36 - }, 37 - "tags": { 38 - "type": "array", 39 - "description": "Tags for categorization", 40 - "items": { 41 - "type": "string", 42 - "maxLength": 64, 43 - "maxGraphemes": 32 44 - }, 45 - "maxLength": 10 46 - }, 47 - "createdAt": { 48 - "type": "string", 49 - "format": "datetime" 50 - } 51 - } 52 - } 2 + "lexicon": 1, 3 + "id": "at.margin.bookmark", 4 + "description": "A bookmark record - save URL for later", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A bookmarked URL (motivation: bookmarking)", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["source", "createdAt"], 13 + "properties": { 14 + "source": { 15 + "type": "string", 16 + "format": "uri", 17 + "description": "The bookmarked URL" 18 + }, 19 + "sourceHash": { 20 + "type": "string", 21 + "description": "SHA256 hash of normalized URL for indexing" 22 + }, 23 + "title": { 24 + "type": "string", 25 + "maxLength": 500, 26 + "description": "Page title" 27 + }, 28 + "description": { 29 + "type": "string", 30 + "maxLength": 1000, 31 + "maxGraphemes": 300, 32 + "description": "Optional description/note" 33 + }, 34 + "tags": { 35 + "type": "array", 36 + "description": "Tags for categorization", 37 + "items": { 38 + "type": "string", 39 + "maxLength": 64, 40 + "maxGraphemes": 32 41 + }, 42 + "maxLength": 10 43 + }, 44 + "createdAt": { 45 + "type": "string", 46 + "format": "datetime" 47 + } 53 48 } 49 + } 54 50 } 55 - } 51 + } 52 + }
+37 -40
lexicons/at/margin/collection.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.collection", 4 - "description": "A collection of annotations (like a folder or notebook)", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A named collection for organizing annotations", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "name", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "name": { 18 - "type": "string", 19 - "maxLength": 100, 20 - "maxGraphemes": 50, 21 - "description": "Collection name" 22 - }, 23 - "description": { 24 - "type": "string", 25 - "maxLength": 500, 26 - "maxGraphemes": 150, 27 - "description": "Collection description" 28 - }, 29 - "icon": { 30 - "type": "string", 31 - "maxLength": 10, 32 - "maxGraphemes": 2, 33 - "description": "Emoji icon for the collection" 34 - }, 35 - "createdAt": { 36 - "type": "string", 37 - "format": "datetime" 38 - } 39 - } 40 - } 2 + "lexicon": 1, 3 + "id": "at.margin.collection", 4 + "description": "A collection of annotations (like a folder or notebook)", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A named collection for organizing annotations", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["name", "createdAt"], 13 + "properties": { 14 + "name": { 15 + "type": "string", 16 + "maxLength": 100, 17 + "maxGraphemes": 50, 18 + "description": "Collection name" 19 + }, 20 + "description": { 21 + "type": "string", 22 + "maxLength": 500, 23 + "maxGraphemes": 150, 24 + "description": "Collection description" 25 + }, 26 + "icon": { 27 + "type": "string", 28 + "maxLength": 10, 29 + "maxGraphemes": 2, 30 + "description": "Emoji icon for the collection" 31 + }, 32 + "createdAt": { 33 + "type": "string", 34 + "format": "datetime" 35 + } 41 36 } 37 + } 42 38 } 43 - } 39 + } 40 + }
+34 -38
lexicons/at/margin/collectionItem.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.collectionItem", 4 - "description": "An item in a collection (links annotation to collection)", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "Associates an annotation with a collection", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "collection", 14 - "annotation", 15 - "createdAt" 16 - ], 17 - "properties": { 18 - "collection": { 19 - "type": "string", 20 - "format": "at-uri", 21 - "description": "AT URI of the collection" 22 - }, 23 - "annotation": { 24 - "type": "string", 25 - "format": "at-uri", 26 - "description": "AT URI of the annotation, highlight, or bookmark" 27 - }, 28 - "position": { 29 - "type": "integer", 30 - "minimum": 0, 31 - "description": "Sort order within the collection" 32 - }, 33 - "createdAt": { 34 - "type": "string", 35 - "format": "datetime" 36 - } 37 - } 38 - } 2 + "lexicon": 1, 3 + "id": "at.margin.collectionItem", 4 + "description": "An item in a collection (links annotation to collection)", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "Associates an annotation with a collection", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["collection", "annotation", "createdAt"], 13 + "properties": { 14 + "collection": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the collection" 18 + }, 19 + "annotation": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "AT URI of the annotation, highlight, or bookmark" 23 + }, 24 + "position": { 25 + "type": "integer", 26 + "minimum": 0, 27 + "description": "Sort order within the collection" 28 + }, 29 + "createdAt": { 30 + "type": "string", 31 + "format": "datetime" 32 + } 39 33 } 34 + } 40 35 } 41 - } 36 + } 37 + }
+39 -42
lexicons/at/margin/highlight.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.highlight", 4 - "description": "A lightweight highlight record - annotation without body text", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "description": "A highlight on a web page (motivation: highlighting)", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "target", 14 - "createdAt" 15 - ], 16 - "properties": { 17 - "target": { 18 - "type": "ref", 19 - "ref": "at.margin.annotation#target", 20 - "description": "The resource and segment being highlighted" 21 - }, 22 - "color": { 23 - "type": "string", 24 - "description": "Highlight color (hex or named)", 25 - "maxLength": 20 26 - }, 27 - "tags": { 28 - "type": "array", 29 - "description": "Tags for categorization", 30 - "items": { 31 - "type": "string", 32 - "maxLength": 64, 33 - "maxGraphemes": 32 34 - }, 35 - "maxLength": 10 36 - }, 37 - "createdAt": { 38 - "type": "string", 39 - "format": "datetime" 40 - } 41 - } 42 - } 2 + "lexicon": 1, 3 + "id": "at.margin.highlight", 4 + "description": "A lightweight highlight record - annotation without body text", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "A highlight on a web page (motivation: highlighting)", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": ["target", "createdAt"], 13 + "properties": { 14 + "target": { 15 + "type": "ref", 16 + "ref": "at.margin.annotation#target", 17 + "description": "The resource and segment being highlighted" 18 + }, 19 + "color": { 20 + "type": "string", 21 + "description": "Highlight color (hex or named)", 22 + "maxLength": 20 23 + }, 24 + "tags": { 25 + "type": "array", 26 + "description": "Tags for categorization", 27 + "items": { 28 + "type": "string", 29 + "maxLength": 64, 30 + "maxGraphemes": 32 31 + }, 32 + "maxLength": 10 33 + }, 34 + "createdAt": { 35 + "type": "string", 36 + "format": "datetime" 37 + } 43 38 } 39 + } 44 40 } 45 - } 41 + } 42 + }
+36 -42
lexicons/at/margin/like.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.like", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "description": "A like on an annotation or reply", 8 - "key": "tid", 9 - "record": { 10 - "type": "object", 11 - "required": [ 12 - "subject", 13 - "createdAt" 14 - ], 15 - "properties": { 16 - "subject": { 17 - "type": "ref", 18 - "ref": "#subjectRef", 19 - "description": "Reference to the annotation or reply being liked" 20 - }, 21 - "createdAt": { 22 - "type": "string", 23 - "format": "datetime" 24 - } 25 - } 26 - } 2 + "lexicon": 1, 3 + "id": "at.margin.like", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A like on an annotation or reply", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["subject", "createdAt"], 12 + "properties": { 13 + "subject": { 14 + "type": "ref", 15 + "ref": "#subjectRef", 16 + "description": "Reference to the annotation or reply being liked" 17 + }, 18 + "createdAt": { 19 + "type": "string", 20 + "format": "datetime" 21 + } 22 + } 23 + } 24 + }, 25 + "subjectRef": { 26 + "type": "object", 27 + "required": ["uri", "cid"], 28 + "properties": { 29 + "uri": { 30 + "type": "string", 31 + "format": "at-uri" 27 32 }, 28 - "subjectRef": { 29 - "type": "object", 30 - "required": [ 31 - "uri", 32 - "cid" 33 - ], 34 - "properties": { 35 - "uri": { 36 - "type": "string", 37 - "format": "at-uri" 38 - }, 39 - "cid": { 40 - "type": "string", 41 - "format": "cid" 42 - } 43 - } 33 + "cid": { 34 + "type": "string", 35 + "format": "cid" 44 36 } 37 + } 45 38 } 46 - } 39 + } 40 + }
+55 -63
lexicons/at/margin/reply.json
··· 1 1 { 2 - "lexicon": 1, 3 - "id": "at.margin.reply", 4 - "revision": 2, 5 - "description": "A reply to an annotation or another reply", 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "description": "A reply to an annotation (motivation: replying)", 10 - "key": "tid", 11 - "record": { 12 - "type": "object", 13 - "required": [ 14 - "parent", 15 - "root", 16 - "text", 17 - "createdAt" 18 - ], 19 - "properties": { 20 - "parent": { 21 - "type": "ref", 22 - "ref": "#replyRef", 23 - "description": "Reference to the parent annotation or reply" 24 - }, 25 - "root": { 26 - "type": "ref", 27 - "ref": "#replyRef", 28 - "description": "Reference to the root annotation of the thread" 29 - }, 30 - "text": { 31 - "type": "string", 32 - "maxLength": 10000, 33 - "maxGraphemes": 3000, 34 - "description": "Reply text content" 35 - }, 36 - "format": { 37 - "type": "string", 38 - "description": "MIME type of the text content", 39 - "default": "text/plain" 40 - }, 41 - "createdAt": { 42 - "type": "string", 43 - "format": "datetime" 44 - } 45 - } 46 - } 2 + "lexicon": 1, 3 + "id": "at.margin.reply", 4 + "revision": 2, 5 + "description": "A reply to an annotation or another reply", 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "description": "A reply to an annotation (motivation: replying)", 10 + "key": "tid", 11 + "record": { 12 + "type": "object", 13 + "required": ["parent", "root", "text", "createdAt"], 14 + "properties": { 15 + "parent": { 16 + "type": "ref", 17 + "ref": "#replyRef", 18 + "description": "Reference to the parent annotation or reply" 19 + }, 20 + "root": { 21 + "type": "ref", 22 + "ref": "#replyRef", 23 + "description": "Reference to the root annotation of the thread" 24 + }, 25 + "text": { 26 + "type": "string", 27 + "maxLength": 10000, 28 + "maxGraphemes": 3000, 29 + "description": "Reply text content" 30 + }, 31 + "format": { 32 + "type": "string", 33 + "description": "MIME type of the text content", 34 + "default": "text/plain" 35 + }, 36 + "createdAt": { 37 + "type": "string", 38 + "format": "datetime" 39 + } 40 + } 41 + } 42 + }, 43 + "replyRef": { 44 + "type": "object", 45 + "description": "Strong reference to an annotation or reply", 46 + "required": ["uri", "cid"], 47 + "properties": { 48 + "uri": { 49 + "type": "string", 50 + "format": "at-uri" 47 51 }, 48 - "replyRef": { 49 - "type": "object", 50 - "description": "Strong reference to an annotation or reply", 51 - "required": [ 52 - "uri", 53 - "cid" 54 - ], 55 - "properties": { 56 - "uri": { 57 - "type": "string", 58 - "format": "at-uri" 59 - }, 60 - "cid": { 61 - "type": "string", 62 - "format": "cid" 63 - } 64 - } 52 + "cid": { 53 + "type": "string", 54 + "format": "cid" 65 55 } 56 + } 66 57 } 67 - } 58 + } 59 + }
+46 -41
web/src/App.jsx
··· 1 1 import { Routes, Route } from "react-router-dom"; 2 2 import { AuthProvider } from "./context/AuthContext"; 3 - import Navbar from "./components/Navbar"; 3 + import Sidebar from "./components/Sidebar"; 4 + import RightSidebar from "./components/RightSidebar"; 5 + import MobileNav from "./components/MobileNav"; 4 6 import Feed from "./pages/Feed"; 5 7 import Url from "./pages/Url"; 6 8 import Profile from "./pages/Profile"; ··· 14 16 import CollectionDetail from "./pages/CollectionDetail"; 15 17 import Privacy from "./pages/Privacy"; 16 18 19 + import Terms from "./pages/Terms"; 20 + 17 21 function AppContent() { 18 22 return ( 19 - <div className="app"> 20 - <Navbar /> 21 - <main className="main-content"> 22 - <Routes> 23 - <Route path="/" element={<Feed />} /> 24 - <Route path="/url" element={<Url />} /> 25 - <Route path="/new" element={<New />} /> 26 - <Route path="/bookmarks" element={<Bookmarks />} /> 27 - <Route path="/highlights" element={<Highlights />} /> 28 - <Route path="/notifications" element={<Notifications />} /> 29 - <Route path="/profile/:handle" element={<Profile />} /> 30 - <Route path="/login" element={<Login />} /> 31 - {} 32 - <Route path="/at/:did/:rkey" element={<AnnotationDetail />} /> 33 - {} 34 - <Route path="/annotation/:uri" element={<AnnotationDetail />} /> 35 - <Route path="/collections" element={<Collections />} /> 36 - <Route path="/collections/:rkey" element={<CollectionDetail />} /> 37 - <Route 38 - path="/:handle/collection/:rkey" 39 - element={<CollectionDetail />} 40 - /> 41 - 42 - <Route 43 - path="/:handle/annotation/:rkey" 44 - element={<AnnotationDetail />} 45 - /> 46 - <Route 47 - path="/:handle/highlight/:rkey" 48 - element={<AnnotationDetail />} 49 - /> 50 - <Route 51 - path="/:handle/bookmark/:rkey" 52 - element={<AnnotationDetail />} 53 - /> 54 - 55 - <Route path="/collection/*" element={<CollectionDetail />} /> 56 - <Route path="/privacy" element={<Privacy />} /> 57 - </Routes> 58 - </main> 23 + <div className="layout"> 24 + <Sidebar /> 25 + <div className="main-layout"> 26 + <main className="main-content-wrapper"> 27 + <Routes> 28 + <Route path="/" element={<Feed />} /> 29 + <Route path="/url" element={<Url />} /> 30 + <Route path="/new" element={<New />} /> 31 + <Route path="/bookmarks" element={<Bookmarks />} /> 32 + <Route path="/highlights" element={<Highlights />} /> 33 + <Route path="/notifications" element={<Notifications />} /> 34 + <Route path="/profile/:handle" element={<Profile />} /> 35 + <Route path="/login" element={<Login />} /> 36 + <Route path="/at/:did/:rkey" element={<AnnotationDetail />} /> 37 + <Route path="/annotation/:uri" element={<AnnotationDetail />} /> 38 + <Route path="/collections" element={<Collections />} /> 39 + <Route path="/collections/:rkey" element={<CollectionDetail />} /> 40 + <Route 41 + path="/:handle/collection/:rkey" 42 + element={<CollectionDetail />} 43 + /> 44 + <Route 45 + path="/:handle/annotation/:rkey" 46 + element={<AnnotationDetail />} 47 + /> 48 + <Route 49 + path="/:handle/highlight/:rkey" 50 + element={<AnnotationDetail />} 51 + /> 52 + <Route 53 + path="/:handle/bookmark/:rkey" 54 + element={<AnnotationDetail />} 55 + /> 56 + <Route path="/collection/*" element={<CollectionDetail />} /> 57 + <Route path="/privacy" element={<Privacy />} /> 58 + <Route path="/terms" element={<Terms />} /> 59 + </Routes> 60 + </main> 61 + </div> 62 + <RightSidebar /> 63 + <MobileNav /> 59 64 </div> 60 65 ); 61 66 }
+3
web/src/api/client.js
··· 427 427 body: JSON.stringify({ handle, invite_code: inviteCode }), 428 428 }); 429 429 } 430 + export async function getTrendingTags(limit = 10) { 431 + return request(`${API_BASE}/tags/trending?limit=${limit}`); 432 + }
+154
web/src/assets/tangled.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + version="1.1" 6 + id="svg1" 7 + width="24.122343" 8 + height="23.274094" 9 + viewBox="0 0 24.122343 23.274094" 10 + sodipodi:docname="tangled_dolly_face_only.svg" 11 + inkscape:export-filename="tangled_logotype_black_on_trans.svg" 12 + inkscape:export-xdpi="96" 13 + inkscape:export-ydpi="96" 14 + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 15 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 17 + xmlns="http://www.w3.org/2000/svg" 18 + xmlns:svg="http://www.w3.org/2000/svg"> 19 + <defs 20 + id="defs1"> 21 + <filter 22 + style="color-interpolation-filters:sRGB" 23 + inkscape:menu-tooltip="Fades hue progressively to white" 24 + inkscape:menu="Color" 25 + inkscape:label="Hue to White" 26 + id="filter24" 27 + x="0" 28 + y="0" 29 + width="1" 30 + height="1"> 31 + <feColorMatrix 32 + values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 " 33 + type="matrix" 34 + result="r" 35 + in="SourceGraphic" 36 + id="feColorMatrix17" /> 37 + <feColorMatrix 38 + values="0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 " 39 + type="matrix" 40 + result="g" 41 + in="SourceGraphic" 42 + id="feColorMatrix18" /> 43 + <feColorMatrix 44 + values="0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 " 45 + type="matrix" 46 + result="b" 47 + in="SourceGraphic" 48 + id="feColorMatrix19" /> 49 + <feBlend 50 + result="minrg" 51 + in="r" 52 + mode="darken" 53 + in2="g" 54 + id="feBlend19" /> 55 + <feBlend 56 + result="p" 57 + in="minrg" 58 + mode="darken" 59 + in2="b" 60 + id="feBlend20" /> 61 + <feBlend 62 + result="maxrg" 63 + in="r" 64 + mode="lighten" 65 + in2="g" 66 + id="feBlend21" /> 67 + <feBlend 68 + result="q" 69 + in="maxrg" 70 + mode="lighten" 71 + in2="b" 72 + id="feBlend22" /> 73 + <feComponentTransfer 74 + result="q2" 75 + in="q" 76 + id="feComponentTransfer22"> 77 + <feFuncR 78 + slope="0" 79 + type="linear" 80 + id="feFuncR22" /> 81 + </feComponentTransfer> 82 + <feBlend 83 + result="pq" 84 + in="p" 85 + mode="lighten" 86 + in2="q2" 87 + id="feBlend23" /> 88 + <feColorMatrix 89 + values="-1 1 0 0 0 -1 1 0 0 0 -1 1 0 0 0 0 0 0 0 1 " 90 + type="matrix" 91 + result="qminp" 92 + in="pq" 93 + id="feColorMatrix23" /> 94 + <feComposite 95 + k3="1" 96 + operator="arithmetic" 97 + result="qminpc" 98 + in="qminp" 99 + in2="qminp" 100 + id="feComposite23" 101 + k1="0" 102 + k2="0" 103 + k4="0" /> 104 + <feBlend 105 + result="result2" 106 + in2="SourceGraphic" 107 + mode="screen" 108 + id="feBlend24" /> 109 + <feComposite 110 + operator="in" 111 + in="result2" 112 + in2="SourceGraphic" 113 + result="result1" 114 + id="feComposite24" /> 115 + </filter> 116 + </defs> 117 + <sodipodi:namedview 118 + id="namedview1" 119 + pagecolor="#ffffff" 120 + bordercolor="#000000" 121 + borderopacity="0.25" 122 + inkscape:showpageshadow="2" 123 + inkscape:pageopacity="0.0" 124 + inkscape:pagecheckerboard="true" 125 + inkscape:deskcolor="#d5d5d5" 126 + inkscape:zoom="7.0916564" 127 + inkscape:cx="38.84847" 128 + inkscape:cy="31.515909" 129 + inkscape:window-width="1920" 130 + inkscape:window-height="1080" 131 + inkscape:window-x="0" 132 + inkscape:window-y="0" 133 + inkscape:window-maximized="0" 134 + inkscape:current-layer="g1"> 135 + <inkscape:page 136 + x="0" 137 + y="0" 138 + width="24.122343" 139 + height="23.274094" 140 + id="page2" 141 + margin="0" 142 + bleed="0" /> 143 + </sodipodi:namedview> 144 + <g 145 + inkscape:groupmode="layer" 146 + inkscape:label="Image" 147 + id="g1" 148 + transform="translate(-0.4388285,-0.8629527)"> 149 + <path 150 + style="fill:#ffffff;fill-opacity:1;stroke-width:0.111183;filter:url(#filter24)" 151 + d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" 152 + id="path4" /> 153 + </g> 154 + </svg>
+28
web/src/components/AnnotationSkeleton.jsx
··· 1 + import React from "react"; 2 + 3 + export default function AnnotationSkeleton() { 4 + return ( 5 + <div className="skeleton-card"> 6 + <div className="skeleton-header"> 7 + <div className="skeleton skeleton-avatar" /> 8 + <div className="skeleton-meta"> 9 + <div className="skeleton skeleton-name" /> 10 + <div className="skeleton skeleton-handle" /> 11 + </div> 12 + </div> 13 + 14 + <div className="skeleton-content"> 15 + <div className="skeleton skeleton-source" /> 16 + <div className="skeleton skeleton-highlight" /> 17 + <div className="skeleton skeleton-text-1" /> 18 + <div className="skeleton skeleton-text-2" /> 19 + </div> 20 + 21 + <div className="skeleton-actions"> 22 + <div className="skeleton skeleton-action" /> 23 + <div className="skeleton skeleton-action" /> 24 + <div className="skeleton skeleton-action" /> 25 + </div> 26 + </div> 27 + ); 28 + }
+61
web/src/components/MobileNav.jsx
··· 1 + import { Link, useLocation } from "react-router-dom"; 2 + import { useAuth } from "../context/AuthContext"; 3 + import { Home, Search, Folder, User, PenSquare } from "lucide-react"; 4 + 5 + export default function MobileNav() { 6 + const { user, isAuthenticated } = useAuth(); 7 + const location = useLocation(); 8 + 9 + const isActive = (path) => { 10 + if (path === "/") return location.pathname === "/"; 11 + return location.pathname.startsWith(path); 12 + }; 13 + 14 + return ( 15 + <nav className="mobile-nav"> 16 + <div className="mobile-nav-inner"> 17 + <Link 18 + to="/" 19 + className={`mobile-nav-item ${isActive("/") ? "active" : ""}`} 20 + > 21 + <Home /> 22 + <span>Home</span> 23 + </Link> 24 + 25 + <Link 26 + to="/url" 27 + className={`mobile-nav-item ${isActive("/url") ? "active" : ""}`} 28 + > 29 + <Search /> 30 + <span>Browse</span> 31 + </Link> 32 + 33 + {isAuthenticated ? ( 34 + <Link to="/new" className="mobile-nav-item mobile-nav-new"> 35 + <PenSquare /> 36 + </Link> 37 + ) : ( 38 + <Link to="/login" className="mobile-nav-item mobile-nav-new"> 39 + <User /> 40 + </Link> 41 + )} 42 + 43 + <Link 44 + to="/collections" 45 + className={`mobile-nav-item ${isActive("/collections") ? "active" : ""}`} 46 + > 47 + <Folder /> 48 + <span>Library</span> 49 + </Link> 50 + 51 + <Link 52 + to={isAuthenticated && user?.did ? `/profile/${user.did}` : "/login"} 53 + className={`mobile-nav-item ${isActive("/profile") ? "active" : ""}`} 54 + > 55 + <User /> 56 + <span>Profile</span> 57 + </Link> 58 + </div> 59 + </nav> 60 + ); 61 + }
-245
web/src/components/Navbar.jsx
··· 1 - import { useState, useRef, useEffect } from "react"; 2 - import { Link, useLocation } from "react-router-dom"; 3 - import { Folder } from "lucide-react"; 4 - import { useAuth } from "../context/AuthContext"; 5 - import { 6 - PenIcon, 7 - BookmarkIcon, 8 - HighlightIcon, 9 - SearchIcon, 10 - LogoutIcon, 11 - BellIcon, 12 - } from "./Icons"; 13 - import { getUnreadNotificationCount } from "../api/client"; 14 - import { SiFirefox, SiGooglechrome } from "react-icons/si"; 15 - import { FaEdge } from "react-icons/fa"; 16 - 17 - import logo from "../assets/logo.svg"; 18 - 19 - const isFirefox = 20 - typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 21 - const isEdge = 22 - typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 23 - const isChrome = 24 - typeof navigator !== "undefined" && 25 - /Chrome/i.test(navigator.userAgent) && 26 - !isEdge; 27 - 28 - export default function Navbar() { 29 - const { user, isAuthenticated, logout, loading } = useAuth(); 30 - const location = useLocation(); 31 - const [menuOpen, setMenuOpen] = useState(false); 32 - const [unreadCount, setUnreadCount] = useState(0); 33 - const menuRef = useRef(null); 34 - 35 - const isActive = (path) => location.pathname === path; 36 - 37 - useEffect(() => { 38 - if (isAuthenticated) { 39 - getUnreadNotificationCount() 40 - .then((data) => setUnreadCount(data.count || 0)) 41 - .catch(() => {}); 42 - const interval = setInterval(() => { 43 - getUnreadNotificationCount() 44 - .then((data) => setUnreadCount(data.count || 0)) 45 - .catch(() => {}); 46 - }, 60000); 47 - return () => clearInterval(interval); 48 - } 49 - }, [isAuthenticated]); 50 - 51 - useEffect(() => { 52 - const handleClickOutside = (e) => { 53 - if (menuRef.current && !menuRef.current.contains(e.target)) { 54 - setMenuOpen(false); 55 - } 56 - }; 57 - document.addEventListener("mousedown", handleClickOutside); 58 - return () => document.removeEventListener("mousedown", handleClickOutside); 59 - }, []); 60 - 61 - const getInitials = () => { 62 - if (user?.displayName) { 63 - return user.displayName.substring(0, 2).toUpperCase(); 64 - } 65 - if (user?.handle) { 66 - return user.handle.substring(0, 2).toUpperCase(); 67 - } 68 - return "U"; 69 - }; 70 - 71 - return ( 72 - <nav className="navbar"> 73 - <div className="navbar-inner"> 74 - {} 75 - <Link to="/" className="navbar-brand"> 76 - <img src={logo} alt="Margin Logo" className="navbar-logo-img" /> 77 - <span className="navbar-title">Margin</span> 78 - </Link> 79 - 80 - {} 81 - <div className="navbar-center"> 82 - <Link 83 - to="/" 84 - className={`navbar-link ${isActive("/") ? "active" : ""}`} 85 - > 86 - Feed 87 - </Link> 88 - <Link 89 - to="/url" 90 - className={`navbar-link ${isActive("/url") ? "active" : ""}`} 91 - > 92 - <SearchIcon size={16} /> 93 - Browse 94 - </Link> 95 - {isFirefox ? ( 96 - <a 97 - href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 98 - target="_blank" 99 - rel="noopener noreferrer" 100 - className="navbar-link navbar-extension-link" 101 - > 102 - <SiFirefox size={16} /> 103 - Get Extension 104 - </a> 105 - ) : isEdge ? ( 106 - <a 107 - href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 108 - target="_blank" 109 - rel="noopener noreferrer" 110 - className="navbar-link navbar-extension-link" 111 - > 112 - <FaEdge size={16} /> 113 - Get Extension 114 - </a> 115 - ) : isChrome ? ( 116 - <a 117 - href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/" 118 - target="_blank" 119 - rel="noopener noreferrer" 120 - className="navbar-link navbar-extension-link" 121 - > 122 - <SiGooglechrome size={16} /> 123 - Get Extension 124 - </a> 125 - ) : ( 126 - <a 127 - href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 128 - target="_blank" 129 - rel="noopener noreferrer" 130 - className="navbar-link navbar-extension-link" 131 - > 132 - <SiFirefox size={16} /> 133 - Get Extension 134 - </a> 135 - )} 136 - </div> 137 - 138 - {} 139 - <div className="navbar-right"> 140 - {!loading && 141 - (isAuthenticated ? ( 142 - <> 143 - <Link 144 - to="/highlights" 145 - className={`navbar-icon-link ${isActive("/highlights") ? "active" : ""}`} 146 - title="Highlights" 147 - > 148 - <HighlightIcon size={20} /> 149 - </Link> 150 - <Link 151 - to="/bookmarks" 152 - className={`navbar-icon-link ${isActive("/bookmarks") ? "active" : ""}`} 153 - title="Bookmarks" 154 - > 155 - <BookmarkIcon size={20} /> 156 - </Link> 157 - <Link 158 - to="/collections" 159 - className={`navbar-icon-link ${isActive("/collections") ? "active" : ""}`} 160 - title="Collections" 161 - > 162 - <Folder size={20} /> 163 - </Link> 164 - <Link 165 - to="/notifications" 166 - className={`navbar-icon-link notification-link ${isActive("/notifications") ? "active" : ""}`} 167 - title="Notifications" 168 - onClick={() => setUnreadCount(0)} 169 - > 170 - <BellIcon size={20} /> 171 - {unreadCount > 0 && ( 172 - <span className="notification-badge">{unreadCount}</span> 173 - )} 174 - </Link> 175 - <Link 176 - to="/new" 177 - className="navbar-new-btn" 178 - title="New Annotation" 179 - > 180 - <PenIcon size={16} /> 181 - <span>New</span> 182 - </Link> 183 - 184 - {} 185 - <div className="navbar-user-menu" ref={menuRef}> 186 - <button 187 - className="navbar-avatar-btn" 188 - onClick={() => setMenuOpen(!menuOpen)} 189 - title={user?.handle} 190 - > 191 - {user?.avatar ? ( 192 - <img 193 - src={user.avatar} 194 - alt={user.displayName} 195 - className="navbar-avatar-img" 196 - /> 197 - ) : ( 198 - <span className="navbar-avatar-text"> 199 - {getInitials()} 200 - </span> 201 - )} 202 - </button> 203 - 204 - {menuOpen && ( 205 - <div className="navbar-dropdown"> 206 - <div className="navbar-dropdown-header"> 207 - <span className="navbar-dropdown-name"> 208 - {user?.displayName} 209 - </span> 210 - <span className="navbar-dropdown-handle"> 211 - @{user?.handle} 212 - </span> 213 - </div> 214 - <div className="navbar-dropdown-divider" /> 215 - <Link 216 - to={`/profile/${user?.did}`} 217 - className="navbar-dropdown-item" 218 - onClick={() => setMenuOpen(false)} 219 - > 220 - View Profile 221 - </Link> 222 - <button 223 - onClick={() => { 224 - logout(); 225 - setMenuOpen(false); 226 - }} 227 - className="navbar-dropdown-item navbar-dropdown-logout" 228 - > 229 - <LogoutIcon size={16} /> 230 - Sign Out 231 - </button> 232 - </div> 233 - )} 234 - </div> 235 - </> 236 - ) : ( 237 - <Link to="/login" className="navbar-signin"> 238 - Sign In 239 - </Link> 240 - ))} 241 - </div> 242 - </div> 243 - </nav> 244 - ); 245 - }
+161
web/src/components/RightSidebar.jsx
··· 1 + import { useState, useEffect } from "react"; 2 + import { Link } from "react-router-dom"; 3 + import { Download, ExternalLink } from "lucide-react"; 4 + import { SiFirefox, SiGooglechrome, SiGithub, SiBluesky } from "react-icons/si"; 5 + import { FaEdge } from "react-icons/fa"; 6 + import { useAuth } from "../context/AuthContext"; 7 + import { getTrendingTags } from "../api/client"; 8 + import tangledIcon from "../assets/tangled.svg"; 9 + 10 + const isFirefox = 11 + typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 12 + const isEdge = 13 + typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 14 + const isChrome = 15 + typeof navigator !== "undefined" && 16 + /Chrome/i.test(navigator.userAgent) && 17 + !isEdge; 18 + 19 + function getExtensionInfo() { 20 + if (isFirefox) { 21 + return { 22 + url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", 23 + icon: SiFirefox, 24 + name: "Firefox", 25 + }; 26 + } 27 + if (isEdge) { 28 + return { 29 + url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", 30 + icon: FaEdge, 31 + name: "Edge", 32 + }; 33 + } 34 + return { 35 + url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", 36 + icon: SiGooglechrome, 37 + name: "Chrome", 38 + }; 39 + } 40 + 41 + export default function RightSidebar() { 42 + const { isAuthenticated } = useAuth(); 43 + const ext = getExtensionInfo(); 44 + const ExtIcon = ext.icon; 45 + const [trendingTags, setTrendingTags] = useState([]); 46 + const [loading, setLoading] = useState(true); 47 + 48 + useEffect(() => { 49 + getTrendingTags() 50 + .then((tags) => setTrendingTags(tags)) 51 + .catch((err) => console.error("Failed to fetch trending tags:", err)) 52 + .finally(() => setLoading(false)); 53 + }, []); 54 + 55 + return ( 56 + <aside className="right-sidebar"> 57 + <div className="right-section"> 58 + <h3 className="right-section-title">Get the Extension</h3> 59 + <p className="right-section-desc"> 60 + Annotate, highlight, and bookmark any webpage 61 + </p> 62 + <a 63 + href={ext.url} 64 + target="_blank" 65 + rel="noopener noreferrer" 66 + className="right-extension-btn" 67 + > 68 + <ExtIcon size={18} /> 69 + Install for {ext.name} 70 + <ExternalLink size={14} /> 71 + </a> 72 + </div> 73 + 74 + {isAuthenticated ? ( 75 + <div className="right-section"> 76 + <h3 className="right-section-title">Trending Tags</h3> 77 + <div className="right-links"> 78 + {loading ? ( 79 + <span className="right-section-desc">Loading...</span> 80 + ) : trendingTags.length > 0 ? ( 81 + trendingTags.map(({ tag, count }) => ( 82 + <Link 83 + key={tag} 84 + to={`/?tag=${encodeURIComponent(tag)}`} 85 + className="right-link" 86 + > 87 + <span>#{tag}</span> 88 + <span style={{ fontSize: "0.75rem", opacity: 0.6 }}> 89 + {count} 90 + </span> 91 + </Link> 92 + )) 93 + ) : ( 94 + <span className="right-section-desc">No trending tags yet</span> 95 + )} 96 + </div> 97 + </div> 98 + ) : ( 99 + <div className="right-section"> 100 + <h3 className="right-section-title">Explore</h3> 101 + <nav className="right-links"> 102 + <Link to="/url" className="right-link"> 103 + Browse by URL 104 + </Link> 105 + <Link to="/highlights" className="right-link"> 106 + Public Highlights 107 + </Link> 108 + </nav> 109 + </div> 110 + )} 111 + 112 + <div className="right-section"> 113 + <h3 className="right-section-title">Resources</h3> 114 + <nav className="right-links"> 115 + <a 116 + href="https://github.com/margin-at/margin" 117 + target="_blank" 118 + rel="noopener noreferrer" 119 + className="right-link" 120 + > 121 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 122 + <SiGithub size={16} /> 123 + GitHub 124 + </div> 125 + <ExternalLink size={12} /> 126 + </a> 127 + <a 128 + href="https://tangled.net" 129 + target="_blank" 130 + rel="noopener noreferrer" 131 + className="right-link" 132 + > 133 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 134 + <div className="tangled-icon" /> 135 + Tangled 136 + </div> 137 + <ExternalLink size={12} /> 138 + </a> 139 + <a 140 + href="https://bsky.app/profile/margin.at" 141 + target="_blank" 142 + rel="noopener noreferrer" 143 + className="right-link" 144 + > 145 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 146 + <SiBluesky size={16} /> 147 + Bluesky 148 + </div> 149 + <ExternalLink size={12} /> 150 + </a> 151 + </nav> 152 + </div> 153 + 154 + <div className="right-footer"> 155 + <Link to="/privacy">Privacy</Link> 156 + <span>·</span> 157 + <Link to="/terms">Terms</Link> 158 + </div> 159 + </aside> 160 + ); 161 + }
+189
web/src/components/Sidebar.jsx
··· 1 + import { useState, useRef, useEffect } from "react"; 2 + import { Link, useLocation } from "react-router-dom"; 3 + import { useAuth } from "../context/AuthContext"; 4 + import { 5 + Home, 6 + Search, 7 + Folder, 8 + Bell, 9 + PenSquare, 10 + User, 11 + LogOut, 12 + MoreHorizontal, 13 + Highlighter, 14 + Bookmark, 15 + } from "lucide-react"; 16 + import { getUnreadNotificationCount } from "../api/client"; 17 + import logo from "../assets/logo.svg"; 18 + 19 + export default function Sidebar() { 20 + const { user, isAuthenticated, logout, loading } = useAuth(); 21 + const location = useLocation(); 22 + const [menuOpen, setMenuOpen] = useState(false); 23 + const [unreadCount, setUnreadCount] = useState(0); 24 + const menuRef = useRef(null); 25 + 26 + const isActive = (path) => { 27 + if (path === "/") return location.pathname === "/"; 28 + return location.pathname.startsWith(path); 29 + }; 30 + 31 + useEffect(() => { 32 + if (isAuthenticated) { 33 + getUnreadNotificationCount() 34 + .then((data) => setUnreadCount(data.count || 0)) 35 + .catch(() => {}); 36 + const interval = setInterval(() => { 37 + getUnreadNotificationCount() 38 + .then((data) => setUnreadCount(data.count || 0)) 39 + .catch(() => {}); 40 + }, 60000); 41 + return () => clearInterval(interval); 42 + } 43 + }, [isAuthenticated]); 44 + 45 + useEffect(() => { 46 + const handleClickOutside = (e) => { 47 + if (menuRef.current && !menuRef.current.contains(e.target)) { 48 + setMenuOpen(false); 49 + } 50 + }; 51 + document.addEventListener("mousedown", handleClickOutside); 52 + return () => document.removeEventListener("mousedown", handleClickOutside); 53 + }, []); 54 + 55 + const getInitials = () => { 56 + if (user?.displayName) { 57 + return user.displayName.substring(0, 2).toUpperCase(); 58 + } 59 + if (user?.handle) { 60 + return user.handle.substring(0, 2).toUpperCase(); 61 + } 62 + return "U"; 63 + }; 64 + 65 + return ( 66 + <aside className="sidebar"> 67 + <Link to="/" className="sidebar-header"> 68 + <img src={logo} alt="Margin" className="sidebar-logo" /> 69 + <span className="sidebar-brand">Margin</span> 70 + </Link> 71 + 72 + <nav className="sidebar-nav"> 73 + <Link 74 + to="/" 75 + className={`sidebar-link ${isActive("/") ? "active" : ""}`} 76 + > 77 + <Home size={20} /> 78 + <span>Home</span> 79 + </Link> 80 + <Link 81 + to="/url" 82 + className={`sidebar-link ${isActive("/url") ? "active" : ""}`} 83 + > 84 + <Search size={20} /> 85 + <span>Browse</span> 86 + </Link> 87 + 88 + {isAuthenticated && ( 89 + <> 90 + <div className="sidebar-section-title">Library</div> 91 + <Link 92 + to="/highlights" 93 + className={`sidebar-link ${isActive("/highlights") ? "active" : ""}`} 94 + > 95 + <Highlighter size={20} /> 96 + <span>Highlights</span> 97 + </Link> 98 + <Link 99 + to="/bookmarks" 100 + className={`sidebar-link ${isActive("/bookmarks") ? "active" : ""}`} 101 + > 102 + <Bookmark size={20} /> 103 + <span>Bookmarks</span> 104 + </Link> 105 + <Link 106 + to="/collections" 107 + className={`sidebar-link ${isActive("/collections") ? "active" : ""}`} 108 + > 109 + <Folder size={20} /> 110 + <span>Collections</span> 111 + </Link> 112 + <Link 113 + to="/notifications" 114 + className={`sidebar-link ${isActive("/notifications") ? "active" : ""}`} 115 + onClick={() => setUnreadCount(0)} 116 + > 117 + <Bell size={20} /> 118 + <span>Notifications</span> 119 + {unreadCount > 0 && ( 120 + <span className="notification-badge">{unreadCount}</span> 121 + )} 122 + </Link> 123 + </> 124 + )} 125 + </nav> 126 + 127 + {isAuthenticated && ( 128 + <Link to="/new" className="sidebar-new-btn"> 129 + <PenSquare size={18} /> 130 + <span>New</span> 131 + </Link> 132 + )} 133 + 134 + <div className="sidebar-footer" ref={menuRef}> 135 + {!loading && 136 + (isAuthenticated ? ( 137 + <> 138 + <div 139 + className="sidebar-user" 140 + onClick={() => setMenuOpen(!menuOpen)} 141 + > 142 + <div className="sidebar-avatar"> 143 + {user?.avatar ? ( 144 + <img src={user.avatar} alt={user.displayName} /> 145 + ) : ( 146 + <span>{getInitials()}</span> 147 + )} 148 + </div> 149 + <div className="sidebar-user-info"> 150 + <div className="sidebar-user-name"> 151 + {user?.displayName || user?.handle} 152 + </div> 153 + <div className="sidebar-user-handle">@{user?.handle}</div> 154 + </div> 155 + <MoreHorizontal size={18} className="sidebar-user-menu" /> 156 + </div> 157 + 158 + {menuOpen && ( 159 + <div className="sidebar-dropdown"> 160 + <Link 161 + to={`/profile/${user?.did}`} 162 + className="sidebar-dropdown-item" 163 + onClick={() => setMenuOpen(false)} 164 + > 165 + <User size={16} /> 166 + View Profile 167 + </Link> 168 + <button 169 + onClick={() => { 170 + logout(); 171 + setMenuOpen(false); 172 + }} 173 + className="sidebar-dropdown-item danger" 174 + > 175 + <LogOut size={16} /> 176 + Sign Out 177 + </button> 178 + </div> 179 + )} 180 + </> 181 + ) : ( 182 + <Link to="/login" className="sidebar-new-btn" style={{ margin: 0 }}> 183 + Sign In 184 + </Link> 185 + ))} 186 + </div> 187 + </aside> 188 + ); 189 + }
+473
web/src/css/annotations.css
··· 1 + .annotation-detail-page { 2 + max-width: 680px; 3 + margin: 0 auto; 4 + padding: 24px 16px; 5 + min-height: 100vh; 6 + } 7 + 8 + .annotation-detail-header { 9 + margin-bottom: 24px; 10 + } 11 + 12 + .back-link { 13 + display: inline-flex; 14 + align-items: center; 15 + color: var(--text-tertiary); 16 + text-decoration: none; 17 + font-size: 0.9rem; 18 + font-weight: 500; 19 + transition: color 0.15s; 20 + } 21 + 22 + .back-link:hover { 23 + color: var(--text-primary); 24 + } 25 + 26 + .replies-section { 27 + margin-top: 32px; 28 + border-top: 1px solid var(--border); 29 + padding-top: 24px; 30 + } 31 + 32 + .replies-title { 33 + display: flex; 34 + align-items: center; 35 + gap: 8px; 36 + font-size: 1.1rem; 37 + font-weight: 600; 38 + color: var(--text-primary); 39 + margin-bottom: 20px; 40 + } 41 + 42 + .annotation-card { 43 + display: flex; 44 + flex-direction: column; 45 + gap: 12px; 46 + padding: 20px 0; 47 + border-bottom: 1px solid var(--border); 48 + transition: background 0.15s ease; 49 + } 50 + 51 + .annotation-card:last-child { 52 + border-bottom: none; 53 + } 54 + 55 + .annotation-header { 56 + display: flex; 57 + justify-content: space-between; 58 + align-items: flex-start; 59 + gap: 12px; 60 + } 61 + 62 + .annotation-header-left { 63 + display: flex; 64 + align-items: center; 65 + gap: 10px; 66 + flex: 1; 67 + min-width: 0; 68 + } 69 + 70 + .annotation-avatar { 71 + width: 36px; 72 + height: 36px; 73 + min-width: 36px; 74 + border-radius: 50%; 75 + background: var(--bg-tertiary); 76 + display: flex; 77 + align-items: center; 78 + justify-content: center; 79 + font-weight: 600; 80 + font-size: 0.85rem; 81 + color: var(--text-secondary); 82 + overflow: hidden; 83 + } 84 + 85 + .annotation-avatar img { 86 + width: 100%; 87 + height: 100%; 88 + object-fit: cover; 89 + } 90 + 91 + .annotation-meta { 92 + display: flex; 93 + flex-direction: column; 94 + justify-content: center; 95 + line-height: 1.3; 96 + } 97 + 98 + .annotation-avatar-link { 99 + text-decoration: none; 100 + border-radius: 50%; 101 + } 102 + 103 + .annotation-author-row { 104 + display: flex; 105 + align-items: baseline; 106 + gap: 6px; 107 + flex-wrap: wrap; 108 + } 109 + 110 + .annotation-author { 111 + font-weight: 600; 112 + color: var(--text-primary); 113 + font-size: 0.9rem; 114 + } 115 + 116 + .annotation-handle { 117 + font-size: 0.85rem; 118 + color: var(--text-tertiary); 119 + text-decoration: none; 120 + } 121 + 122 + .annotation-handle:hover { 123 + color: var(--text-secondary); 124 + } 125 + 126 + .annotation-time { 127 + font-size: 0.75rem; 128 + color: var(--text-tertiary); 129 + } 130 + 131 + .annotation-content { 132 + display: flex; 133 + flex-direction: column; 134 + gap: 10px; 135 + padding-left: 46px; 136 + } 137 + 138 + .annotation-source { 139 + display: inline-flex; 140 + align-items: center; 141 + gap: 6px; 142 + font-size: 0.75rem; 143 + color: var(--text-tertiary); 144 + text-decoration: none; 145 + transition: color 0.15s ease; 146 + max-width: 100%; 147 + overflow: hidden; 148 + text-overflow: ellipsis; 149 + white-space: nowrap; 150 + } 151 + 152 + .annotation-source:hover { 153 + color: var(--text-secondary); 154 + text-decoration: underline; 155 + } 156 + 157 + .annotation-source-title { 158 + color: var(--text-tertiary); 159 + opacity: 0.7; 160 + } 161 + 162 + .annotation-highlight { 163 + display: block; 164 + position: relative; 165 + padding-left: 12px; 166 + margin: 4px 0; 167 + text-decoration: none; 168 + border-left: 2px solid var(--border); 169 + transition: all 0.15s ease; 170 + } 171 + 172 + .annotation-highlight:hover { 173 + border-left-color: var(--text-secondary); 174 + } 175 + 176 + .annotation-highlight mark { 177 + background: transparent; 178 + color: var(--text-primary); 179 + font-style: italic; 180 + font-size: 1rem; 181 + line-height: 1.6; 182 + font-weight: 400; 183 + font-family: var(--font-serif, var(--font-sans)); 184 + display: inline; 185 + } 186 + 187 + .annotation-text { 188 + font-size: 0.95rem; 189 + line-height: 1.6; 190 + color: var(--text-primary); 191 + white-space: pre-wrap; 192 + } 193 + 194 + .annotation-tags { 195 + display: flex; 196 + flex-wrap: wrap; 197 + gap: 6px; 198 + margin-top: 4px; 199 + } 200 + 201 + .annotation-tag { 202 + font-size: 0.8rem; 203 + color: var(--accent); 204 + text-decoration: none; 205 + font-weight: 500; 206 + opacity: 0.9; 207 + transition: opacity 0.15s; 208 + } 209 + 210 + .annotation-tag:hover { 211 + opacity: 1; 212 + text-decoration: underline; 213 + } 214 + 215 + .annotation-actions { 216 + display: flex; 217 + align-items: center; 218 + justify-content: space-between; 219 + margin-top: 4px; 220 + padding-left: 46px; 221 + } 222 + 223 + .annotation-actions-left { 224 + display: flex; 225 + align-items: center; 226 + gap: 16px; 227 + } 228 + 229 + .annotation-action { 230 + display: flex; 231 + align-items: center; 232 + gap: 6px; 233 + color: var(--text-tertiary); 234 + font-size: 0.8rem; 235 + font-weight: 500; 236 + padding: 6px; 237 + margin-left: -6px; 238 + border-radius: var(--radius-sm); 239 + transition: all 0.15s ease; 240 + background: transparent; 241 + cursor: pointer; 242 + border: none; 243 + } 244 + 245 + .annotation-action:hover { 246 + color: var(--text-secondary); 247 + background: var(--bg-tertiary); 248 + } 249 + 250 + .annotation-action.liked { 251 + color: #ef4444; 252 + } 253 + 254 + .annotation-action.liked svg { 255 + fill: #ef4444; 256 + } 257 + 258 + .annotation-action.active { 259 + color: var(--accent); 260 + } 261 + 262 + .action-icon-only { 263 + padding: 6px; 264 + } 265 + 266 + .annotation-header-right { 267 + opacity: 0; 268 + transition: opacity 0.15s; 269 + } 270 + 271 + .annotation-card:hover .annotation-header-right { 272 + opacity: 1; 273 + } 274 + 275 + .inline-replies { 276 + margin-top: 12px; 277 + padding-left: 46px; 278 + } 279 + 280 + @media (max-width: 600px) { 281 + .annotation-content, 282 + .annotation-actions, 283 + .inline-replies { 284 + padding-left: 0; 285 + } 286 + 287 + .annotation-header-right { 288 + opacity: 1; 289 + } 290 + } 291 + 292 + .replies-list-threaded { 293 + margin-top: 16px; 294 + display: flex; 295 + flex-direction: column; 296 + } 297 + 298 + .reply-card-threaded { 299 + position: relative; 300 + padding-left: 0; 301 + transition: background 0.15s; 302 + } 303 + 304 + .reply-header { 305 + display: flex; 306 + align-items: center; 307 + gap: 10px; 308 + margin-bottom: 6px; 309 + } 310 + 311 + .reply-avatar { 312 + width: 28px; 313 + height: 28px; 314 + border-radius: 50%; 315 + background: var(--bg-tertiary); 316 + overflow: hidden; 317 + flex-shrink: 0; 318 + display: flex; 319 + align-items: center; 320 + justify-content: center; 321 + } 322 + 323 + .reply-avatar img { 324 + width: 100%; 325 + height: 100%; 326 + object-fit: cover; 327 + } 328 + 329 + .reply-avatar span { 330 + font-size: 0.7rem; 331 + font-weight: 600; 332 + color: var(--text-secondary); 333 + } 334 + 335 + .reply-meta { 336 + display: flex; 337 + align-items: baseline; 338 + gap: 6px; 339 + flex: 1; 340 + min-width: 0; 341 + } 342 + 343 + .reply-author { 344 + font-weight: 600; 345 + font-size: 0.85rem; 346 + color: var(--text-primary); 347 + white-space: nowrap; 348 + overflow: hidden; 349 + text-overflow: ellipsis; 350 + } 351 + 352 + .reply-handle { 353 + font-size: 0.8rem; 354 + color: var(--text-tertiary); 355 + text-decoration: none; 356 + white-space: nowrap; 357 + overflow: hidden; 358 + text-overflow: ellipsis; 359 + } 360 + 361 + .reply-time { 362 + font-size: 0.75rem; 363 + color: var(--text-tertiary); 364 + white-space: nowrap; 365 + } 366 + 367 + .reply-dot { 368 + color: var(--text-tertiary); 369 + font-size: 0.7rem; 370 + } 371 + 372 + .reply-text { 373 + font-size: 0.9rem; 374 + line-height: 1.5; 375 + color: var(--text-primary); 376 + margin: 0; 377 + padding-left: 38px; 378 + } 379 + 380 + .reply-actions { 381 + display: flex; 382 + align-items: center; 383 + gap: 4px; 384 + opacity: 0; 385 + transition: opacity 0.15s; 386 + } 387 + 388 + .reply-card-threaded:hover .reply-actions { 389 + opacity: 1; 390 + } 391 + 392 + .reply-action-btn { 393 + background: none; 394 + border: none; 395 + padding: 4px; 396 + color: var(--text-tertiary); 397 + cursor: pointer; 398 + border-radius: 4px; 399 + display: flex; 400 + align-items: center; 401 + justify-content: center; 402 + } 403 + 404 + .reply-action-btn:hover { 405 + background: var(--bg-tertiary); 406 + color: var(--text-secondary); 407 + } 408 + 409 + .reply-action-delete:hover { 410 + color: #ef4444; 411 + background: rgba(239, 68, 68, 0.1); 412 + } 413 + 414 + .reply-form { 415 + border: 1px solid var(--border); 416 + border-radius: var(--radius-md); 417 + padding: 16px; 418 + background: var(--bg-secondary); 419 + margin-bottom: 24px; 420 + } 421 + 422 + .replying-to-banner { 423 + display: flex; 424 + justify-content: space-between; 425 + align-items: center; 426 + background: var(--bg-tertiary); 427 + padding: 8px 12px; 428 + border-radius: var(--radius-sm); 429 + margin-bottom: 12px; 430 + font-size: 0.85rem; 431 + color: var(--text-secondary); 432 + } 433 + 434 + .cancel-reply { 435 + background: none; 436 + border: none; 437 + color: var(--text-tertiary); 438 + cursor: pointer; 439 + font-size: 1.2rem; 440 + padding: 0 4px; 441 + line-height: 1; 442 + } 443 + 444 + .cancel-reply:hover { 445 + color: var(--text-primary); 446 + } 447 + 448 + .reply-input { 449 + width: 100%; 450 + background: var(--bg-primary); 451 + border: 1px solid var(--border); 452 + border-radius: var(--radius-sm); 453 + padding: 12px; 454 + color: var(--text-primary); 455 + font-family: inherit; 456 + font-size: 0.95rem; 457 + resize: vertical; 458 + min-height: 80px; 459 + transition: border-color 0.15s; 460 + display: block; 461 + box-sizing: border-box; 462 + } 463 + 464 + .reply-input:focus { 465 + outline: none; 466 + border-color: var(--accent); 467 + } 468 + 469 + .reply-form-actions { 470 + display: flex; 471 + justify-content: flex-end; 472 + margin-top: 12px; 473 + }
+139
web/src/css/base.css
··· 1 + :root { 2 + --bg-primary: #09090b; 3 + --bg-secondary: #0f0f12; 4 + --bg-tertiary: #18181b; 5 + --bg-card: #09090b; 6 + --bg-elevated: #18181b; 7 + --text-primary: #e4e4e7; 8 + --text-secondary: #a1a1aa; 9 + --text-tertiary: #71717a; 10 + --border: #27272a; 11 + --border-hover: #3f3f46; 12 + --accent: #6366f1; 13 + --accent-hover: #4f46e5; 14 + --accent-subtle: rgba(99, 102, 241, 0.1); 15 + --accent-text: #818cf8; 16 + --success: #10b981; 17 + --error: #ef4444; 18 + --warning: #f59e0b; 19 + --info: #3b82f6; 20 + --radius-sm: 4px; 21 + --radius-md: 6px; 22 + --radius-lg: 8px; 23 + --radius-full: 9999px; 24 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 25 + --shadow-md: 26 + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 27 + --shadow-lg: 28 + 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 29 + --font-sans: 30 + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 31 + --font-mono: 32 + "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 + } 34 + 35 + * { 36 + margin: 0; 37 + padding: 0; 38 + box-sizing: border-box; 39 + } 40 + 41 + html { 42 + font-size: 16px; 43 + -webkit-text-size-adjust: 100%; 44 + } 45 + 46 + body { 47 + font-family: var(--font-sans); 48 + background: var(--bg-primary); 49 + color: var(--text-primary); 50 + line-height: 1.5; 51 + min-height: 100vh; 52 + -webkit-font-smoothing: antialiased; 53 + -moz-osx-font-smoothing: grayscale; 54 + } 55 + 56 + a { 57 + color: inherit; 58 + text-decoration: none; 59 + transition: color 0.15s ease; 60 + } 61 + 62 + h1, 63 + h2, 64 + h3, 65 + h4, 66 + h5, 67 + h6 { 68 + font-weight: 600; 69 + line-height: 1.25; 70 + letter-spacing: -0.025em; 71 + color: var(--text-primary); 72 + } 73 + 74 + p { 75 + color: var(--text-secondary); 76 + } 77 + 78 + button { 79 + font-family: inherit; 80 + cursor: pointer; 81 + border: none; 82 + background: none; 83 + } 84 + 85 + input, 86 + textarea, 87 + select { 88 + font-family: inherit; 89 + font-size: inherit; 90 + color: var(--text-primary); 91 + } 92 + 93 + ::selection { 94 + background: var(--accent-subtle); 95 + color: var(--accent-text); 96 + } 97 + 98 + .text-sm { 99 + font-size: 0.875rem; 100 + } 101 + 102 + .text-xs { 103 + font-size: 0.75rem; 104 + } 105 + 106 + .font-medium { 107 + font-weight: 500; 108 + } 109 + 110 + .font-semibold { 111 + font-weight: 600; 112 + } 113 + 114 + .text-muted { 115 + color: var(--text-secondary); 116 + } 117 + 118 + .text-faint { 119 + color: var(--text-tertiary); 120 + } 121 + 122 + ::-webkit-scrollbar { 123 + width: 10px; 124 + height: 10px; 125 + } 126 + 127 + ::-webkit-scrollbar-track { 128 + background: transparent; 129 + } 130 + 131 + ::-webkit-scrollbar-thumb { 132 + background: var(--border); 133 + border-radius: 5px; 134 + border: 2px solid var(--bg-primary); 135 + } 136 + 137 + ::-webkit-scrollbar-thumb:hover { 138 + background: var(--border-hover); 139 + }
+127
web/src/css/buttons.css
··· 1 + .btn { 2 + display: inline-flex; 3 + align-items: center; 4 + justify-content: center; 5 + gap: 8px; 6 + padding: 10px 20px; 7 + font-size: 0.9rem; 8 + font-weight: 500; 9 + border-radius: var(--radius-md); 10 + transition: all 0.15s ease; 11 + } 12 + 13 + .btn-primary { 14 + background: var(--accent); 15 + color: white; 16 + } 17 + 18 + .btn-primary:hover { 19 + background: var(--accent-hover); 20 + transform: translateY(-1px); 21 + box-shadow: var(--shadow-md); 22 + } 23 + 24 + .btn-secondary { 25 + background: var(--bg-tertiary); 26 + color: var(--text-primary); 27 + border: 1px solid var(--border); 28 + } 29 + 30 + .btn-secondary:hover { 31 + background: var(--bg-hover); 32 + border-color: var(--border-hover); 33 + } 34 + 35 + .btn-ghost { 36 + color: var(--text-secondary); 37 + padding: 8px 12px; 38 + } 39 + 40 + .btn-ghost:hover { 41 + color: var(--text-primary); 42 + background: var(--bg-tertiary); 43 + } 44 + 45 + .btn-bluesky { 46 + background: #0085ff; 47 + color: white; 48 + display: flex; 49 + align-items: center; 50 + justify-content: center; 51 + gap: 10px; 52 + transition: 53 + background 0.2s, 54 + transform 0.2s; 55 + } 56 + 57 + .btn-bluesky:hover { 58 + background: #0070dd; 59 + transform: translateY(-1px); 60 + } 61 + 62 + .btn-sm { 63 + padding: 6px 12px; 64 + font-size: 0.85rem; 65 + } 66 + 67 + .btn-text { 68 + background: none; 69 + border: none; 70 + color: var(--text-secondary); 71 + font-size: 0.9rem; 72 + padding: 8px 12px; 73 + cursor: pointer; 74 + transition: color 0.15s; 75 + } 76 + 77 + .btn-text:hover { 78 + color: var(--text-primary); 79 + } 80 + 81 + .btn-block { 82 + width: 100%; 83 + text-align: left; 84 + padding: 8px 12px; 85 + color: var(--text-secondary); 86 + background: var(--bg-tertiary); 87 + border-radius: var(--radius-md); 88 + margin-top: 8px; 89 + font-size: 0.9rem; 90 + cursor: pointer; 91 + transition: all 0.2s; 92 + } 93 + 94 + .btn-block:hover { 95 + background: var(--border); 96 + color: var(--text-primary); 97 + } 98 + 99 + .btn-icon-danger { 100 + padding: 8px; 101 + background: var(--error); 102 + color: white; 103 + border: none; 104 + border-radius: var(--radius-md); 105 + cursor: pointer; 106 + box-shadow: var(--shadow-md); 107 + transition: all 0.15s ease; 108 + display: flex; 109 + align-items: center; 110 + justify-content: center; 111 + } 112 + 113 + .btn-icon-danger:hover { 114 + background: #dc2626; 115 + transform: scale(1.05); 116 + } 117 + 118 + .action-buttons { 119 + display: flex; 120 + gap: 8px; 121 + } 122 + 123 + .action-buttons-end { 124 + display: flex; 125 + justify-content: flex-end; 126 + gap: 8px; 127 + }
+310
web/src/css/collections.css
··· 1 + .collections-list { 2 + display: flex; 3 + flex-direction: column; 4 + gap: 2px; 5 + background: var(--bg-card); 6 + border: 1px solid var(--border); 7 + border-radius: var(--radius-lg); 8 + overflow: hidden; 9 + } 10 + 11 + .collection-row { 12 + display: flex; 13 + align-items: center; 14 + background: var(--bg-card); 15 + transition: background 0.15s ease; 16 + } 17 + 18 + .collection-row:not(:last-child) { 19 + border-bottom: 1px solid var(--border); 20 + } 21 + 22 + .collection-row:hover { 23 + background: var(--bg-secondary); 24 + } 25 + 26 + .collection-row-content { 27 + flex: 1; 28 + display: flex; 29 + align-items: center; 30 + gap: 16px; 31 + padding: 16px 20px; 32 + text-decoration: none; 33 + min-width: 0; 34 + } 35 + 36 + .collection-row-icon { 37 + width: 44px; 38 + height: 44px; 39 + min-width: 44px; 40 + display: flex; 41 + align-items: center; 42 + justify-content: center; 43 + background: linear-gradient( 44 + 135deg, 45 + rgba(79, 70, 229, 0.1), 46 + rgba(168, 85, 247, 0.15) 47 + ); 48 + color: var(--accent); 49 + border-radius: var(--radius-md); 50 + transition: all 0.2s ease; 51 + } 52 + 53 + .collection-row:hover .collection-row-icon { 54 + background: linear-gradient( 55 + 135deg, 56 + rgba(79, 70, 229, 0.15), 57 + rgba(168, 85, 247, 0.2) 58 + ); 59 + transform: scale(1.05); 60 + } 61 + 62 + .collection-row-info { 63 + flex: 1; 64 + min-width: 0; 65 + } 66 + 67 + .collection-row-name { 68 + font-size: 1rem; 69 + font-weight: 600; 70 + color: var(--text-primary); 71 + margin: 0 0 2px 0; 72 + white-space: nowrap; 73 + overflow: hidden; 74 + text-overflow: ellipsis; 75 + } 76 + 77 + .collection-row:hover .collection-row-name { 78 + color: var(--accent); 79 + } 80 + 81 + .collection-row-desc { 82 + font-size: 0.85rem; 83 + color: var(--text-secondary); 84 + margin: 0; 85 + white-space: nowrap; 86 + overflow: hidden; 87 + text-overflow: ellipsis; 88 + } 89 + 90 + .collection-row-arrow { 91 + color: var(--text-tertiary); 92 + opacity: 0; 93 + transition: all 0.2s ease; 94 + } 95 + 96 + .collection-row:hover .collection-row-arrow { 97 + opacity: 1; 98 + color: var(--accent); 99 + transform: translateX(2px); 100 + } 101 + 102 + .collection-row-edit { 103 + padding: 10px; 104 + margin-right: 12px; 105 + color: var(--text-tertiary); 106 + background: none; 107 + border: none; 108 + border-radius: var(--radius-sm); 109 + cursor: pointer; 110 + opacity: 0; 111 + transition: all 0.15s ease; 112 + } 113 + 114 + .collection-row:hover .collection-row-edit { 115 + opacity: 1; 116 + } 117 + 118 + .collection-row-edit:hover { 119 + color: var(--text-primary); 120 + background: var(--bg-tertiary); 121 + } 122 + 123 + .collection-detail-header { 124 + display: flex; 125 + gap: 20px; 126 + padding: 24px; 127 + background: var(--bg-card); 128 + border: 1px solid var(--border); 129 + border-radius: var(--radius-lg); 130 + margin-bottom: 32px; 131 + position: relative; 132 + } 133 + 134 + .collection-detail-icon { 135 + width: 56px; 136 + height: 56px; 137 + min-width: 56px; 138 + display: flex; 139 + align-items: center; 140 + justify-content: center; 141 + background: linear-gradient( 142 + 135deg, 143 + rgba(79, 70, 229, 0.1), 144 + rgba(168, 85, 247, 0.1) 145 + ); 146 + color: var(--accent); 147 + border-radius: var(--radius-md); 148 + } 149 + 150 + .collection-detail-info { 151 + flex: 1; 152 + min-width: 0; 153 + } 154 + 155 + .collection-detail-visibility { 156 + display: flex; 157 + align-items: center; 158 + gap: 6px; 159 + font-size: 0.8rem; 160 + font-weight: 600; 161 + color: var(--accent); 162 + text-transform: capitalize; 163 + margin-bottom: 8px; 164 + } 165 + 166 + .collection-detail-title { 167 + font-size: 1.5rem; 168 + font-weight: 700; 169 + color: var(--text-primary); 170 + margin-bottom: 8px; 171 + line-height: 1.3; 172 + } 173 + 174 + .collection-detail-desc { 175 + color: var(--text-secondary); 176 + font-size: 1rem; 177 + line-height: 1.5; 178 + margin-bottom: 12px; 179 + max-width: 600px; 180 + } 181 + 182 + .collection-detail-stats { 183 + display: flex; 184 + align-items: center; 185 + gap: 8px; 186 + font-size: 0.85rem; 187 + color: var(--text-tertiary); 188 + } 189 + 190 + .collection-detail-actions { 191 + position: absolute; 192 + top: 20px; 193 + right: 20px; 194 + display: flex; 195 + align-items: center; 196 + gap: 8px; 197 + } 198 + 199 + .collection-detail-actions .share-menu-container { 200 + display: flex; 201 + align-items: center; 202 + } 203 + 204 + .collection-detail-actions .annotation-action { 205 + padding: 10px; 206 + color: var(--text-tertiary); 207 + background: none; 208 + border: none; 209 + border-radius: var(--radius-sm); 210 + cursor: pointer; 211 + transition: all 0.15s ease; 212 + } 213 + 214 + .collection-detail-actions .annotation-action:hover { 215 + color: var(--accent); 216 + background: var(--bg-tertiary); 217 + } 218 + 219 + .collection-detail-edit, 220 + .collection-detail-delete { 221 + padding: 10px; 222 + color: var(--text-tertiary); 223 + background: none; 224 + border: none; 225 + border-radius: var(--radius-sm); 226 + cursor: pointer; 227 + transition: all 0.15s ease; 228 + } 229 + 230 + .collection-detail-edit:hover { 231 + color: var(--accent); 232 + background: var(--bg-tertiary); 233 + } 234 + 235 + .collection-detail-delete:hover { 236 + color: var(--error); 237 + background: rgba(239, 68, 68, 0.1); 238 + } 239 + 240 + .collection-item-wrapper { 241 + position: relative; 242 + } 243 + 244 + .collection-item-remove { 245 + position: absolute; 246 + top: 12px; 247 + left: -40px; 248 + z-index: 10; 249 + padding: 8px; 250 + background: var(--bg-card); 251 + border: 1px solid var(--border); 252 + border-radius: var(--radius-sm); 253 + color: var(--text-tertiary); 254 + cursor: pointer; 255 + opacity: 0; 256 + transition: all 0.15s ease; 257 + } 258 + 259 + .collection-item-wrapper:hover .collection-item-remove { 260 + opacity: 1; 261 + } 262 + 263 + .collection-item-remove:hover { 264 + color: var(--error); 265 + border-color: var(--error); 266 + background: rgba(239, 68, 68, 0.05); 267 + } 268 + 269 + .collection-list-item { 270 + width: 100%; 271 + text-align: left; 272 + padding: 12px 16px; 273 + border-radius: var(--radius-md); 274 + background: var(--bg-primary); 275 + border: 1px solid transparent; 276 + color: var(--text-primary); 277 + transition: all 0.15s ease; 278 + display: flex; 279 + align-items: center; 280 + justify-content: space-between; 281 + cursor: pointer; 282 + } 283 + 284 + .collection-list-item:hover { 285 + background: var(--bg-hover); 286 + border-color: var(--border); 287 + } 288 + 289 + .collection-list-item:hover .collection-list-item-icon { 290 + opacity: 1; 291 + } 292 + 293 + .collection-list-item:disabled { 294 + opacity: 0.6; 295 + cursor: not-allowed; 296 + } 297 + 298 + .item-delete-overlay { 299 + position: absolute; 300 + top: 16px; 301 + right: 16px; 302 + z-index: 10; 303 + opacity: 0; 304 + transition: opacity 0.15s ease; 305 + } 306 + 307 + .card:hover .item-delete-overlay, 308 + div:hover > .item-delete-overlay { 309 + opacity: 1; 310 + }
+139
web/src/css/feed.css
··· 1 + .feed { 2 + display: flex; 3 + flex-direction: column; 4 + gap: 16px; 5 + } 6 + 7 + .feed-header { 8 + display: flex; 9 + align-items: center; 10 + justify-content: space-between; 11 + margin-bottom: 8px; 12 + } 13 + 14 + .feed-title { 15 + font-size: 1.5rem; 16 + font-weight: 700; 17 + } 18 + 19 + .feed-filters { 20 + display: flex; 21 + gap: 8px; 22 + margin-bottom: 24px; 23 + padding: 4px; 24 + background: var(--bg-tertiary); 25 + border-radius: var(--radius-lg); 26 + width: fit-content; 27 + } 28 + 29 + .filter-tab { 30 + padding: 8px 16px; 31 + font-size: 0.9rem; 32 + font-weight: 500; 33 + color: var(--text-secondary); 34 + background: transparent; 35 + border: none; 36 + border-radius: var(--radius-md); 37 + cursor: pointer; 38 + transition: all 0.15s ease; 39 + } 40 + 41 + .filter-tab:hover { 42 + color: var(--text-primary); 43 + background: var(--bg-hover); 44 + } 45 + 46 + .filter-tab.active { 47 + color: var(--text-primary); 48 + background: var(--bg-card); 49 + box-shadow: var(--shadow-sm); 50 + } 51 + 52 + .page-header { 53 + margin-bottom: 32px; 54 + } 55 + 56 + .page-title { 57 + font-size: 2rem; 58 + font-weight: 700; 59 + margin-bottom: 8px; 60 + } 61 + 62 + .page-description { 63 + color: var(--text-secondary); 64 + font-size: 1.1rem; 65 + } 66 + 67 + .url-input-wrapper { 68 + margin-bottom: 24px; 69 + } 70 + 71 + .url-input-container { 72 + display: flex; 73 + gap: 12px; 74 + } 75 + 76 + .url-input { 77 + width: 100%; 78 + padding: 16px; 79 + background: var(--bg-secondary); 80 + border: 1px solid var(--border); 81 + border-radius: var(--radius-md); 82 + color: var(--text-primary); 83 + font-size: 1.1rem; 84 + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 85 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 86 + } 87 + 88 + .url-input:focus { 89 + outline: none; 90 + border-color: var(--accent); 91 + box-shadow: 0 0 0 4px var(--accent-subtle); 92 + background: var(--bg-primary); 93 + } 94 + 95 + .url-input::placeholder { 96 + color: var(--text-tertiary); 97 + } 98 + 99 + .url-results-header { 100 + display: flex; 101 + align-items: center; 102 + justify-content: space-between; 103 + margin-bottom: 16px; 104 + flex-wrap: wrap; 105 + gap: 12px; 106 + } 107 + 108 + .back-link { 109 + display: inline-flex; 110 + align-items: center; 111 + gap: 8px; 112 + color: var(--text-secondary); 113 + font-size: 0.9rem; 114 + text-decoration: none; 115 + margin-bottom: 24px; 116 + transition: color 0.15s; 117 + } 118 + 119 + .back-link:hover { 120 + color: var(--accent); 121 + } 122 + 123 + .new-page { 124 + max-width: 600px; 125 + margin: 0 auto; 126 + display: flex; 127 + flex-direction: column; 128 + gap: 32px; 129 + } 130 + 131 + @media (max-width: 640px) { 132 + .main-content { 133 + padding: 16px 12px; 134 + } 135 + 136 + .page-title { 137 + font-size: 1.5rem; 138 + } 139 + }
+463
web/src/css/layout.css
··· 1 + .layout { 2 + display: flex; 3 + min-height: 100vh; 4 + background: var(--bg-primary); 5 + } 6 + 7 + .sidebar { 8 + position: fixed; 9 + left: 0; 10 + top: 0; 11 + bottom: 0; 12 + width: 240px; 13 + background: var(--bg-primary); 14 + border-right: 1px solid var(--border); 15 + display: flex; 16 + flex-direction: column; 17 + z-index: 50; 18 + padding-bottom: 20px; 19 + } 20 + 21 + .sidebar-header { 22 + height: 64px; 23 + display: flex; 24 + align-items: center; 25 + padding: 0 20px; 26 + margin-bottom: 12px; 27 + text-decoration: none; 28 + color: var(--text-primary); 29 + } 30 + 31 + .sidebar-logo { 32 + width: 24px; 33 + height: 24px; 34 + object-fit: contain; 35 + margin-right: 12px; 36 + } 37 + 38 + .sidebar-brand { 39 + font-size: 1rem; 40 + font-weight: 600; 41 + color: var(--text-primary); 42 + letter-spacing: -0.01em; 43 + } 44 + 45 + .sidebar-nav { 46 + flex: 1; 47 + display: flex; 48 + flex-direction: column; 49 + gap: 4px; 50 + padding: 0 12px; 51 + overflow-y: auto; 52 + } 53 + 54 + .sidebar-link { 55 + display: flex; 56 + align-items: center; 57 + gap: 12px; 58 + padding: 8px 12px; 59 + border-radius: var(--radius-md); 60 + color: var(--text-secondary); 61 + text-decoration: none; 62 + font-size: 0.9rem; 63 + font-weight: 500; 64 + transition: all 0.15s ease; 65 + } 66 + 67 + .sidebar-link:hover { 68 + background: var(--bg-tertiary); 69 + color: var(--text-primary); 70 + } 71 + 72 + .sidebar-link.active { 73 + background: var(--bg-tertiary); 74 + color: var(--text-primary); 75 + } 76 + 77 + .sidebar-link svg { 78 + width: 18px; 79 + height: 18px; 80 + color: var(--text-tertiary); 81 + transition: color 0.15s ease; 82 + } 83 + 84 + .sidebar-link:hover svg, 85 + .sidebar-link.active svg { 86 + color: var(--text-primary); 87 + } 88 + 89 + .sidebar-section-title { 90 + padding: 24px 12px 8px; 91 + font-size: 0.75rem; 92 + font-weight: 600; 93 + color: var(--text-tertiary); 94 + text-transform: uppercase; 95 + letter-spacing: 0.05em; 96 + } 97 + 98 + .notification-badge { 99 + background: var(--accent); 100 + color: white; 101 + font-size: 0.7rem; 102 + font-weight: 600; 103 + padding: 0 6px; 104 + height: 18px; 105 + border-radius: 99px; 106 + display: flex; 107 + align-items: center; 108 + justify-content: center; 109 + margin-left: auto; 110 + } 111 + 112 + .sidebar-new-btn { 113 + display: flex; 114 + align-items: center; 115 + gap: 10px; 116 + margin: 0 12px 16px; 117 + padding: 10px 16px; 118 + background: var(--text-primary); 119 + color: var(--bg-primary); 120 + border-radius: var(--radius-md); 121 + font-size: 0.9rem; 122 + font-weight: 600; 123 + text-decoration: none; 124 + transition: opacity 0.15s; 125 + justify-content: center; 126 + } 127 + 128 + .sidebar-new-btn:hover { 129 + opacity: 0.9; 130 + } 131 + 132 + .sidebar-footer { 133 + padding: 0 12px; 134 + margin-top: auto; 135 + } 136 + 137 + .sidebar-user { 138 + display: flex; 139 + align-items: center; 140 + gap: 10px; 141 + padding: 8px 12px; 142 + border-radius: var(--radius-md); 143 + cursor: pointer; 144 + transition: background 0.15s ease; 145 + } 146 + 147 + .sidebar-user:hover, 148 + .sidebar-user.active { 149 + background: var(--bg-tertiary); 150 + } 151 + 152 + .sidebar-avatar { 153 + width: 32px; 154 + height: 32px; 155 + border-radius: 50%; 156 + background: var(--bg-tertiary); 157 + display: flex; 158 + align-items: center; 159 + justify-content: center; 160 + color: var(--text-secondary); 161 + font-size: 0.8rem; 162 + font-weight: 500; 163 + overflow: hidden; 164 + flex-shrink: 0; 165 + border: 1px solid var(--border); 166 + } 167 + 168 + .sidebar-avatar img { 169 + width: 100%; 170 + height: 100%; 171 + object-fit: cover; 172 + } 173 + 174 + .sidebar-user-info { 175 + flex: 1; 176 + min-width: 0; 177 + display: flex; 178 + flex-direction: column; 179 + } 180 + 181 + .sidebar-user-name { 182 + font-size: 0.85rem; 183 + font-weight: 500; 184 + color: var(--text-primary); 185 + } 186 + 187 + .sidebar-user-handle { 188 + font-size: 0.75rem; 189 + color: var(--text-tertiary); 190 + } 191 + 192 + .sidebar-dropdown { 193 + position: absolute; 194 + bottom: 74px; 195 + left: 12px; 196 + width: 216px; 197 + background: var(--bg-card); 198 + border: 1px solid var(--border); 199 + border-radius: var(--radius-md); 200 + box-shadow: var(--shadow-lg); 201 + padding: 4px; 202 + z-index: 1000; 203 + overflow: hidden; 204 + animation: scaleIn 0.1s ease-out; 205 + transform-origin: bottom center; 206 + } 207 + 208 + @keyframes scaleIn { 209 + from { 210 + opacity: 0; 211 + transform: scale(0.95); 212 + } 213 + 214 + to { 215 + opacity: 1; 216 + transform: scale(1); 217 + } 218 + } 219 + 220 + .sidebar-dropdown-item { 221 + display: flex; 222 + align-items: center; 223 + gap: 10px; 224 + width: 100%; 225 + padding: 8px 12px; 226 + font-size: 0.85rem; 227 + color: var(--text-secondary); 228 + text-decoration: none; 229 + background: transparent; 230 + cursor: pointer; 231 + border-radius: var(--radius-sm); 232 + transition: all 0.15s; 233 + border: none; 234 + } 235 + 236 + .sidebar-dropdown-item:hover { 237 + background: var(--bg-tertiary); 238 + color: var(--text-primary); 239 + } 240 + 241 + .sidebar-dropdown-item.danger:hover { 242 + background: rgba(239, 68, 68, 0.1); 243 + color: var(--error); 244 + } 245 + 246 + .main-layout { 247 + flex: 1; 248 + margin-left: 240px; 249 + margin-right: 280px; 250 + min-height: 100vh; 251 + } 252 + 253 + .main-content-wrapper { 254 + max-width: 640px; 255 + margin: 0 auto; 256 + padding: 40px 24px; 257 + } 258 + 259 + .right-sidebar { 260 + position: fixed; 261 + right: 0; 262 + top: 0; 263 + bottom: 0; 264 + width: 280px; 265 + background: var(--bg-primary); 266 + border-left: 1px solid var(--border); 267 + padding: 32px 24px; 268 + overflow-y: auto; 269 + display: flex; 270 + flex-direction: column; 271 + gap: 32px; 272 + } 273 + 274 + .right-section { 275 + display: flex; 276 + flex-direction: column; 277 + gap: 12px; 278 + } 279 + 280 + .right-section-title { 281 + font-size: 0.75rem; 282 + font-weight: 600; 283 + color: var(--text-primary); 284 + margin-bottom: 4px; 285 + } 286 + 287 + .right-section-desc { 288 + font-size: 0.85rem; 289 + line-height: 1.5; 290 + color: var(--text-secondary); 291 + } 292 + 293 + .right-extension-btn { 294 + display: inline-flex; 295 + align-items: center; 296 + gap: 8px; 297 + padding: 8px 12px; 298 + background: var(--bg-primary); 299 + border: 1px solid var(--border); 300 + border-radius: var(--radius-md); 301 + color: var(--text-primary); 302 + font-size: 0.85rem; 303 + font-weight: 500; 304 + text-decoration: none; 305 + transition: all 0.15s ease; 306 + width: fit-content; 307 + } 308 + 309 + .right-extension-btn:hover { 310 + border-color: var(--text-tertiary); 311 + background: var(--bg-tertiary); 312 + } 313 + 314 + .right-links { 315 + display: flex; 316 + flex-direction: column; 317 + gap: 4px; 318 + } 319 + 320 + .right-link { 321 + display: flex; 322 + align-items: center; 323 + justify-content: space-between; 324 + padding: 6px 0; 325 + color: var(--text-secondary); 326 + font-size: 0.9rem; 327 + transition: color 0.15s; 328 + text-decoration: none; 329 + } 330 + 331 + .right-link:hover { 332 + color: var(--text-primary); 333 + } 334 + 335 + .right-link svg { 336 + width: 16px; 337 + height: 16px; 338 + color: var(--text-tertiary); 339 + transition: all 0.15s; 340 + } 341 + 342 + .right-link:hover svg { 343 + color: var(--text-secondary); 344 + } 345 + 346 + .tangled-icon { 347 + width: 16px; 348 + height: 16px; 349 + background-color: var(--text-tertiary); 350 + -webkit-mask: url("../assets/tangled.svg") no-repeat center / contain; 351 + mask: url("../assets/tangled.svg") no-repeat center / contain; 352 + transition: background-color 0.15s; 353 + } 354 + 355 + .right-link:hover .tangled-icon { 356 + background-color: var(--text-secondary); 357 + } 358 + 359 + .right-footer { 360 + margin-top: auto; 361 + display: flex; 362 + flex-wrap: wrap; 363 + gap: 12px; 364 + font-size: 0.75rem; 365 + color: var(--text-tertiary); 366 + } 367 + 368 + .right-footer a { 369 + color: var(--text-tertiary); 370 + } 371 + 372 + .right-footer a:hover { 373 + color: var(--text-secondary); 374 + } 375 + 376 + .mobile-nav { 377 + display: none; 378 + position: fixed; 379 + bottom: 0; 380 + left: 0; 381 + right: 0; 382 + background: rgba(9, 9, 11, 0.9); 383 + backdrop-filter: blur(12px); 384 + -webkit-backdrop-filter: blur(12px); 385 + border-top: 1px solid var(--border); 386 + padding: 8px 16px; 387 + padding-bottom: calc(8px + env(safe-area-inset-bottom, 0)); 388 + z-index: 100; 389 + } 390 + 391 + .mobile-nav-inner { 392 + display: flex; 393 + justify-content: space-between; 394 + align-items: center; 395 + } 396 + 397 + .mobile-nav-item { 398 + display: flex; 399 + flex-direction: column; 400 + align-items: center; 401 + justify-content: center; 402 + gap: 4px; 403 + color: var(--text-tertiary); 404 + text-decoration: none; 405 + font-size: 0.65rem; 406 + font-weight: 500; 407 + width: 60px; 408 + transition: color 0.15s; 409 + } 410 + 411 + .mobile-nav-item.active { 412 + color: var(--text-primary); 413 + } 414 + 415 + .mobile-nav-item svg { 416 + width: 24px; 417 + height: 24px; 418 + } 419 + 420 + .mobile-nav-new { 421 + width: 48px; 422 + height: 36px; 423 + border-radius: var(--radius-md); 424 + background: var(--text-primary); 425 + color: var(--bg-primary); 426 + display: flex; 427 + align-items: center; 428 + justify-content: center; 429 + } 430 + 431 + .mobile-nav-new svg { 432 + width: 20px; 433 + height: 20px; 434 + } 435 + 436 + @media (max-width: 1200px) { 437 + .right-sidebar { 438 + display: none; 439 + } 440 + 441 + .main-layout { 442 + margin-right: 0; 443 + } 444 + } 445 + 446 + @media (max-width: 768px) { 447 + .sidebar { 448 + display: none; 449 + } 450 + 451 + .main-layout { 452 + margin-left: 0; 453 + padding-bottom: 80px; 454 + } 455 + 456 + .main-content-wrapper { 457 + padding: 20px 16px; 458 + } 459 + 460 + .mobile-nav { 461 + display: block; 462 + } 463 + }
+297
web/src/css/login.css
··· 1 + .login-page { 2 + display: flex; 3 + flex-direction: column; 4 + align-items: center; 5 + justify-content: center; 6 + min-height: 70vh; 7 + padding: 60px 20px; 8 + width: 100%; 9 + max-width: 500px; 10 + margin: 0 auto; 11 + } 12 + 13 + .login-at-logo { 14 + font-size: 5rem; 15 + font-weight: 800; 16 + color: var(--accent); 17 + margin-bottom: 24px; 18 + line-height: 1; 19 + } 20 + 21 + .login-logo-img { 22 + width: 80px; 23 + height: 80px; 24 + margin-bottom: 24px; 25 + object-fit: contain; 26 + } 27 + 28 + .login-heading { 29 + font-size: 1.5rem; 30 + font-weight: 600; 31 + margin-bottom: 32px; 32 + display: flex; 33 + align-items: center; 34 + gap: 10px; 35 + text-align: center; 36 + line-height: 1.4; 37 + } 38 + 39 + .login-help-btn { 40 + background: none; 41 + border: none; 42 + color: var(--text-tertiary); 43 + cursor: pointer; 44 + padding: 4px; 45 + display: flex; 46 + align-items: center; 47 + transition: color 0.15s; 48 + flex-shrink: 0; 49 + } 50 + 51 + .login-help-btn:hover { 52 + color: var(--accent); 53 + } 54 + 55 + .login-help-text { 56 + background: var(--bg-elevated); 57 + border: 1px solid var(--border); 58 + border-radius: var(--radius-md); 59 + padding: 16px 20px; 60 + margin-bottom: 24px; 61 + font-size: 0.95rem; 62 + color: var(--text-secondary); 63 + line-height: 1.6; 64 + text-align: center; 65 + } 66 + 67 + .login-help-text code { 68 + background: var(--bg-tertiary); 69 + padding: 2px 8px; 70 + border-radius: var(--radius-sm); 71 + font-size: 0.9rem; 72 + } 73 + 74 + .login-form { 75 + display: flex; 76 + flex-direction: column; 77 + gap: 16px; 78 + width: 100%; 79 + } 80 + 81 + .login-input-wrapper { 82 + position: relative; 83 + } 84 + 85 + .login-input { 86 + width: 100%; 87 + padding: 14px 16px; 88 + background: var(--bg-elevated); 89 + border: 1px solid var(--border); 90 + border-radius: var(--radius-md); 91 + color: var(--text-primary); 92 + font-size: 1rem; 93 + transition: 94 + border-color 0.15s, 95 + box-shadow 0.15s; 96 + } 97 + 98 + .login-input:focus { 99 + outline: none; 100 + border-color: var(--accent); 101 + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); 102 + } 103 + 104 + .login-input::placeholder { 105 + color: var(--text-tertiary); 106 + } 107 + 108 + .login-suggestions { 109 + position: absolute; 110 + top: calc(100% + 4px); 111 + left: 0; 112 + right: 0; 113 + background: var(--bg-card); 114 + border: 1px solid var(--border); 115 + border-radius: var(--radius-md); 116 + box-shadow: var(--shadow-lg); 117 + overflow: hidden; 118 + z-index: 100; 119 + } 120 + 121 + .login-suggestion { 122 + display: flex; 123 + align-items: center; 124 + gap: 12px; 125 + width: 100%; 126 + padding: 12px 16px; 127 + background: transparent; 128 + border: none; 129 + cursor: pointer; 130 + text-align: left; 131 + transition: background 0.1s; 132 + } 133 + 134 + .login-suggestion:hover, 135 + .login-suggestion.selected { 136 + background: var(--bg-elevated); 137 + } 138 + 139 + .login-suggestion-avatar { 140 + width: 40px; 141 + height: 40px; 142 + border-radius: var(--radius-full); 143 + background: linear-gradient(135deg, var(--accent), #a855f7); 144 + display: flex; 145 + align-items: center; 146 + justify-content: center; 147 + flex-shrink: 0; 148 + overflow: hidden; 149 + font-size: 0.875rem; 150 + font-weight: 600; 151 + color: white; 152 + } 153 + 154 + .login-suggestion-avatar img { 155 + width: 100%; 156 + height: 100%; 157 + object-fit: cover; 158 + } 159 + 160 + .login-suggestion-info { 161 + display: flex; 162 + flex-direction: column; 163 + min-width: 0; 164 + } 165 + 166 + .login-suggestion-name { 167 + font-weight: 600; 168 + color: var(--text-primary); 169 + white-space: nowrap; 170 + overflow: hidden; 171 + text-overflow: ellipsis; 172 + } 173 + 174 + .login-suggestion-handle { 175 + font-size: 0.875rem; 176 + color: var(--text-secondary); 177 + white-space: nowrap; 178 + overflow: hidden; 179 + text-overflow: ellipsis; 180 + } 181 + 182 + .login-error { 183 + padding: 12px 16px; 184 + background: rgba(239, 68, 68, 0.1); 185 + border: 1px solid rgba(239, 68, 68, 0.3); 186 + border-radius: var(--radius-md); 187 + color: #ef4444; 188 + font-size: 0.875rem; 189 + } 190 + 191 + .login-legal { 192 + font-size: 0.75rem; 193 + color: var(--text-tertiary); 194 + line-height: 1.5; 195 + margin-top: 16px; 196 + } 197 + 198 + .login-brand { 199 + display: flex; 200 + align-items: center; 201 + justify-content: center; 202 + gap: 12px; 203 + margin-bottom: 24px; 204 + } 205 + 206 + .login-brand-icon { 207 + width: 48px; 208 + height: 48px; 209 + background: linear-gradient(135deg, var(--accent), #a855f7); 210 + border-radius: var(--radius-lg); 211 + display: flex; 212 + align-items: center; 213 + justify-content: center; 214 + font-size: 1.75rem; 215 + font-weight: 800; 216 + color: white; 217 + } 218 + 219 + .login-brand-name { 220 + font-size: 1.75rem; 221 + font-weight: 700; 222 + } 223 + 224 + .login-avatar { 225 + width: 72px; 226 + height: 72px; 227 + border-radius: var(--radius-full); 228 + background: linear-gradient(135deg, var(--accent), #a855f7); 229 + display: flex; 230 + align-items: center; 231 + justify-content: center; 232 + margin: 0 auto 16px; 233 + font-weight: 700; 234 + font-size: 1.5rem; 235 + color: white; 236 + overflow: hidden; 237 + } 238 + 239 + .login-avatar img { 240 + width: 100%; 241 + height: 100%; 242 + object-fit: cover; 243 + } 244 + 245 + .login-avatar-large { 246 + width: 100px; 247 + height: 100px; 248 + border-radius: var(--radius-full); 249 + background: linear-gradient(135deg, var(--accent), #a855f7); 250 + display: flex; 251 + align-items: center; 252 + justify-content: center; 253 + margin-bottom: 20px; 254 + font-weight: 700; 255 + font-size: 2rem; 256 + color: white; 257 + overflow: hidden; 258 + } 259 + 260 + .login-avatar-large img { 261 + width: 100%; 262 + height: 100%; 263 + object-fit: cover; 264 + } 265 + 266 + .login-welcome { 267 + font-size: 1.5rem; 268 + font-weight: 600; 269 + margin-bottom: 32px; 270 + text-align: center; 271 + } 272 + 273 + .login-welcome-name { 274 + font-size: 1.25rem; 275 + font-weight: 600; 276 + margin-bottom: 24px; 277 + } 278 + 279 + .login-actions { 280 + display: flex; 281 + flex-direction: column; 282 + gap: 12px; 283 + width: 100%; 284 + } 285 + 286 + .login-btn { 287 + width: 100%; 288 + padding: 14px 24px; 289 + font-size: 1rem; 290 + font-weight: 600; 291 + } 292 + 293 + .login-submit { 294 + padding: 18px 32px; 295 + font-size: 1.1rem; 296 + font-weight: 600; 297 + }
+262
web/src/css/modals.css
··· 1 + .modal-overlay { 2 + position: fixed; 3 + inset: 0; 4 + background: rgba(0, 0, 0, 0.5); 5 + display: flex; 6 + align-items: center; 7 + justify-content: center; 8 + padding: 16px; 9 + z-index: 50; 10 + animation: fadeIn 0.2s ease-out; 11 + } 12 + 13 + .modal-container { 14 + background: var(--bg-secondary); 15 + border-radius: var(--radius-lg); 16 + width: 100%; 17 + max-width: 28rem; 18 + border: 1px solid var(--border); 19 + box-shadow: var(--shadow-lg); 20 + animation: zoomIn 0.2s ease-out; 21 + } 22 + 23 + .modal-header { 24 + display: flex; 25 + align-items: center; 26 + justify-content: space-between; 27 + padding: 16px; 28 + border-bottom: 1px solid var(--border); 29 + } 30 + 31 + .modal-title { 32 + font-size: 1.25rem; 33 + font-weight: 700; 34 + color: var(--text-primary); 35 + } 36 + 37 + .modal-close-btn { 38 + padding: 8px; 39 + color: var(--text-tertiary); 40 + border-radius: var(--radius-md); 41 + transition: color 0.15s; 42 + } 43 + 44 + .modal-close-btn:hover { 45 + color: var(--text-primary); 46 + background: var(--bg-hover); 47 + } 48 + 49 + .modal-form { 50 + padding: 16px; 51 + display: flex; 52 + flex-direction: column; 53 + gap: 16px; 54 + } 55 + 56 + .icon-picker-tabs { 57 + display: flex; 58 + gap: 4px; 59 + margin-bottom: 12px; 60 + } 61 + 62 + .icon-picker-tab { 63 + flex: 1; 64 + padding: 8px 12px; 65 + background: var(--bg-primary); 66 + border: 1px solid var(--border); 67 + border-radius: var(--radius-md); 68 + color: var(--text-secondary); 69 + font-size: 0.85rem; 70 + font-weight: 500; 71 + cursor: pointer; 72 + transition: all 0.15s ease; 73 + } 74 + 75 + .icon-picker-tab:hover { 76 + background: var(--bg-tertiary); 77 + } 78 + 79 + .icon-picker-tab.active { 80 + background: var(--accent); 81 + border-color: var(--accent); 82 + color: white; 83 + } 84 + 85 + .emoji-picker-wrapper { 86 + display: flex; 87 + flex-direction: column; 88 + gap: 10px; 89 + } 90 + 91 + .emoji-custom-input input { 92 + width: 100%; 93 + } 94 + 95 + .emoji-picker, 96 + .icon-picker { 97 + display: flex; 98 + flex-wrap: wrap; 99 + gap: 4px; 100 + max-height: 120px; 101 + overflow-y: auto; 102 + padding: 8px; 103 + background: var(--bg-primary); 104 + border: 1px solid var(--border); 105 + border-radius: var(--radius-md); 106 + } 107 + 108 + .emoji-option, 109 + .icon-option { 110 + width: 36px; 111 + height: 36px; 112 + display: flex; 113 + align-items: center; 114 + justify-content: center; 115 + font-size: 1.2rem; 116 + background: transparent; 117 + border: 2px solid transparent; 118 + border-radius: var(--radius-sm); 119 + cursor: pointer; 120 + transition: all 0.15s ease; 121 + color: var(--text-secondary); 122 + } 123 + 124 + .emoji-option:hover, 125 + .icon-option:hover { 126 + background: var(--bg-tertiary); 127 + transform: scale(1.1); 128 + color: var(--text-primary); 129 + } 130 + 131 + .emoji-option.selected, 132 + .icon-option.selected { 133 + border-color: var(--accent); 134 + background: var(--accent-subtle); 135 + color: var(--accent); 136 + } 137 + 138 + .modal-actions { 139 + display: flex; 140 + justify-content: flex-end; 141 + gap: 12px; 142 + padding-top: 8px; 143 + } 144 + 145 + @keyframes fadeIn { 146 + from { 147 + opacity: 0; 148 + } 149 + 150 + to { 151 + opacity: 1; 152 + } 153 + } 154 + 155 + @keyframes zoomIn { 156 + from { 157 + opacity: 0; 158 + transform: scale(0.95); 159 + } 160 + 161 + to { 162 + opacity: 1; 163 + transform: scale(1); 164 + } 165 + } 166 + 167 + .form-group { 168 + margin-bottom: 0; 169 + } 170 + 171 + .form-label { 172 + display: block; 173 + font-size: 0.85rem; 174 + font-weight: 600; 175 + color: var(--text-secondary); 176 + margin-bottom: 6px; 177 + } 178 + 179 + .form-input, 180 + .form-textarea, 181 + .form-select { 182 + width: 100%; 183 + padding: 8px 12px; 184 + background: var(--bg-primary); 185 + border: 1px solid var(--border); 186 + border-radius: var(--radius-md); 187 + color: var(--text-primary); 188 + transition: all 0.15s; 189 + } 190 + 191 + .form-input:focus, 192 + .form-textarea:focus, 193 + .form-select:focus { 194 + outline: none; 195 + border-color: var(--accent); 196 + box-shadow: 0 0 0 2px var(--accent-subtle); 197 + } 198 + 199 + .form-textarea { 200 + resize: none; 201 + } 202 + 203 + .input { 204 + width: 100%; 205 + padding: 12px 14px; 206 + font-size: 0.95rem; 207 + color: var(--text-primary); 208 + background: var(--bg-secondary); 209 + border: 1px solid var(--border); 210 + border-radius: var(--radius-md); 211 + outline: none; 212 + transition: all 0.15s ease; 213 + } 214 + 215 + .input:focus { 216 + border-color: var(--accent); 217 + box-shadow: 0 0 0 3px var(--accent-subtle); 218 + } 219 + 220 + .input::placeholder { 221 + color: var(--text-tertiary); 222 + } 223 + 224 + .color-input-container { 225 + display: flex; 226 + align-items: center; 227 + gap: 12px; 228 + background: var(--bg-tertiary); 229 + padding: 8px 12px; 230 + border-radius: var(--radius-md); 231 + border: 1px solid var(--border); 232 + width: fit-content; 233 + } 234 + 235 + .color-input-wrapper { 236 + position: relative; 237 + width: 32px; 238 + height: 32px; 239 + border-radius: var(--radius-full); 240 + overflow: hidden; 241 + border: 2px solid var(--border); 242 + cursor: pointer; 243 + transition: transform 0.1s; 244 + } 245 + 246 + .color-input-wrapper:hover { 247 + transform: scale(1.1); 248 + border-color: var(--accent); 249 + } 250 + 251 + .color-input-wrapper input[type="color"] { 252 + position: absolute; 253 + top: -50%; 254 + left: -50%; 255 + width: 200%; 256 + height: 200%; 257 + padding: 0; 258 + margin: 0; 259 + border: none; 260 + cursor: pointer; 261 + opacity: 0; 262 + }
+65
web/src/css/notifications.css
··· 1 + .notifications-page { 2 + max-width: 680px; 3 + margin: 0 auto; 4 + } 5 + 6 + .notifications-list { 7 + display: flex; 8 + flex-direction: column; 9 + gap: 12px; 10 + } 11 + 12 + .notification-item { 13 + display: flex; 14 + gap: 16px; 15 + align-items: flex-start; 16 + text-decoration: none; 17 + color: inherit; 18 + } 19 + 20 + .notification-item:hover { 21 + background: var(--bg-hover); 22 + } 23 + 24 + .notification-icon { 25 + width: 36px; 26 + height: 36px; 27 + border-radius: var(--radius-full); 28 + display: flex; 29 + align-items: center; 30 + justify-content: center; 31 + background: var(--bg-tertiary); 32 + color: var(--text-secondary); 33 + flex-shrink: 0; 34 + } 35 + 36 + .notification-icon[data-type="like"] { 37 + color: #ef4444; 38 + background: rgba(239, 68, 68, 0.1); 39 + } 40 + 41 + .notification-icon[data-type="reply"] { 42 + color: #3b82f6; 43 + background: rgba(59, 130, 246, 0.1); 44 + } 45 + 46 + .notification-content { 47 + flex: 1; 48 + min-width: 0; 49 + } 50 + 51 + .notification-text { 52 + font-size: 0.95rem; 53 + margin-bottom: 4px; 54 + line-height: 1.4; 55 + color: var(--text-primary); 56 + } 57 + 58 + .notification-text strong { 59 + font-weight: 600; 60 + } 61 + 62 + .notification-time { 63 + font-size: 0.85rem; 64 + color: var(--text-tertiary); 65 + }
+250
web/src/css/profile.css
··· 1 + .profile-header { 2 + display: flex; 3 + align-items: center; 4 + gap: 24px; 5 + margin-bottom: 32px; 6 + padding-bottom: 24px; 7 + border-bottom: 1px solid var(--border); 8 + } 9 + 10 + .profile-avatar { 11 + width: 80px; 12 + height: 80px; 13 + min-width: 80px; 14 + border-radius: 50%; 15 + background: var(--bg-tertiary); 16 + display: flex; 17 + align-items: center; 18 + justify-content: center; 19 + font-weight: 600; 20 + font-size: 2rem; 21 + color: var(--text-secondary); 22 + overflow: hidden; 23 + border: 1px solid var(--border); 24 + } 25 + 26 + .profile-avatar img { 27 + width: 100%; 28 + height: 100%; 29 + object-fit: cover; 30 + } 31 + 32 + .profile-avatar-link { 33 + text-decoration: none; 34 + } 35 + 36 + .profile-info { 37 + flex: 1; 38 + display: flex; 39 + flex-direction: column; 40 + gap: 4px; 41 + } 42 + 43 + .profile-name { 44 + font-size: 1.5rem; 45 + font-weight: 700; 46 + color: var(--text-primary); 47 + line-height: 1.2; 48 + } 49 + 50 + .profile-handle-row { 51 + display: flex; 52 + align-items: center; 53 + gap: 12px; 54 + margin-top: 4px; 55 + flex-wrap: wrap; 56 + } 57 + 58 + .profile-handle-link { 59 + color: var(--text-tertiary); 60 + text-decoration: none; 61 + font-size: 1rem; 62 + transition: color 0.15s; 63 + } 64 + 65 + .profile-handle-link:hover { 66 + color: var(--text-secondary); 67 + } 68 + 69 + .profile-bluesky-link { 70 + display: inline-flex; 71 + align-items: center; 72 + gap: 6px; 73 + color: #3b82f6; 74 + text-decoration: none; 75 + font-size: 0.85rem; 76 + font-weight: 500; 77 + padding: 2px 8px; 78 + border-radius: var(--radius-sm); 79 + background: rgba(59, 130, 246, 0.1); 80 + transition: all 0.15s ease; 81 + } 82 + 83 + .profile-bluesky-link:hover { 84 + background: rgba(59, 130, 246, 0.15); 85 + } 86 + 87 + .profile-stats { 88 + display: flex; 89 + gap: 24px; 90 + margin-top: 12px; 91 + } 92 + 93 + .profile-stat { 94 + color: var(--text-tertiary); 95 + font-size: 0.9rem; 96 + } 97 + 98 + .profile-stat strong { 99 + color: var(--text-primary); 100 + font-weight: 600; 101 + } 102 + 103 + .profile-tabs { 104 + display: flex; 105 + gap: 24px; 106 + margin-bottom: 24px; 107 + border-bottom: 1px solid var(--border); 108 + } 109 + 110 + .profile-tab { 111 + padding: 12px 0; 112 + font-size: 0.95rem; 113 + font-weight: 500; 114 + color: var(--text-tertiary); 115 + background: transparent; 116 + border: none; 117 + cursor: pointer; 118 + transition: all 0.15s ease; 119 + position: relative; 120 + } 121 + 122 + .profile-tab:hover { 123 + color: var(--text-primary); 124 + } 125 + 126 + .profile-tab.active { 127 + color: var(--text-primary); 128 + } 129 + 130 + .profile-tab.active::after { 131 + content: ""; 132 + position: absolute; 133 + bottom: -1px; 134 + left: 0; 135 + right: 0; 136 + height: 2px; 137 + background: var(--text-primary); 138 + } 139 + 140 + .profile-badge-wrapper { 141 + display: inline-flex; 142 + align-items: center; 143 + } 144 + 145 + .profile-badge-clickable { 146 + position: relative; 147 + display: inline-flex; 148 + align-items: center; 149 + cursor: pointer; 150 + margin-left: 8px; 151 + } 152 + 153 + .badge-info-popover { 154 + position: absolute; 155 + top: calc(100% + 8px); 156 + left: 50%; 157 + transform: translateX(-50%); 158 + padding: 16px; 159 + background: var(--bg-elevated); 160 + border: 1px solid var(--border); 161 + border-radius: var(--radius-md); 162 + box-shadow: var(--shadow-lg); 163 + font-size: 0.85rem; 164 + white-space: nowrap; 165 + z-index: 100; 166 + min-width: 200px; 167 + } 168 + 169 + .badge-info-title { 170 + font-weight: 600; 171 + color: var(--text-primary); 172 + margin-bottom: 8px; 173 + } 174 + 175 + .verifier-link { 176 + display: flex; 177 + align-items: center; 178 + gap: 8px; 179 + padding: 8px; 180 + background: var(--bg-tertiary); 181 + border-radius: var(--radius-sm); 182 + text-decoration: none; 183 + transition: background 0.15s ease; 184 + } 185 + 186 + .verifier-link:hover { 187 + background: var(--bg-hover); 188 + } 189 + 190 + .verifier-avatar { 191 + width: 24px; 192 + height: 24px; 193 + border-radius: 50%; 194 + object-fit: cover; 195 + } 196 + 197 + .verifier-name { 198 + color: var(--text-primary); 199 + font-size: 0.85rem; 200 + font-weight: 500; 201 + } 202 + 203 + .profile-suspended { 204 + display: flex; 205 + flex-direction: column; 206 + align-items: center; 207 + justify-content: center; 208 + padding: 60px 20px; 209 + text-align: center; 210 + background: var(--bg-secondary); 211 + border-radius: var(--radius-lg); 212 + margin-top: 20px; 213 + border: 1px solid var(--border); 214 + } 215 + 216 + .suspended-icon { 217 + font-size: 40px; 218 + margin-bottom: 16px; 219 + color: var(--text-tertiary); 220 + } 221 + 222 + .profile-suspended h2 { 223 + color: var(--text-primary); 224 + margin-bottom: 8px; 225 + font-size: 1.25rem; 226 + } 227 + 228 + @media (max-width: 640px) { 229 + .profile-header { 230 + flex-direction: column; 231 + text-align: center; 232 + } 233 + 234 + .profile-info { 235 + align-items: center; 236 + } 237 + 238 + .profile-handle-row { 239 + justify-content: center; 240 + } 241 + 242 + .profile-stats { 243 + justify-content: center; 244 + } 245 + 246 + .profile-tabs { 247 + justify-content: center; 248 + gap: 16px; 249 + } 250 + }
+106
web/src/css/skeleton.css
··· 1 + @keyframes shimmer { 2 + 0% { 3 + background-position: -200% 0; 4 + } 5 + 6 + 100% { 7 + background-position: 200% 0; 8 + } 9 + } 10 + 11 + .skeleton { 12 + background: linear-gradient( 13 + 90deg, 14 + var(--bg-tertiary) 25%, 15 + var(--bg-secondary) 50%, 16 + var(--bg-tertiary) 75% 17 + ); 18 + background-size: 200% 100%; 19 + animation: shimmer 1.5s infinite; 20 + border-radius: var(--radius-sm); 21 + } 22 + 23 + .skeleton-card { 24 + padding: 24px 0; 25 + border-bottom: 1px solid var(--border); 26 + display: flex; 27 + flex-direction: column; 28 + gap: 16px; 29 + } 30 + 31 + .skeleton-header { 32 + display: flex; 33 + align-items: center; 34 + gap: 12px; 35 + } 36 + 37 + .skeleton-avatar { 38 + width: 36px; 39 + height: 36px; 40 + border-radius: 50%; 41 + } 42 + 43 + .skeleton-meta { 44 + display: flex; 45 + flex-direction: column; 46 + gap: 6px; 47 + } 48 + 49 + .skeleton-name { 50 + width: 120px; 51 + height: 14px; 52 + } 53 + 54 + .skeleton-handle { 55 + width: 80px; 56 + height: 12px; 57 + } 58 + 59 + .skeleton-content { 60 + display: flex; 61 + flex-direction: column; 62 + gap: 12px; 63 + padding-left: 48px; 64 + } 65 + 66 + .skeleton-source { 67 + width: 180px; 68 + height: 24px; 69 + border-radius: var(--radius-full); 70 + } 71 + 72 + .skeleton-highlight { 73 + width: 100%; 74 + height: 60px; 75 + border-left: 2px solid var(--border); 76 + } 77 + 78 + .skeleton-text-1 { 79 + width: 90%; 80 + height: 14px; 81 + } 82 + 83 + .skeleton-text-2 { 84 + width: 60%; 85 + height: 14px; 86 + } 87 + 88 + .skeleton-actions { 89 + display: flex; 90 + gap: 24px; 91 + padding-left: 48px; 92 + margin-top: 4px; 93 + } 94 + 95 + .skeleton-action { 96 + width: 24px; 97 + height: 24px; 98 + border-radius: var(--radius-sm); 99 + } 100 + 101 + @media (max-width: 600px) { 102 + .skeleton-content, 103 + .skeleton-actions { 104 + padding-left: 0; 105 + } 106 + }
+730
web/src/css/utilities.css
··· 1 + .legal-content { 2 + max-width: 800px; 3 + margin: 0 auto; 4 + padding: 20px; 5 + } 6 + 7 + .legal-content h1 { 8 + font-size: 2rem; 9 + margin-bottom: 8px; 10 + color: var(--text-primary); 11 + } 12 + 13 + .legal-content h2 { 14 + font-size: 1.4rem; 15 + margin-top: 32px; 16 + margin-bottom: 12px; 17 + color: var(--text-primary); 18 + } 19 + 20 + .legal-content h3 { 21 + font-size: 1.1rem; 22 + margin-top: 20px; 23 + margin-bottom: 8px; 24 + color: var(--text-primary); 25 + } 26 + 27 + .legal-content p { 28 + color: var(--text-secondary); 29 + line-height: 1.7; 30 + margin-bottom: 12px; 31 + } 32 + 33 + .legal-content ul { 34 + color: var(--text-secondary); 35 + line-height: 1.7; 36 + margin-left: 24px; 37 + margin-bottom: 12px; 38 + } 39 + 40 + .legal-content li { 41 + margin-bottom: 6px; 42 + } 43 + 44 + .legal-content a { 45 + color: var(--accent); 46 + text-decoration: none; 47 + } 48 + 49 + .legal-content a:hover { 50 + text-decoration: underline; 51 + } 52 + 53 + .legal-content section { 54 + margin-bottom: 24px; 55 + } 56 + 57 + .text-secondary { 58 + color: var(--text-secondary); 59 + } 60 + 61 + .text-error { 62 + color: var(--error); 63 + } 64 + 65 + .text-center { 66 + text-align: center; 67 + } 68 + 69 + .flex { 70 + display: flex; 71 + } 72 + 73 + .items-center { 74 + align-items: center; 75 + } 76 + 77 + .justify-center { 78 + justify-content: center; 79 + } 80 + 81 + .justify-end { 82 + justify-content: flex-end; 83 + } 84 + 85 + .gap-2 { 86 + gap: 8px; 87 + } 88 + 89 + .gap-3 { 90 + gap: 12px; 91 + } 92 + 93 + .mt-3 { 94 + margin-top: 12px; 95 + } 96 + 97 + .mb-6 { 98 + margin-bottom: 24px; 99 + } 100 + 101 + .composer { 102 + margin-bottom: 24px; 103 + } 104 + 105 + .composer-header { 106 + display: flex; 107 + justify-content: space-between; 108 + align-items: center; 109 + margin-bottom: 12px; 110 + } 111 + 112 + .composer-title { 113 + font-size: 1.1rem; 114 + font-weight: 600; 115 + color: var(--text-primary); 116 + margin: 0; 117 + } 118 + 119 + .composer-input { 120 + width: 100%; 121 + min-height: 120px; 122 + padding: 16px; 123 + background: var(--bg-secondary); 124 + border: 1px solid var(--border); 125 + border-radius: var(--radius-md); 126 + color: var(--text-primary); 127 + font-size: 1rem; 128 + resize: vertical; 129 + transition: all 0.15s ease; 130 + } 131 + 132 + .composer-input:focus { 133 + outline: none; 134 + border-color: var(--accent); 135 + box-shadow: 0 0 0 3px var(--accent-subtle); 136 + } 137 + 138 + .composer-footer { 139 + display: flex; 140 + justify-content: space-between; 141 + align-items: center; 142 + margin-top: 12px; 143 + } 144 + 145 + .composer-actions { 146 + display: flex; 147 + justify-content: flex-end; 148 + gap: 8px; 149 + } 150 + 151 + .composer-count { 152 + font-size: 0.85rem; 153 + color: var(--text-tertiary); 154 + } 155 + 156 + .composer-count.warning { 157 + color: var(--warning); 158 + } 159 + 160 + .composer-count.error { 161 + color: var(--error); 162 + } 163 + 164 + .composer-char-count.warning { 165 + color: var(--warning); 166 + } 167 + 168 + .composer-char-count.error { 169 + color: var(--error); 170 + } 171 + 172 + .composer-add-quote { 173 + width: 100%; 174 + padding: 12px 16px; 175 + margin-bottom: 12px; 176 + background: var(--bg-tertiary); 177 + border: 1px dashed var(--border); 178 + border-radius: var(--radius-md); 179 + color: var(--text-secondary); 180 + font-size: 0.9rem; 181 + cursor: pointer; 182 + transition: all 0.15s ease; 183 + } 184 + 185 + .composer-add-quote:hover { 186 + border-color: var(--accent); 187 + color: var(--accent); 188 + background: var(--accent-subtle); 189 + } 190 + 191 + .composer-quote-input-wrapper { 192 + margin-bottom: 12px; 193 + } 194 + 195 + .composer-quote-input { 196 + width: 100%; 197 + padding: 12px 16px; 198 + background: linear-gradient( 199 + 135deg, 200 + rgba(79, 70, 229, 0.05), 201 + rgba(168, 85, 247, 0.05) 202 + ); 203 + border: 1px solid var(--border); 204 + border-left: 3px solid var(--accent); 205 + border-radius: 0 var(--radius-md) var(--radius-md) 0; 206 + color: var(--text-primary); 207 + font-size: 0.95rem; 208 + font-style: italic; 209 + resize: vertical; 210 + font-family: inherit; 211 + transition: all 0.15s ease; 212 + } 213 + 214 + .composer-quote-input:focus { 215 + outline: none; 216 + border-color: var(--accent); 217 + } 218 + 219 + .composer-quote-input::placeholder { 220 + color: var(--text-tertiary); 221 + font-style: italic; 222 + } 223 + 224 + .composer-quote-remove-btn { 225 + margin-top: 8px; 226 + padding: 6px 12px; 227 + background: none; 228 + border: none; 229 + color: var(--text-tertiary); 230 + font-size: 0.85rem; 231 + cursor: pointer; 232 + } 233 + 234 + .composer-quote-remove-btn:hover { 235 + color: var(--error); 236 + } 237 + 238 + .composer-error { 239 + margin-top: 12px; 240 + padding: 12px; 241 + background: rgba(239, 68, 68, 0.1); 242 + border: 1px solid rgba(239, 68, 68, 0.3); 243 + border-radius: var(--radius-md); 244 + color: var(--error); 245 + font-size: 0.9rem; 246 + } 247 + 248 + .composer-url { 249 + font-size: 0.85rem; 250 + color: var(--text-secondary); 251 + word-break: break-all; 252 + } 253 + 254 + .composer-quote { 255 + position: relative; 256 + padding: 12px 16px; 257 + padding-right: 36px; 258 + background: var(--bg-secondary); 259 + border-left: 3px solid var(--accent); 260 + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 261 + margin-bottom: 16px; 262 + font-style: italic; 263 + color: var(--text-secondary); 264 + } 265 + 266 + .composer-quote-remove { 267 + position: absolute; 268 + top: 8px; 269 + right: 8px; 270 + width: 24px; 271 + height: 24px; 272 + border-radius: var(--radius-full); 273 + background: var(--bg-tertiary); 274 + color: var(--text-secondary); 275 + font-size: 1rem; 276 + display: flex; 277 + align-items: center; 278 + justify-content: center; 279 + } 280 + 281 + .composer-quote-remove:hover { 282 + background: var(--bg-hover); 283 + color: var(--text-primary); 284 + } 285 + 286 + .composer-tags { 287 + flex: 1; 288 + } 289 + 290 + .composer-meta-row { 291 + display: flex; 292 + gap: 12px; 293 + margin-top: 12px; 294 + align-items: flex-start; 295 + } 296 + 297 + .composer-labels-wrapper { 298 + position: relative; 299 + } 300 + 301 + .composer-labels-btn { 302 + display: flex; 303 + align-items: center; 304 + justify-content: center; 305 + width: 42px; 306 + height: 42px; 307 + background: var(--bg-secondary); 308 + border: 1px solid var(--border); 309 + border-radius: var(--radius-md); 310 + cursor: pointer; 311 + color: var(--text-tertiary); 312 + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 313 + position: relative; 314 + } 315 + 316 + .composer-labels-btn:hover { 317 + color: var(--text-primary); 318 + background: var(--bg-hover); 319 + border-color: var(--text-tertiary); 320 + } 321 + 322 + .composer-labels-btn.active { 323 + color: var(--accent); 324 + background: var(--accent-subtle); 325 + border-color: var(--accent); 326 + } 327 + 328 + .composer-labels-badge { 329 + position: absolute; 330 + top: -4px; 331 + right: -4px; 332 + background: var(--error); 333 + color: white; 334 + font-size: 0.7rem; 335 + width: 18px; 336 + height: 18px; 337 + border-radius: 50%; 338 + display: flex; 339 + align-items: center; 340 + justify-content: center; 341 + font-weight: bold; 342 + border: 2px solid var(--bg-primary); 343 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 344 + } 345 + 346 + .composer-labels-picker { 347 + position: absolute; 348 + bottom: 100%; 349 + right: 0; 350 + margin-bottom: 12px; 351 + background: var(--bg-elevated); 352 + border: 1px solid var(--border); 353 + border-radius: var(--radius-md); 354 + padding: 8px 0; 355 + min-width: 200px; 356 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); 357 + z-index: 50; 358 + animation: scaleIn 0.2s ease-out forwards; 359 + transform-origin: bottom right; 360 + } 361 + 362 + @keyframes scaleIn { 363 + from { 364 + opacity: 0; 365 + transform: scale(0.95) translateY(5px); 366 + } 367 + 368 + to { 369 + opacity: 1; 370 + transform: scale(1) translateY(0); 371 + } 372 + } 373 + 374 + .picker-header { 375 + font-size: 0.75rem; 376 + font-weight: 600; 377 + color: var(--text-tertiary); 378 + text-transform: uppercase; 379 + letter-spacing: 0.05em; 380 + margin-bottom: 4px; 381 + padding: 4px 12px 8px; 382 + border-bottom: 1px solid var(--border); 383 + } 384 + 385 + .picker-item { 386 + display: flex; 387 + align-items: center; 388 + gap: 10px; 389 + padding: 10px 14px; 390 + cursor: pointer; 391 + color: var(--text-secondary); 392 + font-size: 0.9rem; 393 + transition: all 0.15s ease; 394 + user-select: none; 395 + } 396 + 397 + .picker-item:hover { 398 + background: var(--bg-hover); 399 + color: var(--text-primary); 400 + } 401 + 402 + .picker-checkbox-wrapper { 403 + position: relative; 404 + width: 18px; 405 + height: 18px; 406 + display: flex; 407 + align-items: center; 408 + justify-content: center; 409 + } 410 + 411 + .picker-checkbox-wrapper input { 412 + position: absolute; 413 + opacity: 0; 414 + width: 100%; 415 + height: 100%; 416 + cursor: pointer; 417 + z-index: 10; 418 + } 419 + 420 + .picker-checkbox-custom { 421 + width: 18px; 422 + height: 18px; 423 + border: 2px solid var(--text-tertiary); 424 + border-radius: 4px; 425 + display: flex; 426 + align-items: center; 427 + justify-content: center; 428 + background: transparent; 429 + transition: all 0.2s ease; 430 + color: white; 431 + } 432 + 433 + .picker-item:hover .picker-checkbox-custom { 434 + border-color: var(--text-secondary); 435 + } 436 + 437 + .picker-checkbox-wrapper input:checked + .picker-checkbox-custom { 438 + background: var(--accent); 439 + border-color: var(--accent); 440 + color: white; 441 + } 442 + 443 + .composer-tags-input { 444 + width: 100%; 445 + padding: 12px 16px; 446 + background: var(--bg-secondary); 447 + border: 1px solid var(--border); 448 + border-radius: var(--radius-md); 449 + color: var(--text-primary); 450 + font-size: 0.95rem; 451 + transition: all 0.15s ease; 452 + } 453 + 454 + .composer-tags-input:focus { 455 + outline: none; 456 + border-color: var(--accent); 457 + box-shadow: 0 0 0 3px var(--accent-subtle); 458 + } 459 + 460 + .composer-tags-input::placeholder { 461 + color: var(--text-tertiary); 462 + } 463 + 464 + .history-panel { 465 + background: var(--bg-tertiary); 466 + border: 1px solid var(--border); 467 + border-radius: var(--radius-md); 468 + padding: 1rem; 469 + margin-bottom: 1rem; 470 + font-size: 0.9rem; 471 + animation: fadeIn 0.2s ease-out; 472 + } 473 + 474 + .history-header { 475 + display: flex; 476 + justify-content: space-between; 477 + align-items: center; 478 + margin-bottom: 1rem; 479 + padding-bottom: 0.5rem; 480 + border-bottom: 1px solid var(--border); 481 + } 482 + 483 + .history-title { 484 + font-weight: 600; 485 + text-transform: uppercase; 486 + letter-spacing: 0.05em; 487 + font-size: 0.75rem; 488 + color: var(--text-secondary); 489 + } 490 + 491 + .history-list { 492 + list-style: none; 493 + display: flex; 494 + flex-direction: column; 495 + gap: 1rem; 496 + } 497 + 498 + .history-item { 499 + position: relative; 500 + padding-left: 1rem; 501 + border-left: 2px solid var(--border); 502 + } 503 + 504 + .history-date { 505 + font-size: 0.75rem; 506 + color: var(--text-tertiary); 507 + margin-bottom: 0.25rem; 508 + } 509 + 510 + .history-content { 511 + color: var(--text-secondary); 512 + white-space: pre-wrap; 513 + } 514 + 515 + .history-close-btn { 516 + color: var(--text-tertiary); 517 + padding: 4px; 518 + border-radius: var(--radius-sm); 519 + transition: all 0.2s; 520 + display: flex; 521 + align-items: center; 522 + justify-content: center; 523 + } 524 + 525 + .history-close-btn:hover { 526 + background: var(--bg-hover); 527 + color: var(--text-primary); 528 + } 529 + 530 + .history-status { 531 + text-align: center; 532 + color: var(--text-tertiary); 533 + font-style: italic; 534 + padding: 1rem; 535 + } 536 + 537 + .share-menu-container { 538 + position: relative; 539 + } 540 + 541 + .share-menu { 542 + position: absolute; 543 + top: 100%; 544 + right: 0; 545 + margin-top: 8px; 546 + background: var(--bg-primary); 547 + border: 1px solid var(--border); 548 + border-radius: var(--radius-lg); 549 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 550 + min-width: 180px; 551 + padding: 8px 0; 552 + z-index: 100; 553 + animation: fadeInUp 0.15s ease; 554 + } 555 + 556 + @keyframes fadeInUp { 557 + from { 558 + opacity: 0; 559 + transform: translateY(-8px); 560 + } 561 + 562 + to { 563 + opacity: 1; 564 + transform: translateY(0); 565 + } 566 + } 567 + 568 + .share-menu-section { 569 + display: flex; 570 + flex-direction: column; 571 + } 572 + 573 + .share-menu-label { 574 + padding: 4px 12px 8px; 575 + font-size: 0.7rem; 576 + font-weight: 600; 577 + text-transform: uppercase; 578 + letter-spacing: 0.05em; 579 + color: var(--text-tertiary); 580 + } 581 + 582 + .share-menu-item { 583 + display: flex; 584 + align-items: center; 585 + gap: 10px; 586 + padding: 10px 14px; 587 + background: none; 588 + border: none; 589 + width: 100%; 590 + text-align: left; 591 + font-size: 0.9rem; 592 + color: var(--text-primary); 593 + cursor: pointer; 594 + transition: all 0.1s ease; 595 + } 596 + 597 + .share-menu-item:hover { 598 + background: var(--bg-tertiary); 599 + } 600 + 601 + .share-menu-icon { 602 + font-size: 1.1rem; 603 + width: 24px; 604 + text-align: center; 605 + } 606 + 607 + .share-menu-divider { 608 + height: 1px; 609 + background: var(--border); 610 + margin: 6px 0; 611 + } 612 + 613 + .bookmark-card { 614 + display: flex; 615 + flex-direction: column; 616 + gap: 16px; 617 + } 618 + 619 + .bookmark-preview { 620 + display: flex; 621 + flex-direction: column; 622 + background: var(--bg-secondary); 623 + border: 1px solid var(--border); 624 + border-radius: var(--radius-md); 625 + overflow: hidden; 626 + text-decoration: none; 627 + transition: all 0.2s ease; 628 + position: relative; 629 + } 630 + 631 + .bookmark-preview:hover { 632 + border-color: var(--accent); 633 + box-shadow: var(--shadow-sm); 634 + transform: translateY(-1px); 635 + } 636 + 637 + .bookmark-preview::before { 638 + content: ""; 639 + position: absolute; 640 + left: 0; 641 + top: 0; 642 + bottom: 0; 643 + width: 4px; 644 + background: var(--accent); 645 + opacity: 0.7; 646 + } 647 + 648 + .bookmark-preview-content { 649 + padding: 16px 20px; 650 + display: flex; 651 + flex-direction: column; 652 + gap: 8px; 653 + } 654 + 655 + .bookmark-preview-header { 656 + display: flex; 657 + align-items: center; 658 + gap: 8px; 659 + margin-bottom: 4px; 660 + } 661 + 662 + .bookmark-preview-site { 663 + display: flex; 664 + align-items: center; 665 + gap: 6px; 666 + font-size: 0.75rem; 667 + font-weight: 600; 668 + color: var(--accent); 669 + text-transform: uppercase; 670 + letter-spacing: 0.03em; 671 + } 672 + 673 + .bookmark-preview-title { 674 + font-size: 1rem; 675 + font-weight: 600; 676 + line-height: 1.4; 677 + color: var(--text-primary); 678 + margin: 0; 679 + display: -webkit-box; 680 + -webkit-line-clamp: 2; 681 + line-clamp: 2; 682 + -webkit-box-orient: vertical; 683 + overflow: hidden; 684 + } 685 + 686 + .bookmark-preview-desc { 687 + font-size: 0.875rem; 688 + color: var(--text-secondary); 689 + line-height: 1.5; 690 + margin: 0; 691 + display: -webkit-box; 692 + -webkit-line-clamp: 2; 693 + line-clamp: 2; 694 + -webkit-box-orient: vertical; 695 + overflow: hidden; 696 + } 697 + 698 + .bookmark-preview-arrow { 699 + display: flex; 700 + align-items: center; 701 + justify-content: center; 702 + color: var(--text-tertiary); 703 + padding: 0 4px; 704 + transition: all 0.2s ease; 705 + } 706 + 707 + .bookmark-preview:hover .bookmark-preview-arrow { 708 + color: var(--accent); 709 + transform: translateX(2px); 710 + } 711 + 712 + .bookmark-description { 713 + font-size: 0.9rem; 714 + color: var(--text-secondary); 715 + margin: 0; 716 + line-height: 1.5; 717 + } 718 + 719 + .bookmark-meta { 720 + display: flex; 721 + align-items: center; 722 + gap: 12px; 723 + margin-top: 12px; 724 + font-size: 0.85rem; 725 + color: var(--text-tertiary); 726 + } 727 + 728 + .bookmark-time { 729 + color: var(--text-tertiary); 730 + }
+13 -3424
web/src/index.css
··· 1 - :root { 2 - --bg-primary: #0c0a14; 3 - --bg-secondary: #110e1c; 4 - --bg-tertiary: #1a1528; 5 - --bg-card: #14111f; 6 - --bg-hover: #1e1932; 7 - --bg-elevated: #1a1528; 8 - 9 - --text-primary: #f4f0ff; 10 - --text-secondary: #a89ec8; 11 - --text-tertiary: #6b5f8a; 12 - 13 - --accent: #a855f7; 14 - --accent-hover: #c084fc; 15 - --accent-subtle: rgba(168, 85, 247, 0.15); 16 - 17 - --border: #2d2640; 18 - --border-hover: #3d3560; 19 - 20 - --success: #22c55e; 21 - --error: #ef4444; 22 - --warning: #f59e0b; 23 - 24 - --radius-sm: 6px; 25 - --radius-md: 10px; 26 - --radius-lg: 16px; 27 - --radius-full: 9999px; 28 - 29 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 30 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 31 - --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 40px rgba(168, 85, 247, 0.1); 32 - --shadow-glow: 0 0 20px rgba(168, 85, 247, 0.3); 33 - 34 - --font-sans: 35 - "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 36 - } 37 - 38 - * { 39 - margin: 0; 40 - padding: 0; 41 - box-sizing: border-box; 42 - } 43 - 44 - html { 45 - font-size: 16px; 46 - } 47 - 48 - body { 49 - font-family: var(--font-sans); 50 - background: var(--bg-primary); 51 - color: var(--text-primary); 52 - line-height: 1.6; 53 - min-height: 100vh; 54 - -webkit-font-smoothing: antialiased; 55 - -moz-osx-font-smoothing: grayscale; 56 - } 57 - 58 - a { 59 - color: var(--accent); 60 - text-decoration: none; 61 - transition: color 0.15s ease; 62 - } 63 - 64 - a:hover { 65 - color: var(--accent-hover); 66 - } 67 - 68 - button { 69 - font-family: inherit; 70 - cursor: pointer; 71 - border: none; 72 - background: none; 73 - } 74 - 75 - input, 76 - textarea { 77 - font-family: inherit; 78 - font-size: inherit; 79 - } 80 - 81 - .app { 82 - min-height: 100vh; 83 - display: flex; 84 - flex-direction: column; 85 - } 86 - 87 - .main-content { 88 - flex: 1; 89 - max-width: 680px; 90 - width: 100%; 91 - margin: 0 auto; 92 - padding: 24px 16px; 93 - } 94 - 95 - .btn { 96 - display: inline-flex; 97 - align-items: center; 98 - justify-content: center; 99 - gap: 8px; 100 - padding: 10px 20px; 101 - font-size: 0.9rem; 102 - font-weight: 500; 103 - border-radius: var(--radius-md); 104 - transition: all 0.15s ease; 105 - } 106 - 107 - .btn-primary { 108 - background: var(--accent); 109 - color: white; 110 - } 111 - 112 - .btn-primary:hover { 113 - background: var(--accent-hover); 114 - transform: translateY(-1px); 115 - box-shadow: var(--shadow-md); 116 - } 117 - 118 - .btn-secondary { 119 - background: var(--bg-tertiary); 120 - color: var(--text-primary); 121 - border: 1px solid var(--border); 122 - } 123 - 124 - .btn-secondary:hover { 125 - background: var(--bg-hover); 126 - border-color: var(--border-hover); 127 - } 128 - 129 - .btn-ghost { 130 - color: var(--text-secondary); 131 - padding: 8px 12px; 132 - } 133 - 134 - .btn-ghost:hover { 135 - color: var(--text-primary); 136 - background: var(--bg-tertiary); 137 - } 138 - 139 - .card { 140 - background: var(--bg-card); 141 - border: 1px solid var(--border); 142 - border-radius: var(--radius-lg); 143 - padding: 24px; 144 - transition: all 0.2s ease; 145 - position: relative; 146 - } 147 - 148 - .card:hover { 149 - border-color: var(--border-hover); 150 - box-shadow: var(--shadow-md); 151 - transform: translateY(-1px); 152 - } 153 - 154 - .annotation-card { 155 - display: flex; 156 - flex-direction: column; 157 - gap: 16px; 158 - } 159 - 160 - .annotation-header { 161 - display: flex; 162 - justify-content: space-between; 163 - align-items: flex-start; 164 - gap: 12px; 165 - } 166 - 167 - .annotation-header-left { 168 - display: flex; 169 - align-items: center; 170 - gap: 12px; 171 - flex: 1; 172 - min-width: 0; 173 - } 174 - 175 - .annotation-avatar { 176 - width: 40px; 177 - height: 40px; 178 - min-width: 40px; 179 - border-radius: var(--radius-full); 180 - background: linear-gradient(135deg, var(--accent), #a855f7); 181 - display: flex; 182 - align-items: center; 183 - justify-content: center; 184 - font-weight: 600; 185 - font-size: 0.95rem; 186 - color: white; 187 - overflow: hidden; 188 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 189 - } 190 - 191 - .annotation-avatar img { 192 - width: 100%; 193 - height: 100%; 194 - object-fit: cover; 195 - } 196 - 197 - .annotation-meta { 198 - display: flex; 199 - flex-direction: column; 200 - justify-content: center; 201 - line-height: 1.3; 202 - } 203 - 204 - .annotation-avatar-link { 205 - text-decoration: none; 206 - border-radius: var(--radius-full); 207 - transition: transform 0.15s ease; 208 - } 209 - 210 - .annotation-avatar-link:hover { 211 - transform: scale(1.05); 212 - } 213 - 214 - .annotation-author-row { 215 - display: flex; 216 - align-items: center; 217 - gap: 6px; 218 - flex-wrap: wrap; 219 - } 220 - 221 - .annotation-author { 222 - font-weight: 600; 223 - color: var(--text-primary); 224 - font-size: 0.95rem; 225 - } 226 - 227 - .annotation-handle { 228 - font-size: 0.85rem; 229 - color: var(--text-tertiary); 230 - text-decoration: none; 231 - display: flex; 232 - align-items: center; 233 - gap: 3px; 234 - } 235 - 236 - .annotation-handle:hover { 237 - color: var(--accent); 238 - } 239 - 240 - .annotation-time { 241 - font-size: 0.8rem; 242 - color: var(--text-tertiary); 243 - } 244 - 245 - .annotation-content { 246 - display: flex; 247 - flex-direction: column; 248 - gap: 12px; 249 - } 250 - 251 - .annotation-source { 252 - display: inline-flex; 253 - align-items: center; 254 - gap: 6px; 255 - font-size: 0.8rem; 256 - color: var(--text-tertiary); 257 - text-decoration: none; 258 - padding: 4px 10px; 259 - background: var(--bg-tertiary); 260 - border-radius: var(--radius-full); 261 - width: fit-content; 262 - transition: all 0.15s ease; 263 - max-width: 100%; 264 - overflow: hidden; 265 - text-overflow: ellipsis; 266 - white-space: nowrap; 267 - } 268 - 269 - .annotation-source:hover { 270 - color: var(--text-primary); 271 - background: var(--bg-hover); 272 - } 273 - 274 - .annotation-source-title { 275 - color: var(--text-secondary); 276 - opacity: 0.8; 277 - } 278 - 279 - .annotation-highlight { 280 - display: block; 281 - position: relative; 282 - padding: 16px 20px; 283 - background: linear-gradient( 284 - 135deg, 285 - rgba(79, 70, 229, 0.03), 286 - rgba(168, 85, 247, 0.03) 287 - ); 288 - border-left: 3px solid var(--accent); 289 - border-radius: 4px var(--radius-md) var(--radius-md) 4px; 290 - text-decoration: none; 291 - transition: all 0.2s ease; 292 - margin: 4px 0; 293 - } 294 - 295 - .annotation-highlight:hover { 296 - background: linear-gradient( 297 - 135deg, 298 - rgba(79, 70, 229, 0.08), 299 - rgba(168, 85, 247, 0.08) 300 - ); 301 - transform: translateX(2px); 302 - } 303 - 304 - .annotation-highlight mark { 305 - background: transparent; 306 - color: var(--text-primary); 307 - font-style: italic; 308 - font-size: 1.05rem; 309 - line-height: 1.6; 310 - font-weight: 400; 311 - display: inline; 312 - } 313 - 314 - .annotation-text { 315 - font-size: 1rem; 316 - line-height: 1.65; 317 - color: var(--text-primary); 318 - white-space: pre-wrap; 319 - } 320 - 321 - .annotation-actions { 322 - display: flex; 323 - align-items: center; 324 - justify-content: space-between; 325 - padding-top: 16px; 326 - margin-top: 8px; 327 - border-top: 1px solid rgba(255, 255, 255, 0.03); 328 - } 329 - 330 - .annotation-actions-left { 331 - display: flex; 332 - align-items: center; 333 - gap: 8px; 334 - } 335 - 336 - .annotation-action { 337 - display: flex; 338 - align-items: center; 339 - gap: 6px; 340 - color: var(--text-tertiary); 341 - font-size: 0.85rem; 342 - font-weight: 500; 343 - padding: 6px 10px; 344 - border-radius: var(--radius-md); 345 - transition: all 0.2s ease; 346 - background: transparent; 347 - cursor: pointer; 348 - } 349 - 350 - .annotation-action:hover { 351 - color: var(--text-secondary); 352 - background: var(--bg-elevated); 353 - } 354 - 355 - .annotation-action.liked { 356 - color: #ef4444; 357 - background: rgba(239, 68, 68, 0.05); 358 - } 359 - 360 - .annotation-action.liked:hover { 361 - background: rgba(239, 68, 68, 0.1); 362 - } 363 - 364 - .annotation-action.active { 365 - color: var(--accent); 366 - background: var(--accent-subtle); 367 - } 368 - 369 - .action-icon-only { 370 - padding: 8px; 371 - } 372 - 373 - .annotation-delete { 374 - background: none; 375 - border: none; 376 - cursor: pointer; 377 - padding: 8px; 378 - font-size: 1rem; 379 - color: var(--text-tertiary); 380 - transition: all 0.2s ease; 381 - border-radius: var(--radius-md); 382 - opacity: 0.6; 383 - } 384 - 385 - .annotation-delete:hover { 386 - color: var(--error); 387 - background: rgba(239, 68, 68, 0.1); 388 - opacity: 1; 389 - } 390 - 391 - .annotation-delete:disabled { 392 - cursor: not-allowed; 393 - opacity: 0.3; 394 - } 395 - 396 - .share-menu-container { 397 - position: relative; 398 - } 399 - 400 - .share-menu { 401 - position: absolute; 402 - top: 100%; 403 - right: 0; 404 - margin-top: 8px; 405 - background: var(--bg-primary); 406 - border: 1px solid var(--border); 407 - border-radius: var(--radius-lg); 408 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 409 - min-width: 180px; 410 - padding: 8px 0; 411 - z-index: 100; 412 - animation: fadeInUp 0.15s ease; 413 - } 414 - 415 - @keyframes fadeInUp { 416 - from { 417 - opacity: 0; 418 - transform: translateY(-8px); 419 - } 420 - 421 - to { 422 - opacity: 1; 423 - transform: translateY(0); 424 - } 425 - } 426 - 427 - .share-menu-section { 428 - display: flex; 429 - flex-direction: column; 430 - } 431 - 432 - .share-menu-label { 433 - padding: 4px 12px 8px; 434 - font-size: 0.7rem; 435 - font-weight: 600; 436 - text-transform: uppercase; 437 - letter-spacing: 0.05em; 438 - color: var(--text-tertiary); 439 - } 440 - 441 - .share-menu-item { 442 - display: flex; 443 - align-items: center; 444 - gap: 10px; 445 - padding: 10px 14px; 446 - background: none; 447 - border: none; 448 - width: 100%; 449 - text-align: left; 450 - font-size: 0.9rem; 451 - color: var(--text-primary); 452 - cursor: pointer; 453 - transition: all 0.1s ease; 454 - } 455 - 456 - .share-menu-item:hover { 457 - background: var(--bg-tertiary); 458 - } 459 - 460 - .share-menu-icon { 461 - font-size: 1.1rem; 462 - width: 24px; 463 - text-align: center; 464 - } 465 - 466 - .share-menu-divider { 467 - height: 1px; 468 - background: var(--border); 469 - margin: 6px 0; 470 - } 471 - 472 - .feed { 473 - display: flex; 474 - flex-direction: column; 475 - gap: 16px; 476 - } 477 - 478 - .feed-header { 479 - display: flex; 480 - align-items: center; 481 - justify-content: space-between; 482 - margin-bottom: 8px; 483 - } 484 - 485 - .feed-title { 486 - font-size: 1.5rem; 487 - font-weight: 700; 488 - } 489 - 490 - .page-header { 491 - margin-bottom: 32px; 492 - } 493 - 494 - .page-title { 495 - font-size: 2rem; 496 - font-weight: 700; 497 - margin-bottom: 8px; 498 - } 499 - 500 - .page-description { 501 - color: var(--text-secondary); 502 - font-size: 1.1rem; 503 - } 504 - 505 - .url-input-wrapper { 506 - margin-bottom: 32px; 507 - } 508 - 509 - .url-input-container { 510 - display: flex; 511 - gap: 12px; 512 - } 513 - 514 - .url-input { 515 - flex: 1; 516 - padding: 14px 18px; 517 - background: var(--bg-secondary); 518 - border: 1px solid var(--border); 519 - border-radius: var(--radius-md); 520 - color: var(--text-primary); 521 - font-size: 1rem; 522 - transition: all 0.15s ease; 523 - } 524 - 525 - .url-input:focus { 526 - outline: none; 527 - border-color: var(--accent); 528 - box-shadow: 0 0 0 3px var(--accent-subtle); 529 - } 530 - 531 - .url-input::placeholder { 532 - color: var(--text-tertiary); 533 - } 534 - 535 - .empty-state { 536 - text-align: center; 537 - padding: 60px 20px; 538 - color: var(--text-secondary); 539 - } 540 - 541 - .empty-state-icon { 542 - font-size: 3rem; 543 - margin-bottom: 16px; 544 - opacity: 0.5; 545 - } 546 - 547 - .empty-state-title { 548 - font-size: 1.25rem; 549 - font-weight: 600; 550 - color: var(--text-primary); 551 - margin-bottom: 8px; 552 - } 553 - 554 - .empty-state-text { 555 - font-size: 1rem; 556 - max-width: 400px; 557 - margin: 0 auto; 558 - } 559 - 560 - .feed-filters { 561 - display: flex; 562 - gap: 8px; 563 - margin-bottom: 24px; 564 - padding: 4px; 565 - background: var(--bg-tertiary); 566 - border-radius: var(--radius-lg); 567 - width: fit-content; 568 - } 569 - 570 - .login-page { 571 - display: flex; 572 - flex-direction: column; 573 - align-items: center; 574 - justify-content: center; 575 - min-height: 70vh; 576 - padding: 60px 20px; 577 - width: 100%; 578 - max-width: 500px; 579 - margin: 0 auto; 580 - } 581 - 582 - .login-at-logo { 583 - font-size: 5rem; 584 - font-weight: 800; 585 - color: var(--accent); 586 - margin-bottom: 24px; 587 - line-height: 1; 588 - } 589 - 590 - .login-heading { 591 - font-size: 1.5rem; 592 - font-weight: 600; 593 - margin-bottom: 32px; 594 - display: flex; 595 - align-items: center; 596 - gap: 10px; 597 - text-align: center; 598 - line-height: 1.4; 599 - } 600 - 601 - .login-help-btn { 602 - background: none; 603 - border: none; 604 - color: var(--text-tertiary); 605 - cursor: pointer; 606 - padding: 4px; 607 - display: flex; 608 - align-items: center; 609 - transition: color 0.15s; 610 - flex-shrink: 0; 611 - } 612 - 613 - .login-help-btn:hover { 614 - color: var(--accent); 615 - } 616 - 617 - .login-help-text { 618 - background: var(--bg-elevated); 619 - border: 1px solid var(--border); 620 - border-radius: var(--radius-md); 621 - padding: 16px 20px; 622 - margin-bottom: 24px; 623 - font-size: 0.95rem; 624 - color: var(--text-secondary); 625 - line-height: 1.6; 626 - text-align: center; 627 - } 628 - 629 - .login-help-text code { 630 - background: var(--bg-tertiary); 631 - padding: 2px 8px; 632 - border-radius: var(--radius-sm); 633 - font-size: 0.9rem; 634 - } 635 - 636 - .login-form { 637 - display: flex; 638 - flex-direction: column; 639 - gap: 20px; 640 - width: 100%; 641 - } 642 - 643 - .login-input-wrapper { 644 - position: relative; 645 - } 646 - 647 - .login-input { 648 - width: 100%; 649 - padding: 18px 20px; 650 - background: var(--bg-elevated); 651 - border: 2px solid var(--border); 652 - border-radius: var(--radius-lg); 653 - color: var(--text-primary); 654 - font-size: 1.1rem; 655 - transition: 656 - border-color 0.15s, 657 - box-shadow 0.15s; 658 - } 659 - 660 - .login-input:focus { 661 - outline: none; 662 - border-color: var(--accent); 663 - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); 664 - } 665 - 666 - .login-input::placeholder { 667 - color: var(--text-tertiary); 668 - } 669 - 670 - .login-suggestions { 671 - position: absolute; 672 - top: calc(100% + 8px); 673 - left: 0; 674 - right: 0; 675 - background: var(--bg-card); 676 - border: 1px solid var(--border); 677 - border-radius: var(--radius-lg); 678 - box-shadow: var(--shadow-lg); 679 - overflow: hidden; 680 - z-index: 100; 681 - } 682 - 683 - .login-suggestion { 684 - display: flex; 685 - align-items: center; 686 - gap: 14px; 687 - width: 100%; 688 - padding: 14px 18px; 689 - background: transparent; 690 - border: none; 691 - cursor: pointer; 692 - text-align: left; 693 - color: var(--text-primary); 694 - transition: background 0.1s; 695 - } 696 - 697 - .login-suggestion:hover, 698 - .login-suggestion.selected { 699 - background: var(--bg-elevated); 700 - } 701 - 702 - .login-suggestion-avatar { 703 - width: 44px; 704 - height: 44px; 705 - border-radius: var(--radius-full); 706 - background: linear-gradient(135deg, var(--accent), #a855f7); 707 - display: flex; 708 - align-items: center; 709 - justify-content: center; 710 - flex-shrink: 0; 711 - overflow: hidden; 712 - font-size: 0.9rem; 713 - font-weight: 600; 714 - color: white; 715 - } 716 - 717 - .login-suggestion-avatar img { 718 - width: 100%; 719 - height: 100%; 720 - object-fit: cover; 721 - } 722 - 723 - .login-suggestion-info { 724 - display: flex; 725 - flex-direction: column; 726 - gap: 2px; 727 - min-width: 0; 728 - } 729 - 730 - .login-suggestion-name { 731 - font-weight: 600; 732 - font-size: 1rem; 733 - color: var(--text-primary); 734 - white-space: nowrap; 735 - overflow: hidden; 736 - text-overflow: ellipsis; 737 - } 738 - 739 - .login-suggestion-handle { 740 - font-size: 0.9rem; 741 - color: var(--text-secondary); 742 - white-space: nowrap; 743 - overflow: hidden; 744 - text-overflow: ellipsis; 745 - } 746 - 747 - .login-error { 748 - padding: 12px 16px; 749 - background: rgba(239, 68, 68, 0.1); 750 - border: 1px solid rgba(239, 68, 68, 0.3); 751 - border-radius: var(--radius-md); 752 - color: #ef4444; 753 - font-size: 0.9rem; 754 - text-align: center; 755 - } 756 - 757 - .login-submit { 758 - padding: 18px 32px; 759 - font-size: 1.1rem; 760 - font-weight: 600; 761 - } 762 - 763 - .login-avatar-large { 764 - width: 100px; 765 - height: 100px; 766 - border-radius: var(--radius-full); 767 - background: linear-gradient(135deg, var(--accent), #a855f7); 768 - display: flex; 769 - align-items: center; 770 - justify-content: center; 771 - margin-bottom: 20px; 772 - font-weight: 700; 773 - font-size: 2rem; 774 - color: white; 775 - overflow: hidden; 776 - } 777 - 778 - .login-avatar-large img { 779 - width: 100%; 780 - height: 100%; 781 - object-fit: cover; 782 - } 783 - 784 - .login-welcome { 785 - font-size: 1.5rem; 786 - font-weight: 600; 787 - margin-bottom: 32px; 788 - text-align: center; 789 - } 790 - 791 - .login-actions { 792 - display: flex; 793 - flex-direction: column; 794 - gap: 12px; 795 - width: 100%; 796 - } 797 - 798 - .login-avatar { 799 - width: 72px; 800 - height: 72px; 801 - border-radius: var(--radius-full); 802 - background: linear-gradient(135deg, var(--accent), #a855f7); 803 - display: flex; 804 - align-items: center; 805 - justify-content: center; 806 - margin: 0 auto 16px; 807 - font-weight: 700; 808 - font-size: 1.5rem; 809 - color: white; 810 - overflow: hidden; 811 - } 812 - 813 - .login-avatar img { 814 - width: 100%; 815 - height: 100%; 816 - object-fit: cover; 817 - } 818 - 819 - .login-welcome-name { 820 - font-size: 1.25rem; 821 - font-weight: 600; 822 - margin-bottom: 24px; 823 - } 824 - 825 - .login-actions { 826 - display: flex; 827 - flex-direction: column; 828 - gap: 12px; 829 - } 830 - 831 - .btn-bluesky { 832 - background: #0085ff; 833 - color: white; 834 - display: flex; 835 - align-items: center; 836 - justify-content: center; 837 - gap: 10px; 838 - transition: 839 - background 0.2s, 840 - transform 0.2s; 841 - } 842 - 843 - .btn-bluesky:hover { 844 - background: #0070dd; 845 - transform: translateY(-1px); 846 - } 847 - 848 - .login-btn { 849 - width: 100%; 850 - padding: 14px 24px; 851 - font-size: 1rem; 852 - font-weight: 600; 853 - } 854 - 855 - .login-brand { 856 - display: flex; 857 - align-items: center; 858 - justify-content: center; 859 - gap: 12px; 860 - margin-bottom: 24px; 861 - } 862 - 863 - .login-brand-icon { 864 - width: 48px; 865 - height: 48px; 866 - background: linear-gradient(135deg, var(--accent), #a855f7); 867 - border-radius: var(--radius-lg); 868 - display: flex; 869 - align-items: center; 870 - justify-content: center; 871 - font-size: 1.75rem; 872 - font-weight: 800; 873 - color: white; 874 - } 875 - 876 - .login-brand-name { 877 - font-size: 1.75rem; 878 - font-weight: 700; 879 - } 880 - 881 - .login-form { 882 - display: flex; 883 - flex-direction: column; 884 - gap: 16px; 885 - } 886 - 887 - .login-input-wrapper { 888 - position: relative; 889 - } 890 - 891 - .login-input { 892 - width: 100%; 893 - padding: 14px 16px; 894 - background: var(--bg-elevated); 895 - border: 1px solid var(--border); 896 - border-radius: var(--radius-md); 897 - color: var(--text-primary); 898 - font-size: 1rem; 899 - transition: 900 - border-color 0.15s, 901 - box-shadow 0.15s; 902 - } 903 - 904 - .login-input:focus { 905 - outline: none; 906 - border-color: var(--accent); 907 - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); 908 - } 909 - 910 - .login-input::placeholder { 911 - color: var(--text-tertiary); 912 - } 913 - 914 - .login-suggestions { 915 - position: absolute; 916 - top: calc(100% + 4px); 917 - left: 0; 918 - right: 0; 919 - background: var(--bg-card); 920 - border: 1px solid var(--border); 921 - border-radius: var(--radius-md); 922 - box-shadow: var(--shadow-lg); 923 - overflow: hidden; 924 - z-index: 100; 925 - } 926 - 927 - .login-suggestion { 928 - display: flex; 929 - align-items: center; 930 - gap: 12px; 931 - width: 100%; 932 - padding: 12px 16px; 933 - background: transparent; 934 - border: none; 935 - cursor: pointer; 936 - text-align: left; 937 - transition: background 0.1s; 938 - } 939 - 940 - .login-suggestion:hover, 941 - .login-suggestion.selected { 942 - background: var(--bg-elevated); 943 - } 944 - 945 - .login-suggestion-avatar { 946 - width: 40px; 947 - height: 40px; 948 - border-radius: var(--radius-full); 949 - background: linear-gradient(135deg, var(--accent), #a855f7); 950 - display: flex; 951 - align-items: center; 952 - justify-content: center; 953 - flex-shrink: 0; 954 - overflow: hidden; 955 - font-size: 0.875rem; 956 - font-weight: 600; 957 - color: white; 958 - } 959 - 960 - .login-suggestion-avatar img { 961 - width: 100%; 962 - height: 100%; 963 - object-fit: cover; 964 - } 965 - 966 - .login-suggestion-info { 967 - display: flex; 968 - flex-direction: column; 969 - min-width: 0; 970 - } 971 - 972 - .login-suggestion-name { 973 - font-weight: 600; 974 - color: var(--text-primary); 975 - white-space: nowrap; 976 - overflow: hidden; 977 - text-overflow: ellipsis; 978 - } 979 - 980 - .login-suggestion-handle { 981 - font-size: 0.875rem; 982 - color: var(--text-secondary); 983 - white-space: nowrap; 984 - overflow: hidden; 985 - text-overflow: ellipsis; 986 - } 987 - 988 - .login-error { 989 - padding: 12px 16px; 990 - background: rgba(239, 68, 68, 0.1); 991 - border: 1px solid rgba(239, 68, 68, 0.3); 992 - border-radius: var(--radius-md); 993 - color: #ef4444; 994 - font-size: 0.875rem; 995 - } 996 - 997 - .login-legal { 998 - font-size: 0.75rem; 999 - color: var(--text-tertiary); 1000 - line-height: 1.5; 1001 - margin-top: 16px; 1002 - } 1003 - 1004 - .profile-header { 1005 - display: flex; 1006 - align-items: center; 1007 - gap: 20px; 1008 - margin-bottom: 32px; 1009 - padding-bottom: 24px; 1010 - border-bottom: 1px solid var(--border); 1011 - } 1012 - 1013 - .profile-avatar { 1014 - width: 80px; 1015 - height: 80px; 1016 - min-width: 80px; 1017 - border-radius: var(--radius-full); 1018 - background: linear-gradient(135deg, var(--accent), #a855f7); 1019 - display: flex; 1020 - align-items: center; 1021 - justify-content: center; 1022 - font-weight: 700; 1023 - font-size: 2rem; 1024 - color: white; 1025 - overflow: hidden; 1026 - } 1027 - 1028 - .profile-avatar img { 1029 - width: 100%; 1030 - height: 100%; 1031 - object-fit: cover; 1032 - } 1033 - 1034 - .profile-avatar-link { 1035 - text-decoration: none; 1036 - } 1037 - 1038 - .profile-info { 1039 - flex: 1; 1040 - } 1041 - 1042 - .profile-name { 1043 - font-size: 1.5rem; 1044 - font-weight: 700; 1045 - } 1046 - 1047 - .profile-handle-link { 1048 - color: var(--text-secondary); 1049 - text-decoration: none; 1050 - } 1051 - 1052 - .profile-handle-link:hover { 1053 - color: var(--accent); 1054 - text-decoration: underline; 1055 - } 1056 - 1057 - .profile-bluesky-link { 1058 - display: inline-flex; 1059 - align-items: center; 1060 - gap: 6px; 1061 - color: #0085ff; 1062 - text-decoration: none; 1063 - font-size: 0.95rem; 1064 - padding: 4px 10px; 1065 - border-radius: var(--radius-md); 1066 - background: rgba(0, 133, 255, 0.1); 1067 - transition: all 0.15s ease; 1068 - } 1069 - 1070 - .profile-bluesky-link:hover { 1071 - background: rgba(0, 133, 255, 0.2); 1072 - color: #0070dd; 1073 - } 1074 - 1075 - .profile-stats { 1076 - display: flex; 1077 - gap: 24px; 1078 - margin-top: 8px; 1079 - } 1080 - 1081 - .profile-stat { 1082 - color: var(--text-secondary); 1083 - font-size: 0.9rem; 1084 - } 1085 - 1086 - .profile-stat strong { 1087 - color: var(--text-primary); 1088 - } 1089 - 1090 - .profile-tabs { 1091 - display: flex; 1092 - gap: 0; 1093 - margin-bottom: 24px; 1094 - border-bottom: 1px solid var(--border); 1095 - } 1096 - 1097 - .profile-tab { 1098 - padding: 12px 20px; 1099 - font-size: 0.9rem; 1100 - font-weight: 500; 1101 - color: var(--text-secondary); 1102 - background: transparent; 1103 - border: none; 1104 - border-bottom: 2px solid transparent; 1105 - cursor: pointer; 1106 - transition: all 0.15s ease; 1107 - margin-bottom: -1px; 1108 - } 1109 - 1110 - .profile-tab:hover { 1111 - color: var(--text-primary); 1112 - background: var(--bg-tertiary); 1113 - } 1114 - 1115 - .profile-tab.active { 1116 - color: var(--accent); 1117 - border-bottom-color: var(--accent); 1118 - } 1119 - 1120 - .bookmark-description { 1121 - font-size: 0.9rem; 1122 - color: var(--text-secondary); 1123 - margin: 0; 1124 - line-height: 1.5; 1125 - } 1126 - 1127 - .bookmark-meta { 1128 - display: flex; 1129 - align-items: center; 1130 - gap: 12px; 1131 - margin-top: 12px; 1132 - font-size: 0.85rem; 1133 - color: var(--text-tertiary); 1134 - } 1135 - 1136 - .bookmark-time { 1137 - color: var(--text-tertiary); 1138 - } 1139 - 1140 - .composer { 1141 - margin-bottom: 24px; 1142 - } 1143 - 1144 - .composer-textarea { 1145 - width: 100%; 1146 - min-height: 120px; 1147 - padding: 16px; 1148 - background: var(--bg-secondary); 1149 - border: 1px solid var(--border); 1150 - border-radius: var(--radius-md); 1151 - color: var(--text-primary); 1152 - font-size: 1rem; 1153 - resize: vertical; 1154 - transition: all 0.15s ease; 1155 - } 1156 - 1157 - .composer-textarea:focus { 1158 - outline: none; 1159 - border-color: var(--accent); 1160 - box-shadow: 0 0 0 3px var(--accent-subtle); 1161 - } 1162 - 1163 - .composer-footer { 1164 - display: flex; 1165 - justify-content: space-between; 1166 - align-items: center; 1167 - margin-top: 12px; 1168 - } 1169 - 1170 - .composer-char-count { 1171 - font-size: 0.85rem; 1172 - color: var(--text-tertiary); 1173 - } 1174 - 1175 - .composer-char-count.warning { 1176 - color: var(--warning); 1177 - } 1178 - 1179 - .composer-char-count.error { 1180 - color: var(--error); 1181 - } 1182 - 1183 - .composer-add-quote { 1184 - width: 100%; 1185 - padding: 12px 16px; 1186 - margin-bottom: 12px; 1187 - background: var(--bg-tertiary); 1188 - border: 1px dashed var(--border); 1189 - border-radius: var(--radius-md); 1190 - color: var(--text-secondary); 1191 - font-size: 0.9rem; 1192 - cursor: pointer; 1193 - transition: all 0.15s ease; 1194 - } 1195 - 1196 - .composer-add-quote:hover { 1197 - border-color: var(--accent); 1198 - color: var(--accent); 1199 - background: var(--accent-subtle); 1200 - } 1201 - 1202 - .composer-quote-input-wrapper { 1203 - margin-bottom: 12px; 1204 - } 1205 - 1206 - .composer-quote-input { 1207 - width: 100%; 1208 - padding: 12px 16px; 1209 - background: linear-gradient( 1210 - 135deg, 1211 - rgba(79, 70, 229, 0.05), 1212 - rgba(168, 85, 247, 0.05) 1213 - ); 1214 - border: 1px solid var(--border); 1215 - border-left: 3px solid var(--accent); 1216 - border-radius: 0 var(--radius-md) var(--radius-md) 0; 1217 - color: var(--text-primary); 1218 - font-size: 0.95rem; 1219 - font-style: italic; 1220 - resize: vertical; 1221 - font-family: inherit; 1222 - transition: all 0.15s ease; 1223 - } 1224 - 1225 - .composer-quote-input:focus { 1226 - outline: none; 1227 - border-color: var(--accent); 1228 - } 1229 - 1230 - .composer-quote-input::placeholder { 1231 - color: var(--text-tertiary); 1232 - font-style: italic; 1233 - } 1234 - 1235 - .composer-quote-remove-btn { 1236 - margin-top: 8px; 1237 - padding: 6px 12px; 1238 - background: none; 1239 - border: none; 1240 - color: var(--text-tertiary); 1241 - font-size: 0.85rem; 1242 - cursor: pointer; 1243 - } 1244 - 1245 - .composer-quote-remove-btn:hover { 1246 - color: var(--error); 1247 - } 1248 - 1249 - @keyframes shimmer { 1250 - 0% { 1251 - background-position: -200% 0; 1252 - } 1253 - 1254 - 100% { 1255 - background-position: 200% 0; 1256 - } 1257 - } 1258 - 1259 - .skeleton { 1260 - background: linear-gradient( 1261 - 90deg, 1262 - var(--bg-tertiary) 25%, 1263 - var(--bg-hover) 50%, 1264 - var(--bg-tertiary) 75% 1265 - ); 1266 - background-size: 200% 100%; 1267 - animation: shimmer 1.5s infinite; 1268 - border-radius: var(--radius-sm); 1269 - } 1270 - 1271 - .skeleton-text { 1272 - height: 1em; 1273 - margin-bottom: 8px; 1274 - } 1275 - 1276 - .skeleton-text:last-child { 1277 - width: 60%; 1278 - } 1279 - 1280 - @media (max-width: 640px) { 1281 - .main-content { 1282 - padding: 16px 12px; 1283 - } 1284 - 1285 - .navbar-inner { 1286 - padding: 0 16px; 1287 - } 1288 - 1289 - .page-title { 1290 - font-size: 1.5rem; 1291 - } 1292 - 1293 - .url-input-container { 1294 - flex-direction: column; 1295 - } 1296 - 1297 - .profile-header { 1298 - flex-direction: column; 1299 - text-align: center; 1300 - } 1301 - 1302 - .profile-stats { 1303 - justify-content: center; 1304 - } 1305 - } 1306 - 1307 - .main { 1308 - flex: 1; 1309 - width: 100%; 1310 - } 1311 - 1312 - .page-container { 1313 - max-width: 680px; 1314 - margin: 0 auto; 1315 - padding: 24px 16px; 1316 - } 1317 - 1318 - .navbar-logo { 1319 - width: 32px; 1320 - height: 32px; 1321 - background: linear-gradient(135deg, var(--accent), #8b5cf6); 1322 - border-radius: var(--radius-sm); 1323 - display: flex; 1324 - align-items: center; 1325 - justify-content: center; 1326 - font-weight: 700; 1327 - font-size: 1rem; 1328 - color: white; 1329 - } 1330 - 1331 - .navbar-user { 1332 - display: flex; 1333 - align-items: center; 1334 - gap: 8px; 1335 - } 1336 - 1337 - .navbar-avatar { 1338 - width: 36px; 1339 - height: 36px; 1340 - border-radius: var(--radius-full); 1341 - background: linear-gradient(135deg, var(--accent), #a855f7); 1342 - display: flex; 1343 - align-items: center; 1344 - justify-content: center; 1345 - font-weight: 600; 1346 - font-size: 0.85rem; 1347 - color: white; 1348 - text-decoration: none; 1349 - } 1350 - 1351 - .btn-sm { 1352 - padding: 6px 12px; 1353 - font-size: 0.85rem; 1354 - } 1355 - 1356 - .composer-url { 1357 - font-size: 0.85rem; 1358 - color: var(--text-secondary); 1359 - word-break: break-all; 1360 - } 1361 - 1362 - .composer-quote { 1363 - position: relative; 1364 - padding: 12px 16px; 1365 - padding-right: 36px; 1366 - background: var(--bg-secondary); 1367 - border-left: 3px solid var(--accent); 1368 - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 1369 - margin-bottom: 16px; 1370 - font-style: italic; 1371 - color: var(--text-secondary); 1372 - } 1373 - 1374 - .composer-quote-remove { 1375 - position: absolute; 1376 - top: 8px; 1377 - right: 8px; 1378 - width: 24px; 1379 - height: 24px; 1380 - border-radius: var(--radius-full); 1381 - background: var(--bg-tertiary); 1382 - color: var(--text-secondary); 1383 - font-size: 1rem; 1384 - display: flex; 1385 - align-items: center; 1386 - justify-content: center; 1387 - } 1388 - 1389 - .composer-quote-remove:hover { 1390 - background: var(--bg-hover); 1391 - color: var(--text-primary); 1392 - } 1393 - 1394 - .composer-input { 1395 - width: 100%; 1396 - min-height: 120px; 1397 - padding: 16px; 1398 - background: var(--bg-secondary); 1399 - border: 1px solid var(--border); 1400 - border-radius: var(--radius-md); 1401 - color: var(--text-primary); 1402 - font-size: 1rem; 1403 - resize: vertical; 1404 - transition: all 0.15s ease; 1405 - } 1406 - 1407 - .composer-input:focus { 1408 - outline: none; 1409 - border-color: var(--accent); 1410 - box-shadow: 0 0 0 3px var(--accent-subtle); 1411 - } 1412 - 1413 - .composer-input::placeholder { 1414 - color: var(--text-tertiary); 1415 - } 1416 - 1417 - .composer-tags { 1418 - margin-top: 12px; 1419 - } 1420 - 1421 - .composer-tags-input { 1422 - width: 100%; 1423 - padding: 12px 16px; 1424 - background: var(--bg-secondary); 1425 - border: 1px solid var(--border); 1426 - border-radius: var(--radius-md); 1427 - color: var(--text-primary); 1428 - font-size: 0.95rem; 1429 - transition: all 0.15s ease; 1430 - } 1431 - 1432 - .composer-tags-input:focus { 1433 - outline: none; 1434 - border-color: var(--accent); 1435 - box-shadow: 0 0 0 3px var(--accent-subtle); 1436 - } 1437 - 1438 - .composer-tags-input::placeholder { 1439 - color: var(--text-tertiary); 1440 - } 1441 - 1442 - .composer-footer { 1443 - display: flex; 1444 - justify-content: space-between; 1445 - align-items: center; 1446 - margin-top: 12px; 1447 - } 1448 - 1449 - .composer-count { 1450 - font-size: 0.85rem; 1451 - color: var(--text-tertiary); 1452 - } 1453 - 1454 - .composer-actions { 1455 - display: flex; 1456 - gap: 8px; 1457 - } 1458 - 1459 - .composer-error { 1460 - margin-top: 12px; 1461 - padding: 12px; 1462 - background: rgba(239, 68, 68, 0.1); 1463 - border: 1px solid rgba(239, 68, 68, 0.3); 1464 - border-radius: var(--radius-md); 1465 - color: var(--error); 1466 - font-size: 0.9rem; 1467 - } 1468 - 1469 - .annotation-tags { 1470 - display: flex; 1471 - flex-wrap: wrap; 1472 - gap: 6px; 1473 - margin-top: 12px; 1474 - margin-bottom: 8px; 1475 - } 1476 - 1477 - .annotation-tag { 1478 - display: inline-flex; 1479 - align-items: center; 1480 - padding: 4px 10px; 1481 - background: var(--bg-tertiary); 1482 - color: var(--text-secondary); 1483 - font-size: 0.8rem; 1484 - font-weight: 500; 1485 - border-radius: var(--radius-full); 1486 - transition: all 0.15s ease; 1487 - border: 1px solid transparent; 1488 - text-decoration: none; 1489 - } 1490 - 1491 - .annotation-tag:hover { 1492 - background: var(--bg-hover); 1493 - color: var(--text-primary); 1494 - border-color: var(--border); 1495 - transform: translateY(-1px); 1496 - } 1497 - 1498 - .url-input-wrapper { 1499 - margin-bottom: 24px; 1500 - } 1501 - 1502 - .url-input { 1503 - width: 100%; 1504 - padding: 16px; 1505 - background: var(--bg-secondary); 1506 - border: 1px solid var(--border); 1507 - border-radius: var(--radius-md); 1508 - color: var(--text-primary); 1509 - font-size: 1.1rem; 1510 - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 1511 - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 1512 - } 1513 - 1514 - .url-input:focus { 1515 - outline: none; 1516 - border-color: var(--accent); 1517 - box-shadow: 0 0 0 4px var(--accent-subtle); 1518 - background: var(--bg-primary); 1519 - } 1520 - 1521 - .url-input::placeholder { 1522 - color: var(--text-tertiary); 1523 - } 1524 - 1525 - .annotation-detail-page { 1526 - max-width: 680px; 1527 - margin: 0 auto; 1528 - padding: 24px 16px; 1529 - } 1530 - 1531 - .annotation-detail-header { 1532 - margin-bottom: 24px; 1533 - } 1534 - 1535 - .back-link { 1536 - color: var(--text-secondary); 1537 - text-decoration: none; 1538 - font-size: 0.9rem; 1539 - } 1540 - 1541 - .back-link:hover { 1542 - color: var(--accent); 1543 - } 1544 - 1545 - .replies-section { 1546 - margin-top: 32px; 1547 - } 1548 - 1549 - .replies-title { 1550 - font-size: 1.1rem; 1551 - font-weight: 600; 1552 - margin-bottom: 16px; 1553 - color: var(--text-primary); 1554 - } 1555 - 1556 - .reply-form { 1557 - margin-bottom: 24px; 1558 - } 1559 - 1560 - .reply-input { 1561 - width: 100%; 1562 - padding: 12px; 1563 - border: 1px solid var(--border); 1564 - border-radius: var(--radius-md); 1565 - font-size: 0.95rem; 1566 - resize: vertical; 1567 - margin-bottom: 12px; 1568 - font-family: inherit; 1569 - } 1570 - 1571 - .reply-input:focus { 1572 - outline: none; 1573 - border-color: var(--accent); 1574 - box-shadow: 0 0 0 3px var(--accent-subtle); 1575 - } 1576 - 1577 - .replies-list { 1578 - display: flex; 1579 - flex-direction: column; 1580 - gap: 12px; 1581 - } 1582 - 1583 - .reply-card { 1584 - padding: 16px; 1585 - background: var(--bg-secondary); 1586 - border-radius: var(--radius-md); 1587 - border: 1px solid var(--border); 1588 - } 1589 - 1590 - .reply-header { 1591 - display: flex; 1592 - align-items: center; 1593 - gap: 12px; 1594 - margin-bottom: 12px; 1595 - } 1596 - 1597 - .reply-avatar-link { 1598 - text-decoration: none; 1599 - } 1600 - 1601 - .reply-avatar { 1602 - width: 36px; 1603 - height: 36px; 1604 - min-width: 36px; 1605 - border-radius: var(--radius-full); 1606 - background: linear-gradient(135deg, var(--accent), #a855f7); 1607 - display: flex; 1608 - align-items: center; 1609 - justify-content: center; 1610 - font-weight: 600; 1611 - font-size: 0.85rem; 1612 - color: white; 1613 - overflow: hidden; 1614 - } 1615 - 1616 - .reply-avatar img { 1617 - width: 100%; 1618 - height: 100%; 1619 - object-fit: cover; 1620 - } 1621 - 1622 - .reply-meta { 1623 - flex: 1; 1624 - min-width: 0; 1625 - } 1626 - 1627 - .reply-author { 1628 - font-weight: 600; 1629 - color: var(--text-primary); 1630 - } 1631 - 1632 - .reply-handle { 1633 - font-size: 0.85rem; 1634 - color: var(--text-tertiary); 1635 - text-decoration: none; 1636 - margin-left: 6px; 1637 - } 1638 - 1639 - .reply-handle:hover { 1640 - color: var(--accent); 1641 - text-decoration: underline; 1642 - } 1643 - 1644 - .reply-time { 1645 - font-size: 0.85rem; 1646 - color: var(--text-tertiary); 1647 - white-space: nowrap; 1648 - } 1649 - 1650 - .reply-text { 1651 - color: var(--text-primary); 1652 - line-height: 1.5; 1653 - margin: 0; 1654 - } 1655 - 1656 - .replies-title { 1657 - display: flex; 1658 - align-items: center; 1659 - gap: 8px; 1660 - } 1661 - 1662 - .replies-title svg { 1663 - color: var(--accent); 1664 - } 1665 - 1666 - .replies-list-threaded { 1667 - display: flex; 1668 - flex-direction: column; 1669 - gap: 8px; 1670 - } 1671 - 1672 - .reply-card-threaded { 1673 - padding: 16px; 1674 - transition: background 0.15s ease; 1675 - } 1676 - 1677 - .reply-card-threaded .reply-header { 1678 - margin-bottom: 8px; 1679 - } 1680 - 1681 - .reply-card-threaded .reply-meta { 1682 - display: flex; 1683 - align-items: center; 1684 - gap: 6px; 1685 - flex-wrap: wrap; 1686 - } 1687 - 1688 - .reply-dot { 1689 - color: var(--text-tertiary); 1690 - font-size: 0.75rem; 1691 - } 1692 - 1693 - .reply-actions { 1694 - display: flex; 1695 - gap: 4px; 1696 - margin-left: auto; 1697 - } 1698 - 1699 - .reply-action-btn { 1700 - background: none; 1701 - border: none; 1702 - padding: 4px 8px; 1703 - color: var(--text-tertiary); 1704 - cursor: pointer; 1705 - border-radius: var(--radius-sm); 1706 - transition: all 0.15s ease; 1707 - display: flex; 1708 - align-items: center; 1709 - justify-content: center; 1710 - } 1711 - 1712 - .reply-action-btn:hover { 1713 - color: var(--accent); 1714 - background: var(--accent-subtle); 1715 - } 1716 - 1717 - .reply-action-delete:hover { 1718 - color: var(--error); 1719 - background: rgba(239, 68, 68, 0.1); 1720 - } 1721 - 1722 - .replying-to-banner { 1723 - display: flex; 1724 - align-items: center; 1725 - justify-content: space-between; 1726 - padding: 8px 12px; 1727 - margin-bottom: 12px; 1728 - background: var(--accent-subtle); 1729 - border-radius: var(--radius-sm); 1730 - font-size: 0.85rem; 1731 - color: var(--text-secondary); 1732 - } 1733 - 1734 - .cancel-reply { 1735 - background: none; 1736 - border: none; 1737 - font-size: 1.2rem; 1738 - color: var(--text-tertiary); 1739 - cursor: pointer; 1740 - padding: 0 4px; 1741 - line-height: 1; 1742 - } 1743 - 1744 - .cancel-reply:hover { 1745 - color: var(--text-primary); 1746 - } 1747 - 1748 - .reply-form.card { 1749 - padding: 16px; 1750 - margin-bottom: 16px; 1751 - } 1752 - 1753 - .reply-form-actions { 1754 - display: flex; 1755 - justify-content: flex-end; 1756 - } 1757 - 1758 - .inline-replies { 1759 - margin-top: 16px; 1760 - padding-top: 16px; 1761 - border-top: 1px solid var(--border); 1762 - display: flex; 1763 - flex-direction: column; 1764 - gap: 16px; 1765 - } 1766 - 1767 - .main-reply-composer { 1768 - margin-top: 16px; 1769 - background: var(--bg-secondary); 1770 - padding: 12px; 1771 - border-radius: var(--radius-md); 1772 - } 1773 - 1774 - .reply-input { 1775 - width: 100%; 1776 - min-height: 80px; 1777 - padding: 12px; 1778 - border: 1px solid var(--border); 1779 - border-radius: var(--radius-md); 1780 - background: var(--bg-card); 1781 - color: var(--text-primary); 1782 - font-family: inherit; 1783 - font-size: 0.95rem; 1784 - resize: vertical; 1785 - display: block; 1786 - } 1787 - 1788 - .reply-input:focus { 1789 - border-color: var(--accent); 1790 - outline: none; 1791 - } 1792 - 1793 - .reply-input.small { 1794 - min-height: 60px; 1795 - font-size: 0.9rem; 1796 - margin-bottom: 8px; 1797 - } 1798 - 1799 - .composer-actions { 1800 - display: flex; 1801 - justify-content: flex-end; 1802 - } 1803 - 1804 - .btn-block { 1805 - width: 100%; 1806 - text-align: left; 1807 - padding: 8px 12px; 1808 - color: var(--text-secondary); 1809 - background: var(--bg-tertiary); 1810 - border-radius: var(--radius-md); 1811 - margin-top: 8px; 1812 - font-size: 0.9rem; 1813 - cursor: pointer; 1814 - transition: all 0.2s; 1815 - } 1816 - 1817 - .btn-block:hover { 1818 - background: var(--border); 1819 - color: var(--text-primary); 1820 - } 1821 - 1822 - .annotation-action.active { 1823 - color: var(--accent); 1824 - } 1825 - 1826 - .new-page { 1827 - max-width: 600px; 1828 - margin: 0 auto; 1829 - display: flex; 1830 - flex-direction: column; 1831 - gap: 32px; 1832 - } 1833 - 1834 - .loading-spinner { 1835 - width: 32px; 1836 - height: 32px; 1837 - border: 3px solid var(--border); 1838 - border-top-color: var(--accent); 1839 - border-radius: 50%; 1840 - animation: spin 0.8s linear infinite; 1841 - margin: 60px auto; 1842 - } 1843 - 1844 - @keyframes spin { 1845 - to { 1846 - transform: rotate(360deg); 1847 - } 1848 - } 1849 - 1850 - .navbar { 1851 - position: sticky; 1852 - top: 0; 1853 - z-index: 1000; 1854 - background: rgba(12, 10, 20, 0.95); 1855 - backdrop-filter: blur(12px); 1856 - -webkit-backdrop-filter: blur(12px); 1857 - border-bottom: 1px solid var(--border); 1858 - } 1859 - 1860 - .navbar-inner { 1861 - max-width: 1200px; 1862 - margin: 0 auto; 1863 - padding: 12px 24px; 1864 - display: flex; 1865 - align-items: center; 1866 - justify-content: space-between; 1867 - gap: 24px; 1868 - } 1869 - 1870 - .navbar-brand { 1871 - display: flex; 1872 - align-items: center; 1873 - gap: 10px; 1874 - text-decoration: none; 1875 - flex-shrink: 0; 1876 - } 1877 - 1878 - .navbar-logo { 1879 - width: 32px; 1880 - height: 32px; 1881 - background: linear-gradient(135deg, var(--accent), #8b5cf6); 1882 - border-radius: 8px; 1883 - display: flex; 1884 - align-items: center; 1885 - justify-content: center; 1886 - font-weight: 700; 1887 - font-size: 1rem; 1888 - color: white; 1889 - } 1890 - 1891 - .navbar-title { 1892 - font-weight: 700; 1893 - font-size: 1.25rem; 1894 - color: var(--text-primary); 1895 - } 1896 - 1897 - .navbar-center { 1898 - display: flex; 1899 - align-items: center; 1900 - gap: 8px; 1901 - background: var(--bg-tertiary); 1902 - padding: 4px; 1903 - border-radius: var(--radius-lg); 1904 - } 1905 - 1906 - .navbar-link { 1907 - display: flex; 1908 - align-items: center; 1909 - gap: 6px; 1910 - padding: 8px 16px; 1911 - font-size: 0.9rem; 1912 - font-weight: 500; 1913 - color: var(--text-secondary); 1914 - text-decoration: none; 1915 - border-radius: var(--radius-md); 1916 - transition: all 0.15s ease; 1917 - } 1918 - 1919 - .navbar-link:hover { 1920 - color: var(--text-primary); 1921 - background: var(--bg-hover); 1922 - } 1923 - 1924 - .navbar-link.active { 1925 - color: var(--text-primary); 1926 - background: var(--bg-card); 1927 - box-shadow: var(--shadow-sm); 1928 - } 1929 - 1930 - .navbar-right { 1931 - display: flex; 1932 - align-items: center; 1933 - gap: 12px; 1934 - flex-shrink: 0; 1935 - } 1936 - 1937 - .navbar-icon-link { 1938 - display: flex; 1939 - align-items: center; 1940 - justify-content: center; 1941 - width: 36px; 1942 - height: 36px; 1943 - color: var(--text-tertiary); 1944 - border-radius: var(--radius-md); 1945 - transition: all 0.15s ease; 1946 - } 1947 - 1948 - .navbar-icon-link:hover { 1949 - color: var(--text-primary); 1950 - background: var(--bg-tertiary); 1951 - } 1952 - 1953 - .navbar-icon-link.active { 1954 - color: var(--accent); 1955 - background: var(--accent-subtle); 1956 - } 1957 - 1958 - .navbar-new-btn { 1959 - display: flex; 1960 - align-items: center; 1961 - gap: 6px; 1962 - padding: 8px 14px; 1963 - background: linear-gradient(135deg, var(--accent), #8b5cf6); 1964 - color: white; 1965 - font-size: 0.85rem; 1966 - font-weight: 600; 1967 - text-decoration: none; 1968 - border-radius: var(--radius-full); 1969 - transition: all 0.2s ease; 1970 - } 1971 - 1972 - .navbar-new-btn:hover { 1973 - transform: translateY(-1px); 1974 - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); 1975 - color: white; 1976 - } 1977 - 1978 - .navbar-user-section { 1979 - display: flex; 1980 - align-items: center; 1981 - gap: 4px; 1982 - } 1983 - 1984 - .navbar-avatar { 1985 - width: 32px; 1986 - height: 32px; 1987 - border-radius: var(--radius-full); 1988 - background: linear-gradient(135deg, var(--accent), #a855f7); 1989 - display: flex; 1990 - align-items: center; 1991 - justify-content: center; 1992 - font-weight: 600; 1993 - font-size: 0.75rem; 1994 - color: white; 1995 - text-decoration: none; 1996 - transition: transform 0.15s ease; 1997 - } 1998 - 1999 - .navbar-avatar:hover { 2000 - transform: scale(1.05); 2001 - } 2002 - 2003 - .navbar-logout { 2004 - width: 24px; 2005 - height: 24px; 2006 - border: none; 2007 - background: transparent; 2008 - color: var(--text-tertiary); 2009 - font-size: 1.25rem; 2010 - cursor: pointer; 2011 - border-radius: var(--radius-sm); 2012 - transition: all 0.15s ease; 2013 - display: flex; 2014 - align-items: center; 2015 - justify-content: center; 2016 - } 2017 - 2018 - .navbar-logout:hover { 2019 - color: var(--error); 2020 - background: rgba(239, 68, 68, 0.1); 2021 - } 2022 - 2023 - .navbar-signin { 2024 - padding: 8px 16px; 2025 - background: var(--accent); 2026 - color: white; 2027 - font-size: 0.9rem; 2028 - font-weight: 500; 2029 - text-decoration: none; 2030 - border-radius: var(--radius-full); 2031 - transition: all 0.15s ease; 2032 - } 2033 - 2034 - .navbar-signin:hover { 2035 - background: var(--accent-hover); 2036 - color: white; 2037 - } 2038 - 2039 - .navbar-user-menu { 2040 - position: relative; 2041 - } 2042 - 2043 - .navbar-avatar-btn { 2044 - width: 36px; 2045 - height: 36px; 2046 - border-radius: var(--radius-full); 2047 - background: linear-gradient(135deg, var(--accent), #a855f7); 2048 - border: none; 2049 - cursor: pointer; 2050 - overflow: hidden; 2051 - display: flex; 2052 - align-items: center; 2053 - justify-content: center; 2054 - transition: 2055 - transform 0.15s ease, 2056 - box-shadow 0.15s ease; 2057 - } 2058 - 2059 - .navbar-avatar-btn:hover { 2060 - transform: scale(1.05); 2061 - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); 2062 - } 2063 - 2064 - .navbar-avatar-img { 2065 - width: 100%; 2066 - height: 100%; 2067 - object-fit: cover; 2068 - } 2069 - 2070 - .navbar-avatar-text { 2071 - font-weight: 600; 2072 - font-size: 0.75rem; 2073 - color: white; 2074 - } 2075 - 2076 - .navbar-dropdown { 2077 - position: absolute; 2078 - top: calc(100% + 8px); 2079 - right: 0; 2080 - min-width: 200px; 2081 - background: var(--bg-card); 2082 - border: 1px solid var(--border); 2083 - border-radius: var(--radius-lg); 2084 - box-shadow: var(--shadow-lg); 2085 - overflow: hidden; 2086 - z-index: 1001; 2087 - animation: dropdownFade 0.15s ease; 2088 - } 2089 - 2090 - @keyframes dropdownFade { 2091 - from { 2092 - opacity: 0; 2093 - transform: translateY(-8px); 2094 - } 2095 - 2096 - to { 2097 - opacity: 1; 2098 - transform: translateY(0); 2099 - } 2100 - } 2101 - 2102 - .navbar-dropdown-header { 2103 - padding: 12px 16px; 2104 - background: var(--bg-secondary); 2105 - } 2106 - 2107 - .navbar-dropdown-name { 2108 - display: block; 2109 - font-weight: 600; 2110 - color: var(--text-primary); 2111 - font-size: 0.9rem; 2112 - } 2113 - 2114 - .navbar-dropdown-handle { 2115 - display: block; 2116 - color: var(--text-tertiary); 2117 - font-size: 0.8rem; 2118 - margin-top: 2px; 2119 - } 2120 - 2121 - .navbar-dropdown-divider { 2122 - height: 1px; 2123 - background: var(--border); 2124 - } 2125 - 2126 - .navbar-dropdown-item { 2127 - display: flex; 2128 - align-items: center; 2129 - gap: 10px; 2130 - width: 100%; 2131 - padding: 12px 16px; 2132 - font-size: 0.9rem; 2133 - color: var(--text-primary); 2134 - text-decoration: none; 2135 - background: none; 2136 - border: none; 2137 - cursor: pointer; 2138 - transition: background 0.15s ease; 2139 - text-align: left; 2140 - } 2141 - 2142 - .navbar-dropdown-item:hover { 2143 - background: var(--bg-tertiary); 2144 - } 2145 - 2146 - .navbar-dropdown-logout { 2147 - color: var(--error); 2148 - border-top: 1px solid var(--border); 2149 - } 2150 - 2151 - .navbar-dropdown-logout:hover { 2152 - background: rgba(239, 68, 68, 0.1); 2153 - } 2154 - 2155 - @media (max-width: 768px) { 2156 - .navbar-inner { 2157 - padding: 10px 16px; 2158 - } 2159 - 2160 - .navbar-title { 2161 - display: none; 2162 - } 2163 - 2164 - .navbar-center { 2165 - display: none; 2166 - } 2167 - 2168 - .navbar-new-btn span { 2169 - display: none; 2170 - } 2171 - 2172 - .navbar-new-btn { 2173 - width: 36px; 2174 - height: 36px; 2175 - padding: 0; 2176 - justify-content: center; 2177 - } 2178 - } 2179 - 2180 - .collections-list { 2181 - display: flex; 2182 - flex-direction: column; 2183 - gap: 2px; 2184 - background: var(--bg-card); 2185 - border: 1px solid var(--border); 2186 - border-radius: var(--radius-lg); 2187 - overflow: hidden; 2188 - } 2189 - 2190 - .collection-row { 2191 - display: flex; 2192 - align-items: center; 2193 - background: var(--bg-card); 2194 - transition: background 0.15s ease; 2195 - } 2196 - 2197 - .collection-row:not(:last-child) { 2198 - border-bottom: 1px solid var(--border); 2199 - } 2200 - 2201 - .collection-row:hover { 2202 - background: var(--bg-secondary); 2203 - } 2204 - 2205 - .collection-row-content { 2206 - flex: 1; 2207 - display: flex; 2208 - align-items: center; 2209 - gap: 16px; 2210 - padding: 16px 20px; 2211 - text-decoration: none; 2212 - min-width: 0; 2213 - } 2214 - 2215 - .collection-row-icon { 2216 - width: 44px; 2217 - height: 44px; 2218 - min-width: 44px; 2219 - display: flex; 2220 - align-items: center; 2221 - justify-content: center; 2222 - background: linear-gradient( 2223 - 135deg, 2224 - rgba(79, 70, 229, 0.1), 2225 - rgba(168, 85, 247, 0.15) 2226 - ); 2227 - color: var(--accent); 2228 - border-radius: var(--radius-md); 2229 - transition: all 0.2s ease; 2230 - } 2231 - 2232 - .collection-row:hover .collection-row-icon { 2233 - background: linear-gradient( 2234 - 135deg, 2235 - rgba(79, 70, 229, 0.15), 2236 - rgba(168, 85, 247, 0.2) 2237 - ); 2238 - transform: scale(1.05); 2239 - } 2240 - 2241 - .collection-row-info { 2242 - flex: 1; 2243 - min-width: 0; 2244 - } 2245 - 2246 - .collection-row-name { 2247 - font-size: 1rem; 2248 - font-weight: 600; 2249 - color: var(--text-primary); 2250 - margin: 0 0 2px 0; 2251 - white-space: nowrap; 2252 - overflow: hidden; 2253 - text-overflow: ellipsis; 2254 - } 2255 - 2256 - .collection-row:hover .collection-row-name { 2257 - color: var(--accent); 2258 - } 2259 - 2260 - .collection-row-desc { 2261 - font-size: 0.85rem; 2262 - color: var(--text-secondary); 2263 - margin: 0; 2264 - white-space: nowrap; 2265 - overflow: hidden; 2266 - text-overflow: ellipsis; 2267 - } 2268 - 2269 - .collection-row-arrow { 2270 - color: var(--text-tertiary); 2271 - opacity: 0; 2272 - transition: all 0.2s ease; 2273 - } 2274 - 2275 - .collection-row:hover .collection-row-arrow { 2276 - opacity: 1; 2277 - color: var(--accent); 2278 - transform: translateX(2px); 2279 - } 2280 - 2281 - .collection-row-edit { 2282 - padding: 10px; 2283 - margin-right: 12px; 2284 - color: var(--text-tertiary); 2285 - background: none; 2286 - border: none; 2287 - border-radius: var(--radius-sm); 2288 - cursor: pointer; 2289 - opacity: 0; 2290 - transition: all 0.15s ease; 2291 - } 2292 - 2293 - .collection-row:hover .collection-row-edit { 2294 - opacity: 1; 2295 - } 2296 - 2297 - .collection-row-edit:hover { 2298 - color: var(--text-primary); 2299 - background: var(--bg-tertiary); 2300 - } 2301 - 2302 - .back-link { 2303 - display: inline-flex; 2304 - align-items: center; 2305 - gap: 6px; 2306 - color: var(--text-tertiary); 2307 - font-size: 0.9rem; 2308 - font-weight: 500; 2309 - text-decoration: none; 2310 - margin-bottom: 24px; 2311 - transition: color 0.15s ease; 2312 - } 2313 - 2314 - .back-link:hover { 2315 - color: var(--accent); 2316 - } 2317 - 2318 - .collection-detail-header { 2319 - display: flex; 2320 - gap: 20px; 2321 - padding: 24px; 2322 - background: var(--bg-card); 2323 - border: 1px solid var(--border); 2324 - border-radius: var(--radius-lg); 2325 - margin-bottom: 32px; 2326 - position: relative; 2327 - } 2328 - 2329 - .collection-detail-icon { 2330 - width: 56px; 2331 - height: 56px; 2332 - min-width: 56px; 2333 - display: flex; 2334 - align-items: center; 2335 - justify-content: center; 2336 - background: linear-gradient( 2337 - 135deg, 2338 - rgba(79, 70, 229, 0.1), 2339 - rgba(168, 85, 247, 0.1) 2340 - ); 2341 - color: var(--accent); 2342 - border-radius: var(--radius-md); 2343 - } 2344 - 2345 - .collection-detail-info { 2346 - flex: 1; 2347 - min-width: 0; 2348 - } 2349 - 2350 - .collection-detail-visibility { 2351 - display: flex; 2352 - align-items: center; 2353 - gap: 6px; 2354 - font-size: 0.8rem; 2355 - font-weight: 600; 2356 - color: var(--accent); 2357 - text-transform: capitalize; 2358 - margin-bottom: 8px; 2359 - } 2360 - 2361 - .collection-detail-title { 2362 - font-size: 1.5rem; 2363 - font-weight: 700; 2364 - color: var(--text-primary); 2365 - margin-bottom: 8px; 2366 - line-height: 1.3; 2367 - } 2368 - 2369 - .collection-detail-desc { 2370 - color: var(--text-secondary); 2371 - font-size: 1rem; 2372 - line-height: 1.5; 2373 - margin-bottom: 12px; 2374 - max-width: 600px; 2375 - } 2376 - 2377 - .collection-detail-stats { 2378 - display: flex; 2379 - align-items: center; 2380 - gap: 8px; 2381 - font-size: 0.85rem; 2382 - color: var(--text-tertiary); 2383 - } 2384 - 2385 - .collection-detail-actions { 2386 - position: absolute; 2387 - top: 20px; 2388 - right: 20px; 2389 - display: flex; 2390 - align-items: center; 2391 - gap: 8px; 2392 - } 2393 - 2394 - .collection-detail-actions .share-menu-container { 2395 - display: flex; 2396 - align-items: center; 2397 - } 2398 - 2399 - .collection-detail-actions .annotation-action { 2400 - padding: 10px; 2401 - color: var(--text-tertiary); 2402 - background: none; 2403 - border: none; 2404 - border-radius: var(--radius-sm); 2405 - cursor: pointer; 2406 - transition: all 0.15s ease; 2407 - } 2408 - 2409 - .collection-detail-actions .annotation-action:hover { 2410 - color: var(--accent); 2411 - background: var(--bg-tertiary); 2412 - } 2413 - 2414 - .collection-detail-edit, 2415 - .collection-detail-delete { 2416 - padding: 10px; 2417 - color: var(--text-tertiary); 2418 - background: none; 2419 - border: none; 2420 - border-radius: var(--radius-sm); 2421 - cursor: pointer; 2422 - transition: all 0.15s ease; 2423 - } 2424 - 2425 - .collection-detail-edit:hover { 2426 - color: var(--accent); 2427 - background: var(--bg-tertiary); 2428 - } 2429 - 2430 - .collection-detail-delete:hover { 2431 - color: var(--error); 2432 - background: rgba(239, 68, 68, 0.1); 2433 - } 2434 - 2435 - .collection-item-wrapper { 2436 - position: relative; 2437 - } 2438 - 2439 - .collection-item-remove { 2440 - position: absolute; 2441 - top: 12px; 2442 - left: -40px; 2443 - z-index: 10; 2444 - padding: 8px; 2445 - background: var(--bg-card); 2446 - border: 1px solid var(--border); 2447 - border-radius: var(--radius-sm); 2448 - color: var(--text-tertiary); 2449 - cursor: pointer; 2450 - opacity: 0; 2451 - transition: all 0.15s ease; 2452 - } 2453 - 2454 - .collection-item-wrapper:hover .collection-item-remove { 2455 - opacity: 1; 2456 - } 2457 - 2458 - .collection-item-remove:hover { 2459 - color: var(--error); 2460 - border-color: var(--error); 2461 - background: rgba(239, 68, 68, 0.05); 2462 - } 2463 - 2464 - .modal-overlay { 2465 - position: fixed; 2466 - inset: 0; 2467 - background: rgba(0, 0, 0, 0.5); 2468 - display: flex; 2469 - align-items: center; 2470 - justify-content: center; 2471 - padding: 16px; 2472 - z-index: 50; 2473 - animation: fadeIn 0.2s ease-out; 2474 - } 2475 - 2476 - .modal-container { 2477 - background: var(--bg-secondary); 2478 - border-radius: var(--radius-lg); 2479 - width: 100%; 2480 - max-width: 28rem; 2481 - border: 1px solid var(--border); 2482 - box-shadow: var(--shadow-lg); 2483 - animation: zoomIn 0.2s ease-out; 2484 - } 2485 - 2486 - .modal-header { 2487 - display: flex; 2488 - align-items: center; 2489 - justify-content: space-between; 2490 - padding: 16px; 2491 - border-bottom: 1px solid var(--border); 2492 - } 2493 - 2494 - .modal-title { 2495 - font-size: 1.25rem; 2496 - font-weight: 700; 2497 - color: var(--text-primary); 2498 - } 2499 - 2500 - .modal-close-btn { 2501 - padding: 8px; 2502 - color: var(--text-tertiary); 2503 - border-radius: var(--radius-md); 2504 - transition: color 0.15s; 2505 - } 2506 - 2507 - .modal-close-btn:hover { 2508 - color: var(--text-primary); 2509 - background: var(--bg-hover); 2510 - } 2511 - 2512 - .modal-form { 2513 - padding: 16px; 2514 - display: flex; 2515 - flex-direction: column; 2516 - gap: 16px; 2517 - } 2518 - 2519 - .icon-picker-tabs { 2520 - display: flex; 2521 - gap: 4px; 2522 - margin-bottom: 12px; 2523 - } 2524 - 2525 - .icon-picker-tab { 2526 - flex: 1; 2527 - padding: 8px 12px; 2528 - background: var(--bg-primary); 2529 - border: 1px solid var(--border); 2530 - border-radius: var(--radius-md); 2531 - color: var(--text-secondary); 2532 - font-size: 0.85rem; 2533 - font-weight: 500; 2534 - cursor: pointer; 2535 - transition: all 0.15s ease; 2536 - } 2537 - 2538 - .icon-picker-tab:hover { 2539 - background: var(--bg-tertiary); 2540 - } 2541 - 2542 - .icon-picker-tab.active { 2543 - background: var(--accent); 2544 - border-color: var(--accent); 2545 - color: white; 2546 - } 2547 - 2548 - .emoji-picker-wrapper { 2549 - display: flex; 2550 - flex-direction: column; 2551 - gap: 10px; 2552 - } 2553 - 2554 - .emoji-custom-input input { 2555 - width: 100%; 2556 - } 2557 - 2558 - .emoji-picker, 2559 - .icon-picker { 2560 - display: flex; 2561 - flex-wrap: wrap; 2562 - gap: 4px; 2563 - max-height: 120px; 2564 - overflow-y: auto; 2565 - padding: 8px; 2566 - background: var(--bg-primary); 2567 - border: 1px solid var(--border); 2568 - border-radius: var(--radius-md); 2569 - } 2570 - 2571 - .emoji-option, 2572 - .icon-option { 2573 - width: 36px; 2574 - height: 36px; 2575 - display: flex; 2576 - align-items: center; 2577 - justify-content: center; 2578 - font-size: 1.2rem; 2579 - background: transparent; 2580 - border: 2px solid transparent; 2581 - border-radius: var(--radius-sm); 2582 - cursor: pointer; 2583 - transition: all 0.15s ease; 2584 - color: var(--text-secondary); 2585 - } 2586 - 2587 - .emoji-option:hover, 2588 - .icon-option:hover { 2589 - background: var(--bg-tertiary); 2590 - transform: scale(1.1); 2591 - color: var(--text-primary); 2592 - } 2593 - 2594 - .emoji-option.selected, 2595 - .icon-option.selected { 2596 - border-color: var(--accent); 2597 - background: var(--accent-subtle); 2598 - color: var(--accent); 2599 - } 2600 - 2601 - .form-group { 2602 - margin-bottom: 0; 2603 - } 2604 - 2605 - .form-label { 2606 - display: block; 2607 - font-size: 0.875rem; 2608 - font-weight: 500; 2609 - color: var(--text-secondary); 2610 - margin-bottom: 4px; 2611 - } 2612 - 2613 - .form-input, 2614 - .form-textarea, 2615 - .form-select { 2616 - width: 100%; 2617 - padding: 8px 12px; 2618 - background: var(--bg-primary); 2619 - border: 1px solid var(--border); 2620 - border-radius: var(--radius-md); 2621 - color: var(--text-primary); 2622 - transition: all 0.15s; 2623 - } 2624 - 2625 - .form-input:focus, 2626 - .form-textarea:focus, 2627 - .form-select:focus { 2628 - outline: none; 2629 - border-color: var(--accent); 2630 - box-shadow: 0 0 0 2px var(--accent-subtle); 2631 - } 2632 - 2633 - .form-textarea { 2634 - resize: none; 2635 - } 2636 - 2637 - .modal-actions { 2638 - display: flex; 2639 - justify-content: flex-end; 2640 - gap: 12px; 2641 - padding-top: 8px; 2642 - } 2643 - 2644 - @keyframes fadeIn { 2645 - from { 2646 - opacity: 0; 2647 - } 2648 - 2649 - to { 2650 - opacity: 1; 2651 - } 2652 - } 2653 - 2654 - @keyframes zoomIn { 2655 - from { 2656 - opacity: 0; 2657 - transform: scale(0.95); 2658 - } 2659 - 2660 - to { 2661 - opacity: 1; 2662 - transform: scale(1); 2663 - } 2664 - } 2665 - 2666 - .annotation-detail-page { 2667 - max-width: 680px; 2668 - margin: 0 auto; 2669 - padding: 24px 16px; 2670 - } 2671 - 2672 - .annotation-detail-header { 2673 - margin-bottom: 24px; 2674 - } 2675 - 2676 - .back-link { 2677 - display: inline-flex; 2678 - align-items: center; 2679 - gap: 8px; 2680 - color: var(--text-secondary); 2681 - font-size: 0.9rem; 2682 - transition: color 0.15s; 2683 - } 2684 - 2685 - .back-link:hover { 2686 - color: var(--text-primary); 2687 - } 2688 - 2689 - .text-secondary { 2690 - color: var(--text-secondary); 2691 - } 2692 - 2693 - .text-error { 2694 - color: var(--error); 2695 - } 2696 - 2697 - .text-center { 2698 - text-align: center; 2699 - } 2700 - 2701 - .flex { 2702 - display: flex; 2703 - } 2704 - 2705 - .items-center { 2706 - align-items: center; 2707 - } 2708 - 2709 - .justify-center { 2710 - justify-content: center; 2711 - } 2712 - 2713 - .justify-end { 2714 - justify-content: flex-end; 2715 - } 2716 - 2717 - .gap-2 { 2718 - gap: 8px; 2719 - } 2720 - 2721 - .gap-3 { 2722 - gap: 12px; 2723 - } 2724 - 2725 - .mt-3 { 2726 - margin-top: 12px; 2727 - } 2728 - 2729 - .mb-6 { 2730 - margin-bottom: 24px; 2731 - } 2732 - 2733 - .btn-text { 2734 - background: none; 2735 - border: none; 2736 - color: var(--text-secondary); 2737 - font-size: 0.9rem; 2738 - padding: 8px 12px; 2739 - cursor: pointer; 2740 - transition: color 0.15s; 2741 - } 2742 - 2743 - .btn-text:hover { 2744 - color: var(--text-primary); 2745 - } 2746 - 2747 - .btn-sm { 2748 - padding: 6px 12px; 2749 - font-size: 0.85rem; 2750 - } 2751 - 2752 - .annotation-edit-btn { 2753 - background: none; 2754 - border: none; 2755 - cursor: pointer; 2756 - padding: 6px 8px; 2757 - color: var(--text-tertiary); 2758 - border-radius: var(--radius-sm); 2759 - transition: all 0.15s ease; 2760 - } 2761 - 2762 - .annotation-edit-btn:hover { 2763 - color: var(--accent); 2764 - background: var(--accent-subtle); 2765 - } 2766 - 2767 - .spinner { 2768 - width: 32px; 2769 - height: 32px; 2770 - border: 3px solid var(--border); 2771 - border-top-color: var(--accent); 2772 - border-radius: 50%; 2773 - animation: spin 0.8s linear infinite; 2774 - } 2775 - 2776 - .spinner-sm { 2777 - width: 16px; 2778 - height: 16px; 2779 - border-width: 2px; 2780 - } 2781 - 2782 - @keyframes spin { 2783 - to { 2784 - transform: rotate(360deg); 2785 - } 2786 - } 2787 - 2788 - .collection-list-item { 2789 - width: 100%; 2790 - text-align: left; 2791 - padding: 12px 16px; 2792 - border-radius: var(--radius-md); 2793 - background: var(--bg-primary); 2794 - border: 1px solid transparent; 2795 - color: var(--text-primary); 2796 - transition: all 0.15s ease; 2797 - display: flex; 2798 - align-items: center; 2799 - justify-content: space-between; 2800 - cursor: pointer; 2801 - } 2802 - 2803 - .collection-list-item:hover { 2804 - background: var(--bg-hover); 2805 - border-color: var(--border); 2806 - } 2807 - 2808 - .collection-list-item:hover .collection-list-item-icon { 2809 - opacity: 1; 2810 - } 2811 - 2812 - .collection-list-item:disabled { 2813 - opacity: 0.6; 2814 - cursor: not-allowed; 2815 - } 2816 - 2817 - .item-delete-overlay { 2818 - position: absolute; 2819 - top: 16px; 2820 - right: 16px; 2821 - z-index: 10; 2822 - opacity: 0; 2823 - transition: opacity 0.15s ease; 2824 - } 2825 - 2826 - .card:hover .item-delete-overlay, 2827 - div:hover > .item-delete-overlay { 2828 - opacity: 1; 2829 - } 2830 - 2831 - .btn-icon-danger { 2832 - padding: 8px; 2833 - background: var(--error); 2834 - color: white; 2835 - border: none; 2836 - border-radius: var(--radius-md); 2837 - cursor: pointer; 2838 - box-shadow: var(--shadow-md); 2839 - transition: all 0.15s ease; 2840 - display: flex; 2841 - align-items: center; 2842 - justify-content: center; 2843 - } 2844 - 2845 - .btn-icon-danger:hover { 2846 - background: #dc2626; 2847 - transform: scale(1.05); 2848 - } 2849 - 2850 - .action-buttons { 2851 - display: flex; 2852 - gap: 8px; 2853 - } 2854 - 2855 - .action-buttons-end { 2856 - display: flex; 2857 - justify-content: flex-end; 2858 - gap: 8px; 2859 - } 2860 - 2861 - .filter-tab { 2862 - padding: 8px 16px; 2863 - font-size: 0.9rem; 2864 - font-weight: 500; 2865 - color: var(--text-secondary); 2866 - background: transparent; 2867 - border: none; 2868 - border-radius: var(--radius-md); 2869 - cursor: pointer; 2870 - transition: all 0.15s ease; 2871 - } 2872 - 2873 - .filter-tab:hover { 2874 - color: var(--text-primary); 2875 - background: var(--bg-hover); 2876 - } 2877 - 2878 - .filter-tab.active { 2879 - color: var(--text-primary); 2880 - background: var(--bg-card); 2881 - box-shadow: var(--shadow-sm); 2882 - } 2883 - 2884 - .inline-reply { 2885 - padding: 12px 16px; 2886 - border-bottom: 1px solid var(--border); 2887 - } 2888 - 2889 - .inline-reply:last-child { 2890 - border-bottom: none; 2891 - } 2892 - 2893 - .inline-reply-avatar { 2894 - width: 28px; 2895 - height: 28px; 2896 - min-width: 28px; 2897 - border-radius: var(--radius-full); 2898 - background: linear-gradient(135deg, var(--accent), #a855f7); 2899 - display: flex; 2900 - align-items: center; 2901 - justify-content: center; 2902 - font-weight: 600; 2903 - font-size: 0.7rem; 2904 - color: white; 2905 - overflow: hidden; 2906 - } 2907 - 2908 - .inline-reply-avatar img, 2909 - .inline-reply-avatar-placeholder { 2910 - width: 100%; 2911 - height: 100%; 2912 - object-fit: cover; 2913 - } 2914 - 2915 - .inline-reply-avatar-placeholder { 2916 - display: flex; 2917 - align-items: center; 2918 - justify-content: center; 2919 - font-weight: 600; 2920 - font-size: 0.7rem; 2921 - color: white; 2922 - } 2923 - 2924 - .inline-reply-content { 2925 - flex: 1; 2926 - min-width: 0; 2927 - } 2928 - 2929 - .inline-reply-header { 2930 - display: flex; 2931 - align-items: center; 2932 - gap: 8px; 2933 - margin-bottom: 4px; 2934 - } 2935 - 2936 - .inline-reply-author { 2937 - font-weight: 600; 2938 - font-size: 0.85rem; 2939 - color: var(--text-primary); 2940 - } 2941 - 2942 - .inline-reply-handle { 2943 - color: var(--text-tertiary); 2944 - font-size: 0.8rem; 2945 - text-decoration: none; 2946 - } 2947 - 2948 - .inline-reply-time { 2949 - color: var(--text-tertiary); 2950 - font-size: 0.75rem; 2951 - margin-left: auto; 2952 - } 2953 - 2954 - .inline-reply-text { 2955 - font-size: 0.9rem; 2956 - color: var(--text-primary); 2957 - line-height: 1.5; 2958 - } 2959 - 2960 - .inline-reply-action { 2961 - display: flex; 2962 - align-items: center; 2963 - gap: 4px; 2964 - padding: 4px 8px; 2965 - font-size: 0.8rem; 2966 - color: var(--text-tertiary); 2967 - background: none; 2968 - border: none; 2969 - border-radius: var(--radius-sm); 2970 - cursor: pointer; 2971 - transition: all 0.15s ease; 2972 - } 2973 - 2974 - .inline-reply-action:hover { 2975 - color: var(--text-secondary); 2976 - background: var(--bg-hover); 2977 - } 2978 - 2979 - .inline-reply-composer { 2980 - display: flex; 2981 - align-items: flex-start; 2982 - gap: 12px; 2983 - padding: 12px 16px; 2984 - } 2985 - 2986 - .history-panel { 2987 - background: var(--bg-tertiary); 2988 - border: 1px solid var(--border); 2989 - border-radius: var(--radius-md); 2990 - padding: 1rem; 2991 - margin-bottom: 1rem; 2992 - font-size: 0.9rem; 2993 - animation: fadeIn 0.2s ease-out; 2994 - } 2995 - 2996 - .history-header { 2997 - display: flex; 2998 - justify-content: space-between; 2999 - align-items: center; 3000 - margin-bottom: 1rem; 3001 - padding-bottom: 0.5rem; 3002 - border-bottom: 1px solid var(--border); 3003 - } 3004 - 3005 - .history-title { 3006 - font-weight: 600; 3007 - text-transform: uppercase; 3008 - letter-spacing: 0.05em; 3009 - font-size: 0.75rem; 3010 - color: var(--text-secondary); 3011 - } 3012 - 3013 - .history-list { 3014 - list-style: none; 3015 - display: flex; 3016 - flex-direction: column; 3017 - gap: 1rem; 3018 - } 3019 - 3020 - .history-item { 3021 - position: relative; 3022 - padding-left: 1rem; 3023 - border-left: 2px solid var(--border); 3024 - } 3025 - 3026 - .history-date { 3027 - font-size: 0.75rem; 3028 - color: var(--text-tertiary); 3029 - margin-bottom: 0.25rem; 3030 - } 3031 - 3032 - .history-content { 3033 - color: var(--text-secondary); 3034 - white-space: pre-wrap; 3035 - } 3036 - 3037 - .history-close-btn { 3038 - color: var(--text-tertiary); 3039 - padding: 4px; 3040 - border-radius: var(--radius-sm); 3041 - transition: all 0.2s; 3042 - display: flex; 3043 - align-items: center; 3044 - justify-content: center; 3045 - } 3046 - 3047 - .history-close-btn:hover { 3048 - background: var(--bg-hover); 3049 - color: var(--text-primary); 3050 - } 3051 - 3052 - .history-status { 3053 - text-align: center; 3054 - color: var(--text-tertiary); 3055 - font-style: italic; 3056 - padding: 1rem; 3057 - } 3058 - 3059 - .form-label { 3060 - display: block; 3061 - font-size: 0.85rem; 3062 - font-weight: 600; 3063 - color: var(--text-secondary); 3064 - margin-bottom: 6px; 3065 - } 3066 - 3067 - .color-input-container { 3068 - display: flex; 3069 - align-items: center; 3070 - gap: 12px; 3071 - background: var(--bg-tertiary); 3072 - padding: 8px 12px; 3073 - border-radius: var(--radius-md); 3074 - border: 1px solid var(--border); 3075 - width: fit-content; 3076 - } 3077 - 3078 - .color-input-wrapper { 3079 - position: relative; 3080 - width: 32px; 3081 - height: 32px; 3082 - border-radius: var(--radius-full); 3083 - overflow: hidden; 3084 - border: 2px solid var(--border); 3085 - cursor: pointer; 3086 - transition: transform 0.1s; 3087 - } 3088 - 3089 - .color-input-wrapper:hover { 3090 - transform: scale(1.1); 3091 - border-color: var(--accent); 3092 - } 3093 - 3094 - .color-input-wrapper input[type="color"] { 3095 - position: absolute; 3096 - top: -50%; 3097 - left: -50%; 3098 - width: 200%; 3099 - height: 200%; 3100 - padding: 0; 3101 - margin: 0; 3102 - border: none; 3103 - cursor: pointer; 3104 - opacity: 0; 3105 - } 3106 - 3107 - .bookmark-card { 3108 - display: flex; 3109 - flex-direction: column; 3110 - gap: 16px; 3111 - } 3112 - 3113 - .bookmark-preview { 3114 - display: flex; 3115 - flex-direction: column; 3116 - background: var(--bg-secondary); 3117 - border: 1px solid var(--border); 3118 - border-radius: var(--radius-md); 3119 - overflow: hidden; 3120 - text-decoration: none; 3121 - transition: all 0.2s ease; 3122 - position: relative; 3123 - } 3124 - 3125 - .bookmark-preview:hover { 3126 - border-color: var(--accent); 3127 - box-shadow: var(--shadow-sm); 3128 - transform: translateY(-1px); 3129 - } 3130 - 3131 - .bookmark-preview::before { 3132 - content: ""; 3133 - position: absolute; 3134 - left: 0; 3135 - top: 0; 3136 - bottom: 0; 3137 - width: 4px; 3138 - background: var(--accent); 3139 - opacity: 0.7; 3140 - } 3141 - 3142 - .bookmark-preview-content { 3143 - padding: 16px 20px; 3144 - display: flex; 3145 - flex-direction: column; 3146 - gap: 8px; 3147 - } 3148 - 3149 - .bookmark-preview-header { 3150 - display: flex; 3151 - align-items: center; 3152 - gap: 8px; 3153 - margin-bottom: 4px; 3154 - } 3155 - 3156 - .bookmark-preview-site { 3157 - font-size: 0.75rem; 3158 - color: var(--accent); 3159 - text-transform: uppercase; 3160 - letter-spacing: 0.05em; 3161 - font-weight: 700; 3162 - display: flex; 3163 - align-items: center; 3164 - gap: 6px; 3165 - } 3166 - 3167 - .bookmark-preview-title { 3168 - font-size: 1.15rem; 3169 - font-weight: 700; 3170 - color: var(--text-primary); 3171 - line-height: 1.4; 3172 - } 3173 - 3174 - .bookmark-preview-desc { 3175 - font-size: 0.95rem; 3176 - color: var(--text-secondary); 3177 - line-height: 1.6; 3178 - } 3179 - 3180 - .bookmark-preview-arrow { 3181 - display: none; 3182 - } 3183 - 3184 - .bookmark-preview:hover { 3185 - background: var(--bg-tertiary); 3186 - border-color: var(--accent-subtle); 3187 - transform: translateY(-1px); 3188 - } 3189 - 3190 - .bookmark-preview-content { 3191 - flex: 1; 3192 - min-width: 0; 3193 - display: flex; 3194 - flex-direction: column; 3195 - gap: 6px; 3196 - } 3197 - 3198 - .bookmark-preview-site { 3199 - display: flex; 3200 - align-items: center; 3201 - gap: 6px; 3202 - font-size: 0.75rem; 3203 - font-weight: 600; 3204 - color: var(--accent); 3205 - text-transform: uppercase; 3206 - letter-spacing: 0.03em; 3207 - } 3208 - 3209 - .bookmark-preview-title { 3210 - font-size: 1rem; 3211 - font-weight: 600; 3212 - line-height: 1.4; 3213 - color: var(--text-primary); 3214 - margin: 0; 3215 - display: -webkit-box; 3216 - -webkit-line-clamp: 2; 3217 - line-clamp: 2; 3218 - -webkit-box-orient: vertical; 3219 - overflow: hidden; 3220 - } 3221 - 3222 - .bookmark-preview-desc { 3223 - font-size: 0.875rem; 3224 - color: var(--text-secondary); 3225 - line-height: 1.5; 3226 - margin: 0; 3227 - display: -webkit-box; 3228 - -webkit-line-clamp: 2; 3229 - line-clamp: 2; 3230 - -webkit-box-orient: vertical; 3231 - overflow: hidden; 3232 - } 3233 - 3234 - .bookmark-preview-arrow { 3235 - display: flex; 3236 - align-items: center; 3237 - justify-content: center; 3238 - color: var(--text-tertiary); 3239 - padding: 0 4px; 3240 - transition: all 0.2s ease; 3241 - } 3242 - 3243 - .bookmark-preview:hover .bookmark-preview-arrow { 3244 - color: var(--accent); 3245 - transform: translateX(2px); 3246 - } 3247 - 3248 - .navbar-logo-img { 3249 - width: 24px; 3250 - height: 24px; 3251 - object-fit: contain; 3252 - } 3253 - 3254 - .login-logo-img { 3255 - width: 80px; 3256 - height: 80px; 3257 - margin-bottom: 24px; 3258 - object-fit: contain; 3259 - } 3260 - 3261 - .legal-content { 3262 - max-width: 800px; 3263 - margin: 0 auto; 3264 - padding: 20px; 3265 - } 3266 - 3267 - .legal-content h1 { 3268 - font-size: 2rem; 3269 - margin-bottom: 8px; 3270 - color: var(--text-primary); 3271 - } 3272 - 3273 - .legal-content h2 { 3274 - font-size: 1.4rem; 3275 - margin-top: 32px; 3276 - margin-bottom: 12px; 3277 - color: var(--text-primary); 3278 - } 3279 - 3280 - .legal-content h3 { 3281 - font-size: 1.1rem; 3282 - margin-top: 20px; 3283 - margin-bottom: 8px; 3284 - color: var(--text-primary); 3285 - } 3286 - 3287 - .legal-content p { 3288 - color: var(--text-secondary); 3289 - line-height: 1.7; 3290 - margin-bottom: 12px; 3291 - } 3292 - 3293 - .legal-content ul { 3294 - color: var(--text-secondary); 3295 - line-height: 1.7; 3296 - margin-left: 24px; 3297 - margin-bottom: 12px; 3298 - } 3299 - 3300 - .legal-content li { 3301 - margin-bottom: 6px; 3302 - } 3303 - 3304 - .legal-content a { 3305 - color: var(--accent); 3306 - text-decoration: none; 3307 - } 3308 - 3309 - .legal-content a:hover { 3310 - text-decoration: underline; 3311 - } 3312 - 3313 - .legal-content section { 3314 - margin-bottom: 24px; 3315 - } 3316 - 3317 - .input { 3318 - width: 100%; 3319 - padding: 12px 14px; 3320 - font-size: 0.95rem; 3321 - color: var(--text-primary); 3322 - background: var(--bg-secondary); 3323 - border: 1px solid var(--border); 3324 - border-radius: var(--radius-md); 3325 - outline: none; 3326 - transition: all 0.15s ease; 3327 - } 3328 - 3329 - .input:focus { 3330 - border-color: var(--accent); 3331 - box-shadow: 0 0 0 3px var(--accent-subtle); 3332 - } 3333 - 3334 - .input::placeholder { 3335 - color: var(--text-tertiary); 3336 - } 3337 - 3338 - .notifications-page { 3339 - max-width: 680px; 3340 - margin: 0 auto; 3341 - } 3342 - 3343 - .notifications-list { 3344 - display: flex; 3345 - flex-direction: column; 3346 - gap: 12px; 3347 - } 3348 - 3349 - .notification-item { 3350 - display: flex; 3351 - gap: 16px; 3352 - align-items: flex-start; 3353 - text-decoration: none; 3354 - color: inherit; 3355 - } 3356 - 3357 - .notification-item:hover { 3358 - background: var(--bg-hover); 3359 - } 3360 - 3361 - .notification-icon { 3362 - width: 36px; 3363 - height: 36px; 3364 - border-radius: var(--radius-full); 3365 - display: flex; 3366 - align-items: center; 3367 - justify-content: center; 3368 - background: var(--bg-tertiary); 3369 - color: var(--text-secondary); 3370 - flex-shrink: 0; 3371 - } 3372 - 3373 - .notification-icon[data-type="like"] { 3374 - color: #ef4444; 3375 - background: rgba(239, 68, 68, 0.1); 3376 - } 3377 - 3378 - .notification-icon[data-type="reply"] { 3379 - color: #3b82f6; 3380 - background: rgba(59, 130, 246, 0.1); 3381 - } 3382 - 3383 - .notification-content { 3384 - flex: 1; 3385 - min-width: 0; 3386 - } 3387 - 3388 - .notification-text { 3389 - font-size: 0.95rem; 3390 - margin-bottom: 4px; 3391 - line-height: 1.4; 3392 - color: var(--text-primary); 3393 - } 3394 - 3395 - .notification-text strong { 3396 - font-weight: 600; 3397 - } 3398 - 3399 - .notification-time { 3400 - font-size: 0.85rem; 3401 - color: var(--text-tertiary); 3402 - } 3403 - 3404 - .notification-link { 3405 - position: relative; 3406 - } 3407 - 3408 - .notification-badge { 3409 - position: absolute; 3410 - top: -2px; 3411 - right: -2px; 3412 - background: var(--error); 3413 - color: white; 3414 - font-size: 0.7rem; 3415 - font-weight: 700; 3416 - min-width: 16px; 3417 - height: 16px; 3418 - border-radius: var(--radius-full); 3419 - display: flex; 3420 - align-items: center; 3421 - justify-content: center; 3422 - padding: 0 4px; 3423 - border: 2px solid var(--bg-primary); 3424 - } 1 + @import "./css/layout.css"; 2 + @import "./css/base.css"; 3 + @import "./css/buttons.css"; 4 + @import "./css/buttons.css"; 5 + @import "./css/feed.css"; 6 + @import "./css/profile.css"; 7 + @import "./css/login.css"; 8 + @import "./css/annotations.css"; 9 + @import "./css/collections.css"; 10 + @import "./css/modals.css"; 11 + @import "./css/notifications.css"; 12 + @import "./css/skeleton.css"; 13 + @import "./css/utilities.css";
+90 -95
web/src/pages/Feed.jsx
··· 3 3 import AnnotationCard, { HighlightCard } from "../components/AnnotationCard"; 4 4 import BookmarkCard from "../components/BookmarkCard"; 5 5 import CollectionItemCard from "../components/CollectionItemCard"; 6 + import AnnotationSkeleton from "../components/AnnotationSkeleton"; 6 7 import { getAnnotationFeed, deleteHighlight } from "../api/client"; 7 8 import { AlertIcon, InboxIcon } from "../components/Icons"; 8 9 import { useAuth } from "../context/AuthContext"; ··· 151 152 </button> 152 153 </div> 153 154 154 - {loading && ( 155 + {loading ? ( 155 156 <div className="feed"> 156 - {[1, 2, 3].map((i) => ( 157 - <div key={i} className="card"> 158 - <div 159 - className="skeleton skeleton-text" 160 - style={{ width: "40%" }} 161 - /> 162 - <div className="skeleton skeleton-text" /> 163 - <div className="skeleton skeleton-text" /> 164 - <div 165 - className="skeleton skeleton-text" 166 - style={{ width: "60%" }} 167 - /> 168 - </div> 157 + {[1, 2, 3, 4, 5].map((i) => ( 158 + <AnnotationSkeleton key={i} /> 169 159 ))} 170 160 </div> 171 - )} 172 - 173 - {error && ( 174 - <div className="empty-state"> 175 - <div className="empty-state-icon"> 176 - <AlertIcon size={32} /> 177 - </div> 178 - <h3 className="empty-state-title">Something went wrong</h3> 179 - <p className="empty-state-text">{error}</p> 180 - </div> 181 - )} 161 + ) : ( 162 + <> 163 + {error && ( 164 + <div className="empty-state"> 165 + <div className="empty-state-icon"> 166 + <AlertIcon size={32} /> 167 + </div> 168 + <h3 className="empty-state-title">Something went wrong</h3> 169 + <p className="empty-state-text">{error}</p> 170 + </div> 171 + )} 182 172 183 - {!loading && !error && filteredAnnotations.length === 0 && ( 184 - <div className="empty-state"> 185 - <div className="empty-state-icon"> 186 - <InboxIcon size={32} /> 187 - </div> 188 - <h3 className="empty-state-title">No items yet</h3> 189 - <p className="empty-state-text"> 190 - {filter === "all" 191 - ? "Be the first to annotate something!" 192 - : `No ${filter} items found.`} 193 - </p> 194 - </div> 195 - )} 173 + {!error && filteredAnnotations.length === 0 && ( 174 + <div className="empty-state"> 175 + <div className="empty-state-icon"> 176 + <InboxIcon size={32} /> 177 + </div> 178 + <h3 className="empty-state-title">No items yet</h3> 179 + <p className="empty-state-text"> 180 + {filter === "all" 181 + ? "Be the first to annotate something!" 182 + : `No ${filter} items found.`} 183 + </p> 184 + </div> 185 + )} 196 186 197 - {!loading && !error && filteredAnnotations.length > 0 && ( 198 - <div className="feed"> 199 - {filteredAnnotations.map((item) => { 200 - if (item.type === "CollectionItem") { 201 - return <CollectionItemCard key={item.id} item={item} />; 202 - } 203 - if ( 204 - item.type === "Highlight" || 205 - item.motivation === "highlighting" 206 - ) { 207 - return ( 208 - <HighlightCard 209 - key={item.id} 210 - highlight={item} 211 - onDelete={async (uri) => { 212 - const rkey = uri.split("/").pop(); 213 - await deleteHighlight(rkey); 214 - setAnnotations((prev) => 215 - prev.filter((a) => a.id !== item.id), 216 - ); 217 - }} 218 - onAddToCollection={() => 219 - setCollectionModalState({ 220 - isOpen: true, 221 - uri: item.uri || item.id, 222 - }) 223 - } 224 - /> 225 - ); 226 - } 227 - if (item.type === "Bookmark" || item.motivation === "bookmarking") { 228 - return ( 229 - <BookmarkCard 230 - key={item.id} 231 - bookmark={item} 232 - onAddToCollection={() => 233 - setCollectionModalState({ 234 - isOpen: true, 235 - uri: item.uri || item.id, 236 - }) 237 - } 238 - /> 239 - ); 240 - } 241 - return ( 242 - <AnnotationCard 243 - key={item.id} 244 - annotation={item} 245 - onAddToCollection={() => 246 - setCollectionModalState({ 247 - isOpen: true, 248 - uri: item.uri || item.id, 249 - }) 187 + {!error && filteredAnnotations.length > 0 && ( 188 + <div className="feed"> 189 + {filteredAnnotations.map((item) => { 190 + if (item.type === "CollectionItem") { 191 + return <CollectionItemCard key={item.id} item={item} />; 250 192 } 251 - /> 252 - ); 253 - })} 254 - </div> 193 + if ( 194 + item.type === "Highlight" || 195 + item.motivation === "highlighting" 196 + ) { 197 + return ( 198 + <HighlightCard 199 + key={item.id} 200 + highlight={item} 201 + onDelete={async (uri) => { 202 + const rkey = uri.split("/").pop(); 203 + await deleteHighlight(rkey); 204 + setAnnotations((prev) => 205 + prev.filter((a) => a.id !== item.id), 206 + ); 207 + }} 208 + onAddToCollection={() => 209 + setCollectionModalState({ 210 + isOpen: true, 211 + uri: item.uri || item.id, 212 + }) 213 + } 214 + /> 215 + ); 216 + } 217 + if ( 218 + item.type === "Bookmark" || 219 + item.motivation === "bookmarking" 220 + ) { 221 + return ( 222 + <BookmarkCard 223 + key={item.id} 224 + bookmark={item} 225 + onAddToCollection={() => 226 + setCollectionModalState({ 227 + isOpen: true, 228 + uri: item.uri || item.id, 229 + }) 230 + } 231 + /> 232 + ); 233 + } 234 + return ( 235 + <AnnotationCard 236 + key={item.id} 237 + annotation={item} 238 + onAddToCollection={() => 239 + setCollectionModalState({ 240 + isOpen: true, 241 + uri: item.uri || item.id, 242 + }) 243 + } 244 + /> 245 + ); 246 + })} 247 + </div> 248 + )} 249 + </> 255 250 )} 256 251 257 252 {collectionModalState.isOpen && (
+81
web/src/pages/Terms.jsx
··· 1 + import { ArrowLeft } from "lucide-react"; 2 + import { Link } from "react-router-dom"; 3 + 4 + export default function Terms() { 5 + return ( 6 + <div className="feed-page"> 7 + <Link to="/" className="back-link"> 8 + <ArrowLeft size={18} /> 9 + <span>Home</span> 10 + </Link> 11 + 12 + <div className="legal-content"> 13 + <h1>Terms of Service</h1> 14 + <p className="text-secondary">Last updated: January 17, 2026</p> 15 + 16 + <section> 17 + <h2>Overview</h2> 18 + <p> 19 + Margin is an open-source project. By using our service, you agree to 20 + these terms ("Terms"). If you do not agree to these Terms, please do 21 + not use the Service. 22 + </p> 23 + </section> 24 + 25 + <section> 26 + <h2>Open Source</h2> 27 + <p> 28 + Margin is open source software. The code is available publicly and 29 + is provided "as is", without warranty of any kind, express or 30 + implied. 31 + </p> 32 + </section> 33 + 34 + <section> 35 + <h2>User Conduct</h2> 36 + <p> 37 + You are responsible for your use of the Service and for any content 38 + you provide, including compliance with applicable laws, rules, and 39 + regulations. 40 + </p> 41 + <p> 42 + We reserve the right to remove any content that violates these 43 + terms, including but not limited to: 44 + </p> 45 + <ul> 46 + <li>Illegal content</li> 47 + <li>Harassment or hate speech</li> 48 + <li>Spam or malicious content</li> 49 + </ul> 50 + </section> 51 + 52 + <section> 53 + <h2>Decentralized Nature</h2> 54 + <p> 55 + Margin interacts with the AT Protocol network. We do not control the 56 + network itself or the data stored on your Personal Data Server 57 + (PDS). Please refer to the terms of your PDS provider for data 58 + storage policies. 59 + </p> 60 + </section> 61 + 62 + <section> 63 + <h2>Disclaimer</h2> 64 + <p> 65 + THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE". WE DISCLAIM ALL 66 + CONDITIONS, REPRESENTATIONS AND WARRANTIES NOT EXPRESSLY SET OUT IN 67 + THESE TERMS. 68 + </p> 69 + </section> 70 + 71 + <section> 72 + <h2>Contact</h2> 73 + <p> 74 + For questions about these Terms, please contact us at{" "} 75 + <a href="mailto:hello@margin.at">hello@margin.at</a> 76 + </p> 77 + </section> 78 + </div> 79 + </div> 80 + ); 81 + }