diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index d9e4f6b..a063f4a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,7 +2,7 @@ name: Build and Publish Docker Image on: push: - branches: [ "main" ] + branches: ["main"] workflow_dispatch: env: diff --git a/.github/workflows/release-extension.yml b/.github/workflows/release-extension.yml index d3356f0..222bcca 100644 --- a/.github/workflows/release-extension.yml +++ b/.github/workflows/release-extension.yml @@ -3,7 +3,7 @@ name: Release Extension on: push: tags: - - 'v*' + - "v*" jobs: release: @@ -19,7 +19,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} echo "Updating manifests to version $VERSION" - + cd extension for manifest in manifest.json manifest.chrome.json manifest.firefox.json; do if [ -f "$manifest" ]; then @@ -36,7 +36,7 @@ jobs: cp manifest.chrome.json manifest.json zip -r ../margin-extension-chrome.zip . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json" cd .. - + - name: Build Extension (Firefox) run: | cd extension diff --git a/README.md b/README.md index 6974e26..5f297eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Margin -*Write in the margins of the web* +_Write in the margins of the web_ A web comments layer built on [AT Protocol](https://atproto.com) that lets you annotate any URL on the internet. diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index f390c4f..1258b20 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -101,6 +101,8 @@ func main() { r.Get("/{handle}/highlight/{rkey}", ogHandler.HandleAnnotationPage) r.Get("/{handle}/bookmark/{rkey}", ogHandler.HandleAnnotationPage) + r.Get("/api/tags/trending", handler.HandleGetTrendingTags) + r.Get("/collection/{uri}", ogHandler.HandleCollectionPage) r.Get("/{handle}/collection/{rkey}", ogHandler.HandleCollectionPage) diff --git a/backend/internal/api/tags.go b/backend/internal/api/tags.go new file mode 100644 index 0000000..3d2e59e --- /dev/null +++ b/backend/internal/api/tags.go @@ -0,0 +1,25 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" +) + +func (h *Handler) HandleGetTrendingTags(w http.ResponseWriter, r *http.Request) { + limit := 10 + if l := r.URL.Query().Get("limit"); l != "" { + if val, err := strconv.Atoi(l); err == nil && val > 0 && val <= 50 { + limit = val + } + } + + tags, err := h.db.GetTrendingTags(limit) + if err != nil { + http.Error(w, `{"error": "Failed to fetch trending tags: `+err.Error()+`"}`, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(tags) +} diff --git a/backend/internal/db/tags.go b/backend/internal/db/tags.go new file mode 100644 index 0000000..b14779c --- /dev/null +++ b/backend/internal/db/tags.go @@ -0,0 +1,46 @@ +package db + +type TrendingTag struct { + Tag string `json:"tag"` + Count int `json:"count"` +} + +func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) { + query := ` + SELECT + json_each.value as tag, + COUNT(*) as count + FROM annotations, json_each(annotations.tags_json) + WHERE tags_json IS NOT NULL + AND tags_json != '' + AND tags_json != '[]' + GROUP BY tag + ORDER BY count DESC + LIMIT ? + ` + + rows, err := db.Query(db.Rebind(query), limit) + if err != nil { + return nil, err + } + defer rows.Close() + + var tags []TrendingTag + for rows.Next() { + var t TrendingTag + if err := rows.Scan(&t.Tag, &t.Count); err != nil { + return nil, err + } + tags = append(tags, t) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + if tags == nil { + return []TrendingTag{}, nil + } + + return tags, nil +} diff --git a/extension/background/service-worker.js b/extension/background/service-worker.js index 4ff44e5..a3a2b6f 100644 --- a/extension/background/service-worker.js +++ b/extension/background/service-worker.js @@ -334,9 +334,14 @@ async function handleMessage(request, sender, sendResponse) { } case "GET_ANNOTATIONS": { + const stored = await chrome.storage.local.get(["apiUrl"]); + const currentApiUrl = stored.apiUrl + ? stored.apiUrl.replace(/\/$/, "") + : API_BASE; + const pageUrl = request.data.url; const res = await fetch( - `${API_BASE}/api/targets?source=${encodeURIComponent(pageUrl)}`, + `${currentApiUrl}/api/targets?source=${encodeURIComponent(pageUrl)}`, ); const data = await res.json(); @@ -422,7 +427,6 @@ async function handleMessage(request, sender, sendResponse) { return; } const { url, selector } = request.data; - let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(url)}`; if (selector) { composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`; @@ -431,6 +435,25 @@ async function handleMessage(request, sender, sendResponse) { break; } + case "OPEN_APP_URL": { + if (!WEB_BASE) { + chrome.runtime.openOptionsPage(); + return; + } + const path = request.data.path; + const safePath = path.startsWith("/") ? path : `/${path}`; + chrome.tabs.create({ url: `${WEB_BASE}${safePath}` }); + break; + } + + case "OPEN_SIDE_PANEL": + if (sender.tab && sender.tab.windowId) { + chrome.sidePanel + .open({ windowId: sender.tab.windowId }) + .catch((err) => console.error("Failed to open side panel", err)); + } + break; + case "CREATE_BOOKMARK": { if (!API_BASE) { sendResponse({ success: false, error: "API URL not configured" }); diff --git a/extension/content/content.js b/extension/content/content.js index 6271643..76f9166 100644 --- a/extension/content/content.js +++ b/extension/content/content.js @@ -1,318 +1,701 @@ (() => { - function buildTextQuoteSelector(selection) { - const exact = selection.toString().trim(); - if (!exact) return null; - - const range = selection.getRangeAt(0); - const contextLength = 32; - - let prefix = ""; - try { - const preRange = document.createRange(); - preRange.selectNodeContents(document.body); - preRange.setEnd(range.startContainer, range.startOffset); - const preText = preRange.toString(); - prefix = preText.slice(-contextLength).trim(); - } catch (e) { - console.warn("Could not get prefix:", e); - } - - let suffix = ""; - try { - const postRange = document.createRange(); - postRange.selectNodeContents(document.body); - postRange.setStart(range.endContainer, range.endOffset); - const postText = postRange.toString(); - suffix = postText.slice(0, contextLength).trim(); - } catch (e) { - console.warn("Could not get suffix:", e); - } - - return { - type: "TextQuoteSelector", - exact: exact, - prefix: prefix || undefined, - suffix: suffix || undefined, - }; - } + let sidebarHost = null; + let sidebarShadow = null; + let popoverEl = null; + + let activeItems = []; + + let hoveredItems = []; + let tooltipEl = null; + let hideTimer = null; + + const OVERLAY_STYLES = ` + :host { all: initial; } + .margin-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + } + .margin-badge { + position: absolute; + background: #6366f1; + color: white; + padding: 4px 10px; + border-radius: 99px; + font-size: 11px; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + cursor: pointer; + pointer-events: auto; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); + display: flex; + align-items: center; + gap: 6px; + transform: translateY(-120%); + white-space: nowrap; + transition: transform 0.15s, background-color 0.15s; + z-index: 2147483647; + } + .margin-badge:hover { + transform: translateY(-125%) scale(1.05); + background: #4f46e5; + z-index: 2147483647; + } + .margin-badge-avatar { + width: 16px; + height: 16px; + border-radius: 50%; + background: rgba(255,255,255,0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + object-fit: cover; + } + .margin-badge-stack { + display: flex; + align-items: center; + } + .margin-badge-stack .margin-badge-avatar { + margin-left: -6px; + border: 1px solid #6366f1; + } + .margin-badge-stack .margin-badge-avatar:first-child { + margin-left: 0; + } + .margin-badge-stem { + position: absolute; + left: 14px; + bottom: -6px; + width: 2px; + height: 6px; + background: #6366f1; + border-radius: 2px; + } - function findAndScrollToText(selector) { - if (!selector || !selector.exact) return false; + .margin-popover { + position: absolute; + width: 320px; + background: #09090b; + border: 1px solid #27272a; + border-radius: 12px; + padding: 0; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + pointer-events: auto; + z-index: 2147483647; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + color: #e4e4e7; + opacity: 0; + transform: scale(0.95); + animation: popover-in 0.15s forwards; + max-height: 480px; + overflow: hidden; + } + @keyframes popover-in { to { opacity: 1; transform: scale(1); } } + .popover-header { + padding: 12px 16px; + border-bottom: 1px solid #27272a; + display: flex; + justify-content: space-between; + align-items: center; + background: #0f0f12; + border-radius: 12px 12px 0 0; + font-weight: 600; + font-size: 13px; + } + .popover-scroll-area { + overflow-y: auto; + max-height: 400px; + } + .popover-item-block { + border-bottom: 1px solid #27272a; + margin-bottom: 0; + animation: fade-in 0.2s; + } + .popover-item-block:last-child { + border-bottom: none; + } + .popover-item-header { + padding: 12px 16px 4px; + display: flex; + align-items: center; + gap: 8px; + } + .popover-avatar { + width: 24px; height: 24px; border-radius: 50%; background: #27272a; + display: flex; align-items: center; justify-content: center; + font-size: 10px; color: #a1a1aa; + } + .popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; } + .popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; } + .popover-close:hover { color: #e4e4e7; } + .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; } + .popover-quote { + margin-top: 8px; padding: 6px 10px; background: #18181b; + border-left: 2px solid #6366f1; border-radius: 4px; + font-size: 11px; color: #a1a1aa; font-style: italic; + } + .popover-actions { + padding: 8px 16px; + display: flex; justify-content: flex-end; gap: 8px; + } + .btn-action { + background: none; border: 1px solid #27272a; border-radius: 4px; + padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; + } + .btn-action:hover { background: #27272a; color: #e4e4e7; } + `; + + class DOMTextMatcher { + constructor() { + this.textNodes = []; + this.corpus = ""; + this.indices = []; + this.buildMap(); + } - const searchText = selector.exact.trim(); - const normalizedSearch = searchText.replace(/\s+/g, " "); + buildMap() { + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_TEXT, + { + acceptNode: (node) => { + if (!node.parentNode) return NodeFilter.FILTER_REJECT; + const tag = node.parentNode.tagName; + if ( + ["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag) + ) + return NodeFilter.FILTER_REJECT; + if (node.textContent.trim().length === 0) + return NodeFilter.FILTER_SKIP; + + if (node.parentNode.offsetParent === null) + return NodeFilter.FILTER_REJECT; + + return NodeFilter.FILTER_ACCEPT; + }, + }, + ); - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_TEXT, - null, - false, - ); + let currentNode; + let index = 0; + while ((currentNode = walker.nextNode())) { + const text = currentNode.textContent; + this.textNodes.push(currentNode); + this.corpus += text; + this.indices.push({ + start: index, + node: currentNode, + length: text.length, + }); + index += text.length; + } + } - let currentNode; - while ((currentNode = treeWalker.nextNode())) { - const nodeText = currentNode.textContent; - const normalizedNode = nodeText.replace(/\s+/g, " "); + findRange(searchText) { + if (!searchText) return null; - let index = nodeText.indexOf(searchText); + let matchIndex = this.corpus.indexOf(searchText); - if (index === -1) { - const normIndex = normalizedNode.indexOf(normalizedSearch); - if (normIndex !== -1) { - index = nodeText.indexOf(searchText.substring(0, 20)); - if (index === -1) index = 0; - } + if (matchIndex === -1) { + const cleaned = searchText.replace(/\s+/g, " "); + return null; } - if (index !== -1 && nodeText.trim().length > 0) { - try { - const range = document.createRange(); - const endIndex = Math.min(index + searchText.length, nodeText.length); - range.setStart(currentNode, index); - range.setEnd(currentNode, endIndex); - - if (typeof CSS !== "undefined" && CSS.highlights) { - const highlight = new Highlight(range); - CSS.highlights.set("margin-scroll-highlight", highlight); - - setTimeout(() => { - CSS.highlights.delete("margin-scroll-highlight"); - }, 3000); - } - - const rect = range.getBoundingClientRect(); - window.scrollTo({ - top: window.scrollY + rect.top - window.innerHeight / 3, - behavior: "smooth", - }); + const start = this.mapIndexToPoint(matchIndex); + const end = this.mapIndexToPoint(matchIndex + searchText.length); - window.scrollTo({ - top: window.scrollY + rect.top - window.innerHeight / 3, - behavior: "smooth", - }); - - return true; - } catch (e) { - console.warn("Could not create range:", e); - } + if (start && end) { + const range = document.createRange(); + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); + return range; } + return null; } - if (window.find) { - window.getSelection()?.removeAllRanges(); - const found = window.find(searchText, false, false, true, false); - if (found) { - const selection = window.getSelection(); - if (selection && selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - const rect = range.getBoundingClientRect(); - window.scrollTo({ - top: window.scrollY + rect.top - window.innerHeight / 3, - behavior: "smooth", - }); + mapIndexToPoint(corpusIndex) { + for (const info of this.indices) { + if ( + corpusIndex >= info.start && + corpusIndex < info.start + info.length + ) { + return { node: info.node, offset: corpusIndex - info.start }; + } + } + if (this.indices.length > 0) { + const last = this.indices[this.indices.length - 1]; + if (corpusIndex === last.start + last.length) { + return { node: last.node, offset: last.length }; } - return true; } + return null; } - - return false; } - function renderPageHighlights(highlights) { - if (!highlights || !Array.isArray(highlights) || !CSS.highlights) return; + function initOverlay() { + sidebarHost = document.createElement("div"); + sidebarHost.id = "margin-overlay-host"; + const getScrollHeight = () => { + const bodyH = document.body?.scrollHeight || 0; + const docH = document.documentElement?.scrollHeight || 0; + return Math.max(bodyH, docH); + }; + + sidebarHost.style.cssText = ` + position: absolute; top: 0; left: 0; width: 100%; + height: ${getScrollHeight()}px; + pointer-events: none; z-index: 2147483647; + `; + document.body?.appendChild(sidebarHost) || + document.documentElement.appendChild(sidebarHost); - const ranges = []; + sidebarShadow = sidebarHost.attachShadow({ mode: "open" }); + const styleEl = document.createElement("style"); + styleEl.textContent = OVERLAY_STYLES; + sidebarShadow.appendChild(styleEl); - highlights.forEach((item) => { - const selector = item.target?.selector; - if (!selector?.exact) return; + const container = document.createElement("div"); + container.className = "margin-overlay"; + container.id = "margin-overlay-container"; + sidebarShadow.appendChild(container); - const searchText = selector.exact; - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_TEXT, - null, - false, - ); + createTooltip(container); - let currentNode; - while ((currentNode = treeWalker.nextNode())) { - const nodeText = currentNode.textContent; - const index = nodeText.indexOf(searchText); - - if (index !== -1) { - try { - const range = document.createRange(); - range.setStart(currentNode, index); - range.setEnd(currentNode, index + searchText.length); - ranges.push(range); - } catch (e) { - console.warn("Could not create range for highlight:", e); - } - break; - } - } + const observer = new ResizeObserver(() => { + sidebarHost.style.height = `${getScrollHeight()}px`; }); + if (document.body) observer.observe(document.body); + if (document.documentElement) observer.observe(document.documentElement); - if (ranges.length > 0) { - const highlight = new Highlight(...ranges); - CSS.highlights.set("margin-page-highlights", highlight); - } + fetchAnnotations(); + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("click", handleDocumentClick); } - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { - const selection = window.getSelection(); - if (!selection || selection.toString().trim().length === 0) { - sendResponse({ selector: null }); - return true; + function createTooltip(container) { + tooltipEl = document.createElement("div"); + tooltipEl.className = "margin-badge"; + tooltipEl.style.opacity = "0"; + tooltipEl.style.transition = "opacity 0.1s, transform 0.1s"; + tooltipEl.style.pointerEvents = "auto"; + + tooltipEl.addEventListener("click", (e) => { + e.stopPropagation(); + if (hoveredItems.length > 0) { + const firstItem = hoveredItems[0]; + const rect = activeItems + .find((x) => x.item === firstItem) + ?.range.getBoundingClientRect(); + if (rect) { + const top = rect.top + window.scrollY; + const left = rect.left + window.scrollX; + showPopover(hoveredItems, top, left); + } } + }); + container.appendChild(tooltipEl); + } - const selector = buildTextQuoteSelector(selection); - sendResponse({ selector: selector }); - return true; + function handleMouseMove(e) { + const x = e.clientX; + const y = e.clientY; + + let foundItems = []; + + for (const { range, item } of activeItems) { + const rects = range.getClientRects(); + for (const rect of rects) { + const padding = 5; + if ( + x >= rect.left - padding && + x <= rect.right + padding && + y >= rect.top - padding && + y <= rect.bottom + padding + ) { + if (!foundItems.includes(item)) { + foundItems.push(item); + } + break; + } + } } - if (request.type === "GET_SELECTOR_FOR_ANNOTATE") { - const selection = window.getSelection(); - if (!selection || selection.toString().trim().length === 0) { - return; + let isOverTooltip = false; + if (tooltipEl && tooltipEl.style.opacity === "1") { + const rect = tooltipEl.getBoundingClientRect(); + if ( + x >= rect.left && + x <= rect.right && + y >= rect.top && + y <= rect.bottom + ) { + isOverTooltip = true; } + } - const selector = buildTextQuoteSelector(selection); - if (selector) { - chrome.runtime.sendMessage({ - type: "OPEN_COMPOSE", - data: { - url: window.location.href, - selector: selector, - }, - }); + if (foundItems.length > 0 || isOverTooltip) { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + if (foundItems.length > 0) { + const currentIds = hoveredItems + .map((i) => i.id || i.cid) + .sort() + .join(","); + const newIds = foundItems + .map((i) => i.id || i.cid) + .sort() + .join(","); + + if (currentIds !== newIds) { + hoveredItems = foundItems; + updateTooltip(); + } } + } else { + if (!hideTimer && hoveredItems.length > 0) { + hideTimer = setTimeout(() => { + hoveredItems = []; + updateTooltip(); + hideTimer = null; + }, 300); + } + } + } + + function updateTooltip() { + if (!tooltipEl) return; + + if (hoveredItems.length === 0) { + tooltipEl.style.opacity = "0"; + tooltipEl.style.transform = "translateY(-105%) scale(0.9)"; + tooltipEl.style.pointerEvents = "none"; + return; } - if (request.type === "GET_SELECTOR_FOR_HIGHLIGHT") { - const selection = window.getSelection(); - if (!selection || selection.toString().trim().length === 0) { - sendResponse({ success: false, error: "No text selected" }); - return true; + tooltipEl.style.pointerEvents = "auto"; + + const authorsMap = new Map(); + hoveredItems.forEach((item) => { + const author = item.author || item.creator || {}; + const id = author.did || author.handle; + if (id && !authorsMap.has(id)) { + authorsMap.set(id, author); } + }); - const selector = buildTextQuoteSelector(selection); - if (selector) { - chrome.runtime - .sendMessage({ - type: "CREATE_HIGHLIGHT", - data: { - url: window.location.href, - title: document.title, - selector: selector, - }, - }) - .then((response) => { - if (response?.success) { - showNotification("Text highlighted!", "success"); - - if (CSS.highlights) { - try { - const range = selection.getRangeAt(0); - const highlight = new Highlight(range); - CSS.highlights.set("margin-highlight-preview", highlight); - } catch (e) { - console.warn("Could not visually highlight:", e); - } - } - - window.getSelection().removeAllRanges(); - } else { - showNotification( - "Failed to highlight: " + (response?.error || "Unknown error"), - "error", - ); - } - sendResponse(response); - }) - .catch((err) => { - console.error("Highlight error:", err); - showNotification("Error creating highlight", "error"); - sendResponse({ success: false, error: err.message }); - }); - return true; + const uniqueAuthors = Array.from(authorsMap.values()); + let contentHtml = ""; + + if (uniqueAuthors.length === 1) { + const author = uniqueAuthors[0] || {}; + const handle = author.handle || "User"; + const avatar = author.avatar; + const count = hoveredItems.length; + + let avatarHtml = `
${handle[0]?.toUpperCase() || "U"}
`; + if (avatar) { + avatarHtml = ``; } - sendResponse({ success: false, error: "Could not build selector" }); - return true; + + contentHtml = `${avatarHtml}${handle}${count > 1 ? ` (${count})` : ""}`; + } else { + let stackHtml = `
`; + const displayAuthors = uniqueAuthors.slice(0, 3); + displayAuthors.forEach((author) => { + const handle = author.handle || "U"; + const avatar = author.avatar; + if (avatar) { + stackHtml += ``; + } else { + stackHtml += `
${handle[0]?.toUpperCase() || "U"}
`; + } + }); + stackHtml += `
`; + + contentHtml = `${stackHtml}${uniqueAuthors.length} people`; } - if (request.type === "SCROLL_TO_TEXT") { - const found = findAndScrollToText(request.selector); - if (!found) { - showNotification("Could not find text on page", "error"); + tooltipEl.innerHTML = ` + ${contentHtml} +
+ `; + + const firstItem = hoveredItems[0]; + const match = activeItems.find((x) => x.item === firstItem); + if (match) { + const rects = match.range.getClientRects(); + if (rects && rects.length > 0) { + const rect = rects[0]; + const top = rect.top + window.scrollY; + const left = rect.left + window.scrollX; + + tooltipEl.style.top = `${top - 36}px`; + tooltipEl.style.left = `${left}px`; + tooltipEl.style.opacity = "1"; + tooltipEl.style.transform = "translateY(0) scale(1)"; } } + } - if (request.type === "RENDER_HIGHLIGHTS") { - renderPageHighlights(request.highlights); + function handleDocumentClick(e) { + if (hoveredItems.length > 0) { + e.preventDefault(); + e.stopPropagation(); + + const item = hoveredItems[0]; + const match = activeItems.find((x) => x.item === item); + if (match) { + const rects = match.range.getClientRects(); + if (rects.length > 0) { + const rect = rects[0]; + const top = rect.top + window.scrollY; + const left = rect.left + window.scrollX; + showPopover(hoveredItems, top, left); + } + } } + } - return true; - }); + function refreshPositions() {} - function showNotification(message, type = "info") { - const existing = document.querySelector(".margin-notification"); - if (existing) existing.remove(); - - const notification = document.createElement("div"); - notification.className = "margin-notification"; - notification.textContent = message; - - const bgColor = - type === "success" ? "#10b981" : type === "error" ? "#ef4444" : "#6366f1"; - notification.style.cssText = ` - position: fixed; - bottom: 24px; - right: 24px; - padding: 12px 20px; - background: ${bgColor}; - color: white; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - font-size: 14px; - font-weight: 500; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - z-index: 999999; - animation: margin-slide-in 0.2s ease; - `; + function renderBadges(annotations) { + if (!sidebarShadow) return; - document.body.appendChild(notification); + const itemsToRender = annotations || []; + activeItems = []; + const rangesByColor = {}; - setTimeout(() => { - notification.style.animation = "margin-slide-out 0.2s ease forwards"; - setTimeout(() => notification.remove(), 200); - }, 3000); + const matcher = new DOMTextMatcher(); + + itemsToRender.forEach((item) => { + const selector = item.target?.selector || item.selector; + if (!selector?.exact) return; + + const range = matcher.findRange(selector.exact); + if (range) { + activeItems.push({ range, item }); + + const color = item.color || "#c084fc"; + if (!rangesByColor[color]) rangesByColor[color] = []; + rangesByColor[color].push(range); + } + }); + + if (CSS.highlights) { + CSS.highlights.clear(); + for (const [color, ranges] of Object.entries(rangesByColor)) { + const highlight = new Highlight(...ranges); + const safeColor = color.replace(/[^a-zA-Z0-9]/g, ""); + const name = `margin-hl-${safeColor}`; + CSS.highlights.set(name, highlight); + injectHighlightStyle(name, color); + } + } } - const style = document.createElement("style"); - style.textContent = ` - @keyframes margin-slide-in { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } + const injectedStyles = new Set(); + function injectHighlightStyle(name, color) { + if (injectedStyles.has(name)) return; + const style = document.createElement("style"); + style.textContent = ` + ::highlight(${name}) { + background-color: ${color}66; + color: inherit; + cursor: pointer; } - @keyframes margin-slide-out { - from { opacity: 1; transform: translateY(0); } - to { opacity: 0; transform: translateY(10px); } + `; + document.head.appendChild(style); + injectedStyles.add(name); + } + + function showPopover(items, top, left) { + if (popoverEl) popoverEl.remove(); + const container = sidebarShadow.getElementById("margin-overlay-container"); + popoverEl = document.createElement("div"); + popoverEl.className = "margin-popover"; + + const popWidth = 320; + const screenWidth = window.innerWidth; + let finalLeft = left; + if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; + + popoverEl.style.top = `${top + 20}px`; + popoverEl.style.left = `${finalLeft}px`; + + const title = + items.length > 1 ? `${items.length} Annotations` : "Annotation"; + + let contentHtml = items + .map((item) => { + const author = item.author || item.creator || {}; + const handle = author.handle || "User"; + const avatar = author.avatar; + const text = item.body?.value || item.text || ""; + const quote = + item.target?.selector?.exact || item.selector?.exact || ""; + const id = item.id || item.uri; + + let avatarHtml = `
${handle[0]?.toUpperCase() || "U"}
`; + if (avatar) { + avatarHtml = ``; } - ::highlight(margin-highlight-preview) { - background-color: rgba(168, 85, 247, 0.3); - color: inherit; + + const isHighlight = item.type === "Highlight"; + + let bodyHtml = ""; + if (isHighlight) { + bodyHtml = `
"${quote}"
`; + } else { + bodyHtml = `
${text}
`; + if (quote) { + bodyHtml += `
"${quote}"
`; + } } - ::highlight(margin-scroll-highlight) { - background-color: rgba(99, 102, 241, 0.4); - color: inherit; + + return ` +
+
+
+ ${avatarHtml} + @${handle} +
+
+
+ ${bodyHtml} +
+
+ ${!isHighlight ? `` : ""} + +
+
+ `; + }) + .join(""); + + popoverEl.innerHTML = ` +
+ ${title} + +
+
+ ${contentHtml} +
+ `; + + popoverEl.querySelector(".popover-close").addEventListener("click", () => { + popoverEl.remove(); + popoverEl = null; + }); + + const replyBtns = popoverEl.querySelectorAll(".btn-reply"); + replyBtns.forEach((btn) => { + btn.addEventListener("click", () => { + const id = btn.getAttribute("data-id"); + if (id) { + chrome.runtime.sendMessage({ + type: "OPEN_APP_URL", + data: { path: `/annotation/${encodeURIComponent(id)}` }, + }); } - ::highlight(margin-page-highlights) { - background-color: rgba(252, 211, 77, 0.3); - color: inherit; + }); + }); + + const shareBtns = popoverEl.querySelectorAll(".btn-share"); + shareBtns.forEach((btn) => { + btn.addEventListener("click", async () => { + const id = btn.getAttribute("data-id"); + const text = btn.getAttribute("data-text"); + const quote = btn.getAttribute("data-quote"); + const u = `https://margin.at/annotation/${encodeURIComponent(id)}`; + const shareText = `${text ? text + "\n" : ""}${quote ? `"${quote}"\n` : ""}${u}`; + + try { + await navigator.clipboard.writeText(shareText); + const originalText = btn.innerText; + btn.innerText = "Copied!"; + setTimeout(() => (btn.innerText = originalText), 2000); + } catch (e) { + console.error("Failed to copy", e); } - `; - document.head.appendChild(style); + }); + }); + + container.appendChild(popoverEl); + + setTimeout(() => { + document.addEventListener("click", closePopoverOutside); + }, 0); + } + + function closePopoverOutside(e) { + if (popoverEl) { + popoverEl.remove(); + popoverEl = null; + document.removeEventListener("click", closePopoverOutside); + } + } + + function fetchAnnotations() { + if (typeof chrome !== "undefined" && chrome.runtime) { + chrome.runtime.sendMessage( + { + type: "GET_ANNOTATIONS", + data: { url: window.location.href }, + }, + (res) => { + if (res && res.success) { + renderBadges(res.data); + } + }, + ); + } + } + + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { + const sel = window.getSelection(); + if (!sel || !sel.toString()) { + sendResponse({ selector: null }); + return true; + } + const exact = sel.toString().trim(); + sendResponse({ selector: { type: "TextQuoteSelector", exact } }); + return true; + } + + if (request.type === "SCROLL_TO_TEXT") { + const selector = request.selector; + if (selector?.exact) { + const matcher = new DOMTextMatcher(); + const range = matcher.findRange(selector.exact); + if (range) { + const rect = range.getBoundingClientRect(); + window.scrollTo({ + top: window.scrollY + rect.top - window.innerHeight / 3, + behavior: "smooth", + }); + const highlight = new Highlight(range); + CSS.highlights.set("margin-scroll-flash", highlight); + injectHighlightStyle("margin-scroll-flash", "#8b5cf6"); + setTimeout(() => CSS.highlights.delete("margin-scroll-flash"), 2000); + } + } + } + return true; + }); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initOverlay); + } else { + initOverlay(); + } })(); diff --git a/extension/icons/site.webmanifest b/extension/icons/site.webmanifest index 45dc8a2..a6b7900 100644 --- a/extension/icons/site.webmanifest +++ b/extension/icons/site.webmanifest @@ -1 +1,19 @@ -{"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"} \ No newline at end of file +{ + "name": "Margin", + "short_name": "Margin", + "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" +} \ No newline at end of file diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 491a5db..eb4a20a 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -1,28 +1,32 @@ :root { - --bg-primary: #0c0a14; - --bg-secondary: #14111f; - --bg-tertiary: #1a1528; - --bg-card: #14111f; - --bg-hover: #1e1932; - - --text-primary: #f4f0ff; - --text-secondary: #a89ec8; - --text-tertiary: #6b5f8a; - - --accent: #a855f7; - --accent-hover: #c084fc; - --accent-subtle: rgba(168, 85, 247, 0.15); - - --border: #2d2640; - --border-hover: #3d3560; - - --success: #22c55e; - --danger: #ef4444; + --bg-primary: #09090b; + --bg-secondary: #0f0f12; + --bg-tertiary: #18181b; + --bg-card: #09090b; + --bg-elevated: #18181b; + --bg-hover: #27272a; + + --text-primary: #e4e4e7; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + --border: #27272a; + --border-hover: #3f3f46; + + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-subtle: rgba(99, 102, 241, 0.1); + --accent-text: #818cf8; + --success: #10b981; + --error: #ef4444; --warning: #f59e0b; - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-full: 9999px; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } * { diff --git a/extension/popup/popup.js b/extension/popup/popup.js index 97a810f..ed328ec 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -358,7 +358,10 @@ document.addEventListener("DOMContentLoaded", async () => { const res = await sendMessage({ type: "CHECK_SESSION" }); if (res.success && res.data?.authenticated) { - if (els.userHandle) els.userHandle.textContent = "@" + res.data.handle; + if (els.userHandle) { + const handle = res.data.handle || res.data.email || "User"; + els.userHandle.textContent = "@" + handle; + } els.userInfo.style.display = "flex"; currentUserDid = res.data.did; showView("main"); diff --git a/extension/sidepanel/sidepanel.css b/extension/sidepanel/sidepanel.css index 54dd052..f64150e 100644 --- a/extension/sidepanel/sidepanel.css +++ b/extension/sidepanel/sidepanel.css @@ -1,33 +1,183 @@ :root { - --bg-primary: #0c0a14; - --bg-secondary: #110e1c; - --bg-tertiary: #1a1528; - --bg-card: #14111f; - --bg-hover: #1e1932; - --bg-elevated: #1a1528; + --bg-primary: #09090b; + --bg-secondary: #0f0f12; + --bg-tertiary: #18181b; + --bg-card: #09090b; + --bg-hover: #18181b; + --bg-elevated: #18181b; + + --text-primary: #e4e4e7; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-subtle: rgba(99, 102, 241, 0.1); + --accent-text: #818cf8; + + --border: #27272a; + --border-hover: #3f3f46; + + --success: #10b981; + --error: #ef4444; + --warning: #f59e0b; - --text-primary: #f4f0ff; - --text-secondary: #a89ec8; - --text-tertiary: #6b5f8a; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-full: 9999px; - --accent: #a855f7; - --accent-hover: #c084fc; - --accent-subtle: rgba(168, 85, 247, 0.15); + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} - --border: #2d2640; - --border-hover: #3d3560; +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} - --success: #22c55e; - --error: #ef4444; - --warning: #f59e0b; +body { + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-full: 9999px; +.sidebar { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--bg-primary); +} + +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--border); + background: var(--bg-primary); +} + +.user-handle { + font-size: 12px; + color: var(--text-secondary); + background: var(--bg-tertiary); + padding: 4px 8px; + border-radius: var(--radius-sm); +} + +.current-page-info { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: var(--bg-primary); + border-bottom: 1px solid var(--border); +} + +.tabs { + display: flex; + border-bottom: 1px solid var(--border); + background: var(--bg-primary); + padding: 4px; + gap: 4px; + margin: 0; +} + +.tab-btn { + flex: 1; + padding: 10px 8px; + background: transparent; + border: none; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + border-radius: var(--radius-sm); + transition: all 0.15s; +} + +.tab-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); +} - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); +.tab-btn.active { + color: var(--text-primary); + background: var(--bg-tertiary); + box-shadow: none; +} + +.quick-actions { + display: flex; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + background: var(--bg-primary); +} + +.create-form { + padding: 16px; + border-bottom: 1px solid var(--border); + background: var(--bg-primary); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + background: var(--bg-primary); + border-bottom: 1px solid var(--border); +} + +.annotation-item { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 12px; + background: var(--bg-primary); + transition: border-color 0.15s; +} + +.annotation-item:hover { + border-color: var(--border-hover); + background: var(--bg-hover); +} + +.sidebar-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-top: 1px solid var(--border); + background: var(--bg-primary); +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 5px; + border: 2px solid var(--bg-primary); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-hover); } * { diff --git a/lexicons/at/margin/annotation.json b/lexicons/at/margin/annotation.json index c90320f..7d656f5 100644 --- a/lexicons/at/margin/annotation.json +++ b/lexicons/at/margin/annotation.json @@ -10,10 +10,7 @@ "key": "tid", "record": { "type": "object", - "required": [ - "target", - "createdAt" - ], + "required": ["target", "createdAt"], "properties": { "motivation": { "type": "string", @@ -87,9 +84,7 @@ "target": { "type": "object", "description": "W3C SpecificResource - the target with optional selector", - "required": [ - "source" - ], + "required": ["source"], "properties": { "source": { "type": "string", @@ -127,9 +122,7 @@ "textQuoteSelector": { "type": "object", "description": "W3C TextQuoteSelector - select text by quoting it with context", - "required": [ - "exact" - ], + "required": ["exact"], "properties": { "type": { "type": "string", @@ -158,10 +151,7 @@ "textPositionSelector": { "type": "object", "description": "W3C TextPositionSelector - select by character offsets", - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "properties": { "type": { "type": "string", @@ -182,9 +172,7 @@ "cssSelector": { "type": "object", "description": "W3C CssSelector - select DOM elements by CSS selector", - "required": [ - "value" - ], + "required": ["value"], "properties": { "type": { "type": "string", @@ -200,9 +188,7 @@ "xpathSelector": { "type": "object", "description": "W3C XPathSelector - select by XPath expression", - "required": [ - "value" - ], + "required": ["value"], "properties": { "type": { "type": "string", @@ -218,9 +204,7 @@ "fragmentSelector": { "type": "object", "description": "W3C FragmentSelector - select by URI fragment", - "required": [ - "value" - ], + "required": ["value"], "properties": { "type": { "type": "string", @@ -241,10 +225,7 @@ "rangeSelector": { "type": "object", "description": "W3C RangeSelector - select range between two selectors", - "required": [ - "startSelector", - "endSelector" - ], + "required": ["startSelector", "endSelector"], "properties": { "type": { "type": "string", @@ -289,4 +270,4 @@ } } } -} \ No newline at end of file +} diff --git a/lexicons/at/margin/bookmark.json b/lexicons/at/margin/bookmark.json index 66a58d1..3c14f5e 100644 --- a/lexicons/at/margin/bookmark.json +++ b/lexicons/at/margin/bookmark.json @@ -1,55 +1,52 @@ { - "lexicon": 1, - "id": "at.margin.bookmark", - "description": "A bookmark record - save URL for later", - "defs": { - "main": { - "type": "record", - "description": "A bookmarked URL (motivation: bookmarking)", - "key": "tid", - "record": { - "type": "object", - "required": [ - "source", - "createdAt" - ], - "properties": { - "source": { - "type": "string", - "format": "uri", - "description": "The bookmarked URL" - }, - "sourceHash": { - "type": "string", - "description": "SHA256 hash of normalized URL for indexing" - }, - "title": { - "type": "string", - "maxLength": 500, - "description": "Page title" - }, - "description": { - "type": "string", - "maxLength": 1000, - "maxGraphemes": 300, - "description": "Optional description/note" - }, - "tags": { - "type": "array", - "description": "Tags for categorization", - "items": { - "type": "string", - "maxLength": 64, - "maxGraphemes": 32 - }, - "maxLength": 10 - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } + "lexicon": 1, + "id": "at.margin.bookmark", + "description": "A bookmark record - save URL for later", + "defs": { + "main": { + "type": "record", + "description": "A bookmarked URL (motivation: bookmarking)", + "key": "tid", + "record": { + "type": "object", + "required": ["source", "createdAt"], + "properties": { + "source": { + "type": "string", + "format": "uri", + "description": "The bookmarked URL" + }, + "sourceHash": { + "type": "string", + "description": "SHA256 hash of normalized URL for indexing" + }, + "title": { + "type": "string", + "maxLength": 500, + "description": "Page title" + }, + "description": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 300, + "description": "Optional description/note" + }, + "tags": { + "type": "array", + "description": "Tags for categorization", + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "maxLength": 10 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } } + } } -} \ No newline at end of file + } +} diff --git a/lexicons/at/margin/collection.json b/lexicons/at/margin/collection.json index 5fc4828..246f55d 100644 --- a/lexicons/at/margin/collection.json +++ b/lexicons/at/margin/collection.json @@ -1,43 +1,40 @@ { - "lexicon": 1, - "id": "at.margin.collection", - "description": "A collection of annotations (like a folder or notebook)", - "defs": { - "main": { - "type": "record", - "description": "A named collection for organizing annotations", - "key": "tid", - "record": { - "type": "object", - "required": [ - "name", - "createdAt" - ], - "properties": { - "name": { - "type": "string", - "maxLength": 100, - "maxGraphemes": 50, - "description": "Collection name" - }, - "description": { - "type": "string", - "maxLength": 500, - "maxGraphemes": 150, - "description": "Collection description" - }, - "icon": { - "type": "string", - "maxLength": 10, - "maxGraphemes": 2, - "description": "Emoji icon for the collection" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } + "lexicon": 1, + "id": "at.margin.collection", + "description": "A collection of annotations (like a folder or notebook)", + "defs": { + "main": { + "type": "record", + "description": "A named collection for organizing annotations", + "key": "tid", + "record": { + "type": "object", + "required": ["name", "createdAt"], + "properties": { + "name": { + "type": "string", + "maxLength": 100, + "maxGraphemes": 50, + "description": "Collection name" + }, + "description": { + "type": "string", + "maxLength": 500, + "maxGraphemes": 150, + "description": "Collection description" + }, + "icon": { + "type": "string", + "maxLength": 10, + "maxGraphemes": 2, + "description": "Emoji icon for the collection" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } } + } } -} \ No newline at end of file + } +} diff --git a/lexicons/at/margin/collectionItem.json b/lexicons/at/margin/collectionItem.json index f8f81b3..261d7c3 100644 --- a/lexicons/at/margin/collectionItem.json +++ b/lexicons/at/margin/collectionItem.json @@ -1,41 +1,37 @@ { - "lexicon": 1, - "id": "at.margin.collectionItem", - "description": "An item in a collection (links annotation to collection)", - "defs": { - "main": { - "type": "record", - "description": "Associates an annotation with a collection", - "key": "tid", - "record": { - "type": "object", - "required": [ - "collection", - "annotation", - "createdAt" - ], - "properties": { - "collection": { - "type": "string", - "format": "at-uri", - "description": "AT URI of the collection" - }, - "annotation": { - "type": "string", - "format": "at-uri", - "description": "AT URI of the annotation, highlight, or bookmark" - }, - "position": { - "type": "integer", - "minimum": 0, - "description": "Sort order within the collection" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } + "lexicon": 1, + "id": "at.margin.collectionItem", + "description": "An item in a collection (links annotation to collection)", + "defs": { + "main": { + "type": "record", + "description": "Associates an annotation with a collection", + "key": "tid", + "record": { + "type": "object", + "required": ["collection", "annotation", "createdAt"], + "properties": { + "collection": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the collection" + }, + "annotation": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the annotation, highlight, or bookmark" + }, + "position": { + "type": "integer", + "minimum": 0, + "description": "Sort order within the collection" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } } + } } -} \ No newline at end of file + } +} diff --git a/lexicons/at/margin/highlight.json b/lexicons/at/margin/highlight.json index 256cc51..d19826a 100644 --- a/lexicons/at/margin/highlight.json +++ b/lexicons/at/margin/highlight.json @@ -1,45 +1,42 @@ { - "lexicon": 1, - "id": "at.margin.highlight", - "description": "A lightweight highlight record - annotation without body text", - "defs": { - "main": { - "type": "record", - "description": "A highlight on a web page (motivation: highlighting)", - "key": "tid", - "record": { - "type": "object", - "required": [ - "target", - "createdAt" - ], - "properties": { - "target": { - "type": "ref", - "ref": "at.margin.annotation#target", - "description": "The resource and segment being highlighted" - }, - "color": { - "type": "string", - "description": "Highlight color (hex or named)", - "maxLength": 20 - }, - "tags": { - "type": "array", - "description": "Tags for categorization", - "items": { - "type": "string", - "maxLength": 64, - "maxGraphemes": 32 - }, - "maxLength": 10 - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } + "lexicon": 1, + "id": "at.margin.highlight", + "description": "A lightweight highlight record - annotation without body text", + "defs": { + "main": { + "type": "record", + "description": "A highlight on a web page (motivation: highlighting)", + "key": "tid", + "record": { + "type": "object", + "required": ["target", "createdAt"], + "properties": { + "target": { + "type": "ref", + "ref": "at.margin.annotation#target", + "description": "The resource and segment being highlighted" + }, + "color": { + "type": "string", + "description": "Highlight color (hex or named)", + "maxLength": 20 + }, + "tags": { + "type": "array", + "description": "Tags for categorization", + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "maxLength": 10 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } } + } } -} \ No newline at end of file + } +} diff --git a/lexicons/at/margin/like.json b/lexicons/at/margin/like.json index e29137f..5bfdae8 100644 --- a/lexicons/at/margin/like.json +++ b/lexicons/at/margin/like.json @@ -1,46 +1,40 @@ { - "lexicon": 1, - "id": "at.margin.like", - "defs": { - "main": { - "type": "record", - "description": "A like on an annotation or reply", - "key": "tid", - "record": { - "type": "object", - "required": [ - "subject", - "createdAt" - ], - "properties": { - "subject": { - "type": "ref", - "ref": "#subjectRef", - "description": "Reference to the annotation or reply being liked" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } + "lexicon": 1, + "id": "at.margin.like", + "defs": { + "main": { + "type": "record", + "description": "A like on an annotation or reply", + "key": "tid", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "#subjectRef", + "description": "Reference to the annotation or reply being liked" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + }, + "subjectRef": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" }, - "subjectRef": { - "type": "object", - "required": [ - "uri", - "cid" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - } - } + "cid": { + "type": "string", + "format": "cid" } + } } -} \ No newline at end of file + } +} diff --git a/lexicons/at/margin/reply.json b/lexicons/at/margin/reply.json index 3f2df04..ab2a3c1 100644 --- a/lexicons/at/margin/reply.json +++ b/lexicons/at/margin/reply.json @@ -1,67 +1,59 @@ { - "lexicon": 1, - "id": "at.margin.reply", - "revision": 2, - "description": "A reply to an annotation or another reply", - "defs": { - "main": { - "type": "record", - "description": "A reply to an annotation (motivation: replying)", - "key": "tid", - "record": { - "type": "object", - "required": [ - "parent", - "root", - "text", - "createdAt" - ], - "properties": { - "parent": { - "type": "ref", - "ref": "#replyRef", - "description": "Reference to the parent annotation or reply" - }, - "root": { - "type": "ref", - "ref": "#replyRef", - "description": "Reference to the root annotation of the thread" - }, - "text": { - "type": "string", - "maxLength": 10000, - "maxGraphemes": 3000, - "description": "Reply text content" - }, - "format": { - "type": "string", - "description": "MIME type of the text content", - "default": "text/plain" - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } + "lexicon": 1, + "id": "at.margin.reply", + "revision": 2, + "description": "A reply to an annotation or another reply", + "defs": { + "main": { + "type": "record", + "description": "A reply to an annotation (motivation: replying)", + "key": "tid", + "record": { + "type": "object", + "required": ["parent", "root", "text", "createdAt"], + "properties": { + "parent": { + "type": "ref", + "ref": "#replyRef", + "description": "Reference to the parent annotation or reply" + }, + "root": { + "type": "ref", + "ref": "#replyRef", + "description": "Reference to the root annotation of the thread" + }, + "text": { + "type": "string", + "maxLength": 10000, + "maxGraphemes": 3000, + "description": "Reply text content" + }, + "format": { + "type": "string", + "description": "MIME type of the text content", + "default": "text/plain" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + }, + "replyRef": { + "type": "object", + "description": "Strong reference to an annotation or reply", + "required": ["uri", "cid"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" }, - "replyRef": { - "type": "object", - "description": "Strong reference to an annotation or reply", - "required": [ - "uri", - "cid" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - } - } + "cid": { + "type": "string", + "format": "cid" } + } } -} \ No newline at end of file + } +} diff --git a/web/src/App.jsx b/web/src/App.jsx index 0a37975..64da75d 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -1,6 +1,8 @@ import { Routes, Route } from "react-router-dom"; import { AuthProvider } from "./context/AuthContext"; -import Navbar from "./components/Navbar"; +import Sidebar from "./components/Sidebar"; +import RightSidebar from "./components/RightSidebar"; +import MobileNav from "./components/MobileNav"; import Feed from "./pages/Feed"; import Url from "./pages/Url"; import Profile from "./pages/Profile"; @@ -14,48 +16,51 @@ import Collections from "./pages/Collections"; import CollectionDetail from "./pages/CollectionDetail"; import Privacy from "./pages/Privacy"; +import Terms from "./pages/Terms"; + function AppContent() { return ( -
- -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {} - } /> - {} - } /> - } /> - } /> - } - /> - - } - /> - } - /> - } - /> - - } /> - } /> - -
+
+ +
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } + /> + } + /> + } + /> + } /> + } /> + } /> + +
+
+ +
); } diff --git a/web/src/api/client.js b/web/src/api/client.js index 45a28af..e97269d 100644 --- a/web/src/api/client.js +++ b/web/src/api/client.js @@ -427,3 +427,6 @@ export async function startLogin(handle, inviteCode) { body: JSON.stringify({ handle, invite_code: inviteCode }), }); } +export async function getTrendingTags(limit = 10) { + return request(`${API_BASE}/tags/trending?limit=${limit}`); +} diff --git a/web/src/assets/tangled.svg b/web/src/assets/tangled.svg new file mode 100644 index 0000000..8c5dd86 --- /dev/null +++ b/web/src/assets/tangled.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/AnnotationSkeleton.jsx b/web/src/components/AnnotationSkeleton.jsx new file mode 100644 index 0000000..538b827 --- /dev/null +++ b/web/src/components/AnnotationSkeleton.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +export default function AnnotationSkeleton() { + return ( +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ ); +} diff --git a/web/src/components/MobileNav.jsx b/web/src/components/MobileNav.jsx new file mode 100644 index 0000000..2575351 --- /dev/null +++ b/web/src/components/MobileNav.jsx @@ -0,0 +1,61 @@ +import { Link, useLocation } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { Home, Search, Folder, User, PenSquare } from "lucide-react"; + +export default function MobileNav() { + const { user, isAuthenticated } = useAuth(); + const location = useLocation(); + + const isActive = (path) => { + if (path === "/") return location.pathname === "/"; + return location.pathname.startsWith(path); + }; + + return ( + + ); +} diff --git a/web/src/components/Navbar.jsx b/web/src/components/Navbar.jsx deleted file mode 100644 index 94236b2..0000000 --- a/web/src/components/Navbar.jsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { Link, useLocation } from "react-router-dom"; -import { Folder } from "lucide-react"; -import { useAuth } from "../context/AuthContext"; -import { - PenIcon, - BookmarkIcon, - HighlightIcon, - SearchIcon, - LogoutIcon, - BellIcon, -} from "./Icons"; -import { getUnreadNotificationCount } from "../api/client"; -import { SiFirefox, SiGooglechrome } from "react-icons/si"; -import { FaEdge } from "react-icons/fa"; - -import logo from "../assets/logo.svg"; - -const isFirefox = - typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); -const isEdge = - typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); -const isChrome = - typeof navigator !== "undefined" && - /Chrome/i.test(navigator.userAgent) && - !isEdge; - -export default function Navbar() { - const { user, isAuthenticated, logout, loading } = useAuth(); - const location = useLocation(); - const [menuOpen, setMenuOpen] = useState(false); - const [unreadCount, setUnreadCount] = useState(0); - const menuRef = useRef(null); - - const isActive = (path) => location.pathname === path; - - useEffect(() => { - if (isAuthenticated) { - getUnreadNotificationCount() - .then((data) => setUnreadCount(data.count || 0)) - .catch(() => {}); - const interval = setInterval(() => { - getUnreadNotificationCount() - .then((data) => setUnreadCount(data.count || 0)) - .catch(() => {}); - }, 60000); - return () => clearInterval(interval); - } - }, [isAuthenticated]); - - useEffect(() => { - const handleClickOutside = (e) => { - if (menuRef.current && !menuRef.current.contains(e.target)) { - setMenuOpen(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - const getInitials = () => { - if (user?.displayName) { - return user.displayName.substring(0, 2).toUpperCase(); - } - if (user?.handle) { - return user.handle.substring(0, 2).toUpperCase(); - } - return "U"; - }; - - return ( - - ); -} diff --git a/web/src/components/RightSidebar.jsx b/web/src/components/RightSidebar.jsx new file mode 100644 index 0000000..8b74227 --- /dev/null +++ b/web/src/components/RightSidebar.jsx @@ -0,0 +1,161 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Download, ExternalLink } from "lucide-react"; +import { SiFirefox, SiGooglechrome, SiGithub, SiBluesky } from "react-icons/si"; +import { FaEdge } from "react-icons/fa"; +import { useAuth } from "../context/AuthContext"; +import { getTrendingTags } from "../api/client"; +import tangledIcon from "../assets/tangled.svg"; + +const isFirefox = + typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); +const isEdge = + typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); +const isChrome = + typeof navigator !== "undefined" && + /Chrome/i.test(navigator.userAgent) && + !isEdge; + +function getExtensionInfo() { + if (isFirefox) { + return { + url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", + icon: SiFirefox, + name: "Firefox", + }; + } + if (isEdge) { + return { + url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", + icon: FaEdge, + name: "Edge", + }; + } + return { + url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", + icon: SiGooglechrome, + name: "Chrome", + }; +} + +export default function RightSidebar() { + const { isAuthenticated } = useAuth(); + const ext = getExtensionInfo(); + const ExtIcon = ext.icon; + const [trendingTags, setTrendingTags] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + getTrendingTags() + .then((tags) => setTrendingTags(tags)) + .catch((err) => console.error("Failed to fetch trending tags:", err)) + .finally(() => setLoading(false)); + }, []); + + return ( + + ); +} diff --git a/web/src/components/Sidebar.jsx b/web/src/components/Sidebar.jsx new file mode 100644 index 0000000..56c46ef --- /dev/null +++ b/web/src/components/Sidebar.jsx @@ -0,0 +1,189 @@ +import { useState, useRef, useEffect } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { + Home, + Search, + Folder, + Bell, + PenSquare, + User, + LogOut, + MoreHorizontal, + Highlighter, + Bookmark, +} from "lucide-react"; +import { getUnreadNotificationCount } from "../api/client"; +import logo from "../assets/logo.svg"; + +export default function Sidebar() { + const { user, isAuthenticated, logout, loading } = useAuth(); + const location = useLocation(); + const [menuOpen, setMenuOpen] = useState(false); + const [unreadCount, setUnreadCount] = useState(0); + const menuRef = useRef(null); + + const isActive = (path) => { + if (path === "/") return location.pathname === "/"; + return location.pathname.startsWith(path); + }; + + useEffect(() => { + if (isAuthenticated) { + getUnreadNotificationCount() + .then((data) => setUnreadCount(data.count || 0)) + .catch(() => {}); + const interval = setInterval(() => { + getUnreadNotificationCount() + .then((data) => setUnreadCount(data.count || 0)) + .catch(() => {}); + }, 60000); + return () => clearInterval(interval); + } + }, [isAuthenticated]); + + useEffect(() => { + const handleClickOutside = (e) => { + if (menuRef.current && !menuRef.current.contains(e.target)) { + setMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const getInitials = () => { + if (user?.displayName) { + return user.displayName.substring(0, 2).toUpperCase(); + } + if (user?.handle) { + return user.handle.substring(0, 2).toUpperCase(); + } + return "U"; + }; + + return ( + + ); +} diff --git a/web/src/css/annotations.css b/web/src/css/annotations.css new file mode 100644 index 0000000..120dfe9 --- /dev/null +++ b/web/src/css/annotations.css @@ -0,0 +1,473 @@ +.annotation-detail-page { + max-width: 680px; + margin: 0 auto; + padding: 24px 16px; + min-height: 100vh; +} + +.annotation-detail-header { + margin-bottom: 24px; +} + +.back-link { + display: inline-flex; + align-items: center; + color: var(--text-tertiary); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + transition: color 0.15s; +} + +.back-link:hover { + color: var(--text-primary); +} + +.replies-section { + margin-top: 32px; + border-top: 1px solid var(--border); + padding-top: 24px; +} + +.replies-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 20px; +} + +.annotation-card { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px 0; + border-bottom: 1px solid var(--border); + transition: background 0.15s ease; +} + +.annotation-card:last-child { + border-bottom: none; +} + +.annotation-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; +} + +.annotation-header-left { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} + +.annotation-avatar { + width: 36px; + height: 36px; + min-width: 36px; + border-radius: 50%; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.85rem; + color: var(--text-secondary); + overflow: hidden; +} + +.annotation-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.annotation-meta { + display: flex; + flex-direction: column; + justify-content: center; + line-height: 1.3; +} + +.annotation-avatar-link { + text-decoration: none; + border-radius: 50%; +} + +.annotation-author-row { + display: flex; + align-items: baseline; + gap: 6px; + flex-wrap: wrap; +} + +.annotation-author { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9rem; +} + +.annotation-handle { + font-size: 0.85rem; + color: var(--text-tertiary); + text-decoration: none; +} + +.annotation-handle:hover { + color: var(--text-secondary); +} + +.annotation-time { + font-size: 0.75rem; + color: var(--text-tertiary); +} + +.annotation-content { + display: flex; + flex-direction: column; + gap: 10px; + padding-left: 46px; +} + +.annotation-source { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + color: var(--text-tertiary); + text-decoration: none; + transition: color 0.15s ease; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.annotation-source:hover { + color: var(--text-secondary); + text-decoration: underline; +} + +.annotation-source-title { + color: var(--text-tertiary); + opacity: 0.7; +} + +.annotation-highlight { + display: block; + position: relative; + padding-left: 12px; + margin: 4px 0; + text-decoration: none; + border-left: 2px solid var(--border); + transition: all 0.15s ease; +} + +.annotation-highlight:hover { + border-left-color: var(--text-secondary); +} + +.annotation-highlight mark { + background: transparent; + color: var(--text-primary); + font-style: italic; + font-size: 1rem; + line-height: 1.6; + font-weight: 400; + font-family: var(--font-serif, var(--font-sans)); + display: inline; +} + +.annotation-text { + font-size: 0.95rem; + line-height: 1.6; + color: var(--text-primary); + white-space: pre-wrap; +} + +.annotation-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 4px; +} + +.annotation-tag { + font-size: 0.8rem; + color: var(--accent); + text-decoration: none; + font-weight: 500; + opacity: 0.9; + transition: opacity 0.15s; +} + +.annotation-tag:hover { + opacity: 1; + text-decoration: underline; +} + +.annotation-actions { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 4px; + padding-left: 46px; +} + +.annotation-actions-left { + display: flex; + align-items: center; + gap: 16px; +} + +.annotation-action { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-tertiary); + font-size: 0.8rem; + font-weight: 500; + padding: 6px; + margin-left: -6px; + border-radius: var(--radius-sm); + transition: all 0.15s ease; + background: transparent; + cursor: pointer; + border: none; +} + +.annotation-action:hover { + color: var(--text-secondary); + background: var(--bg-tertiary); +} + +.annotation-action.liked { + color: #ef4444; +} + +.annotation-action.liked svg { + fill: #ef4444; +} + +.annotation-action.active { + color: var(--accent); +} + +.action-icon-only { + padding: 6px; +} + +.annotation-header-right { + opacity: 0; + transition: opacity 0.15s; +} + +.annotation-card:hover .annotation-header-right { + opacity: 1; +} + +.inline-replies { + margin-top: 12px; + padding-left: 46px; +} + +@media (max-width: 600px) { + .annotation-content, + .annotation-actions, + .inline-replies { + padding-left: 0; + } + + .annotation-header-right { + opacity: 1; + } +} + +.replies-list-threaded { + margin-top: 16px; + display: flex; + flex-direction: column; +} + +.reply-card-threaded { + position: relative; + padding-left: 0; + transition: background 0.15s; +} + +.reply-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 6px; +} + +.reply-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--bg-tertiary); + overflow: hidden; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.reply-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.reply-avatar span { + font-size: 0.7rem; + font-weight: 600; + color: var(--text-secondary); +} + +.reply-meta { + display: flex; + align-items: baseline; + gap: 6px; + flex: 1; + min-width: 0; +} + +.reply-author { + font-weight: 600; + font-size: 0.85rem; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.reply-handle { + font-size: 0.8rem; + color: var(--text-tertiary); + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.reply-time { + font-size: 0.75rem; + color: var(--text-tertiary); + white-space: nowrap; +} + +.reply-dot { + color: var(--text-tertiary); + font-size: 0.7rem; +} + +.reply-text { + font-size: 0.9rem; + line-height: 1.5; + color: var(--text-primary); + margin: 0; + padding-left: 38px; +} + +.reply-actions { + display: flex; + align-items: center; + gap: 4px; + opacity: 0; + transition: opacity 0.15s; +} + +.reply-card-threaded:hover .reply-actions { + opacity: 1; +} + +.reply-action-btn { + background: none; + border: none; + padding: 4px; + color: var(--text-tertiary); + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.reply-action-btn:hover { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.reply-action-delete:hover { + color: #ef4444; + background: rgba(239, 68, 68, 0.1); +} + +.reply-form { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 16px; + background: var(--bg-secondary); + margin-bottom: 24px; +} + +.replying-to-banner { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bg-tertiary); + padding: 8px 12px; + border-radius: var(--radius-sm); + margin-bottom: 12px; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.cancel-reply { + background: none; + border: none; + color: var(--text-tertiary); + cursor: pointer; + font-size: 1.2rem; + padding: 0 4px; + line-height: 1; +} + +.cancel-reply:hover { + color: var(--text-primary); +} + +.reply-input { + width: 100%; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 12px; + color: var(--text-primary); + font-family: inherit; + font-size: 0.95rem; + resize: vertical; + min-height: 80px; + transition: border-color 0.15s; + display: block; + box-sizing: border-box; +} + +.reply-input:focus { + outline: none; + border-color: var(--accent); +} + +.reply-form-actions { + display: flex; + justify-content: flex-end; + margin-top: 12px; +} diff --git a/web/src/css/base.css b/web/src/css/base.css new file mode 100644 index 0000000..27579f5 --- /dev/null +++ b/web/src/css/base.css @@ -0,0 +1,139 @@ +:root { + --bg-primary: #09090b; + --bg-secondary: #0f0f12; + --bg-tertiary: #18181b; + --bg-card: #09090b; + --bg-elevated: #18181b; + --text-primary: #e4e4e7; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + --border: #27272a; + --border-hover: #3f3f46; + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-subtle: rgba(99, 102, 241, 0.1); + --accent-text: #818cf8; + --success: #10b981; + --error: #ef4444; + --warning: #f59e0b; + --info: #3b82f6; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-full: 9999px; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --font-sans: + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: + "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; +} + +body { + font-family: var(--font-sans); + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + color: inherit; + text-decoration: none; + transition: color 0.15s ease; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 600; + line-height: 1.25; + letter-spacing: -0.025em; + color: var(--text-primary); +} + +p { + color: var(--text-secondary); +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; +} + +input, +textarea, +select { + font-family: inherit; + font-size: inherit; + color: var(--text-primary); +} + +::selection { + background: var(--accent-subtle); + color: var(--accent-text); +} + +.text-sm { + font-size: 0.875rem; +} + +.text-xs { + font-size: 0.75rem; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.text-muted { + color: var(--text-secondary); +} + +.text-faint { + color: var(--text-tertiary); +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 5px; + border: 2px solid var(--bg-primary); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-hover); +} diff --git a/web/src/css/buttons.css b/web/src/css/buttons.css new file mode 100644 index 0000000..cb435ce --- /dev/null +++ b/web/src/css/buttons.css @@ -0,0 +1,127 @@ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 20px; + font-size: 0.9rem; + font-weight: 500; + border-radius: var(--radius-md); + transition: all 0.15s ease; +} + +.btn-primary { + background: var(--accent); + color: white; +} + +.btn-primary:hover { + background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg-hover); + border-color: var(--border-hover); +} + +.btn-ghost { + color: var(--text-secondary); + padding: 8px 12px; +} + +.btn-ghost:hover { + color: var(--text-primary); + background: var(--bg-tertiary); +} + +.btn-bluesky { + background: #0085ff; + color: white; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + transition: + background 0.2s, + transform 0.2s; +} + +.btn-bluesky:hover { + background: #0070dd; + transform: translateY(-1px); +} + +.btn-sm { + padding: 6px 12px; + font-size: 0.85rem; +} + +.btn-text { + background: none; + border: none; + color: var(--text-secondary); + font-size: 0.9rem; + padding: 8px 12px; + cursor: pointer; + transition: color 0.15s; +} + +.btn-text:hover { + color: var(--text-primary); +} + +.btn-block { + width: 100%; + text-align: left; + padding: 8px 12px; + color: var(--text-secondary); + background: var(--bg-tertiary); + border-radius: var(--radius-md); + margin-top: 8px; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; +} + +.btn-block:hover { + background: var(--border); + color: var(--text-primary); +} + +.btn-icon-danger { + padding: 8px; + background: var(--error); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + box-shadow: var(--shadow-md); + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-icon-danger:hover { + background: #dc2626; + transform: scale(1.05); +} + +.action-buttons { + display: flex; + gap: 8px; +} + +.action-buttons-end { + display: flex; + justify-content: flex-end; + gap: 8px; +} diff --git a/web/src/css/collections.css b/web/src/css/collections.css new file mode 100644 index 0000000..f3332bf --- /dev/null +++ b/web/src/css/collections.css @@ -0,0 +1,310 @@ +.collections-list { + display: flex; + flex-direction: column; + gap: 2px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.collection-row { + display: flex; + align-items: center; + background: var(--bg-card); + transition: background 0.15s ease; +} + +.collection-row:not(:last-child) { + border-bottom: 1px solid var(--border); +} + +.collection-row:hover { + background: var(--bg-secondary); +} + +.collection-row-content { + flex: 1; + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + text-decoration: none; + min-width: 0; +} + +.collection-row-icon { + width: 44px; + height: 44px; + min-width: 44px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient( + 135deg, + rgba(79, 70, 229, 0.1), + rgba(168, 85, 247, 0.15) + ); + color: var(--accent); + border-radius: var(--radius-md); + transition: all 0.2s ease; +} + +.collection-row:hover .collection-row-icon { + background: linear-gradient( + 135deg, + rgba(79, 70, 229, 0.15), + rgba(168, 85, 247, 0.2) + ); + transform: scale(1.05); +} + +.collection-row-info { + flex: 1; + min-width: 0; +} + +.collection-row-name { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 2px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.collection-row:hover .collection-row-name { + color: var(--accent); +} + +.collection-row-desc { + font-size: 0.85rem; + color: var(--text-secondary); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.collection-row-arrow { + color: var(--text-tertiary); + opacity: 0; + transition: all 0.2s ease; +} + +.collection-row:hover .collection-row-arrow { + opacity: 1; + color: var(--accent); + transform: translateX(2px); +} + +.collection-row-edit { + padding: 10px; + margin-right: 12px; + color: var(--text-tertiary); + background: none; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + opacity: 0; + transition: all 0.15s ease; +} + +.collection-row:hover .collection-row-edit { + opacity: 1; +} + +.collection-row-edit:hover { + color: var(--text-primary); + background: var(--bg-tertiary); +} + +.collection-detail-header { + display: flex; + gap: 20px; + padding: 24px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + margin-bottom: 32px; + position: relative; +} + +.collection-detail-icon { + width: 56px; + height: 56px; + min-width: 56px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient( + 135deg, + rgba(79, 70, 229, 0.1), + rgba(168, 85, 247, 0.1) + ); + color: var(--accent); + border-radius: var(--radius-md); +} + +.collection-detail-info { + flex: 1; + min-width: 0; +} + +.collection-detail-visibility { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.8rem; + font-weight: 600; + color: var(--accent); + text-transform: capitalize; + margin-bottom: 8px; +} + +.collection-detail-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; + line-height: 1.3; +} + +.collection-detail-desc { + color: var(--text-secondary); + font-size: 1rem; + line-height: 1.5; + margin-bottom: 12px; + max-width: 600px; +} + +.collection-detail-stats { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + color: var(--text-tertiary); +} + +.collection-detail-actions { + position: absolute; + top: 20px; + right: 20px; + display: flex; + align-items: center; + gap: 8px; +} + +.collection-detail-actions .share-menu-container { + display: flex; + align-items: center; +} + +.collection-detail-actions .annotation-action { + padding: 10px; + color: var(--text-tertiary); + background: none; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s ease; +} + +.collection-detail-actions .annotation-action:hover { + color: var(--accent); + background: var(--bg-tertiary); +} + +.collection-detail-edit, +.collection-detail-delete { + padding: 10px; + color: var(--text-tertiary); + background: none; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s ease; +} + +.collection-detail-edit:hover { + color: var(--accent); + background: var(--bg-tertiary); +} + +.collection-detail-delete:hover { + color: var(--error); + background: rgba(239, 68, 68, 0.1); +} + +.collection-item-wrapper { + position: relative; +} + +.collection-item-remove { + position: absolute; + top: 12px; + left: -40px; + z-index: 10; + padding: 8px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-tertiary); + cursor: pointer; + opacity: 0; + transition: all 0.15s ease; +} + +.collection-item-wrapper:hover .collection-item-remove { + opacity: 1; +} + +.collection-item-remove:hover { + color: var(--error); + border-color: var(--error); + background: rgba(239, 68, 68, 0.05); +} + +.collection-list-item { + width: 100%; + text-align: left; + padding: 12px 16px; + border-radius: var(--radius-md); + background: var(--bg-primary); + border: 1px solid transparent; + color: var(--text-primary); + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.collection-list-item:hover { + background: var(--bg-hover); + border-color: var(--border); +} + +.collection-list-item:hover .collection-list-item-icon { + opacity: 1; +} + +.collection-list-item:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.item-delete-overlay { + position: absolute; + top: 16px; + right: 16px; + z-index: 10; + opacity: 0; + transition: opacity 0.15s ease; +} + +.card:hover .item-delete-overlay, +div:hover > .item-delete-overlay { + opacity: 1; +} diff --git a/web/src/css/feed.css b/web/src/css/feed.css new file mode 100644 index 0000000..58c01bc --- /dev/null +++ b/web/src/css/feed.css @@ -0,0 +1,139 @@ +.feed { + display: flex; + flex-direction: column; + gap: 16px; +} + +.feed-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.feed-title { + font-size: 1.5rem; + font-weight: 700; +} + +.feed-filters { + display: flex; + gap: 8px; + margin-bottom: 24px; + padding: 4px; + background: var(--bg-tertiary); + border-radius: var(--radius-lg); + width: fit-content; +} + +.filter-tab { + padding: 8px 16px; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-secondary); + background: transparent; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.15s ease; +} + +.filter-tab:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.filter-tab.active { + color: var(--text-primary); + background: var(--bg-card); + box-shadow: var(--shadow-sm); +} + +.page-header { + margin-bottom: 32px; +} + +.page-title { + font-size: 2rem; + font-weight: 700; + margin-bottom: 8px; +} + +.page-description { + color: var(--text-secondary); + font-size: 1.1rem; +} + +.url-input-wrapper { + margin-bottom: 24px; +} + +.url-input-container { + display: flex; + gap: 12px; +} + +.url-input { + width: 100%; + padding: 16px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 1.1rem; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.url-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-subtle); + background: var(--bg-primary); +} + +.url-input::placeholder { + color: var(--text-tertiary); +} + +.url-results-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 12px; +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); + font-size: 0.9rem; + text-decoration: none; + margin-bottom: 24px; + transition: color 0.15s; +} + +.back-link:hover { + color: var(--accent); +} + +.new-page { + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 32px; +} + +@media (max-width: 640px) { + .main-content { + padding: 16px 12px; + } + + .page-title { + font-size: 1.5rem; + } +} diff --git a/web/src/css/layout.css b/web/src/css/layout.css new file mode 100644 index 0000000..fa23e00 --- /dev/null +++ b/web/src/css/layout.css @@ -0,0 +1,463 @@ +.layout { + display: flex; + min-height: 100vh; + background: var(--bg-primary); +} + +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: 240px; + background: var(--bg-primary); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + z-index: 50; + padding-bottom: 20px; +} + +.sidebar-header { + height: 64px; + display: flex; + align-items: center; + padding: 0 20px; + margin-bottom: 12px; + text-decoration: none; + color: var(--text-primary); +} + +.sidebar-logo { + width: 24px; + height: 24px; + object-fit: contain; + margin-right: 12px; +} + +.sidebar-brand { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +.sidebar-nav { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + padding: 0 12px; + overflow-y: auto; +} + +.sidebar-link { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + border-radius: var(--radius-md); + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.15s ease; +} + +.sidebar-link:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-link.active { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-link svg { + width: 18px; + height: 18px; + color: var(--text-tertiary); + transition: color 0.15s ease; +} + +.sidebar-link:hover svg, +.sidebar-link.active svg { + color: var(--text-primary); +} + +.sidebar-section-title { + padding: 24px 12px 8px; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.notification-badge { + background: var(--accent); + color: white; + font-size: 0.7rem; + font-weight: 600; + padding: 0 6px; + height: 18px; + border-radius: 99px; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; +} + +.sidebar-new-btn { + display: flex; + align-items: center; + gap: 10px; + margin: 0 12px 16px; + padding: 10px 16px; + background: var(--text-primary); + color: var(--bg-primary); + border-radius: var(--radius-md); + font-size: 0.9rem; + font-weight: 600; + text-decoration: none; + transition: opacity 0.15s; + justify-content: center; +} + +.sidebar-new-btn:hover { + opacity: 0.9; +} + +.sidebar-footer { + padding: 0 12px; + margin-top: auto; +} + +.sidebar-user { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: var(--radius-md); + cursor: pointer; + transition: background 0.15s ease; +} + +.sidebar-user:hover, +.sidebar-user.active { + background: var(--bg-tertiary); +} + +.sidebar-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + font-size: 0.8rem; + font-weight: 500; + overflow: hidden; + flex-shrink: 0; + border: 1px solid var(--border); +} + +.sidebar-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.sidebar-user-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.sidebar-user-name { + font-size: 0.85rem; + font-weight: 500; + color: var(--text-primary); +} + +.sidebar-user-handle { + font-size: 0.75rem; + color: var(--text-tertiary); +} + +.sidebar-dropdown { + position: absolute; + bottom: 74px; + left: 12px; + width: 216px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: 4px; + z-index: 1000; + overflow: hidden; + animation: scaleIn 0.1s ease-out; + transform-origin: bottom center; +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.sidebar-dropdown-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 12px; + font-size: 0.85rem; + color: var(--text-secondary); + text-decoration: none; + background: transparent; + cursor: pointer; + border-radius: var(--radius-sm); + transition: all 0.15s; + border: none; +} + +.sidebar-dropdown-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-dropdown-item.danger:hover { + background: rgba(239, 68, 68, 0.1); + color: var(--error); +} + +.main-layout { + flex: 1; + margin-left: 240px; + margin-right: 280px; + min-height: 100vh; +} + +.main-content-wrapper { + max-width: 640px; + margin: 0 auto; + padding: 40px 24px; +} + +.right-sidebar { + position: fixed; + right: 0; + top: 0; + bottom: 0; + width: 280px; + background: var(--bg-primary); + border-left: 1px solid var(--border); + padding: 32px 24px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 32px; +} + +.right-section { + display: flex; + flex-direction: column; + gap: 12px; +} + +.right-section-title { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} + +.right-section-desc { + font-size: 0.85rem; + line-height: 1.5; + color: var(--text-secondary); +} + +.right-extension-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 0.85rem; + font-weight: 500; + text-decoration: none; + transition: all 0.15s ease; + width: fit-content; +} + +.right-extension-btn:hover { + border-color: var(--text-tertiary); + background: var(--bg-tertiary); +} + +.right-links { + display: flex; + flex-direction: column; + gap: 4px; +} + +.right-link { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; + color: var(--text-secondary); + font-size: 0.9rem; + transition: color 0.15s; + text-decoration: none; +} + +.right-link:hover { + color: var(--text-primary); +} + +.right-link svg { + width: 16px; + height: 16px; + color: var(--text-tertiary); + transition: all 0.15s; +} + +.right-link:hover svg { + color: var(--text-secondary); +} + +.tangled-icon { + width: 16px; + height: 16px; + background-color: var(--text-tertiary); + -webkit-mask: url("../assets/tangled.svg") no-repeat center / contain; + mask: url("../assets/tangled.svg") no-repeat center / contain; + transition: background-color 0.15s; +} + +.right-link:hover .tangled-icon { + background-color: var(--text-secondary); +} + +.right-footer { + margin-top: auto; + display: flex; + flex-wrap: wrap; + gap: 12px; + font-size: 0.75rem; + color: var(--text-tertiary); +} + +.right-footer a { + color: var(--text-tertiary); +} + +.right-footer a:hover { + color: var(--text-secondary); +} + +.mobile-nav { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: rgba(9, 9, 11, 0.9); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-top: 1px solid var(--border); + padding: 8px 16px; + padding-bottom: calc(8px + env(safe-area-inset-bottom, 0)); + z-index: 100; +} + +.mobile-nav-inner { + display: flex; + justify-content: space-between; + align-items: center; +} + +.mobile-nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + color: var(--text-tertiary); + text-decoration: none; + font-size: 0.65rem; + font-weight: 500; + width: 60px; + transition: color 0.15s; +} + +.mobile-nav-item.active { + color: var(--text-primary); +} + +.mobile-nav-item svg { + width: 24px; + height: 24px; +} + +.mobile-nav-new { + width: 48px; + height: 36px; + border-radius: var(--radius-md); + background: var(--text-primary); + color: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; +} + +.mobile-nav-new svg { + width: 20px; + height: 20px; +} + +@media (max-width: 1200px) { + .right-sidebar { + display: none; + } + + .main-layout { + margin-right: 0; + } +} + +@media (max-width: 768px) { + .sidebar { + display: none; + } + + .main-layout { + margin-left: 0; + padding-bottom: 80px; + } + + .main-content-wrapper { + padding: 20px 16px; + } + + .mobile-nav { + display: block; + } +} diff --git a/web/src/css/login.css b/web/src/css/login.css new file mode 100644 index 0000000..39faaa9 --- /dev/null +++ b/web/src/css/login.css @@ -0,0 +1,297 @@ +.login-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 70vh; + padding: 60px 20px; + width: 100%; + max-width: 500px; + margin: 0 auto; +} + +.login-at-logo { + font-size: 5rem; + font-weight: 800; + color: var(--accent); + margin-bottom: 24px; + line-height: 1; +} + +.login-logo-img { + width: 80px; + height: 80px; + margin-bottom: 24px; + object-fit: contain; +} + +.login-heading { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 32px; + display: flex; + align-items: center; + gap: 10px; + text-align: center; + line-height: 1.4; +} + +.login-help-btn { + background: none; + border: none; + color: var(--text-tertiary); + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + transition: color 0.15s; + flex-shrink: 0; +} + +.login-help-btn:hover { + color: var(--accent); +} + +.login-help-text { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 16px 20px; + margin-bottom: 24px; + font-size: 0.95rem; + color: var(--text-secondary); + line-height: 1.6; + text-align: center; +} + +.login-help-text code { + background: var(--bg-tertiary); + padding: 2px 8px; + border-radius: var(--radius-sm); + font-size: 0.9rem; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} + +.login-input-wrapper { + position: relative; +} + +.login-input { + width: 100%; + padding: 14px 16px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 1rem; + transition: + border-color 0.15s, + box-shadow 0.15s; +} + +.login-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); +} + +.login-input::placeholder { + color: var(--text-tertiary); +} + +.login-suggestions { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + overflow: hidden; + z-index: 100; +} + +.login-suggestion { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 12px 16px; + background: transparent; + border: none; + cursor: pointer; + text-align: left; + transition: background 0.1s; +} + +.login-suggestion:hover, +.login-suggestion.selected { + background: var(--bg-elevated); +} + +.login-suggestion-avatar { + width: 40px; + height: 40px; + border-radius: var(--radius-full); + background: linear-gradient(135deg, var(--accent), #a855f7); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; + font-size: 0.875rem; + font-weight: 600; + color: white; +} + +.login-suggestion-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.login-suggestion-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.login-suggestion-name { + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.login-suggestion-handle { + font-size: 0.875rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.login-error { + padding: 12px 16px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: var(--radius-md); + color: #ef4444; + font-size: 0.875rem; +} + +.login-legal { + font-size: 0.75rem; + color: var(--text-tertiary); + line-height: 1.5; + margin-top: 16px; +} + +.login-brand { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 24px; +} + +.login-brand-icon { + width: 48px; + height: 48px; + background: linear-gradient(135deg, var(--accent), #a855f7); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + font-weight: 800; + color: white; +} + +.login-brand-name { + font-size: 1.75rem; + font-weight: 700; +} + +.login-avatar { + width: 72px; + height: 72px; + border-radius: var(--radius-full); + background: linear-gradient(135deg, var(--accent), #a855f7); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + font-weight: 700; + font-size: 1.5rem; + color: white; + overflow: hidden; +} + +.login-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.login-avatar-large { + width: 100px; + height: 100px; + border-radius: var(--radius-full); + background: linear-gradient(135deg, var(--accent), #a855f7); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + font-weight: 700; + font-size: 2rem; + color: white; + overflow: hidden; +} + +.login-avatar-large img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.login-welcome { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 32px; + text-align: center; +} + +.login-welcome-name { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 24px; +} + +.login-actions { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; +} + +.login-btn { + width: 100%; + padding: 14px 24px; + font-size: 1rem; + font-weight: 600; +} + +.login-submit { + padding: 18px 32px; + font-size: 1.1rem; + font-weight: 600; +} diff --git a/web/src/css/modals.css b/web/src/css/modals.css new file mode 100644 index 0000000..8f5f139 --- /dev/null +++ b/web/src/css/modals.css @@ -0,0 +1,262 @@ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + z-index: 50; + animation: fadeIn 0.2s ease-out; +} + +.modal-container { + background: var(--bg-secondary); + border-radius: var(--radius-lg); + width: 100%; + max-width: 28rem; + border: 1px solid var(--border); + box-shadow: var(--shadow-lg); + animation: zoomIn 0.2s ease-out; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid var(--border); +} + +.modal-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); +} + +.modal-close-btn { + padding: 8px; + color: var(--text-tertiary); + border-radius: var(--radius-md); + transition: color 0.15s; +} + +.modal-close-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.modal-form { + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.icon-picker-tabs { + display: flex; + gap: 4px; + margin-bottom: 12px; +} + +.icon-picker-tab { + flex: 1; + padding: 8px 12px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.icon-picker-tab:hover { + background: var(--bg-tertiary); +} + +.icon-picker-tab.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.emoji-picker-wrapper { + display: flex; + flex-direction: column; + gap: 10px; +} + +.emoji-custom-input input { + width: 100%; +} + +.emoji-picker, +.icon-picker { + display: flex; + flex-wrap: wrap; + gap: 4px; + max-height: 120px; + overflow-y: auto; + padding: 8px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-md); +} + +.emoji-option, +.icon-option { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + background: transparent; + border: 2px solid transparent; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s ease; + color: var(--text-secondary); +} + +.emoji-option:hover, +.icon-option:hover { + background: var(--bg-tertiary); + transform: scale(1.1); + color: var(--text-primary); +} + +.emoji-option.selected, +.icon-option.selected { + border-color: var(--accent); + background: var(--accent-subtle); + color: var(--accent); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 8px; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes zoomIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.form-group { + margin-bottom: 0; +} + +.form-label { + display: block; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.form-input, +.form-textarea, +.form-select { + width: 100%; + padding: 8px 12px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + transition: all 0.15s; +} + +.form-input:focus, +.form-textarea:focus, +.form-select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-subtle); +} + +.form-textarea { + resize: none; +} + +.input { + width: 100%; + padding: 12px 14px; + font-size: 0.95rem; + color: var(--text-primary); + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + outline: none; + transition: all 0.15s ease; +} + +.input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); +} + +.input::placeholder { + color: var(--text-tertiary); +} + +.color-input-container { + display: flex; + align-items: center; + gap: 12px; + background: var(--bg-tertiary); + padding: 8px 12px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + width: fit-content; +} + +.color-input-wrapper { + position: relative; + width: 32px; + height: 32px; + border-radius: var(--radius-full); + overflow: hidden; + border: 2px solid var(--border); + cursor: pointer; + transition: transform 0.1s; +} + +.color-input-wrapper:hover { + transform: scale(1.1); + border-color: var(--accent); +} + +.color-input-wrapper input[type="color"] { + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + padding: 0; + margin: 0; + border: none; + cursor: pointer; + opacity: 0; +} diff --git a/web/src/css/notifications.css b/web/src/css/notifications.css new file mode 100644 index 0000000..c08f3bc --- /dev/null +++ b/web/src/css/notifications.css @@ -0,0 +1,65 @@ +.notifications-page { + max-width: 680px; + margin: 0 auto; +} + +.notifications-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.notification-item { + display: flex; + gap: 16px; + align-items: flex-start; + text-decoration: none; + color: inherit; +} + +.notification-item:hover { + background: var(--bg-hover); +} + +.notification-icon { + width: 36px; + height: 36px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-tertiary); + color: var(--text-secondary); + flex-shrink: 0; +} + +.notification-icon[data-type="like"] { + color: #ef4444; + background: rgba(239, 68, 68, 0.1); +} + +.notification-icon[data-type="reply"] { + color: #3b82f6; + background: rgba(59, 130, 246, 0.1); +} + +.notification-content { + flex: 1; + min-width: 0; +} + +.notification-text { + font-size: 0.95rem; + margin-bottom: 4px; + line-height: 1.4; + color: var(--text-primary); +} + +.notification-text strong { + font-weight: 600; +} + +.notification-time { + font-size: 0.85rem; + color: var(--text-tertiary); +} diff --git a/web/src/css/profile.css b/web/src/css/profile.css new file mode 100644 index 0000000..8486411 --- /dev/null +++ b/web/src/css/profile.css @@ -0,0 +1,250 @@ +.profile-header { + display: flex; + align-items: center; + gap: 24px; + margin-bottom: 32px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border); +} + +.profile-avatar { + width: 80px; + height: 80px; + min-width: 80px; + border-radius: 50%; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 2rem; + color: var(--text-secondary); + overflow: hidden; + border: 1px solid var(--border); +} + +.profile-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-avatar-link { + text-decoration: none; +} + +.profile-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.profile-name { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + line-height: 1.2; +} + +.profile-handle-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 4px; + flex-wrap: wrap; +} + +.profile-handle-link { + color: var(--text-tertiary); + text-decoration: none; + font-size: 1rem; + transition: color 0.15s; +} + +.profile-handle-link:hover { + color: var(--text-secondary); +} + +.profile-bluesky-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: #3b82f6; + text-decoration: none; + font-size: 0.85rem; + font-weight: 500; + padding: 2px 8px; + border-radius: var(--radius-sm); + background: rgba(59, 130, 246, 0.1); + transition: all 0.15s ease; +} + +.profile-bluesky-link:hover { + background: rgba(59, 130, 246, 0.15); +} + +.profile-stats { + display: flex; + gap: 24px; + margin-top: 12px; +} + +.profile-stat { + color: var(--text-tertiary); + font-size: 0.9rem; +} + +.profile-stat strong { + color: var(--text-primary); + font-weight: 600; +} + +.profile-tabs { + display: flex; + gap: 24px; + margin-bottom: 24px; + border-bottom: 1px solid var(--border); +} + +.profile-tab { + padding: 12px 0; + font-size: 0.95rem; + font-weight: 500; + color: var(--text-tertiary); + background: transparent; + border: none; + cursor: pointer; + transition: all 0.15s ease; + position: relative; +} + +.profile-tab:hover { + color: var(--text-primary); +} + +.profile-tab.active { + color: var(--text-primary); +} + +.profile-tab.active::after { + content: ""; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + background: var(--text-primary); +} + +.profile-badge-wrapper { + display: inline-flex; + align-items: center; +} + +.profile-badge-clickable { + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; + margin-left: 8px; +} + +.badge-info-popover { + position: absolute; + top: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + padding: 16px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + font-size: 0.85rem; + white-space: nowrap; + z-index: 100; + min-width: 200px; +} + +.badge-info-title { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +.verifier-link { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + text-decoration: none; + transition: background 0.15s ease; +} + +.verifier-link:hover { + background: var(--bg-hover); +} + +.verifier-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; +} + +.verifier-name { + color: var(--text-primary); + font-size: 0.85rem; + font-weight: 500; +} + +.profile-suspended { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; + background: var(--bg-secondary); + border-radius: var(--radius-lg); + margin-top: 20px; + border: 1px solid var(--border); +} + +.suspended-icon { + font-size: 40px; + margin-bottom: 16px; + color: var(--text-tertiary); +} + +.profile-suspended h2 { + color: var(--text-primary); + margin-bottom: 8px; + font-size: 1.25rem; +} + +@media (max-width: 640px) { + .profile-header { + flex-direction: column; + text-align: center; + } + + .profile-info { + align-items: center; + } + + .profile-handle-row { + justify-content: center; + } + + .profile-stats { + justify-content: center; + } + + .profile-tabs { + justify-content: center; + gap: 16px; + } +} \ No newline at end of file diff --git a/web/src/css/skeleton.css b/web/src/css/skeleton.css new file mode 100644 index 0000000..c7dff59 --- /dev/null +++ b/web/src/css/skeleton.css @@ -0,0 +1,106 @@ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } +} + +.skeleton { + background: linear-gradient( + 90deg, + var(--bg-tertiary) 25%, + var(--bg-secondary) 50%, + var(--bg-tertiary) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-sm); +} + +.skeleton-card { + padding: 24px 0; + border-bottom: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 16px; +} + +.skeleton-header { + display: flex; + align-items: center; + gap: 12px; +} + +.skeleton-avatar { + width: 36px; + height: 36px; + border-radius: 50%; +} + +.skeleton-meta { + display: flex; + flex-direction: column; + gap: 6px; +} + +.skeleton-name { + width: 120px; + height: 14px; +} + +.skeleton-handle { + width: 80px; + height: 12px; +} + +.skeleton-content { + display: flex; + flex-direction: column; + gap: 12px; + padding-left: 48px; +} + +.skeleton-source { + width: 180px; + height: 24px; + border-radius: var(--radius-full); +} + +.skeleton-highlight { + width: 100%; + height: 60px; + border-left: 2px solid var(--border); +} + +.skeleton-text-1 { + width: 90%; + height: 14px; +} + +.skeleton-text-2 { + width: 60%; + height: 14px; +} + +.skeleton-actions { + display: flex; + gap: 24px; + padding-left: 48px; + margin-top: 4px; +} + +.skeleton-action { + width: 24px; + height: 24px; + border-radius: var(--radius-sm); +} + +@media (max-width: 600px) { + .skeleton-content, + .skeleton-actions { + padding-left: 0; + } +} diff --git a/web/src/css/utilities.css b/web/src/css/utilities.css new file mode 100644 index 0000000..262d441 --- /dev/null +++ b/web/src/css/utilities.css @@ -0,0 +1,730 @@ +.legal-content { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +.legal-content h1 { + font-size: 2rem; + margin-bottom: 8px; + color: var(--text-primary); +} + +.legal-content h2 { + font-size: 1.4rem; + margin-top: 32px; + margin-bottom: 12px; + color: var(--text-primary); +} + +.legal-content h3 { + font-size: 1.1rem; + margin-top: 20px; + margin-bottom: 8px; + color: var(--text-primary); +} + +.legal-content p { + color: var(--text-secondary); + line-height: 1.7; + margin-bottom: 12px; +} + +.legal-content ul { + color: var(--text-secondary); + line-height: 1.7; + margin-left: 24px; + margin-bottom: 12px; +} + +.legal-content li { + margin-bottom: 6px; +} + +.legal-content a { + color: var(--accent); + text-decoration: none; +} + +.legal-content a:hover { + text-decoration: underline; +} + +.legal-content section { + margin-bottom: 24px; +} + +.text-secondary { + color: var(--text-secondary); +} + +.text-error { + color: var(--error); +} + +.text-center { + text-align: center; +} + +.flex { + display: flex; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-end { + justify-content: flex-end; +} + +.gap-2 { + gap: 8px; +} + +.gap-3 { + gap: 12px; +} + +.mt-3 { + margin-top: 12px; +} + +.mb-6 { + margin-bottom: 24px; +} + +.composer { + margin-bottom: 24px; +} + +.composer-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.composer-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.composer-input { + width: 100%; + min-height: 120px; + padding: 16px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 1rem; + resize: vertical; + transition: all 0.15s ease; +} + +.composer-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); +} + +.composer-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; +} + +.composer-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.composer-count { + font-size: 0.85rem; + color: var(--text-tertiary); +} + +.composer-count.warning { + color: var(--warning); +} + +.composer-count.error { + color: var(--error); +} + +.composer-char-count.warning { + color: var(--warning); +} + +.composer-char-count.error { + color: var(--error); +} + +.composer-add-quote { + width: 100%; + padding: 12px 16px; + margin-bottom: 12px; + background: var(--bg-tertiary); + border: 1px dashed var(--border); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 0.9rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.composer-add-quote:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-subtle); +} + +.composer-quote-input-wrapper { + margin-bottom: 12px; +} + +.composer-quote-input { + width: 100%; + padding: 12px 16px; + background: linear-gradient( + 135deg, + rgba(79, 70, 229, 0.05), + rgba(168, 85, 247, 0.05) + ); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + color: var(--text-primary); + font-size: 0.95rem; + font-style: italic; + resize: vertical; + font-family: inherit; + transition: all 0.15s ease; +} + +.composer-quote-input:focus { + outline: none; + border-color: var(--accent); +} + +.composer-quote-input::placeholder { + color: var(--text-tertiary); + font-style: italic; +} + +.composer-quote-remove-btn { + margin-top: 8px; + padding: 6px 12px; + background: none; + border: none; + color: var(--text-tertiary); + font-size: 0.85rem; + cursor: pointer; +} + +.composer-quote-remove-btn:hover { + color: var(--error); +} + +.composer-error { + margin-top: 12px; + padding: 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: var(--radius-md); + color: var(--error); + font-size: 0.9rem; +} + +.composer-url { + font-size: 0.85rem; + color: var(--text-secondary); + word-break: break-all; +} + +.composer-quote { + position: relative; + padding: 12px 16px; + padding-right: 36px; + background: var(--bg-secondary); + border-left: 3px solid var(--accent); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + margin-bottom: 16px; + font-style: italic; + color: var(--text-secondary); +} + +.composer-quote-remove { + position: absolute; + top: 8px; + right: 8px; + width: 24px; + height: 24px; + border-radius: var(--radius-full); + background: var(--bg-tertiary); + color: var(--text-secondary); + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; +} + +.composer-quote-remove:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.composer-tags { + flex: 1; +} + +.composer-meta-row { + display: flex; + gap: 12px; + margin-top: 12px; + align-items: flex-start; +} + +.composer-labels-wrapper { + position: relative; +} + +.composer-labels-btn { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + cursor: pointer; + color: var(--text-tertiary); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.composer-labels-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); + border-color: var(--text-tertiary); +} + +.composer-labels-btn.active { + color: var(--accent); + background: var(--accent-subtle); + border-color: var(--accent); +} + +.composer-labels-badge { + position: absolute; + top: -4px; + right: -4px; + background: var(--error); + color: white; + font-size: 0.7rem; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + border: 2px solid var(--bg-primary); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.composer-labels-picker { + position: absolute; + bottom: 100%; + right: 0; + margin-bottom: 12px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 8px 0; + min-width: 200px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); + z-index: 50; + animation: scaleIn 0.2s ease-out forwards; + transform-origin: bottom right; +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95) translateY(5px); + } + + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.picker-header { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 4px; + padding: 4px 12px 8px; + border-bottom: 1px solid var(--border); +} + +.picker-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + cursor: pointer; + color: var(--text-secondary); + font-size: 0.9rem; + transition: all 0.15s ease; + user-select: none; +} + +.picker-item:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.picker-checkbox-wrapper { + position: relative; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; +} + +.picker-checkbox-wrapper input { + position: absolute; + opacity: 0; + width: 100%; + height: 100%; + cursor: pointer; + z-index: 10; +} + +.picker-checkbox-custom { + width: 18px; + height: 18px; + border: 2px solid var(--text-tertiary); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + transition: all 0.2s ease; + color: white; +} + +.picker-item:hover .picker-checkbox-custom { + border-color: var(--text-secondary); +} + +.picker-checkbox-wrapper input:checked + .picker-checkbox-custom { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.composer-tags-input { + width: 100%; + padding: 12px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 0.95rem; + transition: all 0.15s ease; +} + +.composer-tags-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); +} + +.composer-tags-input::placeholder { + color: var(--text-tertiary); +} + +.history-panel { + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 1rem; + margin-bottom: 1rem; + font-size: 0.9rem; + animation: fadeIn 0.2s ease-out; +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.history-title { + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.history-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.history-item { + position: relative; + padding-left: 1rem; + border-left: 2px solid var(--border); +} + +.history-date { + font-size: 0.75rem; + color: var(--text-tertiary); + margin-bottom: 0.25rem; +} + +.history-content { + color: var(--text-secondary); + white-space: pre-wrap; +} + +.history-close-btn { + color: var(--text-tertiary); + padding: 4px; + border-radius: var(--radius-sm); + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.history-close-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.history-status { + text-align: center; + color: var(--text-tertiary); + font-style: italic; + padding: 1rem; +} + +.share-menu-container { + position: relative; +} + +.share-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + min-width: 180px; + padding: 8px 0; + z-index: 100; + animation: fadeInUp 0.15s ease; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(-8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.share-menu-section { + display: flex; + flex-direction: column; +} + +.share-menu-label { + padding: 4px 12px 8px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-tertiary); +} + +.share-menu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: none; + border: none; + width: 100%; + text-align: left; + font-size: 0.9rem; + color: var(--text-primary); + cursor: pointer; + transition: all 0.1s ease; +} + +.share-menu-item:hover { + background: var(--bg-tertiary); +} + +.share-menu-icon { + font-size: 1.1rem; + width: 24px; + text-align: center; +} + +.share-menu-divider { + height: 1px; + background: var(--border); + margin: 6px 0; +} + +.bookmark-card { + display: flex; + flex-direction: column; + gap: 16px; +} + +.bookmark-preview { + display: flex; + flex-direction: column; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + text-decoration: none; + transition: all 0.2s ease; + position: relative; +} + +.bookmark-preview:hover { + border-color: var(--accent); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); +} + +.bookmark-preview::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: var(--accent); + opacity: 0.7; +} + +.bookmark-preview-content { + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.bookmark-preview-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.bookmark-preview-site { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.bookmark-preview-title { + font-size: 1rem; + font-weight: 600; + line-height: 1.4; + color: var(--text-primary); + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.bookmark-preview-desc { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.5; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.bookmark-preview-arrow { + display: flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + padding: 0 4px; + transition: all 0.2s ease; +} + +.bookmark-preview:hover .bookmark-preview-arrow { + color: var(--accent); + transform: translateX(2px); +} + +.bookmark-description { + font-size: 0.9rem; + color: var(--text-secondary); + margin: 0; + line-height: 1.5; +} + +.bookmark-meta { + display: flex; + align-items: center; + gap: 12px; + margin-top: 12px; + font-size: 0.85rem; + color: var(--text-tertiary); +} + +.bookmark-time { + color: var(--text-tertiary); +} diff --git a/web/src/index.css b/web/src/index.css index 181fde3..6f06a3a 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,3424 +1,13 @@ -:root { - --bg-primary: #0c0a14; - --bg-secondary: #110e1c; - --bg-tertiary: #1a1528; - --bg-card: #14111f; - --bg-hover: #1e1932; - --bg-elevated: #1a1528; - - --text-primary: #f4f0ff; - --text-secondary: #a89ec8; - --text-tertiary: #6b5f8a; - - --accent: #a855f7; - --accent-hover: #c084fc; - --accent-subtle: rgba(168, 85, 247, 0.15); - - --border: #2d2640; - --border-hover: #3d3560; - - --success: #22c55e; - --error: #ef4444; - --warning: #f59e0b; - - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-full: 9999px; - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); - --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 40px rgba(168, 85, 247, 0.1); - --shadow-glow: 0 0 20px rgba(168, 85, 247, 0.3); - - --font-sans: - "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html { - font-size: 16px; -} - -body { - font-family: var(--font-sans); - background: var(--bg-primary); - color: var(--text-primary); - line-height: 1.6; - min-height: 100vh; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - color: var(--accent); - text-decoration: none; - transition: color 0.15s ease; -} - -a:hover { - color: var(--accent-hover); -} - -button { - font-family: inherit; - cursor: pointer; - border: none; - background: none; -} - -input, -textarea { - font-family: inherit; - font-size: inherit; -} - -.app { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.main-content { - flex: 1; - max-width: 680px; - width: 100%; - margin: 0 auto; - padding: 24px 16px; -} - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 10px 20px; - font-size: 0.9rem; - font-weight: 500; - border-radius: var(--radius-md); - transition: all 0.15s ease; -} - -.btn-primary { - background: var(--accent); - color: white; -} - -.btn-primary:hover { - background: var(--accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-md); -} - -.btn-secondary { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border); -} - -.btn-secondary:hover { - background: var(--bg-hover); - border-color: var(--border-hover); -} - -.btn-ghost { - color: var(--text-secondary); - padding: 8px 12px; -} - -.btn-ghost:hover { - color: var(--text-primary); - background: var(--bg-tertiary); -} - -.card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 24px; - transition: all 0.2s ease; - position: relative; -} - -.card:hover { - border-color: var(--border-hover); - box-shadow: var(--shadow-md); - transform: translateY(-1px); -} - -.annotation-card { - display: flex; - flex-direction: column; - gap: 16px; -} - -.annotation-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; -} - -.annotation-header-left { - display: flex; - align-items: center; - gap: 12px; - flex: 1; - min-width: 0; -} - -.annotation-avatar { - width: 40px; - height: 40px; - min-width: 40px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.95rem; - color: white; - overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} - -.annotation-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.annotation-meta { - display: flex; - flex-direction: column; - justify-content: center; - line-height: 1.3; -} - -.annotation-avatar-link { - text-decoration: none; - border-radius: var(--radius-full); - transition: transform 0.15s ease; -} - -.annotation-avatar-link:hover { - transform: scale(1.05); -} - -.annotation-author-row { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.annotation-author { - font-weight: 600; - color: var(--text-primary); - font-size: 0.95rem; -} - -.annotation-handle { - font-size: 0.85rem; - color: var(--text-tertiary); - text-decoration: none; - display: flex; - align-items: center; - gap: 3px; -} - -.annotation-handle:hover { - color: var(--accent); -} - -.annotation-time { - font-size: 0.8rem; - color: var(--text-tertiary); -} - -.annotation-content { - display: flex; - flex-direction: column; - gap: 12px; -} - -.annotation-source { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 0.8rem; - color: var(--text-tertiary); - text-decoration: none; - padding: 4px 10px; - background: var(--bg-tertiary); - border-radius: var(--radius-full); - width: fit-content; - transition: all 0.15s ease; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.annotation-source:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.annotation-source-title { - color: var(--text-secondary); - opacity: 0.8; -} - -.annotation-highlight { - display: block; - position: relative; - padding: 16px 20px; - background: linear-gradient( - 135deg, - rgba(79, 70, 229, 0.03), - rgba(168, 85, 247, 0.03) - ); - border-left: 3px solid var(--accent); - border-radius: 4px var(--radius-md) var(--radius-md) 4px; - text-decoration: none; - transition: all 0.2s ease; - margin: 4px 0; -} - -.annotation-highlight:hover { - background: linear-gradient( - 135deg, - rgba(79, 70, 229, 0.08), - rgba(168, 85, 247, 0.08) - ); - transform: translateX(2px); -} - -.annotation-highlight mark { - background: transparent; - color: var(--text-primary); - font-style: italic; - font-size: 1.05rem; - line-height: 1.6; - font-weight: 400; - display: inline; -} - -.annotation-text { - font-size: 1rem; - line-height: 1.65; - color: var(--text-primary); - white-space: pre-wrap; -} - -.annotation-actions { - display: flex; - align-items: center; - justify-content: space-between; - padding-top: 16px; - margin-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.03); -} - -.annotation-actions-left { - display: flex; - align-items: center; - gap: 8px; -} - -.annotation-action { - display: flex; - align-items: center; - gap: 6px; - color: var(--text-tertiary); - font-size: 0.85rem; - font-weight: 500; - padding: 6px 10px; - border-radius: var(--radius-md); - transition: all 0.2s ease; - background: transparent; - cursor: pointer; -} - -.annotation-action:hover { - color: var(--text-secondary); - background: var(--bg-elevated); -} - -.annotation-action.liked { - color: #ef4444; - background: rgba(239, 68, 68, 0.05); -} - -.annotation-action.liked:hover { - background: rgba(239, 68, 68, 0.1); -} - -.annotation-action.active { - color: var(--accent); - background: var(--accent-subtle); -} - -.action-icon-only { - padding: 8px; -} - -.annotation-delete { - background: none; - border: none; - cursor: pointer; - padding: 8px; - font-size: 1rem; - color: var(--text-tertiary); - transition: all 0.2s ease; - border-radius: var(--radius-md); - opacity: 0.6; -} - -.annotation-delete:hover { - color: var(--error); - background: rgba(239, 68, 68, 0.1); - opacity: 1; -} - -.annotation-delete:disabled { - cursor: not-allowed; - opacity: 0.3; -} - -.share-menu-container { - position: relative; -} - -.share-menu { - position: absolute; - top: 100%; - right: 0; - margin-top: 8px; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - min-width: 180px; - padding: 8px 0; - z-index: 100; - animation: fadeInUp 0.15s ease; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(-8px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.share-menu-section { - display: flex; - flex-direction: column; -} - -.share-menu-label { - padding: 4px 12px 8px; - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-tertiary); -} - -.share-menu-item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - background: none; - border: none; - width: 100%; - text-align: left; - font-size: 0.9rem; - color: var(--text-primary); - cursor: pointer; - transition: all 0.1s ease; -} - -.share-menu-item:hover { - background: var(--bg-tertiary); -} - -.share-menu-icon { - font-size: 1.1rem; - width: 24px; - text-align: center; -} - -.share-menu-divider { - height: 1px; - background: var(--border); - margin: 6px 0; -} - -.feed { - display: flex; - flex-direction: column; - gap: 16px; -} - -.feed-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -.feed-title { - font-size: 1.5rem; - font-weight: 700; -} - -.page-header { - margin-bottom: 32px; -} - -.page-title { - font-size: 2rem; - font-weight: 700; - margin-bottom: 8px; -} - -.page-description { - color: var(--text-secondary); - font-size: 1.1rem; -} - -.url-input-wrapper { - margin-bottom: 32px; -} - -.url-input-container { - display: flex; - gap: 12px; -} - -.url-input { - flex: 1; - padding: 14px 18px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 1rem; - transition: all 0.15s ease; -} - -.url-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.url-input::placeholder { - color: var(--text-tertiary); -} - -.empty-state { - text-align: center; - padding: 60px 20px; - color: var(--text-secondary); -} - -.empty-state-icon { - font-size: 3rem; - margin-bottom: 16px; - opacity: 0.5; -} - -.empty-state-title { - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 8px; -} - -.empty-state-text { - font-size: 1rem; - max-width: 400px; - margin: 0 auto; -} - -.feed-filters { - display: flex; - gap: 8px; - margin-bottom: 24px; - padding: 4px; - background: var(--bg-tertiary); - border-radius: var(--radius-lg); - width: fit-content; -} - -.login-page { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 70vh; - padding: 60px 20px; - width: 100%; - max-width: 500px; - margin: 0 auto; -} - -.login-at-logo { - font-size: 5rem; - font-weight: 800; - color: var(--accent); - margin-bottom: 24px; - line-height: 1; -} - -.login-heading { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 32px; - display: flex; - align-items: center; - gap: 10px; - text-align: center; - line-height: 1.4; -} - -.login-help-btn { - background: none; - border: none; - color: var(--text-tertiary); - cursor: pointer; - padding: 4px; - display: flex; - align-items: center; - transition: color 0.15s; - flex-shrink: 0; -} - -.login-help-btn:hover { - color: var(--accent); -} - -.login-help-text { - background: var(--bg-elevated); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 16px 20px; - margin-bottom: 24px; - font-size: 0.95rem; - color: var(--text-secondary); - line-height: 1.6; - text-align: center; -} - -.login-help-text code { - background: var(--bg-tertiary); - padding: 2px 8px; - border-radius: var(--radius-sm); - font-size: 0.9rem; -} - -.login-form { - display: flex; - flex-direction: column; - gap: 20px; - width: 100%; -} - -.login-input-wrapper { - position: relative; -} - -.login-input { - width: 100%; - padding: 18px 20px; - background: var(--bg-elevated); - border: 2px solid var(--border); - border-radius: var(--radius-lg); - color: var(--text-primary); - font-size: 1.1rem; - transition: - border-color 0.15s, - box-shadow 0.15s; -} - -.login-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); -} - -.login-input::placeholder { - color: var(--text-tertiary); -} - -.login-suggestions { - position: absolute; - top: calc(100% + 8px); - left: 0; - right: 0; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - overflow: hidden; - z-index: 100; -} - -.login-suggestion { - display: flex; - align-items: center; - gap: 14px; - width: 100%; - padding: 14px 18px; - background: transparent; - border: none; - cursor: pointer; - text-align: left; - color: var(--text-primary); - transition: background 0.1s; -} - -.login-suggestion:hover, -.login-suggestion.selected { - background: var(--bg-elevated); -} - -.login-suggestion-avatar { - width: 44px; - height: 44px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - overflow: hidden; - font-size: 0.9rem; - font-weight: 600; - color: white; -} - -.login-suggestion-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.login-suggestion-info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.login-suggestion-name { - font-weight: 600; - font-size: 1rem; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.login-suggestion-handle { - font-size: 0.9rem; - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.login-error { - padding: 12px 16px; - background: rgba(239, 68, 68, 0.1); - border: 1px solid rgba(239, 68, 68, 0.3); - border-radius: var(--radius-md); - color: #ef4444; - font-size: 0.9rem; - text-align: center; -} - -.login-submit { - padding: 18px 32px; - font-size: 1.1rem; - font-weight: 600; -} - -.login-avatar-large { - width: 100px; - height: 100px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 20px; - font-weight: 700; - font-size: 2rem; - color: white; - overflow: hidden; -} - -.login-avatar-large img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.login-welcome { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 32px; - text-align: center; -} - -.login-actions { - display: flex; - flex-direction: column; - gap: 12px; - width: 100%; -} - -.login-avatar { - width: 72px; - height: 72px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto 16px; - font-weight: 700; - font-size: 1.5rem; - color: white; - overflow: hidden; -} - -.login-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.login-welcome-name { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 24px; -} - -.login-actions { - display: flex; - flex-direction: column; - gap: 12px; -} - -.btn-bluesky { - background: #0085ff; - color: white; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - transition: - background 0.2s, - transform 0.2s; -} - -.btn-bluesky:hover { - background: #0070dd; - transform: translateY(-1px); -} - -.login-btn { - width: 100%; - padding: 14px 24px; - font-size: 1rem; - font-weight: 600; -} - -.login-brand { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - margin-bottom: 24px; -} - -.login-brand-icon { - width: 48px; - height: 48px; - background: linear-gradient(135deg, var(--accent), #a855f7); - border-radius: var(--radius-lg); - display: flex; - align-items: center; - justify-content: center; - font-size: 1.75rem; - font-weight: 800; - color: white; -} - -.login-brand-name { - font-size: 1.75rem; - font-weight: 700; -} - -.login-form { - display: flex; - flex-direction: column; - gap: 16px; -} - -.login-input-wrapper { - position: relative; -} - -.login-input { - width: 100%; - padding: 14px 16px; - background: var(--bg-elevated); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 1rem; - transition: - border-color 0.15s, - box-shadow 0.15s; -} - -.login-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); -} - -.login-input::placeholder { - color: var(--text-tertiary); -} - -.login-suggestions { - position: absolute; - top: calc(100% + 4px); - left: 0; - right: 0; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - overflow: hidden; - z-index: 100; -} - -.login-suggestion { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - padding: 12px 16px; - background: transparent; - border: none; - cursor: pointer; - text-align: left; - transition: background 0.1s; -} - -.login-suggestion:hover, -.login-suggestion.selected { - background: var(--bg-elevated); -} - -.login-suggestion-avatar { - width: 40px; - height: 40px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - overflow: hidden; - font-size: 0.875rem; - font-weight: 600; - color: white; -} - -.login-suggestion-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.login-suggestion-info { - display: flex; - flex-direction: column; - min-width: 0; -} - -.login-suggestion-name { - font-weight: 600; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.login-suggestion-handle { - font-size: 0.875rem; - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.login-error { - padding: 12px 16px; - background: rgba(239, 68, 68, 0.1); - border: 1px solid rgba(239, 68, 68, 0.3); - border-radius: var(--radius-md); - color: #ef4444; - font-size: 0.875rem; -} - -.login-legal { - font-size: 0.75rem; - color: var(--text-tertiary); - line-height: 1.5; - margin-top: 16px; -} - -.profile-header { - display: flex; - align-items: center; - gap: 20px; - margin-bottom: 32px; - padding-bottom: 24px; - border-bottom: 1px solid var(--border); -} - -.profile-avatar { - width: 80px; - height: 80px; - min-width: 80px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 2rem; - color: white; - overflow: hidden; -} - -.profile-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.profile-avatar-link { - text-decoration: none; -} - -.profile-info { - flex: 1; -} - -.profile-name { - font-size: 1.5rem; - font-weight: 700; -} - -.profile-handle-link { - color: var(--text-secondary); - text-decoration: none; -} - -.profile-handle-link:hover { - color: var(--accent); - text-decoration: underline; -} - -.profile-bluesky-link { - display: inline-flex; - align-items: center; - gap: 6px; - color: #0085ff; - text-decoration: none; - font-size: 0.95rem; - padding: 4px 10px; - border-radius: var(--radius-md); - background: rgba(0, 133, 255, 0.1); - transition: all 0.15s ease; -} - -.profile-bluesky-link:hover { - background: rgba(0, 133, 255, 0.2); - color: #0070dd; -} - -.profile-stats { - display: flex; - gap: 24px; - margin-top: 8px; -} - -.profile-stat { - color: var(--text-secondary); - font-size: 0.9rem; -} - -.profile-stat strong { - color: var(--text-primary); -} - -.profile-tabs { - display: flex; - gap: 0; - margin-bottom: 24px; - border-bottom: 1px solid var(--border); -} - -.profile-tab { - padding: 12px 20px; - font-size: 0.9rem; - font-weight: 500; - color: var(--text-secondary); - background: transparent; - border: none; - border-bottom: 2px solid transparent; - cursor: pointer; - transition: all 0.15s ease; - margin-bottom: -1px; -} - -.profile-tab:hover { - color: var(--text-primary); - background: var(--bg-tertiary); -} - -.profile-tab.active { - color: var(--accent); - border-bottom-color: var(--accent); -} - -.bookmark-description { - font-size: 0.9rem; - color: var(--text-secondary); - margin: 0; - line-height: 1.5; -} - -.bookmark-meta { - display: flex; - align-items: center; - gap: 12px; - margin-top: 12px; - font-size: 0.85rem; - color: var(--text-tertiary); -} - -.bookmark-time { - color: var(--text-tertiary); -} - -.composer { - margin-bottom: 24px; -} - -.composer-textarea { - width: 100%; - min-height: 120px; - padding: 16px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 1rem; - resize: vertical; - transition: all 0.15s ease; -} - -.composer-textarea:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.composer-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 12px; -} - -.composer-char-count { - font-size: 0.85rem; - color: var(--text-tertiary); -} - -.composer-char-count.warning { - color: var(--warning); -} - -.composer-char-count.error { - color: var(--error); -} - -.composer-add-quote { - width: 100%; - padding: 12px 16px; - margin-bottom: 12px; - background: var(--bg-tertiary); - border: 1px dashed var(--border); - border-radius: var(--radius-md); - color: var(--text-secondary); - font-size: 0.9rem; - cursor: pointer; - transition: all 0.15s ease; -} - -.composer-add-quote:hover { - border-color: var(--accent); - color: var(--accent); - background: var(--accent-subtle); -} - -.composer-quote-input-wrapper { - margin-bottom: 12px; -} - -.composer-quote-input { - width: 100%; - padding: 12px 16px; - background: linear-gradient( - 135deg, - rgba(79, 70, 229, 0.05), - rgba(168, 85, 247, 0.05) - ); - border: 1px solid var(--border); - border-left: 3px solid var(--accent); - border-radius: 0 var(--radius-md) var(--radius-md) 0; - color: var(--text-primary); - font-size: 0.95rem; - font-style: italic; - resize: vertical; - font-family: inherit; - transition: all 0.15s ease; -} - -.composer-quote-input:focus { - outline: none; - border-color: var(--accent); -} - -.composer-quote-input::placeholder { - color: var(--text-tertiary); - font-style: italic; -} - -.composer-quote-remove-btn { - margin-top: 8px; - padding: 6px 12px; - background: none; - border: none; - color: var(--text-tertiary); - font-size: 0.85rem; - cursor: pointer; -} - -.composer-quote-remove-btn:hover { - color: var(--error); -} - -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - - 100% { - background-position: 200% 0; - } -} - -.skeleton { - background: linear-gradient( - 90deg, - var(--bg-tertiary) 25%, - var(--bg-hover) 50%, - var(--bg-tertiary) 75% - ); - background-size: 200% 100%; - animation: shimmer 1.5s infinite; - border-radius: var(--radius-sm); -} - -.skeleton-text { - height: 1em; - margin-bottom: 8px; -} - -.skeleton-text:last-child { - width: 60%; -} - -@media (max-width: 640px) { - .main-content { - padding: 16px 12px; - } - - .navbar-inner { - padding: 0 16px; - } - - .page-title { - font-size: 1.5rem; - } - - .url-input-container { - flex-direction: column; - } - - .profile-header { - flex-direction: column; - text-align: center; - } - - .profile-stats { - justify-content: center; - } -} - -.main { - flex: 1; - width: 100%; -} - -.page-container { - max-width: 680px; - margin: 0 auto; - padding: 24px 16px; -} - -.navbar-logo { - width: 32px; - height: 32px; - background: linear-gradient(135deg, var(--accent), #8b5cf6); - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 1rem; - color: white; -} - -.navbar-user { - display: flex; - align-items: center; - gap: 8px; -} - -.navbar-avatar { - width: 36px; - height: 36px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.85rem; - color: white; - text-decoration: none; -} - -.btn-sm { - padding: 6px 12px; - font-size: 0.85rem; -} - -.composer-url { - font-size: 0.85rem; - color: var(--text-secondary); - word-break: break-all; -} - -.composer-quote { - position: relative; - padding: 12px 16px; - padding-right: 36px; - background: var(--bg-secondary); - border-left: 3px solid var(--accent); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; - margin-bottom: 16px; - font-style: italic; - color: var(--text-secondary); -} - -.composer-quote-remove { - position: absolute; - top: 8px; - right: 8px; - width: 24px; - height: 24px; - border-radius: var(--radius-full); - background: var(--bg-tertiary); - color: var(--text-secondary); - font-size: 1rem; - display: flex; - align-items: center; - justify-content: center; -} - -.composer-quote-remove:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.composer-input { - width: 100%; - min-height: 120px; - padding: 16px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 1rem; - resize: vertical; - transition: all 0.15s ease; -} - -.composer-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.composer-input::placeholder { - color: var(--text-tertiary); -} - -.composer-tags { - margin-top: 12px; -} - -.composer-tags-input { - width: 100%; - padding: 12px 16px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 0.95rem; - transition: all 0.15s ease; -} - -.composer-tags-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.composer-tags-input::placeholder { - color: var(--text-tertiary); -} - -.composer-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 12px; -} - -.composer-count { - font-size: 0.85rem; - color: var(--text-tertiary); -} - -.composer-actions { - display: flex; - gap: 8px; -} - -.composer-error { - margin-top: 12px; - padding: 12px; - background: rgba(239, 68, 68, 0.1); - border: 1px solid rgba(239, 68, 68, 0.3); - border-radius: var(--radius-md); - color: var(--error); - font-size: 0.9rem; -} - -.annotation-tags { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 12px; - margin-bottom: 8px; -} - -.annotation-tag { - display: inline-flex; - align-items: center; - padding: 4px 10px; - background: var(--bg-tertiary); - color: var(--text-secondary); - font-size: 0.8rem; - font-weight: 500; - border-radius: var(--radius-full); - transition: all 0.15s ease; - border: 1px solid transparent; - text-decoration: none; -} - -.annotation-tag:hover { - background: var(--bg-hover); - color: var(--text-primary); - border-color: var(--border); - transform: translateY(-1px); -} - -.url-input-wrapper { - margin-bottom: 24px; -} - -.url-input { - width: 100%; - padding: 16px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 1.1rem; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.url-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 4px var(--accent-subtle); - background: var(--bg-primary); -} - -.url-input::placeholder { - color: var(--text-tertiary); -} - -.annotation-detail-page { - max-width: 680px; - margin: 0 auto; - padding: 24px 16px; -} - -.annotation-detail-header { - margin-bottom: 24px; -} - -.back-link { - color: var(--text-secondary); - text-decoration: none; - font-size: 0.9rem; -} - -.back-link:hover { - color: var(--accent); -} - -.replies-section { - margin-top: 32px; -} - -.replies-title { - font-size: 1.1rem; - font-weight: 600; - margin-bottom: 16px; - color: var(--text-primary); -} - -.reply-form { - margin-bottom: 24px; -} - -.reply-input { - width: 100%; - padding: 12px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - font-size: 0.95rem; - resize: vertical; - margin-bottom: 12px; - font-family: inherit; -} - -.reply-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.replies-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.reply-card { - padding: 16px; - background: var(--bg-secondary); - border-radius: var(--radius-md); - border: 1px solid var(--border); -} - -.reply-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} - -.reply-avatar-link { - text-decoration: none; -} - -.reply-avatar { - width: 36px; - height: 36px; - min-width: 36px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.85rem; - color: white; - overflow: hidden; -} - -.reply-avatar img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.reply-meta { - flex: 1; - min-width: 0; -} - -.reply-author { - font-weight: 600; - color: var(--text-primary); -} - -.reply-handle { - font-size: 0.85rem; - color: var(--text-tertiary); - text-decoration: none; - margin-left: 6px; -} - -.reply-handle:hover { - color: var(--accent); - text-decoration: underline; -} - -.reply-time { - font-size: 0.85rem; - color: var(--text-tertiary); - white-space: nowrap; -} - -.reply-text { - color: var(--text-primary); - line-height: 1.5; - margin: 0; -} - -.replies-title { - display: flex; - align-items: center; - gap: 8px; -} - -.replies-title svg { - color: var(--accent); -} - -.replies-list-threaded { - display: flex; - flex-direction: column; - gap: 8px; -} - -.reply-card-threaded { - padding: 16px; - transition: background 0.15s ease; -} - -.reply-card-threaded .reply-header { - margin-bottom: 8px; -} - -.reply-card-threaded .reply-meta { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.reply-dot { - color: var(--text-tertiary); - font-size: 0.75rem; -} - -.reply-actions { - display: flex; - gap: 4px; - margin-left: auto; -} - -.reply-action-btn { - background: none; - border: none; - padding: 4px 8px; - color: var(--text-tertiary); - cursor: pointer; - border-radius: var(--radius-sm); - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.reply-action-btn:hover { - color: var(--accent); - background: var(--accent-subtle); -} - -.reply-action-delete:hover { - color: var(--error); - background: rgba(239, 68, 68, 0.1); -} - -.replying-to-banner { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - margin-bottom: 12px; - background: var(--accent-subtle); - border-radius: var(--radius-sm); - font-size: 0.85rem; - color: var(--text-secondary); -} - -.cancel-reply { - background: none; - border: none; - font-size: 1.2rem; - color: var(--text-tertiary); - cursor: pointer; - padding: 0 4px; - line-height: 1; -} - -.cancel-reply:hover { - color: var(--text-primary); -} - -.reply-form.card { - padding: 16px; - margin-bottom: 16px; -} - -.reply-form-actions { - display: flex; - justify-content: flex-end; -} - -.inline-replies { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid var(--border); - display: flex; - flex-direction: column; - gap: 16px; -} - -.main-reply-composer { - margin-top: 16px; - background: var(--bg-secondary); - padding: 12px; - border-radius: var(--radius-md); -} - -.reply-input { - width: 100%; - min-height: 80px; - padding: 12px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-card); - color: var(--text-primary); - font-family: inherit; - font-size: 0.95rem; - resize: vertical; - display: block; -} - -.reply-input:focus { - border-color: var(--accent); - outline: none; -} - -.reply-input.small { - min-height: 60px; - font-size: 0.9rem; - margin-bottom: 8px; -} - -.composer-actions { - display: flex; - justify-content: flex-end; -} - -.btn-block { - width: 100%; - text-align: left; - padding: 8px 12px; - color: var(--text-secondary); - background: var(--bg-tertiary); - border-radius: var(--radius-md); - margin-top: 8px; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.2s; -} - -.btn-block:hover { - background: var(--border); - color: var(--text-primary); -} - -.annotation-action.active { - color: var(--accent); -} - -.new-page { - max-width: 600px; - margin: 0 auto; - display: flex; - flex-direction: column; - gap: 32px; -} - -.loading-spinner { - width: 32px; - height: 32px; - border: 3px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin: 60px auto; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.navbar { - position: sticky; - top: 0; - z-index: 1000; - background: rgba(12, 10, 20, 0.95); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border); -} - -.navbar-inner { - max-width: 1200px; - margin: 0 auto; - padding: 12px 24px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; -} - -.navbar-brand { - display: flex; - align-items: center; - gap: 10px; - text-decoration: none; - flex-shrink: 0; -} - -.navbar-logo { - width: 32px; - height: 32px; - background: linear-gradient(135deg, var(--accent), #8b5cf6); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 1rem; - color: white; -} - -.navbar-title { - font-weight: 700; - font-size: 1.25rem; - color: var(--text-primary); -} - -.navbar-center { - display: flex; - align-items: center; - gap: 8px; - background: var(--bg-tertiary); - padding: 4px; - border-radius: var(--radius-lg); -} - -.navbar-link { - display: flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - font-size: 0.9rem; - font-weight: 500; - color: var(--text-secondary); - text-decoration: none; - border-radius: var(--radius-md); - transition: all 0.15s ease; -} - -.navbar-link:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.navbar-link.active { - color: var(--text-primary); - background: var(--bg-card); - box-shadow: var(--shadow-sm); -} - -.navbar-right { - display: flex; - align-items: center; - gap: 12px; - flex-shrink: 0; -} - -.navbar-icon-link { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - color: var(--text-tertiary); - border-radius: var(--radius-md); - transition: all 0.15s ease; -} - -.navbar-icon-link:hover { - color: var(--text-primary); - background: var(--bg-tertiary); -} - -.navbar-icon-link.active { - color: var(--accent); - background: var(--accent-subtle); -} - -.navbar-new-btn { - display: flex; - align-items: center; - gap: 6px; - padding: 8px 14px; - background: linear-gradient(135deg, var(--accent), #8b5cf6); - color: white; - font-size: 0.85rem; - font-weight: 600; - text-decoration: none; - border-radius: var(--radius-full); - transition: all 0.2s ease; -} - -.navbar-new-btn:hover { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); - color: white; -} - -.navbar-user-section { - display: flex; - align-items: center; - gap: 4px; -} - -.navbar-avatar { - width: 32px; - height: 32px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.75rem; - color: white; - text-decoration: none; - transition: transform 0.15s ease; -} - -.navbar-avatar:hover { - transform: scale(1.05); -} - -.navbar-logout { - width: 24px; - height: 24px; - border: none; - background: transparent; - color: var(--text-tertiary); - font-size: 1.25rem; - cursor: pointer; - border-radius: var(--radius-sm); - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.navbar-logout:hover { - color: var(--error); - background: rgba(239, 68, 68, 0.1); -} - -.navbar-signin { - padding: 8px 16px; - background: var(--accent); - color: white; - font-size: 0.9rem; - font-weight: 500; - text-decoration: none; - border-radius: var(--radius-full); - transition: all 0.15s ease; -} - -.navbar-signin:hover { - background: var(--accent-hover); - color: white; -} - -.navbar-user-menu { - position: relative; -} - -.navbar-avatar-btn { - width: 36px; - height: 36px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - border: none; - cursor: pointer; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - transition: - transform 0.15s ease, - box-shadow 0.15s ease; -} - -.navbar-avatar-btn:hover { - transform: scale(1.05); - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); -} - -.navbar-avatar-img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.navbar-avatar-text { - font-weight: 600; - font-size: 0.75rem; - color: white; -} - -.navbar-dropdown { - position: absolute; - top: calc(100% + 8px); - right: 0; - min-width: 200px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - overflow: hidden; - z-index: 1001; - animation: dropdownFade 0.15s ease; -} - -@keyframes dropdownFade { - from { - opacity: 0; - transform: translateY(-8px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.navbar-dropdown-header { - padding: 12px 16px; - background: var(--bg-secondary); -} - -.navbar-dropdown-name { - display: block; - font-weight: 600; - color: var(--text-primary); - font-size: 0.9rem; -} - -.navbar-dropdown-handle { - display: block; - color: var(--text-tertiary); - font-size: 0.8rem; - margin-top: 2px; -} - -.navbar-dropdown-divider { - height: 1px; - background: var(--border); -} - -.navbar-dropdown-item { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 12px 16px; - font-size: 0.9rem; - color: var(--text-primary); - text-decoration: none; - background: none; - border: none; - cursor: pointer; - transition: background 0.15s ease; - text-align: left; -} - -.navbar-dropdown-item:hover { - background: var(--bg-tertiary); -} - -.navbar-dropdown-logout { - color: var(--error); - border-top: 1px solid var(--border); -} - -.navbar-dropdown-logout:hover { - background: rgba(239, 68, 68, 0.1); -} - -@media (max-width: 768px) { - .navbar-inner { - padding: 10px 16px; - } - - .navbar-title { - display: none; - } - - .navbar-center { - display: none; - } - - .navbar-new-btn span { - display: none; - } - - .navbar-new-btn { - width: 36px; - height: 36px; - padding: 0; - justify-content: center; - } -} - -.collections-list { - display: flex; - flex-direction: column; - gap: 2px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - overflow: hidden; -} - -.collection-row { - display: flex; - align-items: center; - background: var(--bg-card); - transition: background 0.15s ease; -} - -.collection-row:not(:last-child) { - border-bottom: 1px solid var(--border); -} - -.collection-row:hover { - background: var(--bg-secondary); -} - -.collection-row-content { - flex: 1; - display: flex; - align-items: center; - gap: 16px; - padding: 16px 20px; - text-decoration: none; - min-width: 0; -} - -.collection-row-icon { - width: 44px; - height: 44px; - min-width: 44px; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient( - 135deg, - rgba(79, 70, 229, 0.1), - rgba(168, 85, 247, 0.15) - ); - color: var(--accent); - border-radius: var(--radius-md); - transition: all 0.2s ease; -} - -.collection-row:hover .collection-row-icon { - background: linear-gradient( - 135deg, - rgba(79, 70, 229, 0.15), - rgba(168, 85, 247, 0.2) - ); - transform: scale(1.05); -} - -.collection-row-info { - flex: 1; - min-width: 0; -} - -.collection-row-name { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0 0 2px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.collection-row:hover .collection-row-name { - color: var(--accent); -} - -.collection-row-desc { - font-size: 0.85rem; - color: var(--text-secondary); - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.collection-row-arrow { - color: var(--text-tertiary); - opacity: 0; - transition: all 0.2s ease; -} - -.collection-row:hover .collection-row-arrow { - opacity: 1; - color: var(--accent); - transform: translateX(2px); -} - -.collection-row-edit { - padding: 10px; - margin-right: 12px; - color: var(--text-tertiary); - background: none; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - opacity: 0; - transition: all 0.15s ease; -} - -.collection-row:hover .collection-row-edit { - opacity: 1; -} - -.collection-row-edit:hover { - color: var(--text-primary); - background: var(--bg-tertiary); -} - -.back-link { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--text-tertiary); - font-size: 0.9rem; - font-weight: 500; - text-decoration: none; - margin-bottom: 24px; - transition: color 0.15s ease; -} - -.back-link:hover { - color: var(--accent); -} - -.collection-detail-header { - display: flex; - gap: 20px; - padding: 24px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - margin-bottom: 32px; - position: relative; -} - -.collection-detail-icon { - width: 56px; - height: 56px; - min-width: 56px; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient( - 135deg, - rgba(79, 70, 229, 0.1), - rgba(168, 85, 247, 0.1) - ); - color: var(--accent); - border-radius: var(--radius-md); -} - -.collection-detail-info { - flex: 1; - min-width: 0; -} - -.collection-detail-visibility { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.8rem; - font-weight: 600; - color: var(--accent); - text-transform: capitalize; - margin-bottom: 8px; -} - -.collection-detail-title { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 8px; - line-height: 1.3; -} - -.collection-detail-desc { - color: var(--text-secondary); - font-size: 1rem; - line-height: 1.5; - margin-bottom: 12px; - max-width: 600px; -} - -.collection-detail-stats { - display: flex; - align-items: center; - gap: 8px; - font-size: 0.85rem; - color: var(--text-tertiary); -} - -.collection-detail-actions { - position: absolute; - top: 20px; - right: 20px; - display: flex; - align-items: center; - gap: 8px; -} - -.collection-detail-actions .share-menu-container { - display: flex; - align-items: center; -} - -.collection-detail-actions .annotation-action { - padding: 10px; - color: var(--text-tertiary); - background: none; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.15s ease; -} - -.collection-detail-actions .annotation-action:hover { - color: var(--accent); - background: var(--bg-tertiary); -} - -.collection-detail-edit, -.collection-detail-delete { - padding: 10px; - color: var(--text-tertiary); - background: none; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.15s ease; -} - -.collection-detail-edit:hover { - color: var(--accent); - background: var(--bg-tertiary); -} - -.collection-detail-delete:hover { - color: var(--error); - background: rgba(239, 68, 68, 0.1); -} - -.collection-item-wrapper { - position: relative; -} - -.collection-item-remove { - position: absolute; - top: 12px; - left: -40px; - z-index: 10; - padding: 8px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text-tertiary); - cursor: pointer; - opacity: 0; - transition: all 0.15s ease; -} - -.collection-item-wrapper:hover .collection-item-remove { - opacity: 1; -} - -.collection-item-remove:hover { - color: var(--error); - border-color: var(--error); - background: rgba(239, 68, 68, 0.05); -} - -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - padding: 16px; - z-index: 50; - animation: fadeIn 0.2s ease-out; -} - -.modal-container { - background: var(--bg-secondary); - border-radius: var(--radius-lg); - width: 100%; - max-width: 28rem; - border: 1px solid var(--border); - box-shadow: var(--shadow-lg); - animation: zoomIn 0.2s ease-out; -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - border-bottom: 1px solid var(--border); -} - -.modal-title { - font-size: 1.25rem; - font-weight: 700; - color: var(--text-primary); -} - -.modal-close-btn { - padding: 8px; - color: var(--text-tertiary); - border-radius: var(--radius-md); - transition: color 0.15s; -} - -.modal-close-btn:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.modal-form { - padding: 16px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.icon-picker-tabs { - display: flex; - gap: 4px; - margin-bottom: 12px; -} - -.icon-picker-tab { - flex: 1; - padding: 8px 12px; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-secondary); - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - transition: all 0.15s ease; -} - -.icon-picker-tab:hover { - background: var(--bg-tertiary); -} - -.icon-picker-tab.active { - background: var(--accent); - border-color: var(--accent); - color: white; -} - -.emoji-picker-wrapper { - display: flex; - flex-direction: column; - gap: 10px; -} - -.emoji-custom-input input { - width: 100%; -} - -.emoji-picker, -.icon-picker { - display: flex; - flex-wrap: wrap; - gap: 4px; - max-height: 120px; - overflow-y: auto; - padding: 8px; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: var(--radius-md); -} - -.emoji-option, -.icon-option { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.2rem; - background: transparent; - border: 2px solid transparent; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.15s ease; - color: var(--text-secondary); -} - -.emoji-option:hover, -.icon-option:hover { - background: var(--bg-tertiary); - transform: scale(1.1); - color: var(--text-primary); -} - -.emoji-option.selected, -.icon-option.selected { - border-color: var(--accent); - background: var(--accent-subtle); - color: var(--accent); -} - -.form-group { - margin-bottom: 0; -} - -.form-label { - display: block; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-secondary); - margin-bottom: 4px; -} - -.form-input, -.form-textarea, -.form-select { - width: 100%; - padding: 8px 12px; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - color: var(--text-primary); - transition: all 0.15s; -} - -.form-input:focus, -.form-textarea:focus, -.form-select:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px var(--accent-subtle); -} - -.form-textarea { - resize: none; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 12px; - padding-top: 8px; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes zoomIn { - from { - opacity: 0; - transform: scale(0.95); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -.annotation-detail-page { - max-width: 680px; - margin: 0 auto; - padding: 24px 16px; -} - -.annotation-detail-header { - margin-bottom: 24px; -} - -.back-link { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--text-secondary); - font-size: 0.9rem; - transition: color 0.15s; -} - -.back-link:hover { - color: var(--text-primary); -} - -.text-secondary { - color: var(--text-secondary); -} - -.text-error { - color: var(--error); -} - -.text-center { - text-align: center; -} - -.flex { - display: flex; -} - -.items-center { - align-items: center; -} - -.justify-center { - justify-content: center; -} - -.justify-end { - justify-content: flex-end; -} - -.gap-2 { - gap: 8px; -} - -.gap-3 { - gap: 12px; -} - -.mt-3 { - margin-top: 12px; -} - -.mb-6 { - margin-bottom: 24px; -} - -.btn-text { - background: none; - border: none; - color: var(--text-secondary); - font-size: 0.9rem; - padding: 8px 12px; - cursor: pointer; - transition: color 0.15s; -} - -.btn-text:hover { - color: var(--text-primary); -} - -.btn-sm { - padding: 6px 12px; - font-size: 0.85rem; -} - -.annotation-edit-btn { - background: none; - border: none; - cursor: pointer; - padding: 6px 8px; - color: var(--text-tertiary); - border-radius: var(--radius-sm); - transition: all 0.15s ease; -} - -.annotation-edit-btn:hover { - color: var(--accent); - background: var(--accent-subtle); -} - -.spinner { - width: 32px; - height: 32px; - border: 3px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -.spinner-sm { - width: 16px; - height: 16px; - border-width: 2px; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.collection-list-item { - width: 100%; - text-align: left; - padding: 12px 16px; - border-radius: var(--radius-md); - background: var(--bg-primary); - border: 1px solid transparent; - color: var(--text-primary); - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; -} - -.collection-list-item:hover { - background: var(--bg-hover); - border-color: var(--border); -} - -.collection-list-item:hover .collection-list-item-icon { - opacity: 1; -} - -.collection-list-item:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.item-delete-overlay { - position: absolute; - top: 16px; - right: 16px; - z-index: 10; - opacity: 0; - transition: opacity 0.15s ease; -} - -.card:hover .item-delete-overlay, -div:hover > .item-delete-overlay { - opacity: 1; -} - -.btn-icon-danger { - padding: 8px; - background: var(--error); - color: white; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - box-shadow: var(--shadow-md); - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.btn-icon-danger:hover { - background: #dc2626; - transform: scale(1.05); -} - -.action-buttons { - display: flex; - gap: 8px; -} - -.action-buttons-end { - display: flex; - justify-content: flex-end; - gap: 8px; -} - -.filter-tab { - padding: 8px 16px; - font-size: 0.9rem; - font-weight: 500; - color: var(--text-secondary); - background: transparent; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.15s ease; -} - -.filter-tab:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.filter-tab.active { - color: var(--text-primary); - background: var(--bg-card); - box-shadow: var(--shadow-sm); -} - -.inline-reply { - padding: 12px 16px; - border-bottom: 1px solid var(--border); -} - -.inline-reply:last-child { - border-bottom: none; -} - -.inline-reply-avatar { - width: 28px; - height: 28px; - min-width: 28px; - border-radius: var(--radius-full); - background: linear-gradient(135deg, var(--accent), #a855f7); - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.7rem; - color: white; - overflow: hidden; -} - -.inline-reply-avatar img, -.inline-reply-avatar-placeholder { - width: 100%; - height: 100%; - object-fit: cover; -} - -.inline-reply-avatar-placeholder { - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.7rem; - color: white; -} - -.inline-reply-content { - flex: 1; - min-width: 0; -} - -.inline-reply-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.inline-reply-author { - font-weight: 600; - font-size: 0.85rem; - color: var(--text-primary); -} - -.inline-reply-handle { - color: var(--text-tertiary); - font-size: 0.8rem; - text-decoration: none; -} - -.inline-reply-time { - color: var(--text-tertiary); - font-size: 0.75rem; - margin-left: auto; -} - -.inline-reply-text { - font-size: 0.9rem; - color: var(--text-primary); - line-height: 1.5; -} - -.inline-reply-action { - display: flex; - align-items: center; - gap: 4px; - padding: 4px 8px; - font-size: 0.8rem; - color: var(--text-tertiary); - background: none; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.15s ease; -} - -.inline-reply-action:hover { - color: var(--text-secondary); - background: var(--bg-hover); -} - -.inline-reply-composer { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 12px 16px; -} - -.history-panel { - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 1rem; - margin-bottom: 1rem; - font-size: 0.9rem; - animation: fadeIn 0.2s ease-out; -} - -.history-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--border); -} - -.history-title { - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - font-size: 0.75rem; - color: var(--text-secondary); -} - -.history-list { - list-style: none; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.history-item { - position: relative; - padding-left: 1rem; - border-left: 2px solid var(--border); -} - -.history-date { - font-size: 0.75rem; - color: var(--text-tertiary); - margin-bottom: 0.25rem; -} - -.history-content { - color: var(--text-secondary); - white-space: pre-wrap; -} - -.history-close-btn { - color: var(--text-tertiary); - padding: 4px; - border-radius: var(--radius-sm); - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.history-close-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.history-status { - text-align: center; - color: var(--text-tertiary); - font-style: italic; - padding: 1rem; -} - -.form-label { - display: block; - font-size: 0.85rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 6px; -} - -.color-input-container { - display: flex; - align-items: center; - gap: 12px; - background: var(--bg-tertiary); - padding: 8px 12px; - border-radius: var(--radius-md); - border: 1px solid var(--border); - width: fit-content; -} - -.color-input-wrapper { - position: relative; - width: 32px; - height: 32px; - border-radius: var(--radius-full); - overflow: hidden; - border: 2px solid var(--border); - cursor: pointer; - transition: transform 0.1s; -} - -.color-input-wrapper:hover { - transform: scale(1.1); - border-color: var(--accent); -} - -.color-input-wrapper input[type="color"] { - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - padding: 0; - margin: 0; - border: none; - cursor: pointer; - opacity: 0; -} - -.bookmark-card { - display: flex; - flex-direction: column; - gap: 16px; -} - -.bookmark-preview { - display: flex; - flex-direction: column; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - overflow: hidden; - text-decoration: none; - transition: all 0.2s ease; - position: relative; -} - -.bookmark-preview:hover { - border-color: var(--accent); - box-shadow: var(--shadow-sm); - transform: translateY(-1px); -} - -.bookmark-preview::before { - content: ""; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 4px; - background: var(--accent); - opacity: 0.7; -} - -.bookmark-preview-content { - padding: 16px 20px; - display: flex; - flex-direction: column; - gap: 8px; -} - -.bookmark-preview-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.bookmark-preview-site { - font-size: 0.75rem; - color: var(--accent); - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 700; - display: flex; - align-items: center; - gap: 6px; -} - -.bookmark-preview-title { - font-size: 1.15rem; - font-weight: 700; - color: var(--text-primary); - line-height: 1.4; -} - -.bookmark-preview-desc { - font-size: 0.95rem; - color: var(--text-secondary); - line-height: 1.6; -} - -.bookmark-preview-arrow { - display: none; -} - -.bookmark-preview:hover { - background: var(--bg-tertiary); - border-color: var(--accent-subtle); - transform: translateY(-1px); -} - -.bookmark-preview-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.bookmark-preview-site { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.75rem; - font-weight: 600; - color: var(--accent); - text-transform: uppercase; - letter-spacing: 0.03em; -} - -.bookmark-preview-title { - font-size: 1rem; - font-weight: 600; - line-height: 1.4; - color: var(--text-primary); - margin: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.bookmark-preview-desc { - font-size: 0.875rem; - color: var(--text-secondary); - line-height: 1.5; - margin: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.bookmark-preview-arrow { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-tertiary); - padding: 0 4px; - transition: all 0.2s ease; -} - -.bookmark-preview:hover .bookmark-preview-arrow { - color: var(--accent); - transform: translateX(2px); -} - -.navbar-logo-img { - width: 24px; - height: 24px; - object-fit: contain; -} - -.login-logo-img { - width: 80px; - height: 80px; - margin-bottom: 24px; - object-fit: contain; -} - -.legal-content { - max-width: 800px; - margin: 0 auto; - padding: 20px; -} - -.legal-content h1 { - font-size: 2rem; - margin-bottom: 8px; - color: var(--text-primary); -} - -.legal-content h2 { - font-size: 1.4rem; - margin-top: 32px; - margin-bottom: 12px; - color: var(--text-primary); -} - -.legal-content h3 { - font-size: 1.1rem; - margin-top: 20px; - margin-bottom: 8px; - color: var(--text-primary); -} - -.legal-content p { - color: var(--text-secondary); - line-height: 1.7; - margin-bottom: 12px; -} - -.legal-content ul { - color: var(--text-secondary); - line-height: 1.7; - margin-left: 24px; - margin-bottom: 12px; -} - -.legal-content li { - margin-bottom: 6px; -} - -.legal-content a { - color: var(--accent); - text-decoration: none; -} - -.legal-content a:hover { - text-decoration: underline; -} - -.legal-content section { - margin-bottom: 24px; -} - -.input { - width: 100%; - padding: 12px 14px; - font-size: 0.95rem; - color: var(--text-primary); - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-md); - outline: none; - transition: all 0.15s ease; -} - -.input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-subtle); -} - -.input::placeholder { - color: var(--text-tertiary); -} - -.notifications-page { - max-width: 680px; - margin: 0 auto; -} - -.notifications-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.notification-item { - display: flex; - gap: 16px; - align-items: flex-start; - text-decoration: none; - color: inherit; -} - -.notification-item:hover { - background: var(--bg-hover); -} - -.notification-icon { - width: 36px; - height: 36px; - border-radius: var(--radius-full); - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-tertiary); - color: var(--text-secondary); - flex-shrink: 0; -} - -.notification-icon[data-type="like"] { - color: #ef4444; - background: rgba(239, 68, 68, 0.1); -} - -.notification-icon[data-type="reply"] { - color: #3b82f6; - background: rgba(59, 130, 246, 0.1); -} - -.notification-content { - flex: 1; - min-width: 0; -} - -.notification-text { - font-size: 0.95rem; - margin-bottom: 4px; - line-height: 1.4; - color: var(--text-primary); -} - -.notification-text strong { - font-weight: 600; -} - -.notification-time { - font-size: 0.85rem; - color: var(--text-tertiary); -} - -.notification-link { - position: relative; -} - -.notification-badge { - position: absolute; - top: -2px; - right: -2px; - background: var(--error); - color: white; - font-size: 0.7rem; - font-weight: 700; - min-width: 16px; - height: 16px; - border-radius: var(--radius-full); - display: flex; - align-items: center; - justify-content: center; - padding: 0 4px; - border: 2px solid var(--bg-primary); -} +@import "./css/layout.css"; +@import "./css/base.css"; +@import "./css/buttons.css"; +@import "./css/buttons.css"; +@import "./css/feed.css"; +@import "./css/profile.css"; +@import "./css/login.css"; +@import "./css/annotations.css"; +@import "./css/collections.css"; +@import "./css/modals.css"; +@import "./css/notifications.css"; +@import "./css/skeleton.css"; +@import "./css/utilities.css"; diff --git a/web/src/pages/Feed.jsx b/web/src/pages/Feed.jsx index a74b4e6..44e7d3d 100644 --- a/web/src/pages/Feed.jsx +++ b/web/src/pages/Feed.jsx @@ -3,6 +3,7 @@ import { useSearchParams } from "react-router-dom"; import AnnotationCard, { HighlightCard } from "../components/AnnotationCard"; import BookmarkCard from "../components/BookmarkCard"; import CollectionItemCard from "../components/CollectionItemCard"; +import AnnotationSkeleton from "../components/AnnotationSkeleton"; import { getAnnotationFeed, deleteHighlight } from "../api/client"; import { AlertIcon, InboxIcon } from "../components/Icons"; import { useAuth } from "../context/AuthContext"; @@ -151,107 +152,101 @@ export default function Feed() {
- {loading && ( + {loading ? (
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
+ {[1, 2, 3, 4, 5].map((i) => ( + ))}
- )} - - {error && ( -
-
- -
-

Something went wrong

-

{error}

-
- )} + ) : ( + <> + {error && ( +
+
+ +
+

Something went wrong

+

{error}

+
+ )} - {!loading && !error && filteredAnnotations.length === 0 && ( -
-
- -
-

No items yet

-

- {filter === "all" - ? "Be the first to annotate something!" - : `No ${filter} items found.`} -

-
- )} + {!error && filteredAnnotations.length === 0 && ( +
+
+ +
+

No items yet

+

+ {filter === "all" + ? "Be the first to annotate something!" + : `No ${filter} items found.`} +

+
+ )} - {!loading && !error && filteredAnnotations.length > 0 && ( -
- {filteredAnnotations.map((item) => { - if (item.type === "CollectionItem") { - return ; - } - if ( - item.type === "Highlight" || - item.motivation === "highlighting" - ) { - return ( - { - const rkey = uri.split("/").pop(); - await deleteHighlight(rkey); - setAnnotations((prev) => - prev.filter((a) => a.id !== item.id), - ); - }} - onAddToCollection={() => - setCollectionModalState({ - isOpen: true, - uri: item.uri || item.id, - }) - } - /> - ); - } - if (item.type === "Bookmark" || item.motivation === "bookmarking") { - return ( - - setCollectionModalState({ - isOpen: true, - uri: item.uri || item.id, - }) - } - /> - ); - } - return ( - - setCollectionModalState({ - isOpen: true, - uri: item.uri || item.id, - }) + {!error && filteredAnnotations.length > 0 && ( +
+ {filteredAnnotations.map((item) => { + if (item.type === "CollectionItem") { + return ; } - /> - ); - })} -
+ if ( + item.type === "Highlight" || + item.motivation === "highlighting" + ) { + return ( + { + const rkey = uri.split("/").pop(); + await deleteHighlight(rkey); + setAnnotations((prev) => + prev.filter((a) => a.id !== item.id), + ); + }} + onAddToCollection={() => + setCollectionModalState({ + isOpen: true, + uri: item.uri || item.id, + }) + } + /> + ); + } + if ( + item.type === "Bookmark" || + item.motivation === "bookmarking" + ) { + return ( + + setCollectionModalState({ + isOpen: true, + uri: item.uri || item.id, + }) + } + /> + ); + } + return ( + + setCollectionModalState({ + isOpen: true, + uri: item.uri || item.id, + }) + } + /> + ); + })} +
+ )} + )} {collectionModalState.isOpen && ( diff --git a/web/src/pages/Terms.jsx b/web/src/pages/Terms.jsx new file mode 100644 index 0000000..0728325 --- /dev/null +++ b/web/src/pages/Terms.jsx @@ -0,0 +1,81 @@ +import { ArrowLeft } from "lucide-react"; +import { Link } from "react-router-dom"; + +export default function Terms() { + return ( +
+ + + Home + + +
+

Terms of Service

+

Last updated: January 17, 2026

+ +
+

Overview

+

+ Margin is an open-source project. By using our service, you agree to + these terms ("Terms"). If you do not agree to these Terms, please do + not use the Service. +

+
+ +
+

Open Source

+

+ Margin is open source software. The code is available publicly and + is provided "as is", without warranty of any kind, express or + implied. +

+
+ +
+

User Conduct

+

+ You are responsible for your use of the Service and for any content + you provide, including compliance with applicable laws, rules, and + regulations. +

+

+ We reserve the right to remove any content that violates these + terms, including but not limited to: +

+
    +
  • Illegal content
  • +
  • Harassment or hate speech
  • +
  • Spam or malicious content
  • +
+
+ +
+

Decentralized Nature

+

+ Margin interacts with the AT Protocol network. We do not control the + network itself or the data stored on your Personal Data Server + (PDS). Please refer to the terms of your PDS provider for data + storage policies. +

+
+ +
+

Disclaimer

+

+ THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE". WE DISCLAIM ALL + CONDITIONS, REPRESENTATIONS AND WARRANTIES NOT EXPRESSLY SET OUT IN + THESE TERMS. +

+
+ +
+

Contact

+

+ For questions about these Terms, please contact us at{" "} + hello@margin.at +

+
+
+
+ ); +}