Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

UI redesign + cleanup

+3187 -2886
+16 -16
backend/internal/api/og.go
··· 875 height := 630 876 padding := 100 877 878 - bgPrimary := color.RGBA{12, 10, 20, 255} 879 - accent := color.RGBA{168, 85, 247, 255} 880 - textPrimary := color.RGBA{244, 240, 255, 255} 881 - textSecondary := color.RGBA{168, 158, 200, 255} 882 - border := color.RGBA{45, 38, 64, 255} 883 884 img := image.NewRGBA(image.Rect(0, 0, width, height)) 885 ··· 1118 height := 630 1119 padding := 120 1120 1121 - bgPrimary := color.RGBA{12, 10, 20, 255} 1122 - accent := color.RGBA{168, 85, 247, 255} 1123 - textPrimary := color.RGBA{244, 240, 255, 255} 1124 - textSecondary := color.RGBA{168, 158, 200, 255} 1125 - textTertiary := color.RGBA{107, 95, 138, 255} 1126 - border := color.RGBA{45, 38, 64, 255} 1127 1128 img := image.NewRGBA(image.Rect(0, 0, width, height)) 1129 ··· 1220 height := 630 1221 padding := 100 1222 1223 - bgPrimary := color.RGBA{12, 10, 20, 255} 1224 - accent := color.RGBA{250, 204, 21, 255} 1225 - textPrimary := color.RGBA{244, 240, 255, 255} 1226 - textSecondary := color.RGBA{168, 158, 200, 255} 1227 - border := color.RGBA{45, 38, 64, 255} 1228 1229 img := image.NewRGBA(image.Rect(0, 0, width, height)) 1230
··· 875 height := 630 876 padding := 100 877 878 + bgPrimary := color.RGBA{10, 10, 13, 255} 879 + accent := color.RGBA{149, 122, 134, 255} 880 + textPrimary := color.RGBA{234, 234, 238, 255} 881 + textSecondary := color.RGBA{168, 164, 171, 255} 882 + border := color.RGBA{42, 40, 46, 255} 883 884 img := image.NewRGBA(image.Rect(0, 0, width, height)) 885 ··· 1118 height := 630 1119 padding := 120 1120 1121 + bgPrimary := color.RGBA{10, 10, 13, 255} 1122 + accent := color.RGBA{149, 122, 134, 255} 1123 + textPrimary := color.RGBA{234, 234, 238, 255} 1124 + textSecondary := color.RGBA{168, 164, 171, 255} 1125 + textTertiary := color.RGBA{107, 103, 112, 255} 1126 + border := color.RGBA{42, 40, 46, 255} 1127 1128 img := image.NewRGBA(image.Rect(0, 0, width, height)) 1129 ··· 1220 height := 630 1221 padding := 100 1222 1223 + bgPrimary := color.RGBA{10, 10, 13, 255} 1224 + accent := color.RGBA{149, 122, 134, 255} 1225 + textPrimary := color.RGBA{234, 234, 238, 255} 1226 + textSecondary := color.RGBA{168, 164, 171, 255} 1227 + border := color.RGBA{42, 40, 46, 255} 1228 1229 img := image.NewRGBA(image.Rect(0, 0, width, height)) 1230
+1 -1
extension/background/service-worker.js
··· 398 sendResponse({ success: true, data: allItems }); 399 400 if (sender.tab) { 401 - const count = items.length; 402 chrome.action 403 .setBadgeText({ 404 text: count > 0 ? count.toString() : "",
··· 398 sendResponse({ success: true, data: allItems }); 399 400 if (sender.tab) { 401 + const count = allItems.length; 402 chrome.action 403 .setBadgeText({ 404 text: count > 0 ? count.toString() : "",
+3 -3
extension/content/content.css
··· 1 ::highlight(margin-highlight-preview) { 2 - background-color: rgba(168, 85, 247, 0.3); 3 color: inherit; 4 } 5 6 ::highlight(margin-scroll-highlight) { 7 - background-color: rgba(99, 102, 241, 0.4); 8 color: inherit; 9 } 10 11 ::highlight(margin-page-highlights) { 12 - background-color: rgba(252, 211, 77, 0.3); 13 color: inherit; 14 } 15
··· 1 ::highlight(margin-highlight-preview) { 2 + background-color: rgba(149, 122, 134, 0.3); 3 color: inherit; 4 } 5 6 ::highlight(margin-scroll-highlight) { 7 + background-color: rgba(149, 122, 134, 0.5); 8 color: inherit; 9 } 10 11 ::highlight(margin-page-highlights) { 12 + background-color: rgba(149, 122, 134, 0.25); 13 color: inherit; 14 } 15
+149 -110
extension/content/content.js
··· 9 const OVERLAY_STYLES = ` 10 :host { 11 all: initial; 12 - --bg-primary: #09090b; 13 - --bg-secondary: #0f0f12; 14 - --bg-tertiary: #18181b; 15 - --bg-card: #09090b; 16 - --bg-elevated: #18181b; 17 - --bg-hover: #27272a; 18 19 - --text-primary: #e4e4e7; 20 - --text-secondary: #a1a1aa; 21 - --border: #27272a; 22 23 - --accent: #6366f1; 24 - --accent-hover: #4f46e5; 25 } 26 27 :host(.light) { 28 - --bg-primary: #ffffff; 29 - --bg-secondary: #f4f4f5; 30 - --bg-tertiary: #e4e4e7; 31 --bg-card: #ffffff; 32 - --bg-elevated: #f4f4f5; 33 - --bg-hover: #e4e4e7; 34 35 - --text-primary: #18181b; 36 - --text-secondary: #52525b; 37 - --border: #e4e4e7; 38 39 - --accent: #4f46e5; 40 - --accent-hover: #4338ca; 41 } 42 43 .margin-overlay { ··· 51 52 .margin-popover { 53 position: absolute; 54 - width: 320px; 55 background: var(--bg-card); 56 border: 1px solid var(--border); 57 border-radius: 12px; 58 padding: 0; 59 - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); 60 display: flex; 61 flex-direction: column; 62 pointer-events: auto; 63 z-index: 2147483647; 64 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 65 color: var(--text-primary); 66 opacity: 0; 67 - transform: scale(0.95); 68 animation: popover-in 0.15s forwards; 69 - max-height: 480px; 70 overflow: hidden; 71 } 72 - @keyframes popover-in { to { opacity: 1; transform: scale(1); } } 73 .popover-header { 74 - padding: 12px 16px; 75 border-bottom: 1px solid var(--border); 76 display: flex; 77 justify-content: space-between; 78 align-items: center; 79 - background: var(--bg-secondary); 80 border-radius: 12px 12px 0 0; 81 - font-weight: 600; 82 - font-size: 13px; 83 - color: var(--text-primary); 84 } 85 .popover-scroll-area { 86 overflow-y: auto; 87 - max-height: 400px; 88 } 89 - .popover-item-block { 90 border-bottom: 1px solid var(--border); 91 - margin-bottom: 0; 92 - animation: fade-in 0.2s; 93 } 94 - .popover-item-block:last-child { 95 border-bottom: none; 96 } 97 - .popover-item-header { 98 - padding: 12px 16px 4px; 99 display: flex; 100 align-items: center; 101 gap: 8px; 102 } 103 - .popover-avatar { 104 - width: 24px; height: 24px; border-radius: 50%; background: var(--bg-hover); 105 - display: flex; align-items: center; justify-content: center; 106 - font-size: 10px; color: var(--text-secondary); 107 } 108 - .popover-handle { font-size: 12px; font-weight: 600; color: var(--text-primary); } 109 - .popover-close { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px; } 110 - .popover-close:hover { color: var(--text-primary); } 111 - .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: var(--text-primary); } 112 - .popover-quote { 113 - margin-top: 8px; padding: 6px 10px; background: var(--bg-tertiary); 114 - border-left: 2px solid var(--accent); border-radius: 4px; 115 - font-size: 11px; color: var(--text-secondary); font-style: italic; 116 } 117 - .popover-actions { 118 - padding: 8px 16px; 119 - display: flex; justify-content: flex-end; gap: 8px; 120 } 121 - .btn-action { 122 - background: none; border: 1px solid var(--border); border-radius: 4px; 123 - padding: 4px 8px; color: var(--text-secondary); font-size: 11px; cursor: pointer; 124 } 125 - .btn-action:hover { background: var(--bg-hover); color: var(--text-primary); } 126 127 .margin-selection-popup { 128 position: fixed; ··· 132 background: var(--bg-card); 133 border: 1px solid var(--border); 134 border-radius: 8px; 135 - box-shadow: 0 8px 16px rgba(0,0,0,0.4); 136 z-index: 2147483647; 137 pointer-events: auto; 138 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 139 animation: popover-in 0.15s forwards; 140 } 141 .selection-btn { ··· 168 border-radius: 12px; 169 padding: 16px; 170 box-sizing: border-box; 171 - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); 172 z-index: 2147483647; 173 pointer-events: auto; 174 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 175 color: var(--text-primary); 176 animation: popover-in 0.15s forwards; 177 overflow: hidden; ··· 181 } 182 .inline-compose-quote { 183 padding: 8px 12px; 184 - background: var(--bg-tertiary); 185 - border-left: 3px solid var(--accent); 186 border-radius: 4px; 187 font-size: 12px; 188 color: var(--text-secondary); ··· 247 } 248 .reply-section { 249 border-top: 1px solid var(--border); 250 - padding: 12px 16px; 251 - background: var(--bg-secondary); 252 border-radius: 0 0 12px 12px; 253 } 254 .reply-textarea { 255 width: 100%; 256 - min-height: 60px; 257 padding: 8px 10px; 258 background: var(--bg-elevated); 259 border: 1px solid var(--border); ··· 887 .join(","); 888 popoverEl.dataset.itemIds = ids; 889 890 - const popWidth = 320; 891 const screenWidth = window.innerWidth; 892 let finalLeft = left; 893 if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; ··· 895 popoverEl.style.top = `${top + 20}px`; 896 popoverEl.style.left = `${finalLeft}px`; 897 898 - const hasHighlights = items.some((item) => item.type === "Highlight"); 899 - const hasAnnotations = items.some((item) => item.type !== "Highlight"); 900 - let title; 901 - if (items.length > 1) { 902 - if (hasHighlights && hasAnnotations) { 903 - title = `${items.length} Items`; 904 - } else if (hasHighlights) { 905 - title = `${items.length} Highlights`; 906 - } else { 907 - title = `${items.length} Annotations`; 908 - } 909 - } else { 910 - title = items[0]?.type === "Highlight" ? "Highlight" : "Annotation"; 911 - } 912 913 let contentHtml = items 914 .map((item) => { ··· 916 const handle = author.handle || "User"; 917 const avatar = author.avatar; 918 const text = item.body?.value || item.text || ""; 919 - const quote = 920 - item.target?.selector?.exact || item.selector?.exact || ""; 921 const id = item.id || item.uri; 922 923 - let avatarHtml = `<div class="popover-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 924 if (avatar) { 925 - avatarHtml = `<img src="${avatar}" class="popover-avatar" style="object-fit: cover;">`; 926 } 927 928 - const isHighlight = item.type === "Highlight"; 929 - 930 let bodyHtml = ""; 931 - if (isHighlight) { 932 - bodyHtml = `<div class="popover-text" style="font-style: italic; color: #a1a1aa;">"${quote}"</div>`; 933 } else { 934 - bodyHtml = `<div class="popover-text">${text}</div>`; 935 - if (quote) { 936 - bodyHtml += `<div class="popover-quote">"${quote}"</div>`; 937 - } 938 } 939 940 return ` 941 - <div class="popover-item-block"> 942 - <div class="popover-item-header"> 943 - <div class="popover-author"> 944 - ${avatarHtml} 945 - <span class="popover-handle">@${handle}</span> 946 - </div> 947 - </div> 948 - <div class="popover-content"> 949 - ${bodyHtml} 950 - </div> 951 - <div class="popover-actions"> 952 - ${!isHighlight ? `<button class="btn-action btn-reply" data-id="${id}">Reply</button>` : ""} 953 - <button class="btn-action btn-share" data-id="${id}" data-text="${text}" data-quote="${quote}">Share</button> 954 - </div> 955 </div> 956 `; 957 }) ··· 992 btn.addEventListener("click", async () => { 993 const id = btn.getAttribute("data-id"); 994 const text = btn.getAttribute("data-text"); 995 - const quote = btn.getAttribute("data-quote"); 996 const u = `https://margin.at/annotation/${encodeURIComponent(id)}`; 997 - const shareText = `${text ? text + "\n" : ""}${quote ? `"${quote}"\n` : ""}${u}`; 998 999 try { 1000 await navigator.clipboard.writeText(shareText);
··· 9 const OVERLAY_STYLES = ` 10 :host { 11 all: initial; 12 + --bg-primary: #0a0a0d; 13 + --bg-secondary: #121216; 14 + --bg-tertiary: #1a1a1f; 15 + --bg-card: #0f0f13; 16 + --bg-elevated: #18181d; 17 + --bg-hover: #1e1e24; 18 19 + --text-primary: #eaeaee; 20 + --text-secondary: #b7b6c5; 21 + --text-tertiary: #6e6d7a; 22 + --border: rgba(183, 182, 197, 0.12); 23 24 + --accent: #957a86; 25 + --accent-hover: #a98d98; 26 + --accent-subtle: rgba(149, 122, 134, 0.15); 27 } 28 29 :host(.light) { 30 + --bg-primary: #f8f8fa; 31 + --bg-secondary: #ffffff; 32 + --bg-tertiary: #f0f0f4; 33 --bg-card: #ffffff; 34 + --bg-elevated: #ffffff; 35 + --bg-hover: #eeeef2; 36 37 + --text-primary: #18171c; 38 + --text-secondary: #5c495a; 39 + --text-tertiary: #8a8494; 40 + --border: rgba(92, 73, 90, 0.12); 41 42 + --accent: #7a5f6d; 43 + --accent-hover: #664e5b; 44 + --accent-subtle: rgba(149, 122, 134, 0.12); 45 } 46 47 .margin-overlay { ··· 55 56 .margin-popover { 57 position: absolute; 58 + width: 300px; 59 background: var(--bg-card); 60 border: 1px solid var(--border); 61 border-radius: 12px; 62 padding: 0; 63 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 64 display: flex; 65 flex-direction: column; 66 pointer-events: auto; 67 z-index: 2147483647; 68 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 69 color: var(--text-primary); 70 opacity: 0; 71 + transform: translateY(-4px); 72 animation: popover-in 0.15s forwards; 73 + max-height: 400px; 74 overflow: hidden; 75 } 76 + @keyframes popover-in { to { opacity: 1; transform: translateY(0); } } 77 + 78 .popover-header { 79 + padding: 10px 14px; 80 border-bottom: 1px solid var(--border); 81 display: flex; 82 justify-content: space-between; 83 align-items: center; 84 + background: var(--bg-primary); 85 border-radius: 12px 12px 0 0; 86 + font-weight: 500; 87 + font-size: 11px; 88 + color: var(--text-tertiary); 89 + text-transform: uppercase; 90 + letter-spacing: 0.5px; 91 + } 92 + .popover-close { 93 + background: none; 94 + border: none; 95 + color: var(--text-tertiary); 96 + cursor: pointer; 97 + padding: 2px; 98 + font-size: 16px; 99 + line-height: 1; 100 + opacity: 0.6; 101 + transition: opacity 0.15s; 102 } 103 + .popover-close:hover { opacity: 1; } 104 + 105 .popover-scroll-area { 106 overflow-y: auto; 107 + max-height: 340px; 108 } 109 + 110 + .comment-item { 111 + padding: 12px 14px; 112 border-bottom: 1px solid var(--border); 113 } 114 + .comment-item:last-child { 115 border-bottom: none; 116 } 117 + 118 + .comment-header { 119 display: flex; 120 align-items: center; 121 gap: 8px; 122 + margin-bottom: 6px; 123 } 124 + .comment-avatar { 125 + width: 22px; 126 + height: 22px; 127 + border-radius: 50%; 128 + background: var(--accent); 129 + display: flex; 130 + align-items: center; 131 + justify-content: center; 132 + font-size: 9px; 133 + font-weight: 600; 134 + color: white; 135 } 136 + .comment-handle { 137 + font-size: 12px; 138 + font-weight: 600; 139 + color: var(--text-primary); 140 } 141 + .comment-time { 142 + font-size: 11px; 143 + color: var(--text-tertiary); 144 + margin-left: auto; 145 } 146 + 147 + .comment-text { 148 + font-size: 13px; 149 + line-height: 1.5; 150 + color: var(--text-primary); 151 + margin-bottom: 8px; 152 } 153 + 154 + .highlight-only-badge { 155 + display: inline-flex; 156 + align-items: center; 157 + gap: 4px; 158 + font-size: 11px; 159 + color: var(--text-tertiary); 160 + font-style: italic; 161 + } 162 + 163 + .comment-actions { 164 + display: flex; 165 + gap: 8px; 166 + margin-top: 8px; 167 + } 168 + .highlight-only-badge { 169 + font-size: 11px; 170 + color: var(--text-tertiary); 171 + font-style: italic; 172 + opacity: 0.7; 173 + } 174 + .comment-action-btn { 175 + background: none; 176 + border: none; 177 + padding: 4px 8px; 178 + color: var(--text-tertiary); 179 + font-size: 11px; 180 + cursor: pointer; 181 + border-radius: 4px; 182 + transition: all 0.15s; 183 + } 184 + .comment-action-btn:hover { 185 + background: var(--bg-hover); 186 + color: var(--text-secondary); 187 + } 188 189 .margin-selection-popup { 190 position: fixed; ··· 194 background: var(--bg-card); 195 border: 1px solid var(--border); 196 border-radius: 8px; 197 + box-shadow: 0 8px 24px rgba(0,0,0,0.3); 198 z-index: 2147483647; 199 pointer-events: auto; 200 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 201 animation: popover-in 0.15s forwards; 202 } 203 .selection-btn { ··· 230 border-radius: 12px; 231 padding: 16px; 232 box-sizing: border-box; 233 + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); 234 z-index: 2147483647; 235 pointer-events: auto; 236 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 237 color: var(--text-primary); 238 animation: popover-in 0.15s forwards; 239 overflow: hidden; ··· 243 } 244 .inline-compose-quote { 245 padding: 8px 12px; 246 + background: var(--accent-subtle); 247 + border-left: 2px solid var(--accent); 248 border-radius: 4px; 249 font-size: 12px; 250 color: var(--text-secondary); ··· 309 } 310 .reply-section { 311 border-top: 1px solid var(--border); 312 + padding: 10px 14px; 313 + background: var(--bg-primary); 314 border-radius: 0 0 12px 12px; 315 } 316 .reply-textarea { 317 width: 100%; 318 + min-height: 50px; 319 padding: 8px 10px; 320 background: var(--bg-elevated); 321 border: 1px solid var(--border); ··· 949 .join(","); 950 popoverEl.dataset.itemIds = ids; 951 952 + const popWidth = 300; 953 const screenWidth = window.innerWidth; 954 let finalLeft = left; 955 if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; ··· 957 popoverEl.style.top = `${top + 20}px`; 958 popoverEl.style.left = `${finalLeft}px`; 959 960 + const count = items.length; 961 + const title = count === 1 ? "1 Comment" : `${count} Comments`; 962 963 let contentHtml = items 964 .map((item) => { ··· 966 const handle = author.handle || "User"; 967 const avatar = author.avatar; 968 const text = item.body?.value || item.text || ""; 969 const id = item.id || item.uri; 970 + const isHighlight = item.type === "Highlight"; 971 972 + let avatarHtml = `<div class="comment-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 973 if (avatar) { 974 + avatarHtml = `<img src="${avatar}" class="comment-avatar" style="object-fit: cover;">`; 975 } 976 977 let bodyHtml = ""; 978 + if (isHighlight && !text) { 979 + bodyHtml = `<div class="highlight-only-badge">Highlighted</div>`; 980 } else { 981 + bodyHtml = `<div class="comment-text">${text}</div>`; 982 } 983 984 return ` 985 + <div class="comment-item"> 986 + <div class="comment-header"> 987 + ${avatarHtml} 988 + <span class="comment-handle">@${handle}</span> 989 + </div> 990 + ${bodyHtml} 991 + <div class="comment-actions"> 992 + ${!isHighlight ? `<button class="comment-action-btn btn-reply" data-id="${id}">Reply</button>` : ""} 993 + <button class="comment-action-btn btn-share" data-id="${id}" data-text="${text}">Share</button> 994 + </div> 995 </div> 996 `; 997 }) ··· 1032 btn.addEventListener("click", async () => { 1033 const id = btn.getAttribute("data-id"); 1034 const text = btn.getAttribute("data-text"); 1035 const u = `https://margin.at/annotation/${encodeURIComponent(id)}`; 1036 + const shareText = text ? `${text}\n${u}` : u; 1037 1038 try { 1039 await navigator.clipboard.writeText(shareText);
+166 -166
extension/popup/popup.css
··· 1 :root { 2 - --bg-primary: #09090b; 3 - --bg-secondary: #0f0f12; 4 - --bg-tertiary: #18181b; 5 - --bg-card: #09090b; 6 - --bg-elevated: #18181b; 7 - --bg-hover: #27272a; 8 9 - --text-primary: #e4e4e7; 10 - --text-secondary: #a1a1aa; 11 - --text-tertiary: #71717a; 12 - --border: #27272a; 13 - --border-hover: #3f3f46; 14 15 - --accent: #6366f1; 16 - --accent-hover: #4f46e5; 17 - --accent-subtle: rgba(99, 102, 241, 0.1); 18 - --accent-text: #818cf8; 19 - --success: #10b981; 20 - --error: #ef4444; 21 - --warning: #f59e0b; 22 23 - --radius-sm: 4px; 24 - --radius-md: 6px; 25 - --radius-lg: 8px; 26 --radius-full: 9999px; 27 - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 28 - --shadow-md: 29 - 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 30 } 31 32 @media (prefers-color-scheme: light) { 33 :root { 34 - --bg-primary: #ffffff; 35 - --bg-secondary: #f4f4f5; 36 - --bg-tertiary: #e4e4e7; 37 --bg-card: #ffffff; 38 - --bg-elevated: #f4f4f5; 39 - --bg-hover: #e4e4e7; 40 41 - --text-primary: #18181b; 42 - --text-secondary: #52525b; 43 - --text-tertiary: #71717a; 44 - --border: #e4e4e7; 45 - --border-hover: #d4d4d8; 46 47 - --accent: #4f46e5; 48 - --accent-hover: #4338ca; 49 - --accent-text: #4f46e5; 50 - --accent-subtle: rgba(79, 70, 229, 0.1); 51 52 - --success: #059669; 53 - --error: #dc2626; 54 - --warning: #d97706; 55 } 56 } 57 58 body.light { 59 - --bg-primary: #ffffff; 60 - --bg-secondary: #f4f4f5; 61 - --bg-tertiary: #e4e4e7; 62 --bg-card: #ffffff; 63 - --bg-elevated: #f4f4f5; 64 - --bg-hover: #e4e4e7; 65 66 - --text-primary: #18181b; 67 - --text-secondary: #52525b; 68 - --text-tertiary: #71717a; 69 - --border: #e4e4e7; 70 - --border-hover: #d4d4d8; 71 72 - --accent: #4f46e5; 73 - --accent-hover: #4338ca; 74 - --accent-text: #4f46e5; 75 - --accent-subtle: rgba(79, 70, 229, 0.1); 76 77 - --success: #059669; 78 - --error: #dc2626; 79 - --warning: #d97706; 80 } 81 82 body.dark { 83 - --bg-primary: #09090b; 84 - --bg-secondary: #0f0f12; 85 - --bg-tertiary: #18181b; 86 - --bg-card: #09090b; 87 - --bg-elevated: #18181b; 88 - --bg-hover: #27272a; 89 90 - --text-primary: #e4e4e7; 91 - --text-secondary: #a1a1aa; 92 - --text-tertiary: #71717a; 93 - --border: #27272a; 94 - --border-hover: #3f3f46; 95 96 - --accent: #6366f1; 97 - --accent-hover: #4f46e5; 98 - --accent-subtle: rgba(99, 102, 241, 0.1); 99 - --accent-text: #818cf8; 100 - --success: #10b981; 101 - --error: #ef4444; 102 - --warning: #f59e0b; 103 } 104 105 * { ··· 111 body { 112 width: 380px; 113 height: 520px; 114 - font-family: "Inter", sans-serif; 115 color: var(--text-primary); 116 background-color: var(--bg-primary); 117 overflow: hidden; 118 } 119 120 .popup { ··· 129 display: flex; 130 justify-content: space-between; 131 align-items: center; 132 - background: var(--bg-secondary); 133 - z-index: 10; 134 } 135 136 .popup-brand { ··· 145 146 .popup-title { 147 font-weight: 600; 148 - font-size: 16px; 149 color: var(--text-primary); 150 } 151 152 .user-info { ··· 159 font-size: 12px; 160 color: var(--text-secondary); 161 background: var(--bg-tertiary); 162 - padding: 4px 8px; 163 - border-radius: var(--radius-sm); 164 } 165 166 .tabs { 167 display: flex; 168 border-bottom: 1px solid var(--border); 169 - background: var(--bg-tertiary); 170 - padding: 4px; 171 gap: 4px; 172 } 173 ··· 178 border: none; 179 font-size: 12px; 180 font-weight: 500; 181 - color: var(--text-secondary); 182 cursor: pointer; 183 border-radius: var(--radius-sm); 184 transition: all 0.15s; 185 } 186 187 .tab-btn:hover { 188 - color: var(--text-primary); 189 background: var(--bg-hover); 190 } 191 192 .tab-btn.active { 193 color: var(--text-primary); 194 - background: var(--bg-card); 195 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 196 } 197 198 .tab-content { ··· 220 align-items: center; 221 justify-content: center; 222 height: 100%; 223 - color: var(--text-secondary); 224 gap: 12px; 225 } 226 227 .spinner { 228 - width: 24px; 229 - height: 24px; 230 - border: 3px solid var(--border); 231 border-top-color: var(--accent); 232 border-radius: 50%; 233 animation: spin 1s linear infinite; ··· 251 } 252 253 .login-at-logo { 254 - font-size: 4rem; 255 - font-weight: 800; 256 color: var(--accent); 257 line-height: 1; 258 } 259 260 .login-title { 261 - font-size: 1.1rem; 262 font-weight: 600; 263 color: var(--text-primary); 264 } 265 266 .login-text { 267 - font-size: 14px; 268 color: var(--text-secondary); 269 line-height: 1.5; 270 } ··· 272 .quick-actions { 273 padding: 12px 16px; 274 border-bottom: 1px solid var(--border); 275 - background: var(--bg-secondary); 276 } 277 278 .create-form { 279 padding: 16px; 280 border-bottom: 1px solid var(--border); 281 - background: var(--bg-secondary); 282 } 283 284 .form-header { ··· 289 } 290 291 .form-title { 292 - font-size: 13px; 293 font-weight: 600; 294 color: var(--text-primary); 295 } 296 297 .current-url { ··· 312 font-size: 13px; 313 resize: none; 314 margin-bottom: 10px; 315 - background: var(--bg-tertiary); 316 color: var(--text-primary); 317 - transition: 318 - border-color 0.15s, 319 - box-shadow 0.15s; 320 } 321 322 .annotation-input::placeholder { ··· 326 .annotation-input:focus { 327 outline: none; 328 border-color: var(--accent); 329 - box-shadow: 0 0 0 3px var(--accent-subtle); 330 } 331 332 .form-actions { ··· 338 margin-bottom: 12px; 339 padding: 10px 12px; 340 background: var(--accent-subtle); 341 - border: 1px solid var(--accent); 342 border-radius: var(--radius-sm); 343 } 344 ··· 351 font-weight: 600; 352 text-transform: uppercase; 353 letter-spacing: 0.5px; 354 - color: var(--accent); 355 } 356 357 .quote-preview-clear { ··· 371 .quote-preview-text { 372 font-size: 12px; 373 font-style: italic; 374 - color: var(--text-primary); 375 line-height: 1.4; 376 max-height: 60px; 377 overflow: hidden; ··· 386 justify-content: space-between; 387 align-items: center; 388 padding: 14px 16px; 389 - background: var(--bg-secondary); 390 } 391 392 .section-title { ··· 401 font-size: 11px; 402 background: var(--bg-tertiary); 403 padding: 3px 8px; 404 - border-radius: 10px; 405 color: var(--text-secondary); 406 } 407 408 .annotations { 409 display: flex; 410 flex-direction: column; 411 - gap: 10px; 412 - padding: 12px 16px; 413 } 414 415 .annotation-item { 416 - border: 1px solid var(--border); 417 - border-radius: var(--radius-md); 418 - padding: 12px; 419 - background: var(--bg-card); 420 - transition: border-color 0.15s; 421 } 422 423 .annotation-item:hover { 424 - border-color: var(--border-hover); 425 } 426 427 .annotation-item-header { ··· 432 } 433 434 .annotation-item-avatar { 435 - width: 28px; 436 - height: 28px; 437 border-radius: 50%; 438 - background: linear-gradient(135deg, var(--accent), #c084fc); 439 - color: white; 440 display: flex; 441 align-items: center; 442 justify-content: center; 443 - font-size: 11px; 444 font-weight: 600; 445 } 446 ··· 462 .annotation-type-badge { 463 font-size: 10px; 464 padding: 3px 8px; 465 - border-radius: var(--radius-sm); 466 font-weight: 500; 467 } 468 469 .annotation-type-badge.highlight { 470 - background: rgba(251, 191, 36, 0.2); 471 - color: #fbbf24; 472 } 473 474 .annotation-item-quote { 475 - padding: 10px 12px; 476 - border-left: 3px solid #fbbf24; 477 - margin-bottom: 10px; 478 - font-size: 13px; 479 color: var(--text-secondary); 480 font-style: italic; 481 - background: rgba(251, 191, 36, 0.1); 482 border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 483 } 484 ··· 489 } 490 491 .bookmark-item { 492 - border: 1px solid var(--border); 493 - border-radius: var(--radius-md); 494 - padding: 12px; 495 - background: var(--bg-card); 496 text-decoration: none; 497 color: inherit; 498 display: block; 499 - transition: border-color 0.15s; 500 } 501 502 .bookmark-item:hover { 503 - border-color: var(--accent); 504 } 505 506 .bookmark-title { 507 - font-size: 14px; 508 font-weight: 500; 509 margin-bottom: 4px; 510 white-space: nowrap; ··· 534 .empty-icon { 535 margin-bottom: 12px; 536 color: var(--text-tertiary); 537 - opacity: 0.5; 538 } 539 540 .empty-text { ··· 568 569 .btn-primary:hover { 570 background: var(--accent-hover); 571 - transform: translateY(-1px); 572 } 573 574 .btn-secondary { ··· 586 .btn-icon { 587 background: none; 588 border: none; 589 - color: var(--text-secondary); 590 cursor: pointer; 591 padding: 6px; 592 border-radius: var(--radius-sm); ··· 599 600 .popup-link { 601 font-size: 12px; 602 - color: var(--text-secondary); 603 text-decoration: none; 604 } 605 606 .popup-link:hover { 607 - color: var(--accent); 608 - text-decoration: underline; 609 } 610 611 .popup-footer { 612 padding: 12px 16px; 613 border-top: 1px solid var(--border); 614 - background: var(--bg-secondary); 615 } 616 617 .settings-view { ··· 653 } 654 655 ::-webkit-scrollbar { 656 - width: 6px; 657 } 658 659 ::-webkit-scrollbar-track { 660 - background: var(--bg-secondary); 661 } 662 663 ::-webkit-scrollbar-thumb { 664 - background: var(--border); 665 - border-radius: 3px; 666 } 667 668 ::-webkit-scrollbar-thumb:hover { 669 - background: var(--border-hover); 670 } 671 672 .collection-selector { ··· 695 align-items: center; 696 gap: 12px; 697 padding: 12px; 698 - background: var(--bg-card); 699 border: 1px solid var(--border); 700 border-radius: var(--radius-md); 701 color: var(--text-primary); ··· 711 } 712 713 .collection-select-btn:disabled { 714 - opacity: 0.7; 715 cursor: not-allowed; 716 } 717 ··· 725 .toggle-switch { 726 position: relative; 727 display: inline-block; 728 - width: 44px; 729 - height: 24px; 730 flex-shrink: 0; 731 } 732 ··· 743 left: 0; 744 right: 0; 745 bottom: 0; 746 - background-color: var(--border); 747 transition: 0.2s; 748 - border-radius: 24px; 749 } 750 751 .toggle-slider:before { 752 position: absolute; 753 content: ""; 754 - height: 18px; 755 - width: 18px; 756 left: 3px; 757 bottom: 3px; 758 - background-color: var(--text-secondary); 759 transition: 0.2s; 760 border-radius: 50%; 761 } ··· 765 } 766 767 .toggle-switch input:checked + .toggle-slider:before { 768 - transform: translateX(20px); 769 background-color: white; 770 } 771 772 .settings-input { 773 width: 100%; 774 padding: 10px 12px; 775 - background: var(--bg-tertiary); 776 border: 1px solid var(--border); 777 border-radius: var(--radius-md); 778 color: var(--text-primary); ··· 783 outline: none; 784 border-color: var(--accent); 785 } 786 .theme-toggle-group { 787 display: flex; 788 background: var(--bg-tertiary); 789 - padding: 4px; 790 border-radius: var(--radius-md); 791 gap: 2px; 792 margin-top: 8px; ··· 797 padding: 6px; 798 border: none; 799 background: transparent; 800 - color: var(--text-secondary); 801 font-size: 12px; 802 font-weight: 500; 803 border-radius: var(--radius-sm); ··· 806 } 807 808 .theme-btn:hover { 809 - color: var(--text-primary); 810 - background: rgba(128, 128, 128, 0.1); 811 } 812 813 .theme-btn.active { 814 - background: var(--bg-card); 815 color: var(--text-primary); 816 - box-shadow: var(--shadow-sm); 817 }
··· 1 + @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"); 2 + 3 :root { 4 + --bg-primary: #0a0a0d; 5 + --bg-secondary: #121216; 6 + --bg-tertiary: #1a1a1f; 7 + --bg-card: #0f0f13; 8 + --bg-elevated: #18181d; 9 + --bg-hover: #1e1e24; 10 11 + --text-primary: #eaeaee; 12 + --text-secondary: #b7b6c5; 13 + --text-tertiary: #6e6d7a; 14 15 + --border: rgba(183, 182, 197, 0.12); 16 + --border-hover: rgba(183, 182, 197, 0.2); 17 + 18 + --accent: #957a86; 19 + --accent-hover: #a98d98; 20 + --accent-subtle: rgba(149, 122, 134, 0.15); 21 + --accent-text: #c4a8b2; 22 + 23 + --success: #7fb069; 24 + --error: #d97766; 25 + --warning: #e8a54b; 26 27 + --radius-sm: 6px; 28 + --radius-md: 8px; 29 + --radius-lg: 12px; 30 --radius-full: 9999px; 31 + 32 + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 33 + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 34 } 35 36 @media (prefers-color-scheme: light) { 37 :root { 38 + --bg-primary: #f8f8fa; 39 + --bg-secondary: #ffffff; 40 + --bg-tertiary: #f0f0f4; 41 --bg-card: #ffffff; 42 + --bg-elevated: #ffffff; 43 + --bg-hover: #eeeef2; 44 45 + --text-primary: #18171c; 46 + --text-secondary: #5c495a; 47 + --text-tertiary: #8a8494; 48 49 + --border: rgba(92, 73, 90, 0.12); 50 + --border-hover: rgba(92, 73, 90, 0.22); 51 52 + --accent: #7a5f6d; 53 + --accent-hover: #664e5b; 54 + --accent-subtle: rgba(149, 122, 134, 0.12); 55 + --accent-text: #5c495a; 56 + 57 + --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 58 + --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 59 } 60 } 61 62 body.light { 63 + --bg-primary: #f8f8fa; 64 + --bg-secondary: #ffffff; 65 + --bg-tertiary: #f0f0f4; 66 --bg-card: #ffffff; 67 + --bg-elevated: #ffffff; 68 + --bg-hover: #eeeef2; 69 70 + --text-primary: #18171c; 71 + --text-secondary: #5c495a; 72 + --text-tertiary: #8a8494; 73 74 + --border: rgba(92, 73, 90, 0.12); 75 + --border-hover: rgba(92, 73, 90, 0.22); 76 + 77 + --accent: #7a5f6d; 78 + --accent-hover: #664e5b; 79 + --accent-subtle: rgba(149, 122, 134, 0.12); 80 + --accent-text: #5c495a; 81 82 + --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 83 + --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 84 } 85 86 body.dark { 87 + --bg-primary: #0a0a0d; 88 + --bg-secondary: #121216; 89 + --bg-tertiary: #1a1a1f; 90 + --bg-card: #0f0f13; 91 + --bg-elevated: #18181d; 92 + --bg-hover: #1e1e24; 93 + 94 + --text-primary: #eaeaee; 95 + --text-secondary: #b7b6c5; 96 + --text-tertiary: #6e6d7a; 97 + 98 + --border: rgba(183, 182, 197, 0.12); 99 + --border-hover: rgba(183, 182, 197, 0.2); 100 101 + --accent: #957a86; 102 + --accent-hover: #a98d98; 103 + --accent-subtle: rgba(149, 122, 134, 0.15); 104 + --accent-text: #c4a8b2; 105 106 + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 107 + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 108 } 109 110 * { ··· 116 body { 117 width: 380px; 118 height: 520px; 119 + font-family: 120 + "IBM Plex Sans", 121 + -apple-system, 122 + BlinkMacSystemFont, 123 + sans-serif; 124 color: var(--text-primary); 125 background-color: var(--bg-primary); 126 overflow: hidden; 127 + -webkit-font-smoothing: antialiased; 128 } 129 130 .popup { ··· 139 display: flex; 140 justify-content: space-between; 141 align-items: center; 142 + background: var(--bg-primary); 143 } 144 145 .popup-brand { ··· 154 155 .popup-title { 156 font-weight: 600; 157 + font-size: 15px; 158 color: var(--text-primary); 159 + letter-spacing: -0.02em; 160 } 161 162 .user-info { ··· 169 font-size: 12px; 170 color: var(--text-secondary); 171 background: var(--bg-tertiary); 172 + padding: 4px 10px; 173 + border-radius: var(--radius-full); 174 } 175 176 .tabs { 177 display: flex; 178 border-bottom: 1px solid var(--border); 179 + background: var(--bg-primary); 180 + padding: 4px 8px; 181 gap: 4px; 182 } 183 ··· 188 border: none; 189 font-size: 12px; 190 font-weight: 500; 191 + color: var(--text-tertiary); 192 cursor: pointer; 193 border-radius: var(--radius-sm); 194 transition: all 0.15s; 195 } 196 197 .tab-btn:hover { 198 + color: var(--text-secondary); 199 background: var(--bg-hover); 200 } 201 202 .tab-btn.active { 203 color: var(--text-primary); 204 + background: var(--bg-tertiary); 205 } 206 207 .tab-content { ··· 229 align-items: center; 230 justify-content: center; 231 height: 100%; 232 + color: var(--text-tertiary); 233 gap: 12px; 234 } 235 236 .spinner { 237 + width: 20px; 238 + height: 20px; 239 + border: 2px solid var(--border); 240 border-top-color: var(--accent); 241 border-radius: 50%; 242 animation: spin 1s linear infinite; ··· 260 } 261 262 .login-at-logo { 263 + font-size: 3.5rem; 264 + font-weight: 700; 265 color: var(--accent); 266 line-height: 1; 267 } 268 269 .login-title { 270 + font-size: 1rem; 271 font-weight: 600; 272 color: var(--text-primary); 273 } 274 275 .login-text { 276 + font-size: 13px; 277 color: var(--text-secondary); 278 line-height: 1.5; 279 } ··· 281 .quick-actions { 282 padding: 12px 16px; 283 border-bottom: 1px solid var(--border); 284 + background: var(--bg-primary); 285 } 286 287 .create-form { 288 padding: 16px; 289 border-bottom: 1px solid var(--border); 290 + background: var(--bg-primary); 291 } 292 293 .form-header { ··· 298 } 299 300 .form-title { 301 + font-size: 12px; 302 font-weight: 600; 303 color: var(--text-primary); 304 + letter-spacing: -0.01em; 305 } 306 307 .current-url { ··· 322 font-size: 13px; 323 resize: none; 324 margin-bottom: 10px; 325 + background: var(--bg-elevated); 326 color: var(--text-primary); 327 + transition: border-color 0.15s; 328 } 329 330 .annotation-input::placeholder { ··· 334 .annotation-input:focus { 335 outline: none; 336 border-color: var(--accent); 337 } 338 339 .form-actions { ··· 345 margin-bottom: 12px; 346 padding: 10px 12px; 347 background: var(--accent-subtle); 348 + border-left: 2px solid var(--accent); 349 border-radius: var(--radius-sm); 350 } 351 ··· 358 font-weight: 600; 359 text-transform: uppercase; 360 letter-spacing: 0.5px; 361 + color: var(--accent-text); 362 } 363 364 .quote-preview-clear { ··· 378 .quote-preview-text { 379 font-size: 12px; 380 font-style: italic; 381 + color: var(--text-secondary); 382 line-height: 1.4; 383 max-height: 60px; 384 overflow: hidden; ··· 393 justify-content: space-between; 394 align-items: center; 395 padding: 14px 16px; 396 + background: var(--bg-primary); 397 } 398 399 .section-title { ··· 408 font-size: 11px; 409 background: var(--bg-tertiary); 410 padding: 3px 8px; 411 + border-radius: var(--radius-full); 412 color: var(--text-secondary); 413 } 414 415 .annotations { 416 display: flex; 417 flex-direction: column; 418 + gap: 1px; 419 + background: var(--border); 420 } 421 422 .annotation-item { 423 + padding: 14px 16px; 424 + background: var(--bg-primary); 425 + transition: background 0.15s; 426 } 427 428 .annotation-item:hover { 429 + background: var(--bg-hover); 430 } 431 432 .annotation-item-header { ··· 437 } 438 439 .annotation-item-avatar { 440 + width: 26px; 441 + height: 26px; 442 border-radius: 50%; 443 + background: var(--accent); 444 + color: var(--bg-primary); 445 display: flex; 446 align-items: center; 447 justify-content: center; 448 + font-size: 10px; 449 font-weight: 600; 450 } 451 ··· 467 .annotation-type-badge { 468 font-size: 10px; 469 padding: 3px 8px; 470 + border-radius: var(--radius-full); 471 font-weight: 500; 472 } 473 474 .annotation-type-badge.highlight { 475 + background: var(--accent-subtle); 476 + color: var(--accent-text); 477 } 478 479 .annotation-item-quote { 480 + padding: 8px 12px; 481 + border-left: 2px solid var(--accent); 482 + margin-bottom: 8px; 483 + font-size: 12px; 484 color: var(--text-secondary); 485 font-style: italic; 486 + background: var(--accent-subtle); 487 border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 488 } 489 ··· 494 } 495 496 .bookmark-item { 497 + padding: 14px 16px; 498 + background: var(--bg-primary); 499 text-decoration: none; 500 color: inherit; 501 display: block; 502 + transition: background 0.15s; 503 } 504 505 .bookmark-item:hover { 506 + background: var(--bg-hover); 507 } 508 509 .bookmark-title { 510 + font-size: 13px; 511 font-weight: 500; 512 margin-bottom: 4px; 513 white-space: nowrap; ··· 537 .empty-icon { 538 margin-bottom: 12px; 539 color: var(--text-tertiary); 540 + opacity: 0.4; 541 } 542 543 .empty-text { ··· 571 572 .btn-primary:hover { 573 background: var(--accent-hover); 574 } 575 576 .btn-secondary { ··· 588 .btn-icon { 589 background: none; 590 border: none; 591 + color: var(--text-tertiary); 592 cursor: pointer; 593 padding: 6px; 594 border-radius: var(--radius-sm); ··· 601 602 .popup-link { 603 font-size: 12px; 604 + color: var(--text-tertiary); 605 text-decoration: none; 606 } 607 608 .popup-link:hover { 609 + color: var(--accent-text); 610 } 611 612 .popup-footer { 613 padding: 12px 16px; 614 border-top: 1px solid var(--border); 615 + background: var(--bg-primary); 616 } 617 618 .settings-view { ··· 654 } 655 656 ::-webkit-scrollbar { 657 + width: 8px; 658 } 659 660 ::-webkit-scrollbar-track { 661 + background: transparent; 662 } 663 664 ::-webkit-scrollbar-thumb { 665 + background: var(--bg-hover); 666 + border-radius: var(--radius-full); 667 } 668 669 ::-webkit-scrollbar-thumb:hover { 670 + background: var(--text-tertiary); 671 } 672 673 .collection-selector { ··· 696 align-items: center; 697 gap: 12px; 698 padding: 12px; 699 + background: var(--bg-primary); 700 border: 1px solid var(--border); 701 border-radius: var(--radius-md); 702 color: var(--text-primary); ··· 712 } 713 714 .collection-select-btn:disabled { 715 + opacity: 0.6; 716 cursor: not-allowed; 717 } 718 ··· 726 .toggle-switch { 727 position: relative; 728 display: inline-block; 729 + width: 40px; 730 + height: 22px; 731 flex-shrink: 0; 732 } 733 ··· 744 left: 0; 745 right: 0; 746 bottom: 0; 747 + background-color: var(--bg-tertiary); 748 transition: 0.2s; 749 + border-radius: 22px; 750 } 751 752 .toggle-slider:before { 753 position: absolute; 754 content: ""; 755 + height: 16px; 756 + width: 16px; 757 left: 3px; 758 bottom: 3px; 759 + background-color: var(--text-tertiary); 760 transition: 0.2s; 761 border-radius: 50%; 762 } ··· 766 } 767 768 .toggle-switch input:checked + .toggle-slider:before { 769 + transform: translateX(18px); 770 background-color: white; 771 } 772 773 .settings-input { 774 width: 100%; 775 padding: 10px 12px; 776 + background: var(--bg-elevated); 777 border: 1px solid var(--border); 778 border-radius: var(--radius-md); 779 color: var(--text-primary); ··· 784 outline: none; 785 border-color: var(--accent); 786 } 787 + 788 .theme-toggle-group { 789 display: flex; 790 background: var(--bg-tertiary); 791 + padding: 3px; 792 border-radius: var(--radius-md); 793 gap: 2px; 794 margin-top: 8px; ··· 799 padding: 6px; 800 border: none; 801 background: transparent; 802 + color: var(--text-tertiary); 803 font-size: 12px; 804 font-weight: 500; 805 border-radius: var(--radius-sm); ··· 808 } 809 810 .theme-btn:hover { 811 + color: var(--text-secondary); 812 } 813 814 .theme-btn.active { 815 + background: var(--bg-primary); 816 color: var(--text-primary); 817 }
+164 -327
extension/sidepanel/sidepanel.css
··· 1 :root { 2 - --bg-primary: #09090b; 3 - --bg-secondary: #0f0f12; 4 - --bg-tertiary: #18181b; 5 - --bg-card: #09090b; 6 - --bg-hover: #18181b; 7 - --bg-elevated: #18181b; 8 9 - --text-primary: #e4e4e7; 10 - --text-secondary: #a1a1aa; 11 - --text-tertiary: #71717a; 12 13 - --accent: #6366f1; 14 - --accent-hover: #4f46e5; 15 - --accent-subtle: rgba(99, 102, 241, 0.1); 16 - --accent-text: #818cf8; 17 18 - --border: #27272a; 19 - --border-hover: #3f3f46; 20 21 - --success: #10b981; 22 - --error: #ef4444; 23 - --warning: #f59e0b; 24 25 - --radius-sm: 4px; 26 - --radius-md: 6px; 27 - --radius-lg: 8px; 28 --radius-full: 9999px; 29 30 - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 31 - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 32 } 33 34 @media (prefers-color-scheme: light) { 35 :root { 36 - --bg-primary: #ffffff; 37 - --bg-secondary: #f4f4f5; 38 - --bg-tertiary: #e4e4e7; 39 --bg-card: #ffffff; 40 - --bg-hover: #e4e4e7; 41 - --bg-elevated: #f4f4f5; 42 43 - --text-primary: #18181b; 44 - --text-secondary: #52525b; 45 - --text-tertiary: #71717a; 46 47 - --accent: #4f46e5; 48 - --accent-hover: #4338ca; 49 - --accent-subtle: rgba(79, 70, 229, 0.1); 50 - --accent-text: #4f46e5; 51 52 - --border: #e4e4e7; 53 - --border-hover: #d4d4d8; 54 55 - --success: #059669; 56 - --error: #dc2626; 57 - --warning: #d97706; 58 } 59 } 60 61 body.light { 62 - --bg-primary: #ffffff; 63 - --bg-secondary: #f4f4f5; 64 - --bg-tertiary: #e4e4e7; 65 --bg-card: #ffffff; 66 - --bg-hover: #e4e4e7; 67 - --bg-elevated: #f4f4f5; 68 69 - --text-primary: #18181b; 70 - --text-secondary: #52525b; 71 - --text-tertiary: #71717a; 72 73 - --accent: #4f46e5; 74 - --accent-hover: #4338ca; 75 - --accent-subtle: rgba(79, 70, 229, 0.1); 76 - --accent-text: #4f46e5; 77 78 - --border: #e4e4e7; 79 - --border-hover: #d4d4d8; 80 81 - --success: #059669; 82 - --error: #dc2626; 83 - --warning: #d97706; 84 } 85 86 body.dark { 87 - --bg-primary: #09090b; 88 - --bg-secondary: #0f0f12; 89 - --bg-tertiary: #18181b; 90 - --bg-card: #09090b; 91 - --bg-hover: #18181b; 92 - --bg-elevated: #18181b; 93 94 - --text-primary: #e4e4e7; 95 - --text-secondary: #a1a1aa; 96 - --text-tertiary: #71717a; 97 98 - --accent: #6366f1; 99 - --accent-hover: #4f46e5; 100 - --accent-subtle: rgba(99, 102, 241, 0.1); 101 - --accent-text: #818cf8; 102 103 - --border: #27272a; 104 - --border-hover: #3f3f46; 105 106 - --success: #10b981; 107 - --error: #ef4444; 108 - --warning: #f59e0b; 109 } 110 111 * { ··· 116 117 body { 118 font-family: 119 - "Inter", 120 -apple-system, 121 BlinkMacSystemFont, 122 - "Segoe UI", 123 sans-serif; 124 background: var(--bg-primary); 125 color: var(--text-primary); ··· 143 background: var(--bg-primary); 144 } 145 146 - .user-handle { 147 - font-size: 12px; 148 - color: var(--text-secondary); 149 - background: var(--bg-tertiary); 150 - padding: 4px 8px; 151 - border-radius: var(--radius-sm); 152 - } 153 - 154 - .current-page-info { 155 - display: flex; 156 - align-items: center; 157 - gap: 8px; 158 - padding: 10px 16px; 159 - background: var(--bg-primary); 160 - border-bottom: 1px solid var(--border); 161 - } 162 - 163 - .tabs { 164 - display: flex; 165 - border-bottom: 1px solid var(--border); 166 - background: var(--bg-primary); 167 - padding: 4px; 168 - gap: 4px; 169 - margin: 0; 170 - } 171 - 172 - .tab-btn { 173 - flex: 1; 174 - padding: 10px 8px; 175 - background: transparent; 176 - border: none; 177 - font-size: 12px; 178 - font-weight: 500; 179 - color: var(--text-secondary); 180 - cursor: pointer; 181 - border-radius: var(--radius-sm); 182 - transition: all 0.15s; 183 - } 184 - 185 - .tab-btn:hover { 186 - color: var(--text-primary); 187 - background: var(--bg-hover); 188 - } 189 - 190 - .tab-btn.active { 191 - color: var(--text-primary); 192 - background: var(--bg-tertiary); 193 - box-shadow: none; 194 - } 195 - 196 - .quick-actions { 197 - display: flex; 198 - gap: 8px; 199 - padding: 12px 16px; 200 - border-bottom: 1px solid var(--border); 201 - background: var(--bg-primary); 202 - } 203 - 204 - .create-form { 205 - padding: 16px; 206 - border-bottom: 1px solid var(--border); 207 - background: var(--bg-primary); 208 - } 209 - 210 - .section-header { 211 - display: flex; 212 - justify-content: space-between; 213 - align-items: center; 214 - padding: 14px 16px; 215 - background: var(--bg-primary); 216 - border-bottom: 1px solid var(--border); 217 - } 218 - 219 - .annotation-item { 220 - border: 1px solid var(--border); 221 - border-radius: var(--radius-md); 222 - padding: 12px; 223 - background: var(--bg-primary); 224 - transition: border-color 0.15s; 225 - } 226 - 227 - .annotation-item:hover { 228 - border-color: var(--border-hover); 229 - background: var(--bg-hover); 230 - } 231 - 232 - .sidebar-footer { 233 - display: flex; 234 - align-items: center; 235 - justify-content: space-between; 236 - padding: 12px 16px; 237 - border-top: 1px solid var(--border); 238 - background: var(--bg-primary); 239 - } 240 - 241 - ::-webkit-scrollbar { 242 - width: 10px; 243 - height: 10px; 244 - } 245 - 246 - ::-webkit-scrollbar-track { 247 - background: transparent; 248 - } 249 - 250 - ::-webkit-scrollbar-thumb { 251 - background: var(--border); 252 - border-radius: 5px; 253 - border: 2px solid var(--bg-primary); 254 - } 255 - 256 - ::-webkit-scrollbar-thumb:hover { 257 - background: var(--border-hover); 258 - } 259 - 260 - * { 261 - margin: 0; 262 - padding: 0; 263 - box-sizing: border-box; 264 - } 265 - 266 - body { 267 - font-family: 268 - "Inter", 269 - -apple-system, 270 - BlinkMacSystemFont, 271 - "Segoe UI", 272 - sans-serif; 273 - background: var(--bg-primary); 274 - color: var(--text-primary); 275 - min-height: 100vh; 276 - -webkit-font-smoothing: antialiased; 277 - } 278 - 279 - .sidebar { 280 - display: flex; 281 - flex-direction: column; 282 - height: 100vh; 283 - background: var(--bg-primary); 284 - } 285 - 286 - .sidebar-header { 287 - display: flex; 288 - align-items: center; 289 - justify-content: space-between; 290 - padding: 14px 16px; 291 - border-bottom: 1px solid var(--border); 292 - background: var(--bg-secondary); 293 - } 294 - 295 .sidebar-brand { 296 display: flex; 297 align-items: center; ··· 304 305 .sidebar-title { 306 font-weight: 600; 307 - font-size: 16px; 308 color: var(--text-primary); 309 } 310 311 .user-info { ··· 318 font-size: 12px; 319 color: var(--text-secondary); 320 background: var(--bg-tertiary); 321 - padding: 4px 8px; 322 - border-radius: var(--radius-sm); 323 } 324 325 .current-page-info { ··· 327 align-items: center; 328 gap: 8px; 329 padding: 10px 16px; 330 - background: var(--bg-tertiary); 331 border-bottom: 1px solid var(--border); 332 } 333 334 .page-url { 335 font-size: 12px; 336 - color: var(--text-secondary); 337 white-space: nowrap; 338 overflow: hidden; 339 text-overflow: ellipsis; ··· 352 align-items: center; 353 justify-content: center; 354 height: 100%; 355 - color: var(--text-secondary); 356 gap: 12px; 357 } 358 359 .spinner { 360 - width: 24px; 361 - height: 24px; 362 - border: 3px solid var(--border); 363 border-top-color: var(--accent); 364 border-radius: 50%; 365 animation: spin 1s linear infinite; ··· 383 } 384 385 .login-at-logo { 386 - font-size: 4rem; 387 - font-weight: 800; 388 color: var(--accent); 389 line-height: 1; 390 } 391 392 .login-title { 393 - font-size: 1.1rem; 394 font-weight: 600; 395 color: var(--text-primary); 396 } 397 398 .login-text { 399 - font-size: 14px; 400 color: var(--text-secondary); 401 line-height: 1.5; 402 } ··· 404 .tabs { 405 display: flex; 406 border-bottom: 1px solid var(--border); 407 - background: var(--bg-tertiary); 408 - padding: 4px; 409 gap: 4px; 410 margin: 0; 411 } ··· 417 border: none; 418 font-size: 12px; 419 font-weight: 500; 420 - color: var(--text-secondary); 421 cursor: pointer; 422 border-radius: var(--radius-sm); 423 transition: all 0.15s; 424 } 425 426 .tab-btn:hover { 427 - color: var(--text-primary); 428 background: var(--bg-hover); 429 } 430 431 .tab-btn.active { 432 color: var(--text-primary); 433 - background: var(--bg-card); 434 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 435 } 436 437 .tab-content { ··· 450 gap: 8px; 451 padding: 12px 16px; 452 border-bottom: 1px solid var(--border); 453 - background: var(--bg-secondary); 454 } 455 456 .btn { ··· 479 480 .btn-primary:hover { 481 background: var(--accent-hover); 482 - transform: translateY(-1px); 483 } 484 485 .btn-primary:disabled { 486 opacity: 0.5; 487 cursor: not-allowed; 488 - transform: none; 489 } 490 491 .btn-secondary { ··· 507 .btn-icon { 508 background: none; 509 border: none; 510 - color: var(--text-secondary); 511 cursor: pointer; 512 padding: 6px; 513 border-radius: var(--radius-sm); ··· 521 .create-form { 522 padding: 16px; 523 border-bottom: 1px solid var(--border); 524 - background: var(--bg-secondary); 525 } 526 527 .form-header { ··· 532 } 533 534 .form-title { 535 - font-size: 13px; 536 font-weight: 600; 537 color: var(--text-primary); 538 } 539 540 .annotation-input { ··· 546 font-size: 13px; 547 resize: none; 548 margin-bottom: 10px; 549 - background: var(--bg-tertiary); 550 color: var(--text-primary); 551 - transition: 552 - border-color 0.15s, 553 - box-shadow 0.15s; 554 } 555 556 .annotation-input::placeholder { ··· 560 .annotation-input:focus { 561 outline: none; 562 border-color: var(--accent); 563 - box-shadow: 0 0 0 3px var(--accent-subtle); 564 } 565 566 .form-actions { ··· 572 margin-bottom: 12px; 573 padding: 12px; 574 background: var(--accent-subtle); 575 - border: 1px solid var(--accent); 576 - border-radius: var(--radius-md); 577 } 578 579 .quote-preview-header { ··· 581 justify-content: space-between; 582 align-items: center; 583 margin-bottom: 8px; 584 - font-size: 11px; 585 font-weight: 600; 586 text-transform: uppercase; 587 letter-spacing: 0.5px; 588 - color: var(--accent); 589 } 590 591 .quote-preview-clear { ··· 603 } 604 605 .quote-preview-text { 606 - font-size: 13px; 607 font-style: italic; 608 - color: var(--text-primary); 609 line-height: 1.5; 610 } 611 ··· 618 justify-content: space-between; 619 align-items: center; 620 padding: 14px 16px; 621 - background: var(--bg-secondary); 622 } 623 624 .section-title { ··· 633 font-size: 11px; 634 background: var(--bg-tertiary); 635 padding: 3px 8px; 636 - border-radius: 10px; 637 color: var(--text-secondary); 638 } 639 640 .annotations-list { 641 display: flex; 642 flex-direction: column; 643 - gap: 10px; 644 - padding: 12px 16px; 645 } 646 647 .annotation-item { 648 - border: 1px solid var(--border); 649 - border-radius: var(--radius-md); 650 - padding: 12px; 651 - background: var(--bg-card); 652 - transition: border-color 0.15s; 653 } 654 655 .annotation-item:hover { 656 - border-color: var(--border-hover); 657 } 658 659 .annotation-item-header { ··· 664 } 665 666 .annotation-item-avatar { 667 - width: 28px; 668 - height: 28px; 669 border-radius: 50%; 670 - background: linear-gradient(135deg, var(--accent), #c084fc); 671 - color: white; 672 display: flex; 673 align-items: center; 674 justify-content: center; 675 - font-size: 11px; 676 font-weight: 600; 677 } 678 ··· 694 .annotation-type-badge { 695 font-size: 10px; 696 padding: 3px 8px; 697 - border-radius: var(--radius-sm); 698 font-weight: 500; 699 } 700 701 .annotation-type-badge.highlight { 702 - background: rgba(251, 191, 36, 0.2); 703 - color: #fbbf24; 704 } 705 706 .annotation-item-quote { 707 - padding: 10px 12px; 708 - border-left: 3px solid #fbbf24; 709 - margin-bottom: 10px; 710 - font-size: 13px; 711 color: var(--text-secondary); 712 font-style: italic; 713 - background: rgba(251, 191, 36, 0.1); 714 border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 715 } 716 ··· 723 .bookmarks-list { 724 display: flex; 725 flex-direction: column; 726 - gap: 10px; 727 - padding: 12px 16px; 728 } 729 730 .bookmark-item { 731 - border: 1px solid var(--border); 732 - border-radius: var(--radius-md); 733 - padding: 12px; 734 - background: var(--bg-card); 735 text-decoration: none; 736 color: inherit; 737 display: block; 738 - transition: border-color 0.15s; 739 } 740 741 .bookmark-item:hover { 742 - border-color: var(--accent); 743 } 744 745 .bookmark-title { 746 - font-size: 14px; 747 font-weight: 500; 748 margin-bottom: 4px; 749 white-space: nowrap; ··· 773 .empty-icon { 774 margin-bottom: 12px; 775 color: var(--text-tertiary); 776 - opacity: 0.5; 777 } 778 779 .empty-text { ··· 793 justify-content: space-between; 794 padding: 12px 16px; 795 border-top: 1px solid var(--border); 796 - background: var(--bg-secondary); 797 } 798 799 .sidebar-link { 800 font-size: 12px; 801 - color: var(--text-secondary); 802 text-decoration: none; 803 } 804 805 .sidebar-link:hover { 806 - color: var(--accent); 807 - text-decoration: underline; 808 } 809 810 .settings-view { ··· 852 border-radius: var(--radius-md); 853 font-family: inherit; 854 font-size: 13px; 855 - background: var(--bg-tertiary); 856 color: var(--text-primary); 857 - transition: 858 - border-color 0.15s, 859 - box-shadow 0.15s; 860 } 861 862 .settings-input:focus { 863 outline: none; 864 border-color: var(--accent); 865 - box-shadow: 0 0 0 3px var(--accent-subtle); 866 } 867 868 .setting-help { ··· 877 gap: 4px; 878 padding: 6px 10px; 879 font-size: 11px; 880 - color: var(--accent); 881 background: var(--accent-subtle); 882 border: none; 883 border-radius: var(--radius-sm); ··· 887 } 888 889 .scroll-to-btn:hover { 890 - background: rgba(168, 85, 247, 0.25); 891 } 892 893 ::-webkit-scrollbar { 894 - width: 6px; 895 } 896 897 ::-webkit-scrollbar-track { 898 - background: var(--bg-secondary); 899 } 900 901 ::-webkit-scrollbar-thumb { 902 - background: var(--border); 903 - border-radius: 3px; 904 } 905 906 ::-webkit-scrollbar-thumb:hover { 907 - background: var(--border-hover); 908 } 909 910 .collection-selector { ··· 933 align-items: center; 934 gap: 12px; 935 padding: 12px; 936 - background: var(--bg-card); 937 border: 1px solid var(--border); 938 border-radius: var(--radius-md); 939 color: var(--text-primary); ··· 949 } 950 951 .collection-select-btn:disabled { 952 - opacity: 0.7; 953 cursor: not-allowed; 954 } 955 ··· 963 .toggle-switch { 964 position: relative; 965 display: inline-block; 966 - width: 44px; 967 - height: 24px; 968 flex-shrink: 0; 969 } 970 ··· 981 left: 0; 982 right: 0; 983 bottom: 0; 984 - background-color: var(--border); 985 transition: 0.2s; 986 - border-radius: 24px; 987 } 988 989 .toggle-slider:before { 990 position: absolute; 991 content: ""; 992 - height: 18px; 993 - width: 18px; 994 left: 3px; 995 bottom: 3px; 996 - background-color: var(--text-secondary); 997 transition: 0.2s; 998 border-radius: 50%; 999 } ··· 1003 } 1004 1005 .toggle-switch input:checked + .toggle-slider:before { 1006 - transform: translateX(20px); 1007 background-color: white; 1008 } 1009 .theme-toggle-group { 1010 display: flex; 1011 background: var(--bg-tertiary); 1012 - padding: 4px; 1013 border-radius: var(--radius-md); 1014 gap: 2px; 1015 margin-top: 8px; ··· 1020 padding: 6px; 1021 border: none; 1022 background: transparent; 1023 - color: var(--text-secondary); 1024 font-size: 12px; 1025 font-weight: 500; 1026 border-radius: var(--radius-sm); ··· 1029 } 1030 1031 .theme-btn:hover { 1032 - color: var(--text-primary); 1033 - background: rgba(128, 128, 128, 0.1); 1034 } 1035 1036 .theme-btn.active { 1037 - background: var(--bg-card); 1038 color: var(--text-primary); 1039 - box-shadow: var(--shadow-sm); 1040 }
··· 1 + @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"); 2 + 3 :root { 4 + --bg-primary: #0a0a0d; 5 + --bg-secondary: #121216; 6 + --bg-tertiary: #1a1a1f; 7 + --bg-card: #0f0f13; 8 + --bg-elevated: #18181d; 9 + --bg-hover: #1e1e24; 10 11 + --text-primary: #eaeaee; 12 + --text-secondary: #b7b6c5; 13 + --text-tertiary: #6e6d7a; 14 15 + --border: rgba(183, 182, 197, 0.12); 16 + --border-hover: rgba(183, 182, 197, 0.2); 17 18 + --accent: #957a86; 19 + --accent-hover: #a98d98; 20 + --accent-subtle: rgba(149, 122, 134, 0.15); 21 + --accent-text: #c4a8b2; 22 23 + --success: #7fb069; 24 + --error: #d97766; 25 + --warning: #e8a54b; 26 27 + --radius-sm: 6px; 28 + --radius-md: 8px; 29 + --radius-lg: 12px; 30 --radius-full: 9999px; 31 32 + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 33 + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 34 } 35 36 @media (prefers-color-scheme: light) { 37 :root { 38 + --bg-primary: #f8f8fa; 39 + --bg-secondary: #ffffff; 40 + --bg-tertiary: #f0f0f4; 41 --bg-card: #ffffff; 42 + --bg-elevated: #ffffff; 43 + --bg-hover: #eeeef2; 44 45 + --text-primary: #18171c; 46 + --text-secondary: #5c495a; 47 + --text-tertiary: #8a8494; 48 49 + --border: rgba(92, 73, 90, 0.12); 50 + --border-hover: rgba(92, 73, 90, 0.22); 51 52 + --accent: #7a5f6d; 53 + --accent-hover: #664e5b; 54 + --accent-subtle: rgba(149, 122, 134, 0.12); 55 + --accent-text: #5c495a; 56 57 + --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 58 + --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 59 } 60 } 61 62 body.light { 63 + --bg-primary: #f8f8fa; 64 + --bg-secondary: #ffffff; 65 + --bg-tertiary: #f0f0f4; 66 --bg-card: #ffffff; 67 + --bg-elevated: #ffffff; 68 + --bg-hover: #eeeef2; 69 70 + --text-primary: #18171c; 71 + --text-secondary: #5c495a; 72 + --text-tertiary: #8a8494; 73 74 + --border: rgba(92, 73, 90, 0.12); 75 + --border-hover: rgba(92, 73, 90, 0.22); 76 77 + --accent: #7a5f6d; 78 + --accent-hover: #664e5b; 79 + --accent-subtle: rgba(149, 122, 134, 0.12); 80 + --accent-text: #5c495a; 81 82 + --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 83 + --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 84 } 85 86 body.dark { 87 + --bg-primary: #0a0a0d; 88 + --bg-secondary: #121216; 89 + --bg-tertiary: #1a1a1f; 90 + --bg-card: #0f0f13; 91 + --bg-elevated: #18181d; 92 + --bg-hover: #1e1e24; 93 94 + --text-primary: #eaeaee; 95 + --text-secondary: #b7b6c5; 96 + --text-tertiary: #6e6d7a; 97 98 + --border: rgba(183, 182, 197, 0.12); 99 + --border-hover: rgba(183, 182, 197, 0.2); 100 101 + --accent: #957a86; 102 + --accent-hover: #a98d98; 103 + --accent-subtle: rgba(149, 122, 134, 0.15); 104 + --accent-text: #c4a8b2; 105 106 + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 107 + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 108 } 109 110 * { ··· 115 116 body { 117 font-family: 118 + "IBM Plex Sans", 119 -apple-system, 120 BlinkMacSystemFont, 121 sans-serif; 122 background: var(--bg-primary); 123 color: var(--text-primary); ··· 141 background: var(--bg-primary); 142 } 143 144 .sidebar-brand { 145 display: flex; 146 align-items: center; ··· 153 154 .sidebar-title { 155 font-weight: 600; 156 + font-size: 15px; 157 color: var(--text-primary); 158 + letter-spacing: -0.02em; 159 } 160 161 .user-info { ··· 168 font-size: 12px; 169 color: var(--text-secondary); 170 background: var(--bg-tertiary); 171 + padding: 4px 10px; 172 + border-radius: var(--radius-full); 173 } 174 175 .current-page-info { ··· 177 align-items: center; 178 gap: 8px; 179 padding: 10px 16px; 180 + background: var(--bg-primary); 181 border-bottom: 1px solid var(--border); 182 } 183 184 .page-url { 185 font-size: 12px; 186 + color: var(--text-tertiary); 187 white-space: nowrap; 188 overflow: hidden; 189 text-overflow: ellipsis; ··· 202 align-items: center; 203 justify-content: center; 204 height: 100%; 205 + color: var(--text-tertiary); 206 gap: 12px; 207 } 208 209 .spinner { 210 + width: 20px; 211 + height: 20px; 212 + border: 2px solid var(--border); 213 border-top-color: var(--accent); 214 border-radius: 50%; 215 animation: spin 1s linear infinite; ··· 233 } 234 235 .login-at-logo { 236 + font-size: 3.5rem; 237 + font-weight: 700; 238 color: var(--accent); 239 line-height: 1; 240 } 241 242 .login-title { 243 + font-size: 1rem; 244 font-weight: 600; 245 color: var(--text-primary); 246 } 247 248 .login-text { 249 + font-size: 13px; 250 color: var(--text-secondary); 251 line-height: 1.5; 252 } ··· 254 .tabs { 255 display: flex; 256 border-bottom: 1px solid var(--border); 257 + background: var(--bg-primary); 258 + padding: 4px 8px; 259 gap: 4px; 260 margin: 0; 261 } ··· 267 border: none; 268 font-size: 12px; 269 font-weight: 500; 270 + color: var(--text-tertiary); 271 cursor: pointer; 272 border-radius: var(--radius-sm); 273 transition: all 0.15s; 274 } 275 276 .tab-btn:hover { 277 + color: var(--text-secondary); 278 background: var(--bg-hover); 279 } 280 281 .tab-btn.active { 282 color: var(--text-primary); 283 + background: var(--bg-tertiary); 284 } 285 286 .tab-content { ··· 299 gap: 8px; 300 padding: 12px 16px; 301 border-bottom: 1px solid var(--border); 302 + background: var(--bg-primary); 303 } 304 305 .btn { ··· 328 329 .btn-primary:hover { 330 background: var(--accent-hover); 331 } 332 333 .btn-primary:disabled { 334 opacity: 0.5; 335 cursor: not-allowed; 336 } 337 338 .btn-secondary { ··· 354 .btn-icon { 355 background: none; 356 border: none; 357 + color: var(--text-tertiary); 358 cursor: pointer; 359 padding: 6px; 360 border-radius: var(--radius-sm); ··· 368 .create-form { 369 padding: 16px; 370 border-bottom: 1px solid var(--border); 371 + background: var(--bg-primary); 372 } 373 374 .form-header { ··· 379 } 380 381 .form-title { 382 + font-size: 12px; 383 font-weight: 600; 384 color: var(--text-primary); 385 + letter-spacing: -0.01em; 386 } 387 388 .annotation-input { ··· 394 font-size: 13px; 395 resize: none; 396 margin-bottom: 10px; 397 + background: var(--bg-elevated); 398 color: var(--text-primary); 399 + transition: border-color 0.15s; 400 } 401 402 .annotation-input::placeholder { ··· 406 .annotation-input:focus { 407 outline: none; 408 border-color: var(--accent); 409 } 410 411 .form-actions { ··· 417 margin-bottom: 12px; 418 padding: 12px; 419 background: var(--accent-subtle); 420 + border-left: 2px solid var(--accent); 421 + border-radius: var(--radius-sm); 422 } 423 424 .quote-preview-header { ··· 426 justify-content: space-between; 427 align-items: center; 428 margin-bottom: 8px; 429 + font-size: 10px; 430 font-weight: 600; 431 text-transform: uppercase; 432 letter-spacing: 0.5px; 433 + color: var(--accent-text); 434 } 435 436 .quote-preview-clear { ··· 448 } 449 450 .quote-preview-text { 451 + font-size: 12px; 452 font-style: italic; 453 + color: var(--text-secondary); 454 line-height: 1.5; 455 } 456 ··· 463 justify-content: space-between; 464 align-items: center; 465 padding: 14px 16px; 466 + background: var(--bg-primary); 467 + border-bottom: 1px solid var(--border); 468 } 469 470 .section-title { ··· 479 font-size: 11px; 480 background: var(--bg-tertiary); 481 padding: 3px 8px; 482 + border-radius: var(--radius-full); 483 color: var(--text-secondary); 484 } 485 486 .annotations-list { 487 display: flex; 488 flex-direction: column; 489 + gap: 1px; 490 + background: var(--border); 491 } 492 493 .annotation-item { 494 + padding: 14px 16px; 495 + background: var(--bg-primary); 496 + transition: background 0.15s; 497 } 498 499 .annotation-item:hover { 500 + background: var(--bg-hover); 501 } 502 503 .annotation-item-header { ··· 508 } 509 510 .annotation-item-avatar { 511 + width: 26px; 512 + height: 26px; 513 border-radius: 50%; 514 + background: var(--accent); 515 + color: var(--bg-primary); 516 display: flex; 517 align-items: center; 518 justify-content: center; 519 + font-size: 10px; 520 font-weight: 600; 521 } 522 ··· 538 .annotation-type-badge { 539 font-size: 10px; 540 padding: 3px 8px; 541 + border-radius: var(--radius-full); 542 font-weight: 500; 543 } 544 545 .annotation-type-badge.highlight { 546 + background: var(--accent-subtle); 547 + color: var(--accent-text); 548 } 549 550 .annotation-item-quote { 551 + padding: 8px 12px; 552 + border-left: 2px solid var(--accent); 553 + margin-bottom: 8px; 554 + font-size: 12px; 555 color: var(--text-secondary); 556 font-style: italic; 557 + background: var(--accent-subtle); 558 border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 559 } 560 ··· 567 .bookmarks-list { 568 display: flex; 569 flex-direction: column; 570 + gap: 1px; 571 + background: var(--border); 572 } 573 574 .bookmark-item { 575 + padding: 14px 16px; 576 + background: var(--bg-primary); 577 text-decoration: none; 578 color: inherit; 579 display: block; 580 + transition: background 0.15s; 581 } 582 583 .bookmark-item:hover { 584 + background: var(--bg-hover); 585 } 586 587 .bookmark-title { 588 + font-size: 13px; 589 font-weight: 500; 590 margin-bottom: 4px; 591 white-space: nowrap; ··· 615 .empty-icon { 616 margin-bottom: 12px; 617 color: var(--text-tertiary); 618 + opacity: 0.4; 619 } 620 621 .empty-text { ··· 635 justify-content: space-between; 636 padding: 12px 16px; 637 border-top: 1px solid var(--border); 638 + background: var(--bg-primary); 639 } 640 641 .sidebar-link { 642 font-size: 12px; 643 + color: var(--text-tertiary); 644 text-decoration: none; 645 } 646 647 .sidebar-link:hover { 648 + color: var(--accent-text); 649 } 650 651 .settings-view { ··· 693 border-radius: var(--radius-md); 694 font-family: inherit; 695 font-size: 13px; 696 + background: var(--bg-elevated); 697 color: var(--text-primary); 698 + transition: border-color 0.15s; 699 } 700 701 .settings-input:focus { 702 outline: none; 703 border-color: var(--accent); 704 } 705 706 .setting-help { ··· 715 gap: 4px; 716 padding: 6px 10px; 717 font-size: 11px; 718 + color: var(--accent-text); 719 background: var(--accent-subtle); 720 border: none; 721 border-radius: var(--radius-sm); ··· 725 } 726 727 .scroll-to-btn:hover { 728 + background: rgba(149, 122, 134, 0.25); 729 } 730 731 ::-webkit-scrollbar { 732 + width: 8px; 733 } 734 735 ::-webkit-scrollbar-track { 736 + background: transparent; 737 } 738 739 ::-webkit-scrollbar-thumb { 740 + background: var(--bg-hover); 741 + border-radius: var(--radius-full); 742 } 743 744 ::-webkit-scrollbar-thumb:hover { 745 + background: var(--text-tertiary); 746 } 747 748 .collection-selector { ··· 771 align-items: center; 772 gap: 12px; 773 padding: 12px; 774 + background: var(--bg-primary); 775 border: 1px solid var(--border); 776 border-radius: var(--radius-md); 777 color: var(--text-primary); ··· 787 } 788 789 .collection-select-btn:disabled { 790 + opacity: 0.6; 791 cursor: not-allowed; 792 } 793 ··· 801 .toggle-switch { 802 position: relative; 803 display: inline-block; 804 + width: 40px; 805 + height: 22px; 806 flex-shrink: 0; 807 } 808 ··· 819 left: 0; 820 right: 0; 821 bottom: 0; 822 + background-color: var(--bg-tertiary); 823 transition: 0.2s; 824 + border-radius: 22px; 825 } 826 827 .toggle-slider:before { 828 position: absolute; 829 content: ""; 830 + height: 16px; 831 + width: 16px; 832 left: 3px; 833 bottom: 3px; 834 + background-color: var(--text-tertiary); 835 transition: 0.2s; 836 border-radius: 50%; 837 } ··· 841 } 842 843 .toggle-switch input:checked + .toggle-slider:before { 844 + transform: translateX(18px); 845 background-color: white; 846 } 847 + 848 .theme-toggle-group { 849 display: flex; 850 background: var(--bg-tertiary); 851 + padding: 3px; 852 border-radius: var(--radius-md); 853 gap: 2px; 854 margin-top: 8px; ··· 859 padding: 6px; 860 border: none; 861 background: transparent; 862 + color: var(--text-tertiary); 863 font-size: 12px; 864 font-weight: 500; 865 border-radius: var(--radius-sm); ··· 868 } 869 870 .theme-btn:hover { 871 + color: var(--text-secondary); 872 } 873 874 .theme-btn.active { 875 + background: var(--bg-primary); 876 color: var(--text-primary); 877 }
+19 -21
web/index.html
··· 1 <!doctype html> 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <link rel="icon" href="/favicon.ico" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <meta 8 - name="description" 9 - content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol." 10 - /> 11 - <title>Margin - Write in the margins of the web</title> 12 - <link rel="preconnect" href="https://fonts.googleapis.com" /> 13 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 14 - <link 15 - href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" 16 - rel="stylesheet" 17 - /> 18 - </head> 19 20 - <body> 21 - <div id="root"></div> 22 - <script type="module" src="/src/main.jsx"></script> 23 - </body> 24 - </html>
··· 1 <!doctype html> 2 <html lang="en"> 3 4 + <head> 5 + <meta charset="UTF-8" /> 6 + <link rel="icon" href="/favicon.ico" /> 7 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 8 + <meta name="description" content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol." /> 9 + <title>Margin - Write in the margins of the web</title> 10 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 11 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 12 + <link 13 + href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" 14 + rel="stylesheet" /> 15 + </head> 16 + 17 + <body> 18 + <div id="root"></div> 19 + <script type="module" src="/src/main.jsx"></script> 20 + </body> 21 + 22 + </html>
+40 -44
web/src/App.jsx
··· 1 import { Routes, Route } from "react-router-dom"; 2 import { useEffect } from "react"; 3 import { AuthProvider, useAuth } from "./context/AuthContext"; 4 - import Sidebar from "./components/Sidebar"; 5 - import RightSidebar from "./components/RightSidebar"; 6 import MobileNav from "./components/MobileNav"; 7 import Feed from "./pages/Feed"; 8 import Url from "./pages/Url"; ··· 31 }, [user]); 32 33 return ( 34 - <div className="layout"> 35 <ScrollToTop /> 36 - <Sidebar /> 37 - <div className="main-layout"> 38 - <main className="main-content-wrapper"> 39 - <Routes> 40 - <Route path="/" element={<Feed />} /> 41 - <Route path="/url" element={<Url />} /> 42 - <Route path="/new" element={<New />} /> 43 - <Route path="/bookmarks" element={<Bookmarks />} /> 44 - <Route path="/highlights" element={<Highlights />} /> 45 - <Route path="/notifications" element={<Notifications />} /> 46 - <Route path="/profile" element={<Profile />} /> 47 - <Route path="/profile/:handle" element={<Profile />} /> 48 - <Route path="/login" element={<Login />} /> 49 - <Route path="/at/:did/:rkey" element={<AnnotationDetail />} /> 50 - <Route path="/annotation/:uri" element={<AnnotationDetail />} /> 51 - <Route path="/collections" element={<Collections />} /> 52 - <Route path="/collections/:rkey" element={<CollectionDetail />} /> 53 - <Route 54 - path="/:handle/collection/:rkey" 55 - element={<CollectionDetail />} 56 - /> 57 - <Route 58 - path="/:handle/annotation/:rkey" 59 - element={<AnnotationDetail />} 60 - /> 61 - <Route 62 - path="/:handle/highlight/:rkey" 63 - element={<AnnotationDetail />} 64 - /> 65 - <Route 66 - path="/:handle/bookmark/:rkey" 67 - element={<AnnotationDetail />} 68 - /> 69 - <Route path="/:handle/url/*" element={<UserUrl />} /> 70 - <Route path="/collection/*" element={<CollectionDetail />} /> 71 - <Route path="/privacy" element={<Privacy />} /> 72 - <Route path="/terms" element={<Terms />} /> 73 - </Routes> 74 - </main> 75 - </div> 76 - <RightSidebar /> 77 <MobileNav /> 78 </div> 79 );
··· 1 import { Routes, Route } from "react-router-dom"; 2 import { useEffect } from "react"; 3 import { AuthProvider, useAuth } from "./context/AuthContext"; 4 + import TopNav from "./components/TopNav"; 5 import MobileNav from "./components/MobileNav"; 6 import Feed from "./pages/Feed"; 7 import Url from "./pages/Url"; ··· 30 }, [user]); 31 32 return ( 33 + <div className="app"> 34 <ScrollToTop /> 35 + <TopNav /> 36 + <main className="main-content"> 37 + <Routes> 38 + <Route path="/" element={<Feed />} /> 39 + <Route path="/url" element={<Url />} /> 40 + <Route path="/new" element={<New />} /> 41 + <Route path="/bookmarks" element={<Bookmarks />} /> 42 + <Route path="/highlights" element={<Highlights />} /> 43 + <Route path="/notifications" element={<Notifications />} /> 44 + <Route path="/profile" element={<Profile />} /> 45 + <Route path="/profile/:handle" element={<Profile />} /> 46 + <Route path="/login" element={<Login />} /> 47 + <Route path="/at/:did/:rkey" element={<AnnotationDetail />} /> 48 + <Route path="/annotation/:uri" element={<AnnotationDetail />} /> 49 + <Route path="/collections" element={<Collections />} /> 50 + <Route path="/collections/:rkey" element={<CollectionDetail />} /> 51 + <Route 52 + path="/:handle/collection/:rkey" 53 + element={<CollectionDetail />} 54 + /> 55 + <Route 56 + path="/:handle/annotation/:rkey" 57 + element={<AnnotationDetail />} 58 + /> 59 + <Route 60 + path="/:handle/highlight/:rkey" 61 + element={<AnnotationDetail />} 62 + /> 63 + <Route 64 + path="/:handle/bookmark/:rkey" 65 + element={<AnnotationDetail />} 66 + /> 67 + <Route path="/:handle/url/*" element={<UserUrl />} /> 68 + <Route path="/collection/*" element={<CollectionDetail />} /> 69 + <Route path="/privacy" element={<Privacy />} /> 70 + <Route path="/terms" element={<Terms />} /> 71 + </Routes> 72 + </main> 73 <MobileNav /> 74 </div> 75 );
+135 -239
web/src/components/AnnotationCard.jsx
··· 34 if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) { 35 return baseUrl; 36 } 37 - 38 let fragment = ":~:text="; 39 if (selector.prefix) { 40 fragment += encodeURIComponent(selector.prefix) + "-,"; ··· 43 if (selector.suffix) { 44 fragment += ",-" + encodeURIComponent(selector.suffix); 45 } 46 - 47 return baseUrl + "#" + fragment; 48 } 49 50 - const truncateUrl = (url, maxLength = 60) => { 51 if (!url) return ""; 52 try { 53 const parsed = new URL(url); ··· 60 } 61 }; 62 63 export default function AnnotationCard({ 64 annotation, 65 onDelete, ··· 75 const [editText, setEditText] = useState(data.text || ""); 76 const [editTags, setEditTags] = useState(data.tags?.join(", ") || ""); 77 const [saving, setSaving] = useState(false); 78 - 79 const [showHistory, setShowHistory] = useState(false); 80 const [editHistory, setEditHistory] = useState([]); 81 const [loadingHistory, setLoadingHistory] = useState(false); 82 - 83 const [replies, setReplies] = useState([]); 84 const [replyCount, setReplyCount] = useState(data.replyCount || 0); 85 const [showReplies, setShowReplies] = useState(false); 86 const [replyingTo, setReplyingTo] = useState(null); 87 const [replyText, setReplyText] = useState(""); 88 const [posting, setPosting] = useState(false); 89 90 const isOwner = user?.did && data.author?.did === user.did; 91 - 92 - const [hasEditHistory, setHasEditHistory] = useState(false); 93 94 useEffect(() => { 95 if (data.uri && !data.color && !data.description) { 96 getEditHistory(data.uri) 97 .then((history) => { 98 - if (history && history.length > 0) { 99 - setHasEditHistory(true); 100 - } 101 }) 102 .catch(() => {}); 103 } ··· 122 123 const handlePostReply = async (parentReply) => { 124 if (!replyText.trim()) return; 125 - 126 try { 127 setPosting(true); 128 const parentUri = parentReply ··· 175 } 176 }; 177 178 - const highlightedText = 179 - data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null; 180 - const fragmentUrl = buildTextFragmentUrl(data.url, data.selector); 181 - 182 const handleLike = async () => { 183 if (!user) { 184 login(); ··· 195 const cid = annotation.cid || data.cid || ""; 196 if (data.uri && cid) await likeAnnotation(data.uri, cid); 197 } 198 - } catch (err) { 199 setIsLiked(!isLiked); 200 setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1)); 201 - console.error("Failed to toggle like:", err); 202 } 203 }; 204 ··· 218 } 219 }; 220 221 return ( 222 <article className="card annotation-card"> 223 <header className="annotation-header"> ··· 225 <UserMeta author={data.author} createdAt={data.createdAt} /> 226 </div> 227 <div className="annotation-header-right"> 228 - <div style={{ display: "flex", gap: "4px", alignItems: "center" }}> 229 - {data.uri && data.uri.includes("network.cosmik") && ( 230 - <div 231 - style={{ 232 - display: "flex", 233 - alignItems: "center", 234 - gap: "4px", 235 - fontSize: "0.75rem", 236 - color: "var(--text-tertiary)", 237 - marginRight: "8px", 238 - }} 239 - title="Added using Semble" 240 - > 241 - <span>via Semble</span> 242 - <img 243 - src="/semble-logo.svg" 244 - alt="Semble" 245 - style={{ width: "16px", height: "16px" }} 246 - /> 247 - </div> 248 - )} 249 - {hasEditHistory && !data.color && !data.description && ( 250 <button 251 className="annotation-action action-icon-only" 252 - onClick={fetchHistory} 253 - title="View Edit History" 254 > 255 - <Clock size={16} /> 256 </button> 257 - )} 258 - 259 - {isOwner && !(data.uri && data.uri.includes("network.cosmik")) && ( 260 - <> 261 - {!data.color && !data.description && ( 262 - <button 263 - className="annotation-action action-icon-only" 264 - onClick={() => setIsEditing(!isEditing)} 265 - title="Edit" 266 - > 267 - <Edit2 size={16} /> 268 - </button> 269 - )} 270 - <button 271 - className="annotation-action action-icon-only" 272 - onClick={handleDelete} 273 - disabled={deleting} 274 - title="Delete" 275 - > 276 - <Trash2 size={16} /> 277 - </button> 278 - </> 279 - )} 280 - </div> 281 </div> 282 </header> 283 ··· 286 <div className="history-header"> 287 <h4 className="history-title">Edit History</h4> 288 <button 289 - className="history-close-btn" 290 onClick={() => setShowHistory(false)} 291 - title="Close History" 292 > 293 <X size={14} /> 294 </button> ··· 321 > 322 {truncateUrl(data.url)} 323 {data.title && ( 324 - <span className="annotation-source-title"> • {data.title}</span> 325 )} 326 </a> 327 ··· 331 target="_blank" 332 rel="noopener noreferrer" 333 className="annotation-highlight" 334 - style={{ 335 - borderLeftColor: data.color || "var(--accent)", 336 - }} 337 > 338 - <mark>&quot;{highlightedText}&quot;</mark> 339 </a> 340 )} 341 342 {isEditing ? ( 343 - <div className="mt-3"> 344 <textarea 345 value={editText} 346 onChange={(e) => setEditText(e.target.value)} 347 className="reply-input" 348 rows={3} 349 - style={{ marginBottom: "8px" }} 350 /> 351 <input 352 type="text" ··· 354 placeholder="Tags (comma separated)..." 355 value={editTags} 356 onChange={(e) => setEditTags(e.target.value)} 357 - style={{ marginBottom: "8px" }} 358 /> 359 - <div className="action-buttons-end"> 360 <button 361 onClick={() => setIsEditing(false)} 362 className="btn btn-ghost" ··· 366 <button 367 onClick={handleSaveEdit} 368 disabled={saving} 369 - className="btn btn-primary btn-sm" 370 > 371 {saving ? ( 372 "Saving..." ··· 403 className={`annotation-action ${isLiked ? "liked" : ""}`} 404 onClick={handleLike} 405 > 406 - <Heart filled={isLiked} size={16} /> 407 {likeCount > 0 && <span>{likeCount}</span>} 408 </button> 409 <button 410 className={`annotation-action ${showReplies ? "active" : ""}`} 411 - onClick={async () => { 412 - if (!showReplies && replies.length === 0) { 413 - try { 414 - const res = await getReplies(data.uri); 415 - if (res.items) setReplies(res.items); 416 - } catch (err) { 417 - console.error("Failed to load replies:", err); 418 - } 419 - } 420 - setShowReplies(!showReplies); 421 - }} 422 > 423 <MessageSquare size={16} /> 424 - <span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span> 425 </button> 426 <ShareMenu 427 uri={data.uri} 428 text={data.title || data.url} ··· 430 type="Annotation" 431 url={data.url} 432 /> 433 - <button 434 - className="annotation-action" 435 - onClick={() => { 436 - if (!user) { 437 - login(); 438 - return; 439 - } 440 - if (onAddToCollection) onAddToCollection(); 441 - }} 442 - > 443 <Folder size={16} /> 444 <span>Collect</span> 445 </button> ··· 471 472 <div className="reply-form"> 473 {replyingTo && ( 474 - <div 475 - style={{ 476 - display: "flex", 477 - alignItems: "center", 478 - gap: "8px", 479 - marginBottom: "8px", 480 - fontSize: "0.85rem", 481 - color: "var(--text-secondary)", 482 - }} 483 - > 484 <span> 485 Replying to @ 486 {(replyingTo.creator || replyingTo.author)?.handle || ··· 488 </span> 489 <button 490 onClick={() => setReplyingTo(null)} 491 - style={{ 492 - background: "none", 493 - border: "none", 494 - color: "var(--text-tertiary)", 495 - cursor: "pointer", 496 - padding: "2px 6px", 497 - }} 498 > 499 × 500 </button> ··· 509 } 510 value={replyText} 511 onChange={(e) => setReplyText(e.target.value)} 512 - onFocus={(e) => { 513 - if (!user) { 514 - e.preventDefault(); 515 - alert("Please sign in to like annotations"); 516 - } 517 - }} 518 rows={2} 519 /> 520 - <div className="action-buttons-end"> 521 <button 522 - className="btn btn-primary btn-sm" 523 disabled={posting || !replyText.trim()} 524 onClick={() => { 525 if (!user) { ··· 551 data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null; 552 const fragmentUrl = buildTextFragmentUrl(data.url, data.selector); 553 const isOwner = user?.did && data.author?.did === user.did; 554 const [isEditing, setIsEditing] = useState(false); 555 const [editColor, setEditColor] = useState(data.color || "#f59e0b"); 556 const [editTags, setEditTags] = useState(data.tags?.join(", ") || ""); ··· 561 .split(",") 562 .map((t) => t.trim()) 563 .filter(Boolean); 564 - 565 await updateHighlight(data.uri, editColor, tagList); 566 setIsEditing(false); 567 - if (typeof onUpdate === "function") 568 onUpdate({ ...highlight, color: editColor, tags: tagList }); 569 } catch (err) { 570 alert("Failed to update: " + err.message); 571 } 572 }; 573 574 return ( 575 <article className="card annotation-card"> 576 <header className="annotation-header"> 577 <div className="annotation-header-left"> 578 <UserMeta author={data.author} createdAt={data.createdAt} /> 579 </div> 580 - 581 <div className="annotation-header-right"> 582 - <div style={{ display: "flex", gap: "4px", alignItems: "center" }}> 583 - {data.uri && data.uri.includes("network.cosmik") && ( 584 - <div 585 - style={{ 586 - display: "flex", 587 - alignItems: "center", 588 - gap: "4px", 589 - fontSize: "0.75rem", 590 - color: "var(--text-tertiary)", 591 - marginRight: "8px", 592 }} 593 - title="Added using Semble" 594 > 595 - <span>via Semble</span> 596 - <img 597 - src="/semble-logo.svg" 598 - alt="Semble" 599 - style={{ width: "16px", height: "16px" }} 600 - /> 601 - </div> 602 - )} 603 - {isOwner && ( 604 - <> 605 - <button 606 - className="annotation-action action-icon-only" 607 - onClick={() => setIsEditing(!isEditing)} 608 - title="Edit Color" 609 - > 610 - <Edit2 size={16} /> 611 - </button> 612 - <button 613 - className="annotation-action action-icon-only" 614 - onClick={(e) => { 615 - e.preventDefault(); 616 - onDelete && onDelete(highlight.id || highlight.uri); 617 - }} 618 - > 619 - <TrashIcon size={16} /> 620 - </button> 621 - </> 622 - )} 623 - </div> 624 </div> 625 </header> 626 ··· 644 borderLeftColor: isEditing ? editColor : data.color || "#f59e0b", 645 }} 646 > 647 - <mark>&quot;{highlightedText}&quot;</mark> 648 </a> 649 )} 650 651 {isEditing && ( 652 - <div 653 - className="mt-3" 654 - style={{ 655 - display: "flex", 656 - gap: "8px", 657 - alignItems: "center", 658 - padding: "8px", 659 - background: "var(--bg-secondary)", 660 - borderRadius: "var(--radius-md)", 661 - border: "1px solid var(--border)", 662 - }} 663 - > 664 - <div 665 - className="color-picker-compact" 666 - style={{ 667 - position: "relative", 668 - width: "28px", 669 - height: "28px", 670 - flexShrink: 0, 671 - }} 672 - > 673 <div 674 - style={{ 675 - backgroundColor: editColor, 676 - width: "100%", 677 - height: "100%", 678 - borderRadius: "50%", 679 - border: "2px solid var(--bg-card)", 680 - boxShadow: "0 0 0 1px var(--border)", 681 - }} 682 /> 683 <input 684 type="color" 685 value={editColor} 686 onChange={(e) => setEditColor(e.target.value)} 687 - style={{ 688 - position: "absolute", 689 - top: 0, 690 - left: 0, 691 - width: "100%", 692 - height: "100%", 693 - opacity: 0, 694 - cursor: "pointer", 695 - }} 696 - title="Change Color" 697 /> 698 </div> 699 - 700 <input 701 type="text" 702 className="reply-input" 703 - placeholder="e.g. tag1, tag2" 704 value={editTags} 705 onChange={(e) => setEditTags(e.target.value)} 706 - style={{ 707 - margin: 0, 708 - flex: 1, 709 - fontSize: "0.9rem", 710 - padding: "6px 10px", 711 - height: "32px", 712 - border: "none", 713 - background: "transparent", 714 - }} 715 /> 716 - 717 <button 718 onClick={handleSaveEdit} 719 - className="btn btn-primary btn-sm" 720 - style={{ padding: "0 10px", height: "32px", minWidth: "auto" }} 721 - title="Save" 722 > 723 <Save size={16} /> 724 </button> ··· 744 <div className="annotation-actions-left"> 745 <span 746 className="annotation-action" 747 - style={{ 748 - color: data.color || "#f59e0b", 749 - background: "none", 750 - paddingLeft: 0, 751 - }} 752 > 753 <HighlightIcon size={14} /> Highlight 754 </span> 755 <ShareMenu 756 uri={data.uri} 757 text={data.title || data.description} 758 handle={data.author?.handle} 759 type="Highlight" 760 /> 761 - <button 762 - className="annotation-action" 763 - onClick={() => { 764 - if (!user) { 765 - login(); 766 - return; 767 - } 768 - if (onAddToCollection) onAddToCollection(); 769 - }} 770 - > 771 <Folder size={16} /> 772 <span>Collect</span> 773 </button>
··· 34 if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) { 35 return baseUrl; 36 } 37 let fragment = ":~:text="; 38 if (selector.prefix) { 39 fragment += encodeURIComponent(selector.prefix) + "-,"; ··· 42 if (selector.suffix) { 43 fragment += ",-" + encodeURIComponent(selector.suffix); 44 } 45 return baseUrl + "#" + fragment; 46 } 47 48 + const truncateUrl = (url, maxLength = 50) => { 49 if (!url) return ""; 50 try { 51 const parsed = new URL(url); ··· 58 } 59 }; 60 61 + function SembleBadge() { 62 + return ( 63 + <div className="semble-badge" title="Added using Semble"> 64 + <span>via Semble</span> 65 + <img src="/semble-logo.svg" alt="Semble" /> 66 + </div> 67 + ); 68 + } 69 + 70 export default function AnnotationCard({ 71 annotation, 72 onDelete, ··· 82 const [editText, setEditText] = useState(data.text || ""); 83 const [editTags, setEditTags] = useState(data.tags?.join(", ") || ""); 84 const [saving, setSaving] = useState(false); 85 const [showHistory, setShowHistory] = useState(false); 86 const [editHistory, setEditHistory] = useState([]); 87 const [loadingHistory, setLoadingHistory] = useState(false); 88 const [replies, setReplies] = useState([]); 89 const [replyCount, setReplyCount] = useState(data.replyCount || 0); 90 const [showReplies, setShowReplies] = useState(false); 91 const [replyingTo, setReplyingTo] = useState(null); 92 const [replyText, setReplyText] = useState(""); 93 const [posting, setPosting] = useState(false); 94 + const [hasEditHistory, setHasEditHistory] = useState(false); 95 96 const isOwner = user?.did && data.author?.did === user.did; 97 + const isSemble = data.uri?.includes("network.cosmik"); 98 + const highlightedText = 99 + data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null; 100 + const fragmentUrl = buildTextFragmentUrl(data.url, data.selector); 101 102 useEffect(() => { 103 if (data.uri && !data.color && !data.description) { 104 getEditHistory(data.uri) 105 .then((history) => { 106 + if (history?.length > 0) setHasEditHistory(true); 107 }) 108 .catch(() => {}); 109 } ··· 128 129 const handlePostReply = async (parentReply) => { 130 if (!replyText.trim()) return; 131 try { 132 setPosting(true); 133 const parentUri = parentReply ··· 180 } 181 }; 182 183 const handleLike = async () => { 184 if (!user) { 185 login(); ··· 196 const cid = annotation.cid || data.cid || ""; 197 if (data.uri && cid) await likeAnnotation(data.uri, cid); 198 } 199 + } catch { 200 setIsLiked(!isLiked); 201 setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1)); 202 } 203 }; 204 ··· 218 } 219 }; 220 221 + const loadReplies = async () => { 222 + if (!showReplies && replies.length === 0) { 223 + try { 224 + const res = await getReplies(data.uri); 225 + if (res.items) setReplies(res.items); 226 + } catch (err) { 227 + console.error("Failed to load replies:", err); 228 + } 229 + } 230 + setShowReplies(!showReplies); 231 + }; 232 + 233 + const handleCollect = () => { 234 + if (!user) { 235 + login(); 236 + return; 237 + } 238 + if (onAddToCollection) onAddToCollection(); 239 + }; 240 + 241 return ( 242 <article className="card annotation-card"> 243 <header className="annotation-header"> ··· 245 <UserMeta author={data.author} createdAt={data.createdAt} /> 246 </div> 247 <div className="annotation-header-right"> 248 + {isSemble && <SembleBadge />} 249 + {hasEditHistory && !data.color && !data.description && ( 250 + <button 251 + className="annotation-action action-icon-only" 252 + onClick={fetchHistory} 253 + title="View Edit History" 254 + > 255 + <Clock size={16} /> 256 + </button> 257 + )} 258 + {isOwner && !isSemble && ( 259 + <> 260 + {!data.color && !data.description && ( 261 + <button 262 + className="annotation-action action-icon-only" 263 + onClick={() => setIsEditing(!isEditing)} 264 + title="Edit" 265 + > 266 + <Edit2 size={16} /> 267 + </button> 268 + )} 269 <button 270 className="annotation-action action-icon-only" 271 + onClick={handleDelete} 272 + disabled={deleting} 273 + title="Delete" 274 > 275 + <Trash2 size={16} /> 276 </button> 277 + </> 278 + )} 279 </div> 280 </header> 281 ··· 284 <div className="history-header"> 285 <h4 className="history-title">Edit History</h4> 286 <button 287 + className="annotation-action action-icon-only" 288 onClick={() => setShowHistory(false)} 289 > 290 <X size={14} /> 291 </button> ··· 318 > 319 {truncateUrl(data.url)} 320 {data.title && ( 321 + <span className="annotation-source-title"> · {data.title}</span> 322 )} 323 </a> 324 ··· 328 target="_blank" 329 rel="noopener noreferrer" 330 className="annotation-highlight" 331 + style={{ borderLeftColor: data.color || "var(--accent)" }} 332 > 333 + <mark>&ldquo;{highlightedText}&rdquo;</mark> 334 </a> 335 )} 336 337 {isEditing ? ( 338 + <div className="edit-form"> 339 <textarea 340 value={editText} 341 onChange={(e) => setEditText(e.target.value)} 342 className="reply-input" 343 rows={3} 344 + placeholder="Your annotation..." 345 /> 346 <input 347 type="text" ··· 349 placeholder="Tags (comma separated)..." 350 value={editTags} 351 onChange={(e) => setEditTags(e.target.value)} 352 + style={{ marginTop: "8px" }} 353 /> 354 + <div className="action-buttons-end" style={{ marginTop: "8px" }}> 355 <button 356 onClick={() => setIsEditing(false)} 357 className="btn btn-ghost" ··· 361 <button 362 onClick={handleSaveEdit} 363 disabled={saving} 364 + className="btn btn-primary" 365 > 366 {saving ? ( 367 "Saving..." ··· 398 className={`annotation-action ${isLiked ? "liked" : ""}`} 399 onClick={handleLike} 400 > 401 + <Heart size={16} fill={isLiked ? "currentColor" : "none"} /> 402 {likeCount > 0 && <span>{likeCount}</span>} 403 </button> 404 + 405 <button 406 className={`annotation-action ${showReplies ? "active" : ""}`} 407 + onClick={loadReplies} 408 > 409 <MessageSquare size={16} /> 410 + <span>{replyCount > 0 ? replyCount : "Reply"}</span> 411 </button> 412 + 413 <ShareMenu 414 uri={data.uri} 415 text={data.title || data.url} ··· 417 type="Annotation" 418 url={data.url} 419 /> 420 + 421 + <button className="annotation-action" onClick={handleCollect}> 422 <Folder size={16} /> 423 <span>Collect</span> 424 </button> ··· 450 451 <div className="reply-form"> 452 {replyingTo && ( 453 + <div className="replying-to-banner"> 454 <span> 455 Replying to @ 456 {(replyingTo.creator || replyingTo.author)?.handle || ··· 458 </span> 459 <button 460 onClick={() => setReplyingTo(null)} 461 + className="cancel-reply" 462 > 463 × 464 </button> ··· 473 } 474 value={replyText} 475 onChange={(e) => setReplyText(e.target.value)} 476 rows={2} 477 /> 478 + <div className="reply-form-actions"> 479 <button 480 + className="btn btn-primary" 481 disabled={posting || !replyText.trim()} 482 onClick={() => { 483 if (!user) { ··· 509 data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null; 510 const fragmentUrl = buildTextFragmentUrl(data.url, data.selector); 511 const isOwner = user?.did && data.author?.did === user.did; 512 + const isSemble = data.uri?.includes("network.cosmik"); 513 + 514 const [isEditing, setIsEditing] = useState(false); 515 const [editColor, setEditColor] = useState(data.color || "#f59e0b"); 516 const [editTags, setEditTags] = useState(data.tags?.join(", ") || ""); ··· 521 .split(",") 522 .map((t) => t.trim()) 523 .filter(Boolean); 524 await updateHighlight(data.uri, editColor, tagList); 525 setIsEditing(false); 526 + if (typeof onUpdate === "function") { 527 onUpdate({ ...highlight, color: editColor, tags: tagList }); 528 + } 529 } catch (err) { 530 alert("Failed to update: " + err.message); 531 } 532 }; 533 534 + const handleCollect = () => { 535 + if (!user) { 536 + login(); 537 + return; 538 + } 539 + if (onAddToCollection) onAddToCollection(); 540 + }; 541 + 542 return ( 543 <article className="card annotation-card"> 544 <header className="annotation-header"> 545 <div className="annotation-header-left"> 546 <UserMeta author={data.author} createdAt={data.createdAt} /> 547 </div> 548 <div className="annotation-header-right"> 549 + {isSemble && ( 550 + <div className="semble-badge" title="Added using Semble"> 551 + <span>via Semble</span> 552 + <img src="/semble-logo.svg" alt="Semble" /> 553 + </div> 554 + )} 555 + {isOwner && ( 556 + <> 557 + <button 558 + className="annotation-action action-icon-only" 559 + onClick={() => setIsEditing(!isEditing)} 560 + title="Edit Color" 561 + > 562 + <Edit2 size={16} /> 563 + </button> 564 + <button 565 + className="annotation-action action-icon-only" 566 + onClick={(e) => { 567 + e.preventDefault(); 568 + onDelete && onDelete(highlight.id || highlight.uri); 569 }} 570 + title="Delete" 571 > 572 + <TrashIcon size={16} /> 573 + </button> 574 + </> 575 + )} 576 </div> 577 </header> 578 ··· 596 borderLeftColor: isEditing ? editColor : data.color || "#f59e0b", 597 }} 598 > 599 + <mark>&ldquo;{highlightedText}&rdquo;</mark> 600 </a> 601 )} 602 603 {isEditing && ( 604 + <div className="color-edit-form"> 605 + <div className="color-picker-wrapper"> 606 <div 607 + className="color-preview" 608 + style={{ backgroundColor: editColor }} 609 /> 610 <input 611 type="color" 612 value={editColor} 613 onChange={(e) => setEditColor(e.target.value)} 614 + className="color-input" 615 /> 616 </div> 617 <input 618 type="text" 619 className="reply-input" 620 + placeholder="Tags (comma separated)" 621 value={editTags} 622 onChange={(e) => setEditTags(e.target.value)} 623 + style={{ flex: 1, margin: 0 }} 624 /> 625 <button 626 onClick={handleSaveEdit} 627 + className="btn btn-primary" 628 + style={{ padding: "0 12px", height: "32px" }} 629 > 630 <Save size={16} /> 631 </button> ··· 651 <div className="annotation-actions-left"> 652 <span 653 className="annotation-action" 654 + style={{ color: data.color || "#f59e0b", cursor: "default" }} 655 > 656 <HighlightIcon size={14} /> Highlight 657 </span> 658 + 659 <ShareMenu 660 uri={data.uri} 661 text={data.title || data.description} 662 handle={data.author?.handle} 663 type="Highlight" 664 /> 665 + 666 + <button className="annotation-action" onClick={handleCollect}> 667 <Folder size={16} /> 668 <span>Collect</span> 669 </button>
+36 -56
web/src/components/BookmarkCard.jsx
··· 8 getLikeCount, 9 deleteBookmark, 10 } from "../api/client"; 11 - import { HeartIcon, TrashIcon, BookmarkIcon } from "./Icons"; 12 - import { Folder } from "lucide-react"; 13 import ShareMenu from "./ShareMenu"; 14 import UserMeta from "./UserMeta"; 15 ··· 28 const [deleting, setDeleting] = useState(false); 29 30 const isOwner = user?.did && data.author?.did === user.did; 31 32 useEffect(() => { 33 let mounted = true; ··· 75 onDelete(data.uri); 76 return; 77 } 78 - 79 if (!confirm("Delete this bookmark?")) return; 80 try { 81 setDeleting(true); ··· 90 } 91 }; 92 93 - let domain = ""; 94 - try { 95 - if (data.url) domain = new URL(data.url).hostname.replace("www.", ""); 96 - } catch { 97 - /* ignore */ 98 - } 99 100 return ( 101 <article className="card annotation-card bookmark-card"> ··· 103 <div className="annotation-header-left"> 104 <UserMeta author={data.author} createdAt={data.createdAt} /> 105 </div> 106 - 107 <div className="annotation-header-right"> 108 - <div style={{ display: "flex", gap: "4px", alignItems: "center" }}> 109 - {data.uri && data.uri.includes("network.cosmik") && ( 110 - <div 111 - style={{ 112 - display: "flex", 113 - alignItems: "center", 114 - gap: "4px", 115 - fontSize: "0.75rem", 116 - color: "var(--text-tertiary)", 117 - marginRight: "8px", 118 - }} 119 - title="Added using Semble" 120 - > 121 - <span>via Semble</span> 122 - <img 123 - src="/semble-logo.svg" 124 - alt="Semble" 125 - style={{ width: "16px", height: "16px" }} 126 - /> 127 - </div> 128 - )} 129 - <div style={{ display: "flex", gap: "4px" }}> 130 - {((isOwner && 131 - !(data.uri && data.uri.includes("network.cosmik"))) || 132 - onDelete) && ( 133 - <button 134 - className="annotation-action action-icon-only" 135 - onClick={handleDelete} 136 - disabled={deleting} 137 - title="Delete" 138 - > 139 - <TrashIcon size={16} /> 140 - </button> 141 - )} 142 </div> 143 - </div> 144 </div> 145 </header> 146 ··· 153 > 154 <div className="bookmark-preview-content"> 155 <div className="bookmark-preview-site"> 156 - <BookmarkIcon size={14} /> 157 <span>{domain}</span> 158 </div> 159 <h3 className="bookmark-preview-title">{data.title || data.url}</h3> ··· 183 <HeartIcon filled={isLiked} size={16} /> 184 {likeCount > 0 && <span>{likeCount}</span>} 185 </button> 186 <ShareMenu 187 uri={data.uri} 188 text={data.title || data.description} ··· 190 type="Bookmark" 191 url={data.url} 192 /> 193 - <button 194 - className="annotation-action" 195 - onClick={() => { 196 - if (!user) { 197 - login(); 198 - return; 199 - } 200 - if (onAddToCollection) onAddToCollection(); 201 - }} 202 - > 203 <Folder size={16} /> 204 <span>Collect</span> 205 </button>
··· 8 getLikeCount, 9 deleteBookmark, 10 } from "../api/client"; 11 + import { HeartIcon, TrashIcon } from "./Icons"; 12 + import { Folder, ExternalLink } from "lucide-react"; 13 import ShareMenu from "./ShareMenu"; 14 import UserMeta from "./UserMeta"; 15 ··· 28 const [deleting, setDeleting] = useState(false); 29 30 const isOwner = user?.did && data.author?.did === user.did; 31 + const isSemble = data.uri?.includes("network.cosmik"); 32 + 33 + let domain = ""; 34 + try { 35 + if (data.url) domain = new URL(data.url).hostname.replace("www.", ""); 36 + } catch { 37 + /* ignore */ 38 + } 39 40 useEffect(() => { 41 let mounted = true; ··· 83 onDelete(data.uri); 84 return; 85 } 86 if (!confirm("Delete this bookmark?")) return; 87 try { 88 setDeleting(true); ··· 97 } 98 }; 99 100 + const handleCollect = () => { 101 + if (!user) { 102 + login(); 103 + return; 104 + } 105 + if (onAddToCollection) onAddToCollection(); 106 + }; 107 108 return ( 109 <article className="card annotation-card bookmark-card"> ··· 111 <div className="annotation-header-left"> 112 <UserMeta author={data.author} createdAt={data.createdAt} /> 113 </div> 114 <div className="annotation-header-right"> 115 + {isSemble && ( 116 + <div className="semble-badge" title="Added using Semble"> 117 + <span>via Semble</span> 118 + <img src="/semble-logo.svg" alt="Semble" /> 119 </div> 120 + )} 121 + {((isOwner && !isSemble) || onDelete) && ( 122 + <button 123 + className="annotation-action action-icon-only" 124 + onClick={handleDelete} 125 + disabled={deleting} 126 + title="Delete" 127 + > 128 + <TrashIcon size={16} /> 129 + </button> 130 + )} 131 </div> 132 </header> 133 ··· 140 > 141 <div className="bookmark-preview-content"> 142 <div className="bookmark-preview-site"> 143 + <ExternalLink size={12} /> 144 <span>{domain}</span> 145 </div> 146 <h3 className="bookmark-preview-title">{data.title || data.url}</h3> ··· 170 <HeartIcon filled={isLiked} size={16} /> 171 {likeCount > 0 && <span>{likeCount}</span>} 172 </button> 173 + 174 <ShareMenu 175 uri={data.uri} 176 text={data.title || data.description} ··· 178 type="Bookmark" 179 url={data.url} 180 /> 181 + 182 + <button className="annotation-action" onClick={handleCollect}> 183 <Folder size={16} /> 184 <span>Collect</span> 185 </button>
+55 -70
web/src/components/CollectionItemCard.jsx
··· 5 import CollectionIcon from "./CollectionIcon"; 6 import ShareMenu from "./ShareMenu"; 7 8 - export default function CollectionItemCard({ item }) { 9 const author = item.creator; 10 const collection = item.collection; 11 12 if (!author || !collection) return null; 13 14 - let inner = null; 15 - if (item.annotation) { 16 - inner = <AnnotationCard annotation={item.annotation} />; 17 - } else if (item.highlight) { 18 - inner = <HighlightCard highlight={item.highlight} />; 19 - } else if (item.bookmark) { 20 - inner = <BookmarkCard bookmark={item.bookmark} />; 21 - } 22 23 - if (!inner) return null; 24 25 return ( 26 - <div className="collection-feed-item" style={{ marginBottom: "20px" }}> 27 - <div 28 - className="feed-context-header" 29 - style={{ 30 - display: "flex", 31 - alignItems: "center", 32 - gap: "8px", 33 - marginBottom: "8px", 34 - fontSize: "14px", 35 - color: "var(--text-secondary)", 36 - }} 37 - > 38 - {author.avatar && ( 39 - <img 40 - src={author.avatar} 41 - alt={author.handle} 42 - style={{ 43 - width: "24px", 44 - height: "24px", 45 - borderRadius: "50%", 46 - objectFit: "cover", 47 - }} 48 - /> 49 - )} 50 - <span> 51 - <span style={{ fontWeight: 600, color: "var(--text-primary)" }}> 52 - {author.displayName || author.handle} 53 - </span>{" "} 54 - added to{" "} 55 - <Link 56 - to={`/${author.handle}/collection/${collection.uri.split("/").pop()}`} 57 - style={{ 58 - display: "inline-flex", 59 - alignItems: "center", 60 - gap: "4px", 61 - fontWeight: 500, 62 - color: "var(--primary)", 63 - textDecoration: "none", 64 - }} 65 - > 66 - <CollectionIcon icon={collection.icon} size={14} /> 67 - {collection.name} 68 - </Link> 69 - </span> 70 - <div style={{ marginLeft: "auto" }}> 71 - <ShareMenu 72 - uri={collection.uri} 73 - handle={author.handle} 74 - type="Collection" 75 - text={`Check out this collection by ${author.displayName}: ${collection.name}`} 76 - /> 77 </div> 78 - </div> 79 - <div 80 - className="feed-context-body" 81 - style={{ 82 - paddingLeft: "16px", 83 - borderLeft: "2px solid var(--border-color)", 84 - }} 85 - > 86 - {inner} 87 </div> 88 </div> 89 ); 90 }
··· 5 import CollectionIcon from "./CollectionIcon"; 6 import ShareMenu from "./ShareMenu"; 7 8 + export default function CollectionItemCard({ item, onAddToCollection }) { 9 const author = item.creator; 10 const collection = item.collection; 11 12 if (!author || !collection) return null; 13 14 + const innerItem = item.annotation || item.highlight || item.bookmark; 15 + if (!innerItem) return null; 16 17 + const innerUri = innerItem.uri || innerItem.id; 18 19 return ( 20 + <div className="collection-feed-item"> 21 + <div className="collection-context-badge"> 22 + <div className="collection-context-inner"> 23 + {author.avatar && ( 24 + <img 25 + src={author.avatar} 26 + alt={author.handle} 27 + className="collection-context-avatar" 28 + /> 29 + )} 30 + <span className="collection-context-text"> 31 + <Link 32 + to={`/profile/${author.did}`} 33 + className="collection-context-author" 34 + > 35 + {author.displayName || author.handle} 36 + </Link>{" "} 37 + added to{" "} 38 + <Link 39 + to={`/${author.handle}/collection/${collection.uri.split("/").pop()}`} 40 + className="collection-context-link" 41 + > 42 + <CollectionIcon icon={collection.icon} size={14} /> 43 + {collection.name} 44 + </Link> 45 + </span> 46 </div> 47 + <ShareMenu 48 + uri={collection.uri} 49 + handle={author.handle} 50 + type="Collection" 51 + text={`Check out this collection: ${collection.name}`} 52 + /> 53 </div> 54 + 55 + {item.annotation && ( 56 + <AnnotationCard 57 + annotation={item.annotation} 58 + onAddToCollection={() => onAddToCollection?.(innerUri)} 59 + /> 60 + )} 61 + {item.highlight && ( 62 + <HighlightCard 63 + highlight={item.highlight} 64 + onAddToCollection={() => onAddToCollection?.(innerUri)} 65 + /> 66 + )} 67 + {item.bookmark && ( 68 + <BookmarkCard 69 + bookmark={item.bookmark} 70 + onAddToCollection={() => onAddToCollection?.(innerUri)} 71 + /> 72 + )} 73 </div> 74 ); 75 }
+47 -1
web/src/components/CollectionModal.jsx
··· 41 Moon, 42 Flame, 43 Leaf, 44 } from "lucide-react"; 45 - import { createCollection, updateCollection } from "../api/client"; 46 47 const EMOJI_OPTIONS = [ 48 "📁", ··· 125 onClose, 126 onSuccess, 127 collectionToEdit, 128 }) { 129 const [name, setName] = useState(""); 130 const [description, setDescription] = useState(""); ··· 132 const [customEmoji, setCustomEmoji] = useState(""); 133 const [activeTab, setActiveTab] = useState("emoji"); 134 const [loading, setLoading] = useState(false); 135 const [error, setError] = useState(null); 136 137 useEffect(() => { ··· 211 } 212 }; 213 214 return ( 215 <div className="modal-overlay" onClick={onClose}> 216 <div ··· 327 </div> 328 329 <div className="modal-actions"> 330 <button type="button" onClick={onClose} className="btn btn-ghost"> 331 Cancel 332 </button>
··· 41 Moon, 42 Flame, 43 Leaf, 44 + Trash2, 45 } from "lucide-react"; 46 + import { 47 + createCollection, 48 + updateCollection, 49 + deleteCollection, 50 + } from "../api/client"; 51 52 const EMOJI_OPTIONS = [ 53 "📁", ··· 130 onClose, 131 onSuccess, 132 collectionToEdit, 133 + onDelete, 134 }) { 135 const [name, setName] = useState(""); 136 const [description, setDescription] = useState(""); ··· 138 const [customEmoji, setCustomEmoji] = useState(""); 139 const [activeTab, setActiveTab] = useState("emoji"); 140 const [loading, setLoading] = useState(false); 141 + const [deleting, setDeleting] = useState(false); 142 const [error, setError] = useState(null); 143 144 useEffect(() => { ··· 218 } 219 }; 220 221 + const handleDelete = async () => { 222 + if ( 223 + !confirm( 224 + "Delete this collection and all its items? This cannot be undone.", 225 + ) 226 + ) { 227 + return; 228 + } 229 + setDeleting(true); 230 + setError(null); 231 + 232 + try { 233 + await deleteCollection(collectionToEdit.uri); 234 + if (onDelete) { 235 + onDelete(); 236 + } else { 237 + onSuccess(); 238 + } 239 + onClose(); 240 + } catch (err) { 241 + console.error(err); 242 + setError(err.message || "Failed to delete collection"); 243 + } finally { 244 + setDeleting(false); 245 + } 246 + }; 247 + 248 return ( 249 <div className="modal-overlay" onClick={onClose}> 250 <div ··· 361 </div> 362 363 <div className="modal-actions"> 364 + {collectionToEdit && ( 365 + <button 366 + type="button" 367 + onClick={handleDelete} 368 + disabled={deleting} 369 + className="btn btn-danger" 370 + > 371 + <Trash2 size={16} /> 372 + {deleting ? "Deleting..." : "Delete"} 373 + </button> 374 + )} 375 + <div style={{ flex: 1 }} /> 376 <button type="button" onClick={onClose} className="btn btn-ghost"> 377 Cancel 378 </button>
+52
web/src/components/IOSInstallBanner.jsx
···
··· 1 + import { useState } from "react"; 2 + import { X } from "lucide-react"; 3 + import { SiApple } from "react-icons/si"; 4 + 5 + function shouldShowBanner() { 6 + if (typeof window === "undefined") return false; 7 + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); 8 + if (!isIOS) return false; 9 + 10 + const dismissedAt = localStorage.getItem("ios-shortcut-dismissed"); 11 + const daysSinceDismissed = dismissedAt 12 + ? (Date.now() - parseInt(dismissedAt, 10)) / (1000 * 60 * 60 * 24) 13 + : Infinity; 14 + return daysSinceDismissed > 7; 15 + } 16 + 17 + export default function IOSInstallBanner() { 18 + const [show, setShow] = useState(shouldShowBanner); 19 + 20 + const handleDismiss = () => { 21 + setShow(false); 22 + localStorage.setItem("ios-shortcut-dismissed", Date.now().toString()); 23 + }; 24 + 25 + if (!show) return null; 26 + 27 + return ( 28 + <div className="ios-shortcut-banner"> 29 + <button 30 + className="ios-shortcut-banner-close" 31 + onClick={handleDismiss} 32 + aria-label="Dismiss" 33 + > 34 + <X size={14} /> 35 + </button> 36 + <div className="ios-shortcut-banner-content"> 37 + <div className="ios-shortcut-banner-text"> 38 + <p>Save pages directly from Safari</p> 39 + </div> 40 + <a 41 + href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd" 42 + target="_blank" 43 + rel="noopener noreferrer" 44 + className="ios-shortcut-banner-btn" 45 + > 46 + <SiApple size={14} /> 47 + Get iOS Shortcut 48 + </a> 49 + </div> 50 + </div> 51 + ); 52 + }
+68 -39
web/src/components/MobileNav.jsx
··· 1 import { Link, useLocation } from "react-router-dom"; 2 import { useAuth } from "../context/AuthContext"; 3 - import { Home, Search, Folder, User, PenSquare } from "lucide-react"; 4 5 export default function MobileNav() { 6 const { user, isAuthenticated } = useAuth(); ··· 12 }; 13 14 return ( 15 - <nav className="mobile-nav"> 16 - <div className="mobile-nav-inner"> 17 - <Link 18 - to="/" 19 - className={`mobile-nav-item ${isActive("/") ? "active" : ""}`} 20 - > 21 - <Home /> 22 - <span>Home</span> 23 - </Link> 24 25 - <Link 26 - to="/url" 27 - className={`mobile-nav-item ${isActive("/url") ? "active" : ""}`} 28 - > 29 - <Search /> 30 - <span>Browse</span> 31 - </Link> 32 33 - {isAuthenticated ? ( 34 - <Link to="/new" className="mobile-nav-item mobile-nav-new"> 35 - <PenSquare /> 36 </Link> 37 - ) : ( 38 - <Link to="/login" className="mobile-nav-item mobile-nav-new"> 39 - <User /> 40 </Link> 41 - )} 42 43 - <Link 44 - to="/collections" 45 - className={`mobile-nav-item ${isActive("/collections") ? "active" : ""}`} 46 - > 47 - <Folder /> 48 - <span>Library</span> 49 - </Link> 50 51 - <Link 52 - to={isAuthenticated && user?.did ? `/profile/${user.did}` : "/login"} 53 - className={`mobile-nav-item ${isActive("/profile") ? "active" : ""}`} 54 - > 55 - <User /> 56 - <span>Profile</span> 57 - </Link> 58 - </div> 59 </nav> 60 ); 61 }
··· 1 import { Link, useLocation } from "react-router-dom"; 2 import { useAuth } from "../context/AuthContext"; 3 + import { Home, Search, Folder, User, PenSquare, Bookmark } from "lucide-react"; 4 5 export default function MobileNav() { 6 const { user, isAuthenticated } = useAuth(); ··· 12 }; 13 14 return ( 15 + <nav className="mobile-bottom-nav"> 16 + <Link 17 + to="/" 18 + className={`mobile-bottom-nav-item ${isActive("/") ? "active" : ""}`} 19 + > 20 + <Home size={22} /> 21 + <span>Home</span> 22 + </Link> 23 24 + <Link 25 + to="/url" 26 + className={`mobile-bottom-nav-item ${isActive("/url") ? "active" : ""}`} 27 + > 28 + <Search size={22} /> 29 + <span>Browse</span> 30 + </Link> 31 32 + {isAuthenticated ? ( 33 + <> 34 + <Link 35 + to="/new" 36 + className="mobile-bottom-nav-item mobile-bottom-nav-new" 37 + > 38 + <div className="mobile-nav-new-btn"> 39 + <PenSquare size={20} /> 40 + </div> 41 + </Link> 42 + 43 + <Link 44 + to="/bookmarks" 45 + className={`mobile-bottom-nav-item ${isActive("/bookmarks") || isActive("/collections") ? "active" : ""}`} 46 + > 47 + <Bookmark size={22} /> 48 + <span>Library</span> 49 + </Link> 50 + 51 + <Link 52 + to={user?.did ? `/profile/${user.did}` : "/profile"} 53 + className={`mobile-bottom-nav-item ${isActive("/profile") ? "active" : ""}`} 54 + > 55 + {user?.avatar ? ( 56 + <img src={user.avatar} alt="" className="mobile-nav-avatar" /> 57 + ) : ( 58 + <User size={22} /> 59 + )} 60 + <span>You</span> 61 </Link> 62 + </> 63 + ) : ( 64 + <> 65 + <Link 66 + to="/login" 67 + className="mobile-bottom-nav-item mobile-bottom-nav-new" 68 + > 69 + <div className="mobile-nav-new-btn"> 70 + <User size={20} /> 71 + </div> 72 </Link> 73 74 + <Link 75 + to="/collections" 76 + className={`mobile-bottom-nav-item ${isActive("/collections") ? "active" : ""}`} 77 + > 78 + <Folder size={22} /> 79 + <span>Library</span> 80 + </Link> 81 82 + <Link to="/login" className={`mobile-bottom-nav-item`}> 83 + <User size={22} /> 84 + <span>Sign In</span> 85 + </Link> 86 + </> 87 + )} 88 </nav> 89 ); 90 }
-226
web/src/components/RightSidebar.jsx
··· 1 - import { useState, useEffect } from "react"; 2 - import { Link } from "react-router-dom"; 3 - import { ExternalLink, Sun, Moon, Monitor } from "lucide-react"; 4 - import { 5 - SiFirefox, 6 - SiGooglechrome, 7 - SiGithub, 8 - SiBluesky, 9 - SiApple, 10 - SiKofi, 11 - SiDiscord, 12 - } from "react-icons/si"; 13 - import { FaEdge } from "react-icons/fa"; 14 - import { useAuth } from "../context/AuthContext"; 15 - import { useTheme } from "../context/ThemeContext"; 16 - import { getTrendingTags } from "../api/client"; 17 - 18 - const isFirefox = 19 - typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 20 - const isEdge = 21 - typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 22 - const isMobileSafari = 23 - typeof navigator !== "undefined" && 24 - /iPhone|iPad|iPod/.test(navigator.userAgent) && 25 - /Safari/.test(navigator.userAgent) && 26 - !/CriOS|FxiOS|OPiOS|EdgiOS/.test(navigator.userAgent); 27 - 28 - function getExtensionInfo() { 29 - if (isMobileSafari) { 30 - return { 31 - url: "https://margin.at/soon", 32 - icon: SiApple, 33 - name: "iOS", 34 - label: "Coming Soon", 35 - }; 36 - } 37 - if (isFirefox) { 38 - return { 39 - url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", 40 - icon: SiFirefox, 41 - name: "Firefox", 42 - label: "Install for Firefox", 43 - }; 44 - } 45 - if (isEdge) { 46 - return { 47 - url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", 48 - icon: FaEdge, 49 - name: "Edge", 50 - label: "Install for Edge", 51 - }; 52 - } 53 - return { 54 - url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", 55 - icon: SiGooglechrome, 56 - name: "Chrome", 57 - label: "Install for Chrome", 58 - }; 59 - } 60 - 61 - export default function RightSidebar() { 62 - const { theme, setTheme } = useTheme(); 63 - const { isAuthenticated } = useAuth(); 64 - const ext = getExtensionInfo(); 65 - const ExtIcon = ext.icon; 66 - const [trendingTags, setTrendingTags] = useState([]); 67 - const [loading, setLoading] = useState(true); 68 - 69 - useEffect(() => { 70 - getTrendingTags() 71 - .then((tags) => setTrendingTags(tags)) 72 - .catch((err) => console.error("Failed to fetch trending tags:", err)) 73 - .finally(() => setLoading(false)); 74 - }, []); 75 - 76 - return ( 77 - <aside className="right-sidebar"> 78 - <div className="right-section"> 79 - <h3 className="right-section-title"> 80 - {isMobileSafari ? "Save from Safari" : "Get the Extension"} 81 - </h3> 82 - <p className="right-section-desc"> 83 - {isMobileSafari 84 - ? "Bookmark pages using Safari's share sheet" 85 - : "Annotate, highlight, and bookmark any webpage"} 86 - </p> 87 - <a 88 - href={ext.url} 89 - target="_blank" 90 - rel="noopener noreferrer" 91 - className="right-extension-btn" 92 - > 93 - <ExtIcon size={18} /> 94 - {ext.label} 95 - <ExternalLink size={14} /> 96 - </a> 97 - </div> 98 - 99 - {isAuthenticated ? ( 100 - <div className="right-section"> 101 - <h3 className="right-section-title">Trending Tags</h3> 102 - <div className="right-links"> 103 - {loading ? ( 104 - <span className="right-section-desc">Loading...</span> 105 - ) : trendingTags.length > 0 ? ( 106 - trendingTags.map(({ tag, count }) => ( 107 - <Link 108 - key={tag} 109 - to={`/?tag=${encodeURIComponent(tag)}`} 110 - className="right-link" 111 - > 112 - <span>#{tag}</span> 113 - <span style={{ fontSize: "0.75rem", opacity: 0.6 }}> 114 - {count} 115 - </span> 116 - </Link> 117 - )) 118 - ) : ( 119 - <span className="right-section-desc">No trending tags yet</span> 120 - )} 121 - </div> 122 - </div> 123 - ) : ( 124 - <div className="right-section"> 125 - <h3 className="right-section-title">Explore</h3> 126 - <nav className="right-links"> 127 - <Link to="/url" className="right-link"> 128 - Browse by URL 129 - </Link> 130 - </nav> 131 - </div> 132 - )} 133 - 134 - <div className="right-section"> 135 - <h3 className="right-section-title">Resources</h3> 136 - <nav className="right-links"> 137 - <a 138 - href="https://github.com/margin-at/margin" 139 - target="_blank" 140 - rel="noopener noreferrer" 141 - className="right-link" 142 - > 143 - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 144 - <SiGithub size={16} /> 145 - GitHub 146 - </div> 147 - <ExternalLink size={12} /> 148 - </a> 149 - <a 150 - href="https://tangled.org/margin.at/margin" 151 - target="_blank" 152 - rel="noopener noreferrer" 153 - className="right-link" 154 - > 155 - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 156 - <div className="tangled-icon" /> 157 - Tangled 158 - </div> 159 - <ExternalLink size={12} /> 160 - </a> 161 - <a 162 - href="https://bsky.app/profile/margin.at" 163 - target="_blank" 164 - rel="noopener noreferrer" 165 - className="right-link" 166 - > 167 - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 168 - <SiBluesky size={16} /> 169 - Bluesky 170 - </div> 171 - <ExternalLink size={12} /> 172 - </a> 173 - <a 174 - href="https://discord.gg/ZQbkGqwzBH" 175 - target="_blank" 176 - rel="noopener noreferrer" 177 - className="right-link" 178 - > 179 - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 180 - <SiDiscord size={16} /> 181 - Discord 182 - </div> 183 - <ExternalLink size={12} /> 184 - </a> 185 - <a 186 - href="https://ko-fi.com/scan" 187 - target="_blank" 188 - rel="noopener noreferrer" 189 - className="right-link" 190 - > 191 - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 192 - <SiKofi size={16} /> 193 - Donate 194 - </div> 195 - <ExternalLink size={12} /> 196 - </a> 197 - </nav> 198 - </div> 199 - 200 - <div className="right-footer"> 201 - <div className="footer-links"> 202 - <Link to="/privacy">Privacy</Link> 203 - <span>·</span> 204 - <Link to="/terms">Terms</Link> 205 - </div> 206 - <button 207 - onClick={() => { 208 - const next = 209 - theme === "system" 210 - ? "light" 211 - : theme === "light" 212 - ? "dark" 213 - : "system"; 214 - setTheme(next); 215 - }} 216 - className="theme-toggle-mini" 217 - title={`Theme: ${theme}`} 218 - > 219 - {theme === "system" && <Monitor size={14} />} 220 - {theme === "light" && <Sun size={14} />} 221 - {theme === "dark" && <Moon size={14} />} 222 - </button> 223 - </div> 224 - </aside> 225 - ); 226 - }
···
-189
web/src/components/Sidebar.jsx
··· 1 - import { useState, useRef, useEffect } from "react"; 2 - import { Link, useLocation } from "react-router-dom"; 3 - import { useAuth } from "../context/AuthContext"; 4 - import { 5 - Home, 6 - Search, 7 - Folder, 8 - Bell, 9 - PenSquare, 10 - User, 11 - LogOut, 12 - MoreHorizontal, 13 - Highlighter, 14 - Bookmark, 15 - } from "lucide-react"; 16 - import { getUnreadNotificationCount } from "../api/client"; 17 - import logo from "../assets/logo.svg"; 18 - 19 - export default function Sidebar() { 20 - const { user, isAuthenticated, logout, loading } = useAuth(); 21 - const location = useLocation(); 22 - const [menuOpen, setMenuOpen] = useState(false); 23 - const [unreadCount, setUnreadCount] = useState(0); 24 - const menuRef = useRef(null); 25 - 26 - const isActive = (path) => { 27 - if (path === "/") return location.pathname === "/"; 28 - return location.pathname.startsWith(path); 29 - }; 30 - 31 - useEffect(() => { 32 - if (isAuthenticated) { 33 - getUnreadNotificationCount() 34 - .then((data) => setUnreadCount(data.count || 0)) 35 - .catch(() => {}); 36 - const interval = setInterval(() => { 37 - getUnreadNotificationCount() 38 - .then((data) => setUnreadCount(data.count || 0)) 39 - .catch(() => {}); 40 - }, 60000); 41 - return () => clearInterval(interval); 42 - } 43 - }, [isAuthenticated]); 44 - 45 - useEffect(() => { 46 - const handleClickOutside = (e) => { 47 - if (menuRef.current && !menuRef.current.contains(e.target)) { 48 - setMenuOpen(false); 49 - } 50 - }; 51 - document.addEventListener("mousedown", handleClickOutside); 52 - return () => document.removeEventListener("mousedown", handleClickOutside); 53 - }, []); 54 - 55 - const getInitials = () => { 56 - if (user?.displayName) { 57 - return user.displayName.substring(0, 2).toUpperCase(); 58 - } 59 - if (user?.handle) { 60 - return user.handle.substring(0, 2).toUpperCase(); 61 - } 62 - return "U"; 63 - }; 64 - 65 - return ( 66 - <aside className="sidebar"> 67 - <Link to="/" className="sidebar-header"> 68 - <img src={logo} alt="Margin" className="sidebar-logo" /> 69 - <span className="sidebar-brand">Margin</span> 70 - </Link> 71 - 72 - <nav className="sidebar-nav"> 73 - <Link 74 - to="/" 75 - className={`sidebar-link ${isActive("/") ? "active" : ""}`} 76 - > 77 - <Home size={20} /> 78 - <span>Home</span> 79 - </Link> 80 - <Link 81 - to="/url" 82 - className={`sidebar-link ${isActive("/url") ? "active" : ""}`} 83 - > 84 - <Search size={20} /> 85 - <span>Browse</span> 86 - </Link> 87 - 88 - {isAuthenticated && ( 89 - <> 90 - <div className="sidebar-section-title">Library</div> 91 - <Link 92 - to="/highlights" 93 - className={`sidebar-link ${isActive("/highlights") ? "active" : ""}`} 94 - > 95 - <Highlighter size={20} /> 96 - <span>Highlights</span> 97 - </Link> 98 - <Link 99 - to="/bookmarks" 100 - className={`sidebar-link ${isActive("/bookmarks") ? "active" : ""}`} 101 - > 102 - <Bookmark size={20} /> 103 - <span>Bookmarks</span> 104 - </Link> 105 - <Link 106 - to="/collections" 107 - className={`sidebar-link ${isActive("/collections") ? "active" : ""}`} 108 - > 109 - <Folder size={20} /> 110 - <span>Collections</span> 111 - </Link> 112 - <Link 113 - to="/notifications" 114 - className={`sidebar-link ${isActive("/notifications") ? "active" : ""}`} 115 - onClick={() => setUnreadCount(0)} 116 - > 117 - <Bell size={20} /> 118 - <span>Notifications</span> 119 - {unreadCount > 0 && ( 120 - <span className="notification-badge">{unreadCount}</span> 121 - )} 122 - </Link> 123 - </> 124 - )} 125 - </nav> 126 - 127 - {isAuthenticated && ( 128 - <Link to="/new" className="sidebar-new-btn"> 129 - <PenSquare size={18} /> 130 - <span>New</span> 131 - </Link> 132 - )} 133 - 134 - <div className="sidebar-footer" ref={menuRef}> 135 - {!loading && 136 - (isAuthenticated ? ( 137 - <> 138 - <div 139 - className="sidebar-user" 140 - onClick={() => setMenuOpen(!menuOpen)} 141 - > 142 - <div className="sidebar-avatar"> 143 - {user?.avatar ? ( 144 - <img src={user.avatar} alt={user.displayName} /> 145 - ) : ( 146 - <span>{getInitials()}</span> 147 - )} 148 - </div> 149 - <div className="sidebar-user-info"> 150 - <div className="sidebar-user-name"> 151 - {user?.displayName || user?.handle} 152 - </div> 153 - <div className="sidebar-user-handle">@{user?.handle}</div> 154 - </div> 155 - <MoreHorizontal size={18} className="sidebar-user-menu" /> 156 - </div> 157 - 158 - {menuOpen && ( 159 - <div className="sidebar-dropdown"> 160 - <Link 161 - to={`/profile/${user?.did}`} 162 - className="sidebar-dropdown-item" 163 - onClick={() => setMenuOpen(false)} 164 - > 165 - <User size={16} /> 166 - View Profile 167 - </Link> 168 - <button 169 - onClick={() => { 170 - logout(); 171 - setMenuOpen(false); 172 - }} 173 - className="sidebar-dropdown-item danger" 174 - > 175 - <LogOut size={16} /> 176 - Sign Out 177 - </button> 178 - </div> 179 - )} 180 - </> 181 - ) : ( 182 - <Link to="/login" className="sidebar-new-btn" style={{ margin: 0 }}> 183 - Sign In 184 - </Link> 185 - ))} 186 - </div> 187 - </aside> 188 - ); 189 - }
···
+408
web/src/components/TopNav.jsx
···
··· 1 + import { useState, useRef, useEffect } from "react"; 2 + import { Link, useLocation } from "react-router-dom"; 3 + import { useAuth } from "../context/AuthContext"; 4 + import { useTheme } from "../context/ThemeContext"; 5 + import { 6 + Home, 7 + Search, 8 + Folder, 9 + Bell, 10 + PenSquare, 11 + User, 12 + LogOut, 13 + ChevronDown, 14 + Highlighter, 15 + Bookmark, 16 + Sun, 17 + Moon, 18 + Monitor, 19 + ExternalLink, 20 + Menu, 21 + X, 22 + } from "lucide-react"; 23 + import { 24 + SiFirefox, 25 + SiGooglechrome, 26 + SiGithub, 27 + SiBluesky, 28 + SiDiscord, 29 + } from "react-icons/si"; 30 + import { FaEdge } from "react-icons/fa"; 31 + import tangledLogo from "../assets/tangled.svg"; 32 + import { getUnreadNotificationCount } from "../api/client"; 33 + import logo from "../assets/logo.svg"; 34 + 35 + const isFirefox = 36 + typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 37 + const isEdge = 38 + typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 39 + 40 + function getExtensionInfo() { 41 + if (isFirefox) { 42 + return { 43 + url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", 44 + icon: SiFirefox, 45 + label: "Firefox", 46 + }; 47 + } 48 + if (isEdge) { 49 + return { 50 + url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", 51 + icon: FaEdge, 52 + label: "Edge", 53 + }; 54 + } 55 + return { 56 + url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", 57 + icon: SiGooglechrome, 58 + label: "Chrome", 59 + }; 60 + } 61 + 62 + export default function TopNav() { 63 + const { user, isAuthenticated, logout, loading } = useAuth(); 64 + const { theme, setTheme } = useTheme(); 65 + const location = useLocation(); 66 + const [userMenuOpen, setUserMenuOpen] = useState(false); 67 + const [moreMenuOpen, setMoreMenuOpen] = useState(false); 68 + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 69 + const [unreadCount, setUnreadCount] = useState(0); 70 + const userMenuRef = useRef(null); 71 + const moreMenuRef = useRef(null); 72 + 73 + const isActive = (path) => { 74 + if (path === "/") return location.pathname === "/"; 75 + return location.pathname.startsWith(path); 76 + }; 77 + 78 + const ext = getExtensionInfo(); 79 + const ExtIcon = ext.icon; 80 + 81 + useEffect(() => { 82 + if (isAuthenticated) { 83 + getUnreadNotificationCount() 84 + .then((data) => setUnreadCount(data.count || 0)) 85 + .catch(() => {}); 86 + const interval = setInterval(() => { 87 + getUnreadNotificationCount() 88 + .then((data) => setUnreadCount(data.count || 0)) 89 + .catch(() => {}); 90 + }, 60000); 91 + return () => clearInterval(interval); 92 + } 93 + }, [isAuthenticated]); 94 + 95 + useEffect(() => { 96 + const handleClickOutside = (e) => { 97 + if (userMenuRef.current && !userMenuRef.current.contains(e.target)) { 98 + setUserMenuOpen(false); 99 + } 100 + if (moreMenuRef.current && !moreMenuRef.current.contains(e.target)) { 101 + setMoreMenuOpen(false); 102 + } 103 + }; 104 + document.addEventListener("mousedown", handleClickOutside); 105 + return () => document.removeEventListener("mousedown", handleClickOutside); 106 + }, []); 107 + 108 + const closeMobileMenu = () => setMobileMenuOpen(false); 109 + 110 + const getInitials = () => { 111 + if (user?.displayName) 112 + return user.displayName.substring(0, 2).toUpperCase(); 113 + if (user?.handle) return user.handle.substring(0, 2).toUpperCase(); 114 + return "U"; 115 + }; 116 + 117 + const cycleTheme = () => { 118 + const next = 119 + theme === "system" ? "light" : theme === "light" ? "dark" : "system"; 120 + setTheme(next); 121 + }; 122 + 123 + return ( 124 + <header className="top-nav"> 125 + <div className="top-nav-inner"> 126 + <Link to="/" className="top-nav-logo"> 127 + <img src={logo} alt="Margin" /> 128 + <span>Margin</span> 129 + </Link> 130 + 131 + <nav className="top-nav-links"> 132 + <Link 133 + to="/" 134 + className={`top-nav-link ${isActive("/") ? "active" : ""}`} 135 + > 136 + Home 137 + </Link> 138 + <Link 139 + to="/url" 140 + className={`top-nav-link ${isActive("/url") ? "active" : ""}`} 141 + > 142 + Browse 143 + </Link> 144 + {isAuthenticated && ( 145 + <> 146 + <Link 147 + to="/highlights" 148 + className={`top-nav-link ${isActive("/highlights") ? "active" : ""}`} 149 + > 150 + Highlights 151 + </Link> 152 + <Link 153 + to="/bookmarks" 154 + className={`top-nav-link ${isActive("/bookmarks") ? "active" : ""}`} 155 + > 156 + Bookmarks 157 + </Link> 158 + <Link 159 + to="/collections" 160 + className={`top-nav-link ${isActive("/collections") ? "active" : ""}`} 161 + > 162 + Collections 163 + </Link> 164 + </> 165 + )} 166 + </nav> 167 + 168 + <div className="top-nav-actions"> 169 + <a 170 + href={ext.url} 171 + target="_blank" 172 + rel="noopener noreferrer" 173 + className="top-nav-link extension-link" 174 + title={`Get ${ext.label} Extension`} 175 + > 176 + <ExtIcon size={16} /> 177 + <span>Get Extension</span> 178 + </a> 179 + 180 + <div className="top-nav-dropdown" ref={moreMenuRef}> 181 + <button 182 + className="top-nav-icon-btn" 183 + onClick={() => setMoreMenuOpen(!moreMenuOpen)} 184 + title="More" 185 + > 186 + <ChevronDown size={18} /> 187 + </button> 188 + {moreMenuOpen && ( 189 + <div className="dropdown-menu dropdown-right"> 190 + <a 191 + href="https://github.com/margin-at/margin" 192 + target="_blank" 193 + rel="noopener noreferrer" 194 + className="dropdown-item" 195 + > 196 + <SiGithub size={16} /> 197 + GitHub 198 + <ExternalLink size={12} className="dropdown-external" /> 199 + </a> 200 + <a 201 + href="https://tangled.sh/@margin.at/margin" 202 + target="_blank" 203 + rel="noopener noreferrer" 204 + className="dropdown-item" 205 + > 206 + <span className="tangled-icon-wrapper"> 207 + <img src={tangledLogo} alt="" /> 208 + </span> 209 + Tangled 210 + <ExternalLink size={12} className="dropdown-external" /> 211 + </a> 212 + <a 213 + href="https://bsky.app/profile/margin.at" 214 + target="_blank" 215 + rel="noopener noreferrer" 216 + className="dropdown-item" 217 + > 218 + <SiBluesky size={16} /> 219 + Bluesky 220 + <ExternalLink size={12} className="dropdown-external" /> 221 + </a> 222 + <a 223 + href="https://discord.gg/ZQbkGqwzBH" 224 + target="_blank" 225 + rel="noopener noreferrer" 226 + className="dropdown-item" 227 + > 228 + <SiDiscord size={16} /> 229 + Discord 230 + <ExternalLink size={12} className="dropdown-external" /> 231 + </a> 232 + <div className="dropdown-divider" /> 233 + <button className="dropdown-item" onClick={cycleTheme}> 234 + {theme === "system" && <Monitor size={16} />} 235 + {theme === "dark" && <Moon size={16} />} 236 + {theme === "light" && <Sun size={16} />} 237 + Theme: {theme} 238 + </button> 239 + <div className="dropdown-divider" /> 240 + <Link 241 + to="/privacy" 242 + className="dropdown-item" 243 + onClick={() => setMoreMenuOpen(false)} 244 + > 245 + Privacy 246 + </Link> 247 + <Link 248 + to="/terms" 249 + className="dropdown-item" 250 + onClick={() => setMoreMenuOpen(false)} 251 + > 252 + Terms 253 + </Link> 254 + </div> 255 + )} 256 + </div> 257 + 258 + {isAuthenticated && ( 259 + <> 260 + <Link 261 + to="/notifications" 262 + className="top-nav-icon-btn" 263 + onClick={() => setUnreadCount(0)} 264 + title="Notifications" 265 + > 266 + <Bell size={18} /> 267 + {unreadCount > 0 && <span className="notif-dot" />} 268 + </Link> 269 + 270 + <Link to="/new" className="top-nav-new-btn"> 271 + <PenSquare size={16} /> 272 + <span>New</span> 273 + </Link> 274 + </> 275 + )} 276 + 277 + {!loading && 278 + (isAuthenticated ? ( 279 + <div className="top-nav-dropdown" ref={userMenuRef}> 280 + <button 281 + className="top-nav-avatar" 282 + onClick={() => setUserMenuOpen(!userMenuOpen)} 283 + > 284 + {user?.avatar ? ( 285 + <img src={user.avatar} alt={user.displayName} /> 286 + ) : ( 287 + <span>{getInitials()}</span> 288 + )} 289 + </button> 290 + {userMenuOpen && ( 291 + <div className="dropdown-menu dropdown-right"> 292 + <div className="dropdown-user-info"> 293 + <span className="dropdown-user-name"> 294 + {user?.displayName || user?.handle} 295 + </span> 296 + <span className="dropdown-user-handle"> 297 + @{user?.handle} 298 + </span> 299 + </div> 300 + <div className="dropdown-divider" /> 301 + <Link 302 + to={`/profile/${user?.did}`} 303 + className="dropdown-item" 304 + onClick={() => setUserMenuOpen(false)} 305 + > 306 + <User size={16} /> 307 + View Profile 308 + </Link> 309 + <button 310 + onClick={() => { 311 + logout(); 312 + setUserMenuOpen(false); 313 + }} 314 + className="dropdown-item danger" 315 + > 316 + <LogOut size={16} /> 317 + Sign Out 318 + </button> 319 + </div> 320 + )} 321 + </div> 322 + ) : ( 323 + <Link to="/login" className="top-nav-new-btn"> 324 + Sign In 325 + </Link> 326 + ))} 327 + 328 + <button 329 + className="top-nav-mobile-toggle" 330 + onClick={() => setMobileMenuOpen(!mobileMenuOpen)} 331 + > 332 + {mobileMenuOpen ? <X size={22} /> : <Menu size={22} />} 333 + </button> 334 + </div> 335 + </div> 336 + 337 + {mobileMenuOpen && ( 338 + <div className="mobile-menu"> 339 + <Link 340 + to="/" 341 + className={`mobile-menu-link ${isActive("/") ? "active" : ""}`} 342 + onClick={closeMobileMenu} 343 + > 344 + <Home size={20} /> Home 345 + </Link> 346 + <Link 347 + to="/url" 348 + className={`mobile-menu-link ${isActive("/url") ? "active" : ""}`} 349 + onClick={closeMobileMenu} 350 + > 351 + <Search size={20} /> Browse 352 + </Link> 353 + {isAuthenticated && ( 354 + <> 355 + <Link 356 + to="/highlights" 357 + className={`mobile-menu-link ${isActive("/highlights") ? "active" : ""}`} 358 + onClick={closeMobileMenu} 359 + > 360 + <Highlighter size={20} /> Highlights 361 + </Link> 362 + <Link 363 + to="/bookmarks" 364 + className={`mobile-menu-link ${isActive("/bookmarks") ? "active" : ""}`} 365 + onClick={closeMobileMenu} 366 + > 367 + <Bookmark size={20} /> Bookmarks 368 + </Link> 369 + <Link 370 + to="/collections" 371 + className={`mobile-menu-link ${isActive("/collections") ? "active" : ""}`} 372 + onClick={closeMobileMenu} 373 + > 374 + <Folder size={20} /> Collections 375 + </Link> 376 + <Link 377 + to="/notifications" 378 + className={`mobile-menu-link ${isActive("/notifications") ? "active" : ""}`} 379 + onClick={closeMobileMenu} 380 + > 381 + <Bell size={20} /> Notifications 382 + {unreadCount > 0 && ( 383 + <span className="notification-badge">{unreadCount}</span> 384 + )} 385 + </Link> 386 + <Link 387 + to="/new" 388 + className={`mobile-menu-link ${isActive("/new") ? "active" : ""}`} 389 + onClick={closeMobileMenu} 390 + > 391 + <PenSquare size={20} /> New 392 + </Link> 393 + </> 394 + )} 395 + <div className="mobile-menu-divider" /> 396 + <a 397 + href={ext.url} 398 + target="_blank" 399 + rel="noopener noreferrer" 400 + className="mobile-menu-link" 401 + > 402 + <ExtIcon size={20} /> Get Extension 403 + </a> 404 + </div> 405 + )} 406 + </header> 407 + ); 408 + }
+126 -96
web/src/css/annotations.css
··· 1 .annotation-detail-page { 2 - max-width: 680px; 3 margin: 0 auto; 4 - padding: 24px 16px; 5 min-height: 100vh; 6 } 7 8 .annotation-detail-header { 9 - margin-bottom: 24px; 10 } 11 12 .back-link { ··· 14 align-items: center; 15 color: var(--text-tertiary); 16 text-decoration: none; 17 - font-size: 0.9rem; 18 font-weight: 500; 19 transition: color 0.15s; 20 } ··· 24 } 25 26 .replies-section { 27 - margin-top: 32px; 28 border-top: 1px solid var(--border); 29 - padding-top: 24px; 30 } 31 32 .replies-title { 33 display: flex; 34 align-items: center; 35 - gap: 8px; 36 - font-size: 1.1rem; 37 font-weight: 600; 38 color: var(--text-primary); 39 - margin-bottom: 20px; 40 } 41 42 .annotation-card { 43 display: flex; 44 flex-direction: column; 45 - gap: 12px; 46 - padding: 20px 0; 47 - border-bottom: 1px solid var(--border); 48 - transition: background 0.15s ease; 49 } 50 51 - .annotation-card:last-child { 52 - border-bottom: none; 53 } 54 55 .annotation-header { 56 display: flex; 57 justify-content: space-between; 58 align-items: flex-start; 59 - gap: 12px; 60 } 61 62 .annotation-header-left { 63 display: flex; 64 align-items: center; 65 - gap: 10px; 66 flex: 1; 67 min-width: 0; 68 } 69 70 .annotation-avatar { 71 - width: 36px; 72 - height: 36px; 73 - min-width: 36px; 74 - border-radius: 50%; 75 background: var(--bg-tertiary); 76 display: flex; 77 align-items: center; 78 justify-content: center; 79 font-weight: 600; 80 - font-size: 0.85rem; 81 color: var(--text-secondary); 82 overflow: hidden; 83 } ··· 92 display: flex; 93 flex-direction: column; 94 justify-content: center; 95 - line-height: 1.3; 96 } 97 98 .annotation-avatar-link { 99 text-decoration: none; 100 - border-radius: 50%; 101 } 102 103 .annotation-author-row { 104 display: flex; 105 align-items: baseline; 106 - gap: 6px; 107 flex-wrap: wrap; 108 } 109 110 .annotation-author { 111 font-weight: 600; 112 color: var(--text-primary); 113 - font-size: 0.9rem; 114 } 115 116 .annotation-handle { 117 - font-size: 0.85rem; 118 color: var(--text-tertiary); 119 text-decoration: none; 120 } ··· 131 .annotation-content { 132 display: flex; 133 flex-direction: column; 134 - gap: 10px; 135 - padding-left: 46px; 136 } 137 138 .annotation-source { 139 display: inline-flex; 140 align-items: center; 141 gap: 6px; 142 - font-size: 0.75rem; 143 - color: var(--text-tertiary); 144 text-decoration: none; 145 transition: color 0.15s ease; 146 max-width: 100%; 147 overflow: hidden; 148 - text-overflow: ellipsis; 149 - white-space: nowrap; 150 } 151 152 .annotation-source:hover { 153 - color: var(--text-secondary); 154 text-decoration: underline; 155 } 156 157 .annotation-source-title { 158 - color: var(--text-tertiary); 159 - opacity: 0.7; 160 } 161 162 .annotation-highlight { 163 display: block; 164 position: relative; 165 - padding-left: 12px; 166 - margin: 4px 0; 167 text-decoration: none; 168 - border-left: 2px solid var(--border); 169 transition: all 0.15s ease; 170 } 171 172 .annotation-highlight:hover { 173 - border-left-color: var(--text-secondary); 174 } 175 176 .annotation-highlight mark { 177 background: transparent; 178 color: var(--text-primary); 179 font-style: italic; 180 - font-size: 1rem; 181 - line-height: 1.6; 182 font-weight: 400; 183 - font-family: var(--font-serif, var(--font-sans)); 184 - display: inline; 185 - overflow-wrap: anywhere; 186 - word-break: break-all; 187 - padding-right: 4px; 188 } 189 190 .annotation-text { 191 - font-size: 0.95rem; 192 - line-height: 1.6; 193 color: var(--text-primary); 194 white-space: pre-wrap; 195 } ··· 198 display: flex; 199 flex-wrap: wrap; 200 gap: 6px; 201 - margin-top: 4px; 202 } 203 204 .annotation-tag { 205 - font-size: 0.8rem; 206 color: var(--accent); 207 text-decoration: none; 208 font-weight: 500; 209 - opacity: 0.9; 210 transition: opacity 0.15s; 211 } 212 213 .annotation-tag:hover { 214 - opacity: 1; 215 text-decoration: underline; 216 } 217 218 .annotation-actions { 219 display: flex; 220 align-items: center; 221 - justify-content: space-between; 222 margin-top: 4px; 223 - padding-left: 46px; 224 } 225 226 .annotation-actions-left { 227 display: flex; 228 align-items: center; 229 - gap: 16px; 230 } 231 232 .annotation-action { 233 display: flex; 234 align-items: center; 235 - gap: 6px; 236 color: var(--text-tertiary); 237 font-size: 0.8rem; 238 font-weight: 500; 239 - padding: 6px; 240 - margin-left: -6px; 241 - border-radius: var(--radius-sm); 242 transition: all 0.15s ease; 243 background: transparent; 244 cursor: pointer; ··· 251 } 252 253 .annotation-action.liked { 254 - color: #ef4444; 255 } 256 257 .annotation-action.liked svg { 258 - fill: #ef4444; 259 } 260 261 .annotation-action.active { ··· 263 } 264 265 .action-icon-only { 266 - padding: 6px; 267 } 268 269 .annotation-header-right { ··· 276 } 277 278 .inline-replies { 279 - margin-top: 12px; 280 - padding-left: 46px; 281 } 282 283 .annotation-text, ··· 288 max-width: 100%; 289 } 290 291 - .annotation-highlight mark { 292 - overflow-wrap: break-word; 293 - word-break: break-word; 294 - display: inline; 295 - } 296 - 297 .annotation-header-left, 298 .annotation-meta, 299 .reply-meta { ··· 306 max-width: 100%; 307 } 308 309 - .annotation-source { 310 - max-width: 100%; 311 - } 312 - 313 @media (max-width: 768px) { 314 .annotation-content, 315 .annotation-actions, ··· 320 .annotation-header-right { 321 opacity: 1; 322 } 323 } 324 325 .replies-list-threaded { 326 - margin-top: 16px; 327 display: flex; 328 flex-direction: column; 329 } ··· 331 .reply-card-threaded { 332 position: relative; 333 padding-left: 0; 334 transition: background 0.15s; 335 } 336 337 .reply-header { 338 display: flex; 339 align-items: center; 340 - gap: 10px; 341 - margin-bottom: 6px; 342 } 343 344 .reply-avatar { 345 width: 28px; 346 height: 28px; 347 - border-radius: 50%; 348 background: var(--bg-tertiary); 349 overflow: hidden; 350 flex-shrink: 0; ··· 368 .reply-meta { 369 display: flex; 370 align-items: baseline; 371 - gap: 6px; 372 flex: 1; 373 min-width: 0; 374 } 375 376 .reply-author { 377 font-weight: 600; 378 - font-size: 0.85rem; 379 color: var(--text-primary); 380 white-space: nowrap; 381 overflow: hidden; ··· 392 } 393 394 .reply-time { 395 - font-size: 0.75rem; 396 color: var(--text-tertiary); 397 white-space: nowrap; 398 } ··· 407 line-height: 1.5; 408 color: var(--text-primary); 409 margin: 0; 410 - padding-left: 38px; 411 } 412 413 .reply-actions { ··· 428 padding: 4px; 429 color: var(--text-tertiary); 430 cursor: pointer; 431 - border-radius: 4px; 432 display: flex; 433 align-items: center; 434 justify-content: center; ··· 440 } 441 442 .reply-action-delete:hover { 443 - color: #ef4444; 444 - background: rgba(239, 68, 68, 0.1); 445 } 446 447 .reply-form { 448 border: 1px solid var(--border); 449 border-radius: var(--radius-md); 450 - padding: 16px; 451 background: var(--bg-secondary); 452 - margin-bottom: 24px; 453 } 454 455 .replying-to-banner { ··· 457 justify-content: space-between; 458 align-items: center; 459 background: var(--bg-tertiary); 460 - padding: 8px 12px; 461 border-radius: var(--radius-sm); 462 - margin-bottom: 12px; 463 - font-size: 0.85rem; 464 color: var(--text-secondary); 465 } 466 ··· 469 border: none; 470 color: var(--text-tertiary); 471 cursor: pointer; 472 - font-size: 1.2rem; 473 padding: 0 4px; 474 line-height: 1; 475 } ··· 483 background: var(--bg-primary); 484 border: 1px solid var(--border); 485 border-radius: var(--radius-sm); 486 - padding: 12px; 487 color: var(--text-primary); 488 font-family: inherit; 489 - font-size: 0.95rem; 490 resize: vertical; 491 - min-height: 80px; 492 transition: border-color 0.15s; 493 display: block; 494 box-sizing: border-box; ··· 502 .reply-form-actions { 503 display: flex; 504 justify-content: flex-end; 505 - margin-top: 12px; 506 } 507 508 .rich-text-link {
··· 1 .annotation-detail-page { 2 + max-width: 640px; 3 margin: 0 auto; 4 min-height: 100vh; 5 } 6 7 .annotation-detail-header { 8 + margin-bottom: var(--spacing-md); 9 } 10 11 .back-link { ··· 13 align-items: center; 14 color: var(--text-tertiary); 15 text-decoration: none; 16 + font-size: 0.8rem; 17 font-weight: 500; 18 transition: color 0.15s; 19 } ··· 23 } 24 25 .replies-section { 26 + margin-top: var(--spacing-lg); 27 border-top: 1px solid var(--border); 28 + padding-top: var(--spacing-md); 29 } 30 31 .replies-title { 32 display: flex; 33 align-items: center; 34 + gap: 6px; 35 + font-size: 0.9rem; 36 font-weight: 600; 37 color: var(--text-primary); 38 + margin-bottom: var(--spacing-md); 39 } 40 41 .annotation-card { 42 display: flex; 43 flex-direction: column; 44 + gap: 8px; 45 + padding: 16px 20px; 46 + transition: all 0.15s ease; 47 + width: 100%; 48 + box-sizing: border-box; 49 + overflow: visible; 50 + background: var(--bg-primary); 51 + border: none; 52 + position: relative; 53 } 54 55 + .feed > .annotation-card, 56 + .feed > .card { 57 + border-radius: 0; 58 + } 59 + 60 + .feed > .annotation-card:first-child, 61 + .feed > .card:first-child { 62 + border-top-left-radius: var(--radius-lg) !important; 63 + border-top-right-radius: var(--radius-lg) !important; 64 + } 65 + 66 + .feed > .annotation-card:last-child, 67 + .feed > .card:last-child { 68 + border-bottom-left-radius: var(--radius-lg) !important; 69 + border-bottom-right-radius: var(--radius-lg) !important; 70 + } 71 + 72 + .feed > .annotation-card:only-child, 73 + .feed > .card:only-child { 74 + border-radius: var(--radius-lg) !important; 75 } 76 77 .annotation-header { 78 display: flex; 79 justify-content: space-between; 80 align-items: flex-start; 81 + gap: var(--spacing-sm); 82 } 83 84 .annotation-header-left { 85 display: flex; 86 align-items: center; 87 + gap: 8px; 88 flex: 1; 89 min-width: 0; 90 } 91 92 .annotation-avatar { 93 + width: 32px; 94 + height: 32px; 95 + min-width: 32px; 96 + border-radius: var(--radius-full); 97 background: var(--bg-tertiary); 98 display: flex; 99 align-items: center; 100 justify-content: center; 101 font-weight: 600; 102 + font-size: 0.75rem; 103 color: var(--text-secondary); 104 overflow: hidden; 105 } ··· 114 display: flex; 115 flex-direction: column; 116 justify-content: center; 117 + line-height: 1.4; 118 + min-width: 0; 119 + flex: 1; 120 } 121 122 .annotation-avatar-link { 123 text-decoration: none; 124 + border-radius: var(--radius-full); 125 } 126 127 .annotation-author-row { 128 display: flex; 129 align-items: baseline; 130 + gap: 8px; 131 flex-wrap: wrap; 132 } 133 134 .annotation-author { 135 font-weight: 600; 136 color: var(--text-primary); 137 + font-size: 0.875rem; 138 } 139 140 .annotation-handle { 141 + font-size: 0.8rem; 142 color: var(--text-tertiary); 143 text-decoration: none; 144 } ··· 155 .annotation-content { 156 display: flex; 157 flex-direction: column; 158 + gap: 8px; 159 + padding-left: 0; 160 + max-width: 100%; 161 + overflow: hidden; 162 } 163 164 .annotation-source { 165 display: inline-flex; 166 align-items: center; 167 gap: 6px; 168 + font-size: 0.8rem; 169 + color: var(--accent); 170 text-decoration: none; 171 transition: color 0.15s ease; 172 max-width: 100%; 173 overflow: hidden; 174 } 175 176 .annotation-source:hover { 177 text-decoration: underline; 178 } 179 180 .annotation-source-title { 181 + color: var(--text-primary); 182 + font-weight: 500; 183 + overflow: hidden; 184 + text-overflow: ellipsis; 185 + white-space: nowrap; 186 } 187 188 .annotation-highlight { 189 display: block; 190 position: relative; 191 + padding: 10px 14px; 192 + margin: 0; 193 text-decoration: none; 194 + background: var(--bg-tertiary); 195 + border-left: 3px solid var(--accent); 196 + border-radius: 0 var(--radius-md) var(--radius-md) 0; 197 transition: all 0.15s ease; 198 + max-width: 100%; 199 + overflow: hidden; 200 } 201 202 .annotation-highlight:hover { 203 + background: var(--bg-hover); 204 } 205 206 .annotation-highlight mark { 207 background: transparent; 208 color: var(--text-primary); 209 font-style: italic; 210 + font-size: 0.875rem; 211 + line-height: 1.5; 212 font-weight: 400; 213 + display: block; 214 + overflow-wrap: break-word; 215 + word-break: break-word; 216 } 217 218 .annotation-text { 219 + font-size: 1rem; 220 + line-height: 1.7; 221 color: var(--text-primary); 222 white-space: pre-wrap; 223 } ··· 226 display: flex; 227 flex-wrap: wrap; 228 gap: 6px; 229 + margin-top: 2px; 230 } 231 232 .annotation-tag { 233 + font-size: 0.75rem; 234 color: var(--accent); 235 text-decoration: none; 236 font-weight: 500; 237 transition: opacity 0.15s; 238 } 239 240 .annotation-tag:hover { 241 + opacity: 0.8; 242 text-decoration: underline; 243 } 244 245 .annotation-actions { 246 display: flex; 247 align-items: center; 248 + justify-content: flex-start; 249 + gap: 4px; 250 + padding-left: 0; 251 margin-top: 4px; 252 + position: relative; 253 } 254 255 .annotation-actions-left { 256 display: flex; 257 align-items: center; 258 + gap: 8px; 259 } 260 261 .annotation-action { 262 display: flex; 263 align-items: center; 264 + gap: 5px; 265 color: var(--text-tertiary); 266 font-size: 0.8rem; 267 font-weight: 500; 268 + padding: 6px 10px; 269 + border-radius: var(--radius-md); 270 transition: all 0.15s ease; 271 background: transparent; 272 cursor: pointer; ··· 279 } 280 281 .annotation-action.liked { 282 + color: var(--error); 283 } 284 285 .annotation-action.liked svg { 286 + fill: var(--error); 287 } 288 289 .annotation-action.active { ··· 291 } 292 293 .action-icon-only { 294 + padding: 4px; 295 } 296 297 .annotation-header-right { ··· 304 } 305 306 .inline-replies { 307 + margin-top: var(--spacing-sm); 308 + padding-left: 0; 309 + position: relative; 310 } 311 312 .annotation-text, ··· 317 max-width: 100%; 318 } 319 320 .annotation-header-left, 321 .annotation-meta, 322 .reply-meta { ··· 329 max-width: 100%; 330 } 331 332 @media (max-width: 768px) { 333 .annotation-content, 334 .annotation-actions, ··· 339 .annotation-header-right { 340 opacity: 1; 341 } 342 + 343 + .annotation-card { 344 + padding: 16px; 345 + } 346 + 347 + .annotation-avatar { 348 + width: 36px; 349 + height: 36px; 350 + min-width: 36px; 351 + } 352 } 353 354 .replies-list-threaded { 355 + margin-top: var(--spacing-md); 356 display: flex; 357 flex-direction: column; 358 } ··· 360 .reply-card-threaded { 361 position: relative; 362 padding-left: 0; 363 + padding: var(--spacing-sm) 0; 364 transition: background 0.15s; 365 } 366 367 .reply-header { 368 display: flex; 369 align-items: center; 370 + gap: 8px; 371 + margin-bottom: 4px; 372 } 373 374 .reply-avatar { 375 width: 28px; 376 height: 28px; 377 + border-radius: var(--radius-full); 378 background: var(--bg-tertiary); 379 overflow: hidden; 380 flex-shrink: 0; ··· 398 .reply-meta { 399 display: flex; 400 align-items: baseline; 401 + gap: 8px; 402 flex: 1; 403 min-width: 0; 404 } 405 406 .reply-author { 407 font-weight: 600; 408 + font-size: 0.875rem; 409 color: var(--text-primary); 410 white-space: nowrap; 411 overflow: hidden; ··· 422 } 423 424 .reply-time { 425 + font-size: 0.8rem; 426 color: var(--text-tertiary); 427 white-space: nowrap; 428 } ··· 437 line-height: 1.5; 438 color: var(--text-primary); 439 margin: 0; 440 + padding-left: 36px; 441 } 442 443 .reply-actions { ··· 458 padding: 4px; 459 color: var(--text-tertiary); 460 cursor: pointer; 461 + border-radius: var(--radius-sm); 462 display: flex; 463 align-items: center; 464 justify-content: center; ··· 470 } 471 472 .reply-action-delete:hover { 473 + color: var(--error); 474 + background: rgba(255, 69, 58, 0.1); 475 } 476 477 .reply-form { 478 border: 1px solid var(--border); 479 border-radius: var(--radius-md); 480 + padding: var(--spacing-md); 481 background: var(--bg-secondary); 482 + margin-bottom: var(--spacing-md); 483 } 484 485 .replying-to-banner { ··· 487 justify-content: space-between; 488 align-items: center; 489 background: var(--bg-tertiary); 490 + padding: 6px 10px; 491 border-radius: var(--radius-sm); 492 + margin-bottom: var(--spacing-sm); 493 + font-size: 0.8rem; 494 color: var(--text-secondary); 495 } 496 ··· 499 border: none; 500 color: var(--text-tertiary); 501 cursor: pointer; 502 + font-size: 1rem; 503 padding: 0 4px; 504 line-height: 1; 505 } ··· 513 background: var(--bg-primary); 514 border: 1px solid var(--border); 515 border-radius: var(--radius-sm); 516 + padding: 10px 12px; 517 color: var(--text-primary); 518 font-family: inherit; 519 + font-size: 0.875rem; 520 resize: vertical; 521 + min-height: 60px; 522 transition: border-color 0.15s; 523 display: block; 524 box-sizing: border-box; ··· 532 .reply-form-actions { 533 display: flex; 534 justify-content: flex-end; 535 + margin-top: var(--spacing-sm); 536 } 537 538 .rich-text-link {
+136 -80
web/src/css/base.css
··· 1 :root { 2 - --bg-primary: #09090b; 3 - --bg-secondary: #0f0f12; 4 - --bg-tertiary: #18181b; 5 - --bg-card: #09090b; 6 - --bg-elevated: #18181b; 7 - --text-primary: #e4e4e7; 8 - --text-secondary: #a1a1aa; 9 - --text-tertiary: #71717a; 10 - --border: #27272a; 11 - --border-hover: #3f3f46; 12 - --accent: #6366f1; 13 - --accent-hover: #4f46e5; 14 - --accent-subtle: rgba(99, 102, 241, 0.1); 15 - --accent-text: #818cf8; 16 - --success: #10b981; 17 - --error: #ef4444; 18 - --warning: #f59e0b; 19 - --info: #3b82f6; 20 - --radius-sm: 4px; 21 - --radius-md: 6px; 22 - --radius-lg: 8px; 23 --radius-full: 9999px; 24 - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 25 - --shadow-md: 26 - 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 27 - --shadow-lg: 28 - 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 29 - --font-sans: 30 - "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 31 - --font-mono: 32 - "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 - --nav-bg: rgba(9, 9, 11, 0.9); 34 } 35 36 [data-theme="light"] { 37 - --bg-primary: #ffffff; 38 - --bg-secondary: #f4f4f5; 39 - --bg-tertiary: #e4e4e7; 40 --bg-card: #ffffff; 41 - --bg-elevated: #f4f4f5; 42 - --text-primary: #18181b; 43 - --text-secondary: #52525b; 44 - --text-tertiary: #71717a; 45 - --border: #e4e4e7; 46 - --border-hover: #d4d4d8; 47 - --accent: #4f46e5; 48 - --accent-hover: #4338ca; 49 - --accent-subtle: rgba(79, 70, 229, 0.1); 50 - --accent-text: #4f46e5; 51 - --success: #059669; 52 - --error: #dc2626; 53 - --warning: #d97706; 54 - --info: #2563eb; 55 - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 56 - --shadow-md: 57 - 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 58 - --nav-bg: rgba(255, 255, 255, 0.9); 59 } 60 61 * { ··· 74 font-family: var(--font-sans); 75 background: var(--bg-primary); 76 color: var(--text-primary); 77 - line-height: 1.5; 78 min-height: 100vh; 79 -webkit-font-smoothing: antialiased; 80 -moz-osx-font-smoothing: grayscale; 81 overflow-x: hidden; 82 max-width: 100vw; 83 - } 84 - 85 - a { 86 - color: inherit; 87 - text-decoration: none; 88 - transition: color 0.15s ease; 89 } 90 91 h1, ··· 94 h4, 95 h5, 96 h6 { 97 font-weight: 600; 98 - line-height: 1.25; 99 - letter-spacing: -0.025em; 100 color: var(--text-primary); 101 } 102 103 p { 104 color: var(--text-secondary); 105 } 106 107 button { ··· 124 color: var(--accent-text); 125 } 126 127 .text-sm { 128 - font-size: 0.875rem; 129 } 130 131 .text-xs { 132 - font-size: 0.75rem; 133 } 134 135 .font-medium { ··· 140 font-weight: 600; 141 } 142 143 .text-muted { 144 color: var(--text-secondary); 145 } ··· 148 color: var(--text-tertiary); 149 } 150 151 - ::-webkit-scrollbar { 152 - width: 10px; 153 - height: 10px; 154 - } 155 - 156 - ::-webkit-scrollbar-track { 157 - background: transparent; 158 - } 159 - 160 - ::-webkit-scrollbar-thumb { 161 - background: var(--border); 162 - border-radius: 5px; 163 - border: 2px solid var(--bg-primary); 164 - } 165 - 166 - ::-webkit-scrollbar-thumb:hover { 167 - background: var(--border-hover); 168 }
··· 1 + @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"); 2 + 3 :root { 4 + --bg-primary: #0a0a0d; 5 + --bg-secondary: #121216; 6 + --bg-tertiary: #1a1a1f; 7 + --bg-card: #0f0f13; 8 + --bg-elevated: #18181d; 9 + --bg-hover: #1e1e24; 10 + 11 + --glass-border: rgba(234, 234, 238, 0.08); 12 + --glass-bg: rgba(10, 10, 13, 0.92); 13 + 14 + --text-primary: #eaeaee; 15 + --text-secondary: #b7b6c5; 16 + --text-tertiary: #6e6d7a; 17 + 18 + --border: rgba(183, 182, 197, 0.12); 19 + --border-hover: rgba(183, 182, 197, 0.2); 20 + --accent: #957a86; 21 + --accent-hover: #a98d98; 22 + --accent-subtle: rgba(149, 122, 134, 0.15); 23 + --accent-text: #c4a8b2; 24 + 25 + --success: #7fb069; 26 + --error: #d97766; 27 + --warning: #e8a54b; 28 + --info: #6eb5ff; 29 + 30 + --radius-xs: 4px; 31 + --radius-sm: 6px; 32 + --radius-md: 8px; 33 + --radius-lg: 12px; 34 + --radius-xl: 16px; 35 --radius-full: 9999px; 36 + 37 + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 38 + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 39 + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); 40 + --shadow-glow: 0 0 20px rgba(149, 122, 134, 0.2); 41 + 42 + --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 43 + --font-display: "IBM Plex Sans", sans-serif; 44 + --font-mono: "IBM Plex Mono", monospace; 45 + 46 + --nav-bg: rgba(10, 10, 13, 0.95); 47 + 48 + --sidebar-width: 200px; 49 + --right-sidebar-width: 260px; 50 + --content-max-width: 600px; 51 + --spacing-xs: 4px; 52 + --spacing-sm: 8px; 53 + --spacing-md: 12px; 54 + --spacing-lg: 20px; 55 + --spacing-xl: 28px; 56 } 57 58 [data-theme="light"] { 59 + --bg-primary: #f8f8fa; 60 + --bg-secondary: #ffffff; 61 + --bg-tertiary: #f0f0f4; 62 --bg-card: #ffffff; 63 + --bg-elevated: #ffffff; 64 + --bg-hover: #eeeef2; 65 + 66 + --glass-border: rgba(92, 73, 90, 0.1); 67 + --glass-bg: rgba(248, 248, 250, 0.95); 68 + 69 + --text-primary: #18171c; 70 + --text-secondary: #5c495a; 71 + --text-tertiary: #8a8494; 72 + 73 + --border: rgba(92, 73, 90, 0.12); 74 + --border-hover: rgba(92, 73, 90, 0.22); 75 + 76 + --accent: #7a5f6d; 77 + --accent-hover: #664e5b; 78 + --accent-subtle: rgba(149, 122, 134, 0.12); 79 + --accent-text: #5c495a; 80 + 81 + --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 82 + --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 83 + --shadow-lg: 0 8px 24px rgba(92, 73, 90, 0.1); 84 + --shadow-glow: 0 0 20px rgba(149, 122, 134, 0.1); 85 + 86 + --nav-bg: rgba(255, 255, 255, 0.95); 87 } 88 89 * { ··· 102 font-family: var(--font-sans); 103 background: var(--bg-primary); 104 color: var(--text-primary); 105 + line-height: 1.55; 106 min-height: 100vh; 107 -webkit-font-smoothing: antialiased; 108 -moz-osx-font-smoothing: grayscale; 109 overflow-x: hidden; 110 max-width: 100vw; 111 + font-size: 0.9375rem; 112 } 113 114 h1, ··· 117 h4, 118 h5, 119 h6 { 120 + font-family: var(--font-display); 121 font-weight: 600; 122 + letter-spacing: -0.02em; 123 color: var(--text-primary); 124 + line-height: 1.3; 125 + } 126 + 127 + h1 { 128 + font-size: 1.5rem; 129 + } 130 + h2 { 131 + font-size: 1.25rem; 132 + } 133 + h3 { 134 + font-size: 1.1rem; 135 } 136 137 p { 138 color: var(--text-secondary); 139 + line-height: 1.6; 140 + } 141 + 142 + a { 143 + color: inherit; 144 + text-decoration: none; 145 + transition: color 0.2s ease; 146 } 147 148 button { ··· 165 color: var(--accent-text); 166 } 167 168 + ::-webkit-scrollbar { 169 + width: 10px; 170 + height: 10px; 171 + } 172 + 173 + ::-webkit-scrollbar-track { 174 + background: var(--bg-secondary); 175 + } 176 + 177 + ::-webkit-scrollbar-thumb { 178 + background: var(--bg-hover); 179 + border-radius: var(--radius-full); 180 + border: 2px solid var(--bg-secondary); 181 + } 182 + 183 + ::-webkit-scrollbar-thumb:hover { 184 + background: var(--text-tertiary); 185 + } 186 + 187 + :focus-visible { 188 + outline: 2px solid var(--accent); 189 + outline-offset: 3px; 190 + } 191 + 192 .text-sm { 193 + font-size: 0.9rem; 194 } 195 196 .text-xs { 197 + font-size: 0.8rem; 198 } 199 200 .font-medium { ··· 205 font-weight: 600; 206 } 207 208 + .font-mono { 209 + font-family: var(--font-mono); 210 + } 211 + 212 .text-muted { 213 color: var(--text-secondary); 214 } ··· 217 color: var(--text-tertiary); 218 } 219 220 + .card { 221 + background: var(--bg-card); 222 + border-radius: var(--radius-lg); 223 + border: 1px solid var(--border); 224 }
+39 -25
web/src/css/buttons.css
··· 2 display: inline-flex; 3 align-items: center; 4 justify-content: center; 5 - gap: 8px; 6 - padding: 10px 20px; 7 - font-size: 0.9rem; 8 font-weight: 500; 9 border-radius: var(--radius-md); 10 transition: all 0.15s ease; 11 - white-space: pre; 12 } 13 14 .btn-primary { ··· 18 19 .btn-primary:hover { 20 background: var(--accent-hover); 21 - transform: translateY(-1px); 22 - box-shadow: var(--shadow-md); 23 } 24 25 .btn-secondary { ··· 36 .btn-ghost { 37 color: var(--text-secondary); 38 padding: 8px 12px; 39 } 40 41 .btn-ghost:hover { ··· 49 display: flex; 50 align-items: center; 51 justify-content: center; 52 - gap: 10px; 53 - transition: 54 - background 0.2s, 55 - transform 0.2s; 56 } 57 58 .btn-bluesky:hover { 59 background: #0070dd; 60 - transform: translateY(-1px); 61 } 62 63 .btn-sm { 64 padding: 6px 12px; 65 - font-size: 0.85rem; 66 } 67 68 .btn-text { 69 background: none; 70 border: none; 71 color: var(--text-secondary); 72 - font-size: 0.9rem; 73 - padding: 8px 12px; 74 cursor: pointer; 75 transition: color 0.15s; 76 } 77 78 .btn-text:hover { 79 color: var(--text-primary); 80 } 81 82 .btn-block { 83 width: 100%; 84 text-align: left; 85 - padding: 8px 12px; 86 color: var(--text-secondary); 87 background: var(--bg-tertiary); 88 border-radius: var(--radius-md); 89 margin-top: 8px; 90 - font-size: 0.9rem; 91 cursor: pointer; 92 - transition: all 0.2s; 93 } 94 95 .btn-block:hover { 96 - background: var(--border); 97 color: var(--text-primary); 98 } 99 100 .btn-icon-danger { 101 padding: 8px; 102 - background: var(--error); 103 - color: white; 104 border: none; 105 border-radius: var(--radius-md); 106 cursor: pointer; 107 - box-shadow: var(--shadow-md); 108 transition: all 0.15s ease; 109 display: flex; 110 align-items: center; ··· 112 } 113 114 .btn-icon-danger:hover { 115 - background: #dc2626; 116 - transform: scale(1.05); 117 } 118 119 .action-buttons { 120 display: flex; 121 - gap: 8px; 122 flex-wrap: wrap; 123 } 124 125 .action-buttons-end { 126 display: flex; 127 justify-content: flex-end; 128 - gap: 8px; 129 }
··· 2 display: inline-flex; 3 align-items: center; 4 justify-content: center; 5 + gap: 6px; 6 + padding: 8px 16px; 7 + font-size: 0.85rem; 8 font-weight: 500; 9 border-radius: var(--radius-md); 10 transition: all 0.15s ease; 11 + white-space: nowrap; 12 + border: none; 13 + cursor: pointer; 14 } 15 16 .btn-primary { ··· 20 21 .btn-primary:hover { 22 background: var(--accent-hover); 23 + box-shadow: var(--shadow-glow); 24 } 25 26 .btn-secondary { ··· 37 .btn-ghost { 38 color: var(--text-secondary); 39 padding: 8px 12px; 40 + background: transparent; 41 } 42 43 .btn-ghost:hover { ··· 51 display: flex; 52 align-items: center; 53 justify-content: center; 54 + gap: 8px; 55 + transition: all 0.15s; 56 } 57 58 .btn-bluesky:hover { 59 background: #0070dd; 60 } 61 62 .btn-sm { 63 padding: 6px 12px; 64 + font-size: 0.8rem; 65 } 66 67 .btn-text { 68 background: none; 69 border: none; 70 color: var(--text-secondary); 71 + font-size: 0.85rem; 72 + padding: 6px 10px; 73 cursor: pointer; 74 transition: color 0.15s; 75 + border-radius: var(--radius-sm); 76 } 77 78 .btn-text:hover { 79 color: var(--text-primary); 80 + background: var(--bg-tertiary); 81 } 82 83 .btn-block { 84 width: 100%; 85 text-align: left; 86 + padding: 10px 14px; 87 color: var(--text-secondary); 88 background: var(--bg-tertiary); 89 border-radius: var(--radius-md); 90 margin-top: 8px; 91 + font-size: 0.85rem; 92 cursor: pointer; 93 + transition: all 0.15s; 94 + border: 1px solid transparent; 95 } 96 97 .btn-block:hover { 98 + background: var(--bg-hover); 99 color: var(--text-primary); 100 + border-color: var(--border); 101 } 102 103 .btn-icon-danger { 104 padding: 8px; 105 + background: rgba(255, 69, 58, 0.1); 106 + color: var(--error); 107 border: none; 108 border-radius: var(--radius-md); 109 cursor: pointer; 110 transition: all 0.15s ease; 111 display: flex; 112 align-items: center; ··· 114 } 115 116 .btn-icon-danger:hover { 117 + background: var(--error); 118 + color: white; 119 + } 120 + 121 + .btn-danger { 122 + background: rgba(255, 69, 58, 0.1); 123 + color: var(--error); 124 + border: 1px solid rgba(255, 69, 58, 0.2); 125 + } 126 + 127 + .btn-danger:hover { 128 + background: var(--error); 129 + color: white; 130 + border-color: var(--error); 131 } 132 133 .action-buttons { 134 display: flex; 135 + gap: var(--spacing-sm); 136 flex-wrap: wrap; 137 } 138 139 .action-buttons-end { 140 display: flex; 141 justify-content: flex-end; 142 + gap: var(--spacing-sm); 143 }
+270
web/src/css/cards.css
···
··· 1 + .card { 2 + background: var(--bg-primary); 3 + border: none; 4 + border-radius: 0; 5 + transition: all 0.15s ease; 6 + position: relative; 7 + overflow: visible; 8 + } 9 + 10 + .semble-badge { 11 + display: flex; 12 + align-items: center; 13 + gap: 4px; 14 + font-size: 0.75rem; 15 + color: var(--text-tertiary); 16 + margin-right: 4px; 17 + } 18 + 19 + .semble-badge img { 20 + width: 14px; 21 + height: 14px; 22 + } 23 + 24 + .bookmark-preview { 25 + display: block; 26 + padding: 14px 16px; 27 + background: linear-gradient( 28 + 135deg, 29 + var(--bg-tertiary) 0%, 30 + var(--bg-secondary) 100% 31 + ); 32 + border: 1px solid var(--border); 33 + border-left: 3px solid var(--accent); 34 + border-radius: var(--radius-md); 35 + text-decoration: none; 36 + transition: all 0.2s ease; 37 + position: relative; 38 + z-index: 1; 39 + } 40 + 41 + .bookmark-preview:hover { 42 + background: var(--bg-hover); 43 + border-left-color: var(--accent-hover); 44 + } 45 + 46 + .bookmark-preview-content { 47 + display: flex; 48 + flex-direction: column; 49 + gap: 4px; 50 + } 51 + 52 + .bookmark-preview-site { 53 + display: flex; 54 + align-items: center; 55 + gap: 6px; 56 + font-size: 0.7rem; 57 + color: var(--text-tertiary); 58 + text-transform: uppercase; 59 + letter-spacing: 0.06em; 60 + font-weight: 500; 61 + } 62 + 63 + .bookmark-preview-site svg { 64 + color: var(--accent); 65 + } 66 + 67 + .bookmark-preview-title { 68 + font-size: 0.95rem; 69 + font-weight: 600; 70 + color: var(--text-primary); 71 + line-height: 1.35; 72 + margin: 0; 73 + display: -webkit-box; 74 + -webkit-line-clamp: 2; 75 + -webkit-box-orient: vertical; 76 + overflow: hidden; 77 + } 78 + 79 + .bookmark-preview-desc { 80 + font-size: 0.8rem; 81 + color: var(--text-secondary); 82 + line-height: 1.45; 83 + margin: 0; 84 + display: -webkit-box; 85 + -webkit-line-clamp: 2; 86 + -webkit-box-orient: vertical; 87 + overflow: hidden; 88 + } 89 + 90 + .bookmark-card .annotation-content { 91 + padding-left: 0; 92 + overflow: visible; 93 + } 94 + 95 + .bookmark-card { 96 + overflow: visible !important; 97 + } 98 + 99 + .bookmark-card:hover { 100 + z-index: 100 !important; 101 + overflow: visible !important; 102 + } 103 + 104 + .bookmark-site { 105 + display: flex; 106 + align-items: center; 107 + gap: 6px; 108 + font-size: 0.8rem; 109 + color: var(--text-tertiary); 110 + text-transform: uppercase; 111 + letter-spacing: 0.02em; 112 + } 113 + 114 + .bookmark-title { 115 + font-size: 1rem; 116 + font-weight: 600; 117 + color: var(--text-primary); 118 + line-height: 1.4; 119 + margin: 0; 120 + } 121 + 122 + .bookmark-desc { 123 + font-size: 0.875rem; 124 + color: var(--text-secondary); 125 + line-height: 1.5; 126 + margin: 0; 127 + display: -webkit-box; 128 + -webkit-line-clamp: 2; 129 + -webkit-box-orient: vertical; 130 + overflow: hidden; 131 + } 132 + 133 + .edit-form { 134 + display: flex; 135 + flex-direction: column; 136 + gap: 8px; 137 + } 138 + 139 + .edit-textarea, 140 + .edit-input { 141 + width: 100%; 142 + padding: 10px 12px; 143 + background: var(--bg-primary); 144 + border: 1px solid var(--border); 145 + border-radius: var(--radius-md); 146 + color: var(--text-primary); 147 + font-family: inherit; 148 + font-size: 0.9rem; 149 + transition: border-color 0.15s ease; 150 + } 151 + 152 + .edit-textarea { 153 + resize: vertical; 154 + min-height: 80px; 155 + } 156 + 157 + .edit-textarea:focus, 158 + .edit-input:focus { 159 + outline: none; 160 + border-color: var(--accent); 161 + } 162 + 163 + .edit-actions { 164 + display: flex; 165 + justify-content: flex-end; 166 + gap: 8px; 167 + } 168 + 169 + .color-edit-form { 170 + display: flex; 171 + align-items: center; 172 + gap: 8px; 173 + padding: 10px 12px; 174 + background: var(--bg-secondary); 175 + border: 1px solid var(--border); 176 + border-radius: var(--radius-md); 177 + } 178 + 179 + .color-picker-wrapper { 180 + position: relative; 181 + width: 28px; 182 + height: 28px; 183 + flex-shrink: 0; 184 + } 185 + 186 + .color-preview { 187 + width: 100%; 188 + height: 100%; 189 + border-radius: 50%; 190 + border: 2px solid var(--bg-card); 191 + box-shadow: 0 0 0 1px var(--border); 192 + } 193 + 194 + .color-input { 195 + position: absolute; 196 + top: 0; 197 + left: 0; 198 + width: 100%; 199 + height: 100%; 200 + opacity: 0; 201 + cursor: pointer; 202 + } 203 + 204 + .color-edit-form .edit-input { 205 + margin: 0; 206 + flex: 1; 207 + padding: 6px 10px; 208 + height: 32px; 209 + border: none; 210 + background: transparent; 211 + } 212 + 213 + .btn-icon { 214 + padding: 0 10px; 215 + height: 32px; 216 + min-width: auto; 217 + } 218 + 219 + .history-panel { 220 + padding: 12px; 221 + background: var(--bg-secondary); 222 + border: 1px solid var(--border); 223 + border-radius: var(--radius-md); 224 + } 225 + 226 + .history-header { 227 + display: flex; 228 + justify-content: space-between; 229 + align-items: center; 230 + margin-bottom: 12px; 231 + } 232 + 233 + .history-title { 234 + font-size: 0.9rem; 235 + font-weight: 600; 236 + color: var(--text-primary); 237 + } 238 + 239 + .history-status { 240 + font-size: 0.85rem; 241 + color: var(--text-tertiary); 242 + font-style: italic; 243 + } 244 + 245 + .history-list { 246 + list-style: none; 247 + padding: 0; 248 + margin: 0; 249 + display: flex; 250 + flex-direction: column; 251 + gap: 8px; 252 + } 253 + 254 + .history-item { 255 + padding: 8px 10px; 256 + background: var(--bg-tertiary); 257 + border-radius: var(--radius-sm); 258 + } 259 + 260 + .history-date { 261 + font-size: 0.75rem; 262 + color: var(--text-tertiary); 263 + margin-bottom: 4px; 264 + } 265 + 266 + .history-content { 267 + font-size: 0.85rem; 268 + color: var(--text-secondary); 269 + line-height: 1.5; 270 + }
+163 -159
web/src/css/collections.css
··· 1 .collections-list { 2 display: flex; 3 flex-direction: column; 4 - gap: 2px; 5 background: var(--bg-card); 6 border: 1px solid var(--border); 7 border-radius: var(--radius-lg); 8 - overflow: hidden; 9 } 10 11 .collection-row { 12 display: flex; 13 align-items: center; 14 - background: var(--bg-card); 15 transition: background 0.15s ease; 16 } 17 18 - .collection-row:not(:last-child) { 19 - border-bottom: 1px solid var(--border); 20 - } 21 - 22 .collection-row:hover { 23 background: var(--bg-secondary); 24 } ··· 27 flex: 1; 28 display: flex; 29 align-items: center; 30 - gap: 16px; 31 - padding: 16px 20px; 32 text-decoration: none; 33 min-width: 0; 34 } 35 36 .collection-row-icon { 37 - width: 44px; 38 - height: 44px; 39 - min-width: 44px; 40 display: flex; 41 align-items: center; 42 justify-content: center; 43 - background: linear-gradient( 44 - 135deg, 45 - rgba(79, 70, 229, 0.1), 46 - rgba(168, 85, 247, 0.15) 47 - ); 48 color: var(--accent); 49 border-radius: var(--radius-md); 50 - transition: all 0.2s ease; 51 } 52 53 .collection-row:hover .collection-row-icon { 54 - background: linear-gradient( 55 - 135deg, 56 - rgba(79, 70, 229, 0.15), 57 - rgba(168, 85, 247, 0.2) 58 - ); 59 - transform: scale(1.05); 60 } 61 62 .collection-row-info { 63 flex: 1; 64 min-width: 0; 65 } 66 67 .collection-row-name { 68 - font-size: 1rem; 69 font-weight: 600; 70 color: var(--text-primary); 71 - margin: 0 0 2px 0; 72 white-space: nowrap; 73 overflow: hidden; 74 text-overflow: ellipsis; 75 } 76 77 - .collection-row:hover .collection-row-name { 78 - color: var(--accent); 79 - } 80 - 81 .collection-row-desc { 82 - font-size: 0.85rem; 83 color: var(--text-secondary); 84 - margin: 0; 85 white-space: nowrap; 86 overflow: hidden; 87 text-overflow: ellipsis; ··· 90 .collection-row-arrow { 91 color: var(--text-tertiary); 92 opacity: 0; 93 - transition: all 0.2s ease; 94 } 95 96 .collection-row:hover .collection-row-arrow { 97 opacity: 1; 98 - color: var(--accent); 99 - transform: translateX(2px); 100 } 101 102 .collection-row-edit { 103 - padding: 10px; 104 - margin-right: 12px; 105 color: var(--text-tertiary); 106 - background: none; 107 - border: none; 108 border-radius: var(--radius-sm); 109 - cursor: pointer; 110 opacity: 0; 111 - transition: all 0.15s ease; 112 } 113 114 .collection-row:hover .collection-row-edit { ··· 116 } 117 118 .collection-row-edit:hover { 119 - color: var(--text-primary); 120 background: var(--bg-tertiary); 121 } 122 123 .collection-detail-header { 124 display: flex; 125 - gap: 20px; 126 - padding: 24px; 127 - background: var(--bg-card); 128 border: 1px solid var(--border); 129 border-radius: var(--radius-lg); 130 - margin-bottom: 32px; 131 position: relative; 132 } 133 ··· 138 display: flex; 139 align-items: center; 140 justify-content: center; 141 - background: linear-gradient( 142 - 135deg, 143 - rgba(79, 70, 229, 0.1), 144 - rgba(168, 85, 247, 0.1) 145 - ); 146 color: var(--accent); 147 - border-radius: var(--radius-md); 148 } 149 150 .collection-detail-info { 151 - flex: 1; 152 - min-width: 0; 153 } 154 155 .collection-detail-visibility { 156 - display: flex; 157 align-items: center; 158 - gap: 6px; 159 - font-size: 0.8rem; 160 font-weight: 600; 161 color: var(--accent); 162 - text-transform: capitalize; 163 - margin-bottom: 8px; 164 } 165 166 .collection-detail-title { 167 font-size: 1.5rem; 168 font-weight: 700; 169 color: var(--text-primary); 170 - margin-bottom: 8px; 171 - line-height: 1.3; 172 - } 173 - 174 - @media (max-width: 600px) { 175 - .collection-detail-header { 176 - flex-direction: column; 177 - padding: 16px; 178 - gap: 16px; 179 - } 180 - 181 - .collection-detail-actions { 182 - position: static; 183 - margin-top: -8px; 184 - justify-content: flex-end; 185 - } 186 } 187 188 .collection-detail-desc { 189 color: var(--text-secondary); 190 - font-size: 1rem; 191 line-height: 1.5; 192 - margin-bottom: 12px; 193 - max-width: 600px; 194 - overflow-wrap: break-word; 195 - word-break: break-word; 196 } 197 198 .collection-detail-stats { 199 display: flex; 200 align-items: center; 201 - gap: 8px; 202 - font-size: 0.85rem; 203 color: var(--text-tertiary); 204 } 205 206 .collection-detail-actions { 207 position: absolute; 208 - top: 20px; 209 - right: 20px; 210 - display: flex; 211 - align-items: center; 212 - gap: 8px; 213 - } 214 - 215 - .collection-detail-actions .share-menu-container { 216 display: flex; 217 - align-items: center; 218 - } 219 - 220 - .collection-detail-actions .annotation-action { 221 - padding: 10px; 222 - color: var(--text-tertiary); 223 - background: none; 224 - border: none; 225 - border-radius: var(--radius-sm); 226 - cursor: pointer; 227 - transition: all 0.15s ease; 228 - } 229 - 230 - .collection-detail-actions .annotation-action:hover { 231 - color: var(--accent); 232 - background: var(--bg-tertiary); 233 } 234 235 .collection-detail-edit, 236 .collection-detail-delete { 237 - padding: 10px; 238 color: var(--text-tertiary); 239 - background: none; 240 border: none; 241 - border-radius: var(--radius-sm); 242 cursor: pointer; 243 - transition: all 0.15s ease; 244 } 245 246 .collection-detail-edit:hover { 247 - color: var(--accent); 248 - background: var(--bg-tertiary); 249 } 250 251 .collection-detail-delete:hover { 252 - color: var(--error); 253 - background: rgba(239, 68, 68, 0.1); 254 - } 255 - 256 - .collection-item-wrapper { 257 - position: relative; 258 - } 259 - 260 - .collection-item-remove { 261 - position: absolute; 262 - top: 12px; 263 - left: -40px; 264 - z-index: 10; 265 - padding: 8px; 266 - background: var(--bg-card); 267 - border: 1px solid var(--border); 268 - border-radius: var(--radius-sm); 269 - color: var(--text-tertiary); 270 - cursor: pointer; 271 - opacity: 0; 272 - transition: all 0.15s ease; 273 - } 274 - 275 - .collection-item-wrapper:hover .collection-item-remove { 276 - opacity: 1; 277 - } 278 - 279 - .collection-item-remove:hover { 280 color: var(--error); 281 - border-color: var(--error); 282 - background: rgba(239, 68, 68, 0.05); 283 } 284 285 .collection-list-item { 286 width: 100%; 287 text-align: left; 288 - padding: 12px 16px; 289 border-radius: var(--radius-md); 290 - background: var(--bg-primary); 291 - border: 1px solid transparent; 292 color: var(--text-primary); 293 - transition: all 0.15s ease; 294 display: flex; 295 align-items: center; 296 justify-content: space-between; 297 cursor: pointer; 298 } 299 300 .collection-list-item:hover { 301 background: var(--bg-hover); 302 - border-color: var(--border); 303 - } 304 - 305 - .collection-list-item:hover .collection-list-item-icon { 306 - opacity: 1; 307 } 308 309 .collection-list-item:disabled { 310 - opacity: 0.6; 311 cursor: not-allowed; 312 } 313 314 - .item-delete-overlay { 315 position: absolute; 316 - top: 16px; 317 - right: 16px; 318 - z-index: 10; 319 opacity: 0; 320 - transition: opacity 0.15s ease; 321 } 322 323 - .card:hover .item-delete-overlay, 324 - div:hover > .item-delete-overlay { 325 opacity: 1; 326 }
··· 1 + .collection-feed-item { 2 + display: flex; 3 + flex-direction: column; 4 + background: var(--bg-primary); 5 + overflow: visible; 6 + } 7 + 8 + .collection-context-badge { 9 + display: flex; 10 + align-items: center; 11 + justify-content: space-between; 12 + gap: var(--spacing-sm); 13 + padding: 10px 20px; 14 + background: var(--bg-secondary); 15 + border-bottom: 1px solid var(--border); 16 + } 17 + 18 + .collection-context-inner { 19 + display: flex; 20 + align-items: center; 21 + gap: 8px; 22 + font-size: 0.8rem; 23 + color: var(--text-secondary); 24 + } 25 + 26 + .collection-context-avatar { 27 + width: 20px; 28 + height: 20px; 29 + border-radius: var(--radius-full); 30 + object-fit: cover; 31 + } 32 + 33 + .collection-context-text { 34 + display: flex; 35 + align-items: center; 36 + gap: 4px; 37 + flex-wrap: wrap; 38 + } 39 + 40 + .collection-context-author { 41 + font-weight: 600; 42 + color: var(--text-primary); 43 + text-decoration: none; 44 + } 45 + 46 + .collection-context-author:hover { 47 + text-decoration: underline; 48 + } 49 + 50 + .collection-context-link { 51 + display: inline-flex; 52 + align-items: center; 53 + gap: 5px; 54 + font-weight: 600; 55 + color: var(--accent); 56 + text-decoration: none; 57 + background: var(--accent-subtle); 58 + padding: 2px 8px; 59 + border-radius: var(--radius-sm); 60 + } 61 + 62 + .collection-context-link:hover { 63 + background: var(--accent); 64 + color: var(--bg-primary); 65 + } 66 + 67 .collections-list { 68 display: flex; 69 flex-direction: column; 70 + gap: 12px; 71 + } 72 + 73 + .collections-list > * { 74 background: var(--bg-card); 75 border: 1px solid var(--border); 76 border-radius: var(--radius-lg); 77 } 78 79 .collection-row { 80 display: flex; 81 align-items: center; 82 transition: background 0.15s ease; 83 } 84 85 .collection-row:hover { 86 background: var(--bg-secondary); 87 } ··· 90 flex: 1; 91 display: flex; 92 align-items: center; 93 + gap: var(--spacing-md); 94 + padding: var(--spacing-md); 95 text-decoration: none; 96 min-width: 0; 97 } 98 99 .collection-row-icon { 100 + width: 40px; 101 + height: 40px; 102 + min-width: 40px; 103 display: flex; 104 align-items: center; 105 justify-content: center; 106 + background: var(--bg-tertiary); 107 color: var(--accent); 108 border-radius: var(--radius-md); 109 + transition: all 0.15s ease; 110 + font-size: 1.1rem; 111 } 112 113 .collection-row:hover .collection-row-icon { 114 + background: var(--accent-subtle); 115 } 116 117 .collection-row-info { 118 flex: 1; 119 min-width: 0; 120 + display: flex; 121 + flex-direction: column; 122 + gap: 2px; 123 } 124 125 .collection-row-name { 126 + font-size: 0.9rem; 127 font-weight: 600; 128 color: var(--text-primary); 129 white-space: nowrap; 130 overflow: hidden; 131 text-overflow: ellipsis; 132 } 133 134 .collection-row-desc { 135 + font-size: 0.8rem; 136 color: var(--text-secondary); 137 white-space: nowrap; 138 overflow: hidden; 139 text-overflow: ellipsis; ··· 142 .collection-row-arrow { 143 color: var(--text-tertiary); 144 opacity: 0; 145 + transition: opacity 0.15s; 146 } 147 148 .collection-row:hover .collection-row-arrow { 149 opacity: 1; 150 } 151 152 .collection-row-edit { 153 + padding: 8px; 154 + margin-right: var(--spacing-sm); 155 color: var(--text-tertiary); 156 + background: transparent; 157 border-radius: var(--radius-sm); 158 + transition: all 0.15s; 159 opacity: 0; 160 + border: none; 161 + cursor: pointer; 162 } 163 164 .collection-row:hover .collection-row-edit { ··· 166 } 167 168 .collection-row-edit:hover { 169 background: var(--bg-tertiary); 170 + color: var(--text-primary); 171 } 172 173 .collection-detail-header { 174 display: flex; 175 + flex-direction: column; 176 + gap: var(--spacing-md); 177 + padding: var(--spacing-lg); 178 + background: var(--bg-secondary); 179 border: 1px solid var(--border); 180 border-radius: var(--radius-lg); 181 + margin-bottom: var(--spacing-lg); 182 position: relative; 183 } 184 ··· 189 display: flex; 190 align-items: center; 191 justify-content: center; 192 + background: var(--bg-tertiary); 193 color: var(--accent); 194 + border-radius: var(--radius-lg); 195 + font-size: 1.5rem; 196 } 197 198 .collection-detail-info { 199 + display: flex; 200 + flex-direction: column; 201 + gap: 6px; 202 } 203 204 .collection-detail-visibility { 205 + display: inline-flex; 206 align-items: center; 207 + gap: 4px; 208 + font-size: 0.65rem; 209 font-weight: 600; 210 + letter-spacing: 0.05em; 211 + text-transform: uppercase; 212 color: var(--accent); 213 + padding: 2px 8px; 214 + background: var(--accent-subtle); 215 + border-radius: var(--radius-full); 216 + width: fit-content; 217 } 218 219 .collection-detail-title { 220 + font-family: var(--font-display); 221 font-size: 1.5rem; 222 font-weight: 700; 223 color: var(--text-primary); 224 + line-height: 1.2; 225 + letter-spacing: -0.02em; 226 } 227 228 .collection-detail-desc { 229 color: var(--text-secondary); 230 + font-size: 0.9rem; 231 line-height: 1.5; 232 } 233 234 .collection-detail-stats { 235 display: flex; 236 align-items: center; 237 + gap: var(--spacing-md); 238 + font-size: 0.8rem; 239 color: var(--text-tertiary); 240 + margin-top: var(--spacing-xs); 241 } 242 243 .collection-detail-actions { 244 position: absolute; 245 + top: var(--spacing-md); 246 + right: var(--spacing-md); 247 display: flex; 248 + gap: var(--spacing-xs); 249 } 250 251 + .collection-detail-actions .annotation-action, 252 .collection-detail-edit, 253 .collection-detail-delete { 254 + padding: 6px; 255 color: var(--text-tertiary); 256 + background: var(--bg-tertiary); 257 + border-radius: var(--radius-sm); 258 + transition: all 0.15s; 259 border: none; 260 cursor: pointer; 261 } 262 263 + .collection-detail-actions .annotation-action:hover, 264 .collection-detail-edit:hover { 265 + background: var(--bg-hover); 266 + color: var(--text-primary); 267 } 268 269 .collection-detail-delete:hover { 270 + background: rgba(255, 69, 58, 0.1); 271 color: var(--error); 272 } 273 274 .collection-list-item { 275 width: 100%; 276 text-align: left; 277 + padding: 12px 14px; 278 border-radius: var(--radius-md); 279 + background: var(--bg-secondary); 280 + border: 1px solid var(--border); 281 color: var(--text-primary); 282 + transition: all 0.15s; 283 display: flex; 284 align-items: center; 285 justify-content: space-between; 286 cursor: pointer; 287 + margin-bottom: var(--spacing-sm); 288 } 289 290 .collection-list-item:hover { 291 background: var(--bg-hover); 292 + border-color: var(--accent); 293 } 294 295 .collection-list-item:disabled { 296 + opacity: 0.5; 297 cursor: not-allowed; 298 } 299 300 + .collection-item-wrapper { 301 + position: relative; 302 + } 303 + 304 + .collection-item-remove { 305 position: absolute; 306 + left: -40px; 307 + top: 20px; 308 + width: 28px; 309 + height: 28px; 310 + display: flex; 311 + align-items: center; 312 + justify-content: center; 313 + background: var(--bg-secondary); 314 + border: 1px solid var(--border); 315 + border-radius: var(--radius-sm); 316 + color: var(--text-tertiary); 317 + cursor: pointer; 318 + transition: all 0.15s ease; 319 opacity: 0; 320 } 321 322 + .collection-item-wrapper:hover .collection-item-remove { 323 opacity: 1; 324 } 325 + 326 + .collection-item-remove:hover { 327 + background: rgba(255, 69, 58, 0.1); 328 + border-color: rgba(255, 69, 58, 0.3); 329 + color: var(--error); 330 + }
+222 -109
web/src/css/feed.css
··· 1 .feed { 2 display: flex; 3 flex-direction: column; 4 - gap: 16px; 5 } 6 7 .feed-header { 8 display: flex; 9 align-items: center; 10 justify-content: space-between; 11 - margin-bottom: 8px; 12 } 13 14 .feed-title { 15 - font-size: 1.5rem; 16 - font-weight: 700; 17 } 18 19 .feed-filters { 20 display: flex; 21 - gap: 8px; 22 - margin-bottom: 24px; 23 - padding: 4px; 24 - background: var(--bg-tertiary); 25 - border-radius: var(--radius-lg); 26 - width: fit-content; 27 - max-width: 100%; 28 flex-wrap: wrap; 29 } 30 31 .filter-tab { 32 - padding: 8px 16px; 33 - font-size: 0.9rem; 34 font-weight: 500; 35 - color: var(--text-secondary); 36 background: transparent; 37 border: none; 38 border-radius: var(--radius-md); ··· 41 } 42 43 .filter-tab:hover { 44 - color: var(--text-primary); 45 - background: var(--bg-hover); 46 } 47 48 .filter-tab.active { 49 color: var(--text-primary); 50 - background: var(--bg-card); 51 - box-shadow: var(--shadow-sm); 52 } 53 54 .page-header { 55 - margin-bottom: 32px; 56 } 57 58 .page-title { 59 font-size: 2rem; 60 font-weight: 700; 61 margin-bottom: 8px; 62 } 63 64 .page-description { 65 color: var(--text-secondary); 66 font-size: 1.1rem; 67 } 68 69 .url-input-wrapper { 70 - margin-bottom: 24px; 71 } 72 73 .url-input-container { 74 display: flex; 75 - gap: 12px; 76 } 77 78 .url-input { 79 width: 100%; 80 - padding: 16px; 81 background: var(--bg-secondary); 82 border: 1px solid var(--border); 83 border-radius: var(--radius-md); 84 color: var(--text-primary); 85 - font-size: 1.1rem; 86 - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 87 - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 88 } 89 90 .url-input:focus { 91 outline: none; 92 border-color: var(--accent); 93 - box-shadow: 0 0 0 4px var(--accent-subtle); 94 - background: var(--bg-primary); 95 } 96 97 .url-input::placeholder { ··· 102 display: flex; 103 align-items: center; 104 justify-content: space-between; 105 - margin-bottom: 16px; 106 - flex-wrap: wrap; 107 - gap: 12px; 108 } 109 110 .back-link { 111 display: inline-flex; 112 align-items: center; 113 - gap: 8px; 114 color: var(--text-secondary); 115 - font-size: 0.9rem; 116 text-decoration: none; 117 - margin-bottom: 24px; 118 - transition: color 0.15s; 119 } 120 121 .back-link:hover { 122 - color: var(--accent); 123 - } 124 - 125 - .new-page { 126 - max-width: 600px; 127 - margin: 0 auto; 128 - display: flex; 129 - flex-direction: column; 130 - gap: 32px; 131 - } 132 - 133 - @media (max-width: 640px) { 134 - .main-content { 135 - padding: 16px 12px; 136 - } 137 - 138 - .page-title { 139 - font-size: 1.5rem; 140 - } 141 - } 142 - 143 - .user-url-page { 144 - max-width: 800px; 145 } 146 147 .url-target-info { 148 display: flex; 149 flex-direction: column; 150 gap: 4px; 151 - padding: 16px; 152 background: var(--bg-secondary); 153 border: 1px solid var(--border); 154 border-radius: var(--radius-md); 155 - margin-bottom: 24px; 156 } 157 158 .url-target-label { 159 - font-size: 0.875rem; 160 - color: var(--text-secondary); 161 } 162 163 .url-target-link { 164 color: var(--accent); 165 - font-size: 0.95rem; 166 word-break: break-all; 167 - text-decoration: none; 168 } 169 170 .url-target-link:hover { ··· 175 display: flex; 176 align-items: center; 177 justify-content: space-between; 178 - gap: 16px; 179 padding: 12px 16px; 180 - background: var(--accent-subtle); 181 - border: 1px solid var(--accent); 182 border-radius: var(--radius-md); 183 - margin-bottom: 16px; 184 } 185 186 .share-notes-info { 187 display: flex; 188 align-items: center; 189 - gap: 8px; 190 color: var(--text-primary); 191 - font-size: 0.9rem; 192 } 193 194 .share-notes-actions { 195 display: flex; 196 - gap: 8px; 197 } 198 199 @media (max-width: 640px) { 200 - .share-notes-banner { 201 - flex-direction: column; 202 - align-items: stretch; 203 } 204 205 - .share-notes-actions { 206 - justify-content: flex-end; 207 } 208 } 209 210 - .feed-tab { 211 - padding: 8px 16px; 212 - font-size: 1rem; 213 - font-weight: 500; 214 - color: var(--text-secondary); 215 background: transparent; 216 border: none; 217 - border-bottom: 2px solid transparent; 218 cursor: pointer; 219 - transition: all 0.2s ease; 220 - margin-bottom: -1px; 221 - } 222 - 223 - .feed-tab:hover { 224 - color: var(--text-primary); 225 } 226 227 - .feed-tab.active { 228 - color: var(--text-primary); 229 - border-bottom-color: var(--text-primary); 230 - font-weight: 600; 231 } 232 233 - .filter-pill { 234 - padding: 6px 16px; 235 - font-size: 0.9rem; 236 - font-weight: 500; 237 - color: var(--text-secondary); 238 - background: var(--bg-tertiary); 239 - border: 1px solid transparent; 240 - border-radius: 999px; 241 - cursor: pointer; 242 - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 243 } 244 245 - .filter-pill:hover { 246 - background: var(--bg-secondary); 247 - color: var(--text-primary); 248 - border-color: var(--border); 249 } 250 251 - .filter-pill.active { 252 - background: var(--text-primary); 253 - color: var(--bg-primary); 254 - font-weight: 600; 255 }
··· 1 + .feed-container { 2 + background: var(--bg-elevated); 3 + border: 1px solid var(--border-hover); 4 + border-radius: var(--radius-xl); 5 + overflow: visible; 6 + padding: 8px; 7 + position: relative; 8 + } 9 + 10 .feed { 11 display: flex; 12 flex-direction: column; 13 + gap: 0; 14 + width: 100%; 15 + overflow: visible; 16 + border-radius: var(--radius-lg); 17 + position: relative; 18 + } 19 + 20 + .feed > * { 21 + border-bottom: 1px solid var(--border); 22 + position: relative; 23 + } 24 + 25 + .feed > *:last-child { 26 + border-bottom: none; 27 + } 28 + 29 + .feed > *:hover { 30 + z-index: 10; 31 + } 32 + 33 + .feed-page { 34 + animation: fadeIn 0.3s ease-out; 35 + } 36 + 37 + @keyframes fadeIn { 38 + from { 39 + opacity: 0; 40 + } 41 + to { 42 + opacity: 1; 43 + } 44 } 45 46 .feed-header { 47 display: flex; 48 align-items: center; 49 justify-content: space-between; 50 + margin-bottom: 20px; 51 } 52 53 .feed-title { 54 + font-family: var(--font-display); 55 + font-size: 1.25rem; 56 + font-weight: 600; 57 + letter-spacing: -0.02em; 58 } 59 60 .feed-filters { 61 display: flex; 62 + gap: 4px; 63 + margin-bottom: 20px; 64 + background: transparent; 65 + padding: 0; 66 + border: none; 67 flex-wrap: wrap; 68 } 69 70 .filter-tab { 71 + padding: 8px 14px; 72 + font-size: 0.875rem; 73 font-weight: 500; 74 + color: var(--text-tertiary); 75 background: transparent; 76 border: none; 77 border-radius: var(--radius-md); ··· 80 } 81 82 .filter-tab:hover { 83 + color: var(--text-secondary); 84 + background: var(--bg-tertiary); 85 } 86 87 .filter-tab.active { 88 color: var(--text-primary); 89 + background: var(--bg-tertiary); 90 + } 91 + 92 + .filter-pill { 93 + padding: 8px 14px; 94 + font-size: 0.8rem; 95 + font-weight: 600; 96 + color: var(--text-secondary); 97 + background: var(--bg-tertiary); 98 + border: none; 99 + border-radius: var(--radius-full); 100 + cursor: pointer; 101 + transition: all 0.15s; 102 + } 103 + 104 + .filter-pill:hover { 105 + background: var(--bg-hover); 106 + color: var(--text-primary); 107 + } 108 + 109 + .filter-pill.active { 110 + background: var(--accent); 111 + color: var(--bg-primary); 112 } 113 114 .page-header { 115 + margin-bottom: 28px; 116 } 117 118 .page-title { 119 + font-family: var(--font-display); 120 font-size: 2rem; 121 font-weight: 700; 122 margin-bottom: 8px; 123 + letter-spacing: -0.02em; 124 + color: var(--text-primary); 125 } 126 127 .page-description { 128 color: var(--text-secondary); 129 font-size: 1.1rem; 130 + line-height: 1.5; 131 } 132 133 .url-input-wrapper { 134 + margin-bottom: var(--spacing-lg); 135 + position: relative; 136 } 137 138 .url-input-container { 139 display: flex; 140 + gap: var(--spacing-sm); 141 } 142 143 .url-input { 144 width: 100%; 145 + padding: 12px 16px; 146 background: var(--bg-secondary); 147 border: 1px solid var(--border); 148 border-radius: var(--radius-md); 149 color: var(--text-primary); 150 + font-size: 0.9rem; 151 + transition: all 0.15s ease; 152 } 153 154 .url-input:focus { 155 outline: none; 156 border-color: var(--accent); 157 + box-shadow: 0 0 0 3px var(--accent-subtle); 158 } 159 160 .url-input::placeholder { ··· 165 display: flex; 166 align-items: center; 167 justify-content: space-between; 168 + margin-bottom: var(--spacing-md); 169 } 170 171 .back-link { 172 display: inline-flex; 173 align-items: center; 174 + gap: 6px; 175 color: var(--text-secondary); 176 + font-size: 0.8rem; 177 + font-weight: 500; 178 text-decoration: none; 179 + margin-bottom: var(--spacing-lg); 180 + padding: 6px 12px; 181 + background: var(--bg-tertiary); 182 + border-radius: var(--radius-sm); 183 + transition: all 0.15s; 184 } 185 186 .back-link:hover { 187 + background: var(--bg-hover); 188 + color: var(--text-primary); 189 } 190 191 .url-target-info { 192 display: flex; 193 flex-direction: column; 194 gap: 4px; 195 + padding: 12px 16px; 196 background: var(--bg-secondary); 197 border: 1px solid var(--border); 198 border-radius: var(--radius-md); 199 + margin-bottom: var(--spacing-lg); 200 } 201 202 .url-target-label { 203 + font-size: 0.65rem; 204 + text-transform: uppercase; 205 + letter-spacing: 0.05em; 206 + font-weight: 600; 207 + color: var(--text-tertiary); 208 } 209 210 .url-target-link { 211 color: var(--accent); 212 + font-size: 0.85rem; 213 + font-weight: 500; 214 + text-decoration: none; 215 word-break: break-all; 216 + line-height: 1.4; 217 } 218 219 .url-target-link:hover { ··· 224 display: flex; 225 align-items: center; 226 justify-content: space-between; 227 + gap: var(--spacing-md); 228 padding: 12px 16px; 229 + background: var(--bg-secondary); 230 + border: 1px solid var(--border); 231 border-radius: var(--radius-md); 232 + margin-bottom: var(--spacing-md); 233 } 234 235 .share-notes-info { 236 display: flex; 237 align-items: center; 238 + gap: var(--spacing-sm); 239 color: var(--text-primary); 240 + font-size: 0.85rem; 241 + font-weight: 500; 242 } 243 244 .share-notes-actions { 245 display: flex; 246 + gap: var(--spacing-sm); 247 + } 248 + 249 + .empty-state { 250 + display: flex; 251 + flex-direction: column; 252 + align-items: center; 253 + justify-content: center; 254 + padding: 48px 24px; 255 + text-align: center; 256 + } 257 + 258 + .empty-state-icon { 259 + width: 56px; 260 + height: 56px; 261 + display: flex; 262 + align-items: center; 263 + justify-content: center; 264 + background: var(--bg-tertiary); 265 + border-radius: var(--radius-lg); 266 + color: var(--text-tertiary); 267 + margin-bottom: 16px; 268 + } 269 + 270 + .empty-state-title { 271 + font-size: 1.1rem; 272 + font-weight: 600; 273 + color: var(--text-primary); 274 + margin-bottom: 6px; 275 + } 276 + 277 + .empty-state-text { 278 + font-size: 0.9rem; 279 + color: var(--text-secondary); 280 + max-width: 300px; 281 + line-height: 1.5; 282 } 283 284 @media (max-width: 640px) { 285 + .feed-filters { 286 + gap: 4px; 287 } 288 289 + .filter-tab, 290 + .filter-pill { 291 + padding: 6px 10px; 292 + font-size: 0.75rem; 293 } 294 } 295 296 + .feed-controls { 297 + display: flex; 298 + flex-direction: column; 299 + gap: var(--spacing-sm); 300 + margin-bottom: var(--spacing-lg); 301 + } 302 + 303 + .active-filter-banner { 304 + display: inline-flex; 305 + align-items: center; 306 + gap: var(--spacing-sm); 307 + padding: 6px 10px 6px 12px; 308 + background: var(--accent-subtle); 309 + border: 1px solid var(--accent); 310 + border-radius: var(--radius-full); 311 + font-size: 0.8rem; 312 + color: var(--accent); 313 + margin-bottom: var(--spacing-md); 314 + width: fit-content; 315 + } 316 + 317 + .active-filter-banner strong { 318 + color: var(--accent-text); 319 + } 320 + 321 + .active-filter-clear { 322 + display: flex; 323 + align-items: center; 324 + justify-content: center; 325 + width: 20px; 326 + height: 20px; 327 background: transparent; 328 border: none; 329 + border-radius: var(--radius-full); 330 + color: var(--accent); 331 cursor: pointer; 332 + transition: all 0.15s; 333 } 334 335 + .active-filter-clear:hover { 336 + background: var(--accent); 337 + color: white; 338 } 339 340 + .keyboard-hint { 341 + display: none; 342 + align-items: center; 343 + gap: 4px; 344 + font-size: 0.7rem; 345 + color: var(--text-tertiary); 346 + margin-left: auto; 347 } 348 349 + @media (min-width: 768px) { 350 + .keyboard-hint { 351 + display: flex; 352 + } 353 } 354 355 + .kbd { 356 + display: inline-flex; 357 + align-items: center; 358 + justify-content: center; 359 + min-width: 20px; 360 + height: 20px; 361 + padding: 0 6px; 362 + background: var(--bg-tertiary); 363 + border: 1px solid var(--border); 364 + border-radius: var(--radius-xs); 365 + font-size: 0.65rem; 366 + font-family: var(--font-mono); 367 + color: var(--text-secondary); 368 }
+310 -342
web/src/css/layout.css
··· 1 - .layout { 2 - display: flex; 3 min-height: 100vh; 4 background: var(--bg-primary); 5 } 6 7 - .sidebar { 8 - position: fixed; 9 - left: 0; 10 top: 0; 11 - bottom: 0; 12 - width: 240px; 13 - background: var(--bg-primary); 14 - border-right: 1px solid var(--border); 15 display: flex; 16 - flex-direction: column; 17 - z-index: 50; 18 - padding-bottom: 20px; 19 } 20 21 - .sidebar-header { 22 - height: 64px; 23 display: flex; 24 align-items: center; 25 - padding: 0 20px; 26 - margin-bottom: 12px; 27 text-decoration: none; 28 color: var(--text-primary); 29 - } 30 - 31 - .sidebar-logo { 32 - width: 24px; 33 - height: 24px; 34 - object-fit: contain; 35 - margin-right: 12px; 36 } 37 38 - .sidebar-brand { 39 - font-size: 1rem; 40 - font-weight: 600; 41 - color: var(--text-primary); 42 - letter-spacing: -0.01em; 43 } 44 45 - .sidebar-nav { 46 - flex: 1; 47 display: flex; 48 - flex-direction: column; 49 gap: 4px; 50 - padding: 0 12px; 51 - overflow-y: auto; 52 } 53 54 - .sidebar-link { 55 - display: flex; 56 - align-items: center; 57 - gap: 12px; 58 - padding: 8px 12px; 59 - border-radius: var(--radius-md); 60 color: var(--text-secondary); 61 text-decoration: none; 62 font-size: 0.9rem; 63 font-weight: 500; 64 - transition: all 0.15s ease; 65 } 66 67 - .sidebar-link:hover { 68 - background: var(--bg-tertiary); 69 color: var(--text-primary); 70 } 71 72 - .sidebar-link.active { 73 background: var(--bg-tertiary); 74 - color: var(--text-primary); 75 } 76 77 - .sidebar-link svg { 78 - width: 18px; 79 - height: 18px; 80 - color: var(--text-tertiary); 81 - transition: color 0.15s ease; 82 } 83 84 - .sidebar-link:hover svg, 85 - .sidebar-link.active svg { 86 - color: var(--text-primary); 87 - } 88 - 89 - .sidebar-section-title { 90 - padding: 24px 12px 8px; 91 - font-size: 0.75rem; 92 - font-weight: 600; 93 - color: var(--text-tertiary); 94 - text-transform: uppercase; 95 - letter-spacing: 0.05em; 96 - } 97 - 98 - .notification-badge { 99 - background: var(--accent); 100 - color: white; 101 - font-size: 0.7rem; 102 - font-weight: 600; 103 - padding: 0 6px; 104 - height: 18px; 105 - border-radius: 99px; 106 display: flex; 107 align-items: center; 108 - justify-content: center; 109 - margin-left: auto; 110 } 111 112 - .sidebar-new-btn { 113 display: flex; 114 align-items: center; 115 - gap: 10px; 116 - margin: 0 12px 16px; 117 - padding: 10px 16px; 118 - background: var(--text-primary); 119 - color: var(--bg-primary); 120 border-radius: var(--radius-md); 121 - font-size: 0.9rem; 122 - font-weight: 600; 123 text-decoration: none; 124 - transition: opacity 0.15s; 125 - justify-content: center; 126 } 127 128 - .sidebar-new-btn:hover { 129 - opacity: 0.9; 130 } 131 132 - .sidebar-footer { 133 - padding: 0 12px; 134 - margin-top: auto; 135 } 136 137 - .sidebar-user { 138 display: flex; 139 align-items: center; 140 - gap: 10px; 141 - padding: 8px 12px; 142 border-radius: var(--radius-md); 143 - cursor: pointer; 144 - transition: background 0.15s ease; 145 } 146 147 - .sidebar-user:hover, 148 - .sidebar-user.active { 149 - background: var(--bg-tertiary); 150 } 151 152 - .sidebar-avatar { 153 - width: 32px; 154 - height: 32px; 155 - border-radius: 50%; 156 background: var(--bg-tertiary); 157 display: flex; 158 align-items: center; 159 justify-content: center; 160 color: var(--text-secondary); 161 font-size: 0.8rem; 162 - font-weight: 500; 163 - overflow: hidden; 164 - flex-shrink: 0; 165 - border: 1px solid var(--border); 166 } 167 168 - .sidebar-avatar img { 169 width: 100%; 170 height: 100%; 171 object-fit: cover; 172 } 173 174 - .sidebar-user-info { 175 - flex: 1; 176 - min-width: 0; 177 - display: flex; 178 - flex-direction: column; 179 - } 180 - 181 - .sidebar-user-name { 182 - font-size: 0.85rem; 183 - font-weight: 500; 184 color: var(--text-primary); 185 } 186 187 - .sidebar-user-handle { 188 - font-size: 0.75rem; 189 - color: var(--text-tertiary); 190 } 191 192 - .sidebar-dropdown { 193 position: absolute; 194 - bottom: 74px; 195 - left: 12px; 196 - width: 216px; 197 - background: var(--bg-card); 198 border: 1px solid var(--border); 199 - border-radius: var(--radius-md); 200 box-shadow: var(--shadow-lg); 201 - padding: 4px; 202 - z-index: 1000; 203 - overflow: hidden; 204 - animation: scaleIn 0.1s ease-out; 205 - transform-origin: bottom center; 206 } 207 208 - @keyframes scaleIn { 209 - from { 210 - opacity: 0; 211 - transform: scale(0.95); 212 - } 213 - 214 - to { 215 - opacity: 1; 216 - transform: scale(1); 217 - } 218 } 219 220 - .sidebar-dropdown-item { 221 display: flex; 222 align-items: center; 223 gap: 10px; 224 width: 100%; 225 - padding: 8px 12px; 226 - font-size: 0.85rem; 227 color: var(--text-secondary); 228 text-decoration: none; 229 - background: transparent; 230 - cursor: pointer; 231 - border-radius: var(--radius-sm); 232 transition: all 0.15s; 233 border: none; 234 } 235 236 - .sidebar-dropdown-item:hover { 237 - background: var(--bg-tertiary); 238 color: var(--text-primary); 239 } 240 241 - .sidebar-dropdown-item.danger:hover { 242 - background: rgba(239, 68, 68, 0.1); 243 color: var(--error); 244 } 245 246 - .main-layout { 247 - flex: 1; 248 - margin-left: 240px; 249 - margin-right: 280px; 250 - min-height: 100vh; 251 } 252 253 - .main-content-wrapper { 254 - max-width: 640px; 255 - margin: 0 auto; 256 - padding: 40px 24px; 257 - } 258 - 259 - .right-sidebar { 260 - position: fixed; 261 - right: 0; 262 - top: 0; 263 - bottom: 0; 264 - width: 280px; 265 - background: var(--bg-primary); 266 - border-left: 1px solid var(--border); 267 - padding: 32px 24px; 268 - overflow-y: auto; 269 display: flex; 270 - flex-direction: column; 271 - gap: 32px; 272 } 273 274 - .right-section { 275 - display: flex; 276 - flex-direction: column; 277 - gap: 12px; 278 } 279 280 - .right-section-title { 281 - font-size: 0.75rem; 282 - font-weight: 600; 283 - color: var(--text-primary); 284 - margin-bottom: 4px; 285 } 286 287 - .right-section-desc { 288 - font-size: 0.85rem; 289 - line-height: 1.5; 290 - color: var(--text-secondary); 291 } 292 293 - .right-extension-btn { 294 - display: inline-flex; 295 - align-items: center; 296 - gap: 8px; 297 - padding: 8px 12px; 298 - background: var(--bg-primary); 299 - border: 1px solid var(--border); 300 - border-radius: var(--radius-md); 301 - color: var(--text-primary); 302 - font-size: 0.85rem; 303 - font-weight: 500; 304 - text-decoration: none; 305 - transition: all 0.15s ease; 306 - width: fit-content; 307 } 308 309 - .right-extension-btn:hover { 310 - border-color: var(--text-tertiary); 311 - background: var(--bg-tertiary); 312 } 313 314 - .right-links { 315 display: flex; 316 flex-direction: column; 317 - gap: 4px; 318 - } 319 - 320 - .right-link { 321 - display: flex; 322 - align-items: center; 323 - justify-content: space-between; 324 - padding: 6px 0; 325 - color: var(--text-secondary); 326 - font-size: 0.9rem; 327 - transition: color 0.15s; 328 - text-decoration: none; 329 } 330 331 - .right-link:hover { 332 color: var(--text-primary); 333 } 334 335 - .right-link svg { 336 - width: 16px; 337 - height: 16px; 338 color: var(--text-tertiary); 339 - transition: all 0.15s; 340 } 341 342 - .right-link:hover svg { 343 - color: var(--text-secondary); 344 } 345 346 - .tangled-icon { 347 - width: 16px; 348 - height: 16px; 349 - background-color: var(--text-tertiary); 350 - -webkit-mask: url("../assets/tangled.svg") no-repeat center / contain; 351 - mask: url("../assets/tangled.svg") no-repeat center / contain; 352 - transition: background-color 0.15s; 353 - } 354 - 355 - .right-link:hover .tangled-icon { 356 - background-color: var(--text-secondary); 357 } 358 359 - .right-footer { 360 - margin-top: auto; 361 display: flex; 362 align-items: center; 363 - justify-content: space-between; 364 - padding-top: 16px; 365 - border-top: 1px solid var(--border); 366 } 367 368 - .footer-links { 369 - display: flex; 370 - align-items: center; 371 - gap: 8px; 372 - font-size: 12px; 373 - color: var(--text-tertiary); 374 } 375 376 - .footer-links a { 377 - color: var(--text-tertiary); 378 - text-decoration: none; 379 } 380 381 - .footer-links a:hover { 382 - text-decoration: underline; 383 - color: var(--text-secondary); 384 } 385 386 - .theme-toggle-mini { 387 - background: none; 388 - border: none; 389 - cursor: pointer; 390 - padding: 4px; 391 - color: var(--text-tertiary); 392 - display: flex; 393 - align-items: center; 394 - justify-content: center; 395 - border-radius: 4px; 396 - transition: all 0.2s; 397 } 398 399 - .theme-toggle-mini:hover { 400 - color: var(--text-primary); 401 - background: var(--bg-hover); 402 - } 403 - 404 - .mobile-nav { 405 display: none; 406 position: fixed; 407 bottom: 0; ··· 411 backdrop-filter: blur(12px); 412 -webkit-backdrop-filter: blur(12px); 413 border-top: 1px solid var(--border); 414 - padding: 8px 16px; 415 - padding-bottom: calc(8px + env(safe-area-inset-bottom, 0)); 416 z-index: 100; 417 } 418 419 - .mobile-nav-inner { 420 - display: flex; 421 - justify-content: space-between; 422 align-items: center; 423 } 424 425 - .mobile-nav-item { 426 display: flex; 427 flex-direction: column; 428 align-items: center; 429 - justify-content: center; 430 gap: 4px; 431 color: var(--text-tertiary); 432 text-decoration: none; 433 font-size: 0.65rem; 434 font-weight: 500; 435 - width: 60px; 436 transition: color 0.15s; 437 } 438 439 - .mobile-nav-item.active { 440 - color: var(--text-primary); 441 } 442 443 - .mobile-nav-item svg { 444 width: 24px; 445 height: 24px; 446 } 447 448 - .mobile-nav-new { 449 - width: 48px; 450 - height: 36px; 451 - border-radius: var(--radius-md); 452 - background: var(--text-primary); 453 - color: var(--bg-primary); 454 display: flex; 455 align-items: center; 456 justify-content: center; 457 } 458 459 - .mobile-nav-new svg { 460 - width: 20px; 461 - height: 20px; 462 } 463 464 - @media (max-width: 1200px) { 465 - .right-sidebar { 466 - display: none; 467 - } 468 469 - .main-layout { 470 - margin-right: 0; 471 - } 472 } 473 474 - @media (max-width: 768px) { 475 - .sidebar { 476 - display: none; 477 - } 478 479 - .main-layout { 480 - margin-left: 0; 481 - padding-bottom: 80px; 482 - width: 100%; 483 - min-width: 0; 484 - } 485 486 - .main-content-wrapper { 487 - padding: 20px 16px; 488 - max-width: 100%; 489 - width: 100%; 490 - overflow-x: hidden; 491 - min-width: 0; 492 - } 493 494 - .mobile-nav { 495 display: block; 496 - max-width: 100vw; 497 } 498 499 - .card, 500 - .annotation-card, 501 - .collection-card, 502 - .profile-header, 503 - .api-keys-section { 504 - overflow-x: hidden; 505 - max-width: 100%; 506 } 507 508 - code { 509 - word-break: break-all; 510 - overflow-wrap: break-word; 511 - } 512 - 513 - pre { 514 - overflow-x: auto; 515 - max-width: 100%; 516 } 517 518 - input, 519 - textarea { 520 - max-width: 100%; 521 } 522 523 - .flex-row, 524 - [style*="display: flex"][style*="gap"] { 525 - flex-wrap: wrap; 526 } 527 528 - .static-page { 529 - overflow-x: hidden; 530 } 531 532 - .static-page ol, 533 - .static-page ul { 534 - padding-left: 1.25rem; 535 } 536 537 - .static-page code { 538 - font-size: 0.75rem; 539 - word-break: break-all; 540 } 541 }
··· 1 + .app { 2 min-height: 100vh; 3 background: var(--bg-primary); 4 } 5 6 + .top-nav { 7 + position: sticky; 8 top: 0; 9 + z-index: 100; 10 + background: var(--nav-bg); 11 + backdrop-filter: blur(12px); 12 + -webkit-backdrop-filter: blur(12px); 13 + border-bottom: 1px solid var(--border); 14 + } 15 + 16 + .top-nav-inner { 17 + max-width: 1200px; 18 + margin: 0 auto; 19 + padding: 0 32px; 20 + height: 56px; 21 display: flex; 22 + align-items: center; 23 + gap: 32px; 24 } 25 26 + .top-nav-logo { 27 display: flex; 28 align-items: center; 29 + gap: 10px; 30 text-decoration: none; 31 color: var(--text-primary); 32 + font-weight: 700; 33 + font-size: 1.1rem; 34 + flex-shrink: 0; 35 } 36 37 + .top-nav-logo img { 38 + width: 26px; 39 + height: 26px; 40 } 41 42 + .top-nav-links { 43 display: flex; 44 + align-items: center; 45 gap: 4px; 46 + flex: 1; 47 } 48 49 + .top-nav-link { 50 + padding: 8px 14px; 51 color: var(--text-secondary); 52 text-decoration: none; 53 font-size: 0.9rem; 54 font-weight: 500; 55 + border-radius: var(--radius-md); 56 + transition: all 0.15s; 57 } 58 59 + .top-nav-link:hover { 60 color: var(--text-primary); 61 + background: var(--bg-hover); 62 } 63 64 + .top-nav-link.active { 65 + color: var(--text-primary); 66 background: var(--bg-tertiary); 67 } 68 69 + .top-nav-link.extension-link { 70 + display: flex; 71 + align-items: center; 72 + gap: 6px; 73 } 74 75 + .top-nav-actions { 76 display: flex; 77 align-items: center; 78 + gap: 8px; 79 } 80 81 + .top-nav-icon-btn { 82 display: flex; 83 align-items: center; 84 + justify-content: center; 85 + width: 36px; 86 + height: 36px; 87 border-radius: var(--radius-md); 88 + background: transparent; 89 + border: none; 90 + color: var(--text-secondary); 91 + cursor: pointer; 92 + transition: all 0.15s; 93 + position: relative; 94 text-decoration: none; 95 } 96 97 + .top-nav-icon-btn:hover { 98 + background: var(--bg-hover); 99 + color: var(--text-primary); 100 } 101 102 + .notif-dot { 103 + position: absolute; 104 + top: 6px; 105 + right: 6px; 106 + width: 8px; 107 + height: 8px; 108 + background: var(--accent); 109 + border-radius: 50%; 110 + border: 2px solid var(--bg-primary); 111 } 112 113 + .top-nav-new-btn { 114 display: flex; 115 align-items: center; 116 + gap: 6px; 117 + padding: 8px 16px; 118 + background: var(--accent); 119 + color: var(--bg-primary); 120 border-radius: var(--radius-md); 121 + font-size: 0.875rem; 122 + font-weight: 600; 123 + text-decoration: none; 124 + transition: all 0.15s; 125 } 126 127 + .top-nav-new-btn:hover { 128 + background: var(--accent-hover); 129 } 130 131 + .top-nav-avatar { 132 + width: 34px; 133 + height: 34px; 134 + border-radius: var(--radius-md); 135 background: var(--bg-tertiary); 136 + border: none; 137 + cursor: pointer; 138 + overflow: hidden; 139 display: flex; 140 align-items: center; 141 justify-content: center; 142 color: var(--text-secondary); 143 font-size: 0.8rem; 144 + font-weight: 600; 145 + transition: opacity 0.15s; 146 } 147 148 + .top-nav-avatar:hover { 149 + opacity: 0.85; 150 + } 151 + 152 + .top-nav-avatar img { 153 width: 100%; 154 height: 100%; 155 object-fit: cover; 156 } 157 158 + .top-nav-mobile-toggle { 159 + display: none; 160 + align-items: center; 161 + justify-content: center; 162 + width: 40px; 163 + height: 40px; 164 + border: none; 165 + background: transparent; 166 color: var(--text-primary); 167 + cursor: pointer; 168 } 169 170 + .top-nav-dropdown { 171 + position: relative; 172 } 173 174 + .dropdown-menu { 175 position: absolute; 176 + top: calc(100% + 8px); 177 + min-width: 200px; 178 + background: var(--bg-elevated); 179 border: 1px solid var(--border); 180 + border-radius: var(--radius-lg); 181 + padding: 6px; 182 box-shadow: var(--shadow-lg); 183 + z-index: 200; 184 } 185 186 + .dropdown-right { 187 + right: 0; 188 } 189 190 + .dropdown-item { 191 display: flex; 192 align-items: center; 193 gap: 10px; 194 width: 100%; 195 + padding: 10px 12px; 196 + border-radius: var(--radius-md); 197 color: var(--text-secondary); 198 + font-size: 0.875rem; 199 + font-weight: 500; 200 text-decoration: none; 201 transition: all 0.15s; 202 + background: none; 203 border: none; 204 + cursor: pointer; 205 + text-align: left; 206 } 207 208 + .dropdown-item:hover { 209 + background: var(--bg-hover); 210 color: var(--text-primary); 211 } 212 213 + .dropdown-item.danger:hover { 214 + background: rgba(217, 119, 102, 0.12); 215 color: var(--error); 216 } 217 218 + .dropdown-external { 219 + margin-left: auto; 220 + opacity: 0.4; 221 } 222 223 + .tangled-icon-wrapper { 224 + width: 16px; 225 + height: 16px; 226 display: flex; 227 + align-items: center; 228 + justify-content: center; 229 } 230 231 + .tangled-icon-wrapper img { 232 + width: 16px; 233 + height: 16px; 234 + filter: grayscale(100%) brightness(1.5); 235 + opacity: 0.6; 236 + transition: all 0.15s; 237 } 238 239 + .dropdown-item:hover .tangled-icon-wrapper img { 240 + opacity: 0.9; 241 } 242 243 + [data-theme="light"] .tangled-icon-wrapper img { 244 + filter: grayscale(100%) brightness(0) invert(0.35); 245 + opacity: 1; 246 } 247 248 + [data-theme="light"] .dropdown-item:hover .tangled-icon-wrapper img { 249 + filter: grayscale(100%) brightness(0) invert(0.1); 250 + opacity: 1; 251 } 252 253 + .dropdown-divider { 254 + height: 1px; 255 + background: var(--border); 256 + margin: 6px 0; 257 } 258 259 + .dropdown-user-info { 260 + padding: 8px 12px; 261 display: flex; 262 flex-direction: column; 263 + gap: 2px; 264 } 265 266 + .dropdown-user-name { 267 + font-weight: 600; 268 color: var(--text-primary); 269 + font-size: 0.9rem; 270 } 271 272 + .dropdown-user-handle { 273 color: var(--text-tertiary); 274 + font-size: 0.8rem; 275 } 276 277 + .main-content { 278 + max-width: 1300px; 279 + margin: 0 auto; 280 + padding: 32px 56px 80px; 281 } 282 283 + .mobile-menu { 284 + display: none; 285 + position: absolute; 286 + top: 100%; 287 + left: 0; 288 + right: 0; 289 + background: var(--bg-secondary); 290 + border-bottom: 1px solid var(--border); 291 + padding: 12px 16px; 292 } 293 294 + .mobile-menu-link { 295 display: flex; 296 align-items: center; 297 + gap: 12px; 298 + padding: 12px 16px; 299 + color: var(--text-secondary); 300 + text-decoration: none; 301 + font-size: 0.95rem; 302 + font-weight: 500; 303 + border-radius: var(--radius-md); 304 + transition: all 0.15s; 305 } 306 307 + .mobile-menu-link:hover, 308 + .mobile-menu-link.active { 309 + background: var(--bg-hover); 310 + color: var(--text-primary); 311 } 312 313 + .mobile-menu-link.active { 314 + color: var(--accent); 315 } 316 317 + .mobile-menu-divider { 318 + height: 1px; 319 + background: var(--border); 320 + margin: 8px 0; 321 } 322 323 + .notification-badge { 324 + background: var(--accent); 325 + color: var(--bg-primary); 326 + font-size: 0.7rem; 327 + font-weight: 700; 328 + padding: 2px 6px; 329 + border-radius: var(--radius-full); 330 + margin-left: auto; 331 } 332 333 + .mobile-bottom-nav { 334 display: none; 335 position: fixed; 336 bottom: 0; ··· 340 backdrop-filter: blur(12px); 341 -webkit-backdrop-filter: blur(12px); 342 border-top: 1px solid var(--border); 343 + padding: 8px 8px calc(8px + env(safe-area-inset-bottom)); 344 z-index: 100; 345 } 346 347 + .mobile-bottom-nav { 348 + display: none; 349 + justify-content: space-around; 350 align-items: center; 351 } 352 353 + .mobile-bottom-nav-item { 354 display: flex; 355 flex-direction: column; 356 align-items: center; 357 gap: 4px; 358 + padding: 6px 12px; 359 color: var(--text-tertiary); 360 text-decoration: none; 361 font-size: 0.65rem; 362 font-weight: 500; 363 transition: color 0.15s; 364 + min-width: 56px; 365 } 366 367 + .mobile-bottom-nav-item.active { 368 + color: var(--accent); 369 } 370 371 + .mobile-bottom-nav-item:active { 372 + transform: scale(0.95); 373 + } 374 + 375 + .mobile-bottom-nav-new { 376 + padding: 6px 16px; 377 + } 378 + 379 + .mobile-nav-new-btn { 380 + display: flex; 381 + align-items: center; 382 + justify-content: center; 383 + width: 44px; 384 + height: 44px; 385 + background: var(--accent); 386 + color: var(--bg-primary); 387 + border-radius: var(--radius-full); 388 + box-shadow: var(--shadow-md); 389 + } 390 + 391 + .mobile-nav-avatar { 392 width: 24px; 393 height: 24px; 394 + border-radius: var(--radius-full); 395 + object-fit: cover; 396 } 397 398 + .ios-shortcut-banner { 399 + display: none; 400 + position: relative; 401 + padding: 20px; 402 + margin-bottom: 12px; 403 + text-align: center; 404 + } 405 + 406 + .ios-shortcut-banner-close { 407 + position: absolute; 408 + top: 8px; 409 + right: 8px; 410 + background: none; 411 + border: none; 412 + color: var(--text-tertiary); 413 + cursor: pointer; 414 + padding: 6px; 415 display: flex; 416 align-items: center; 417 justify-content: center; 418 + opacity: 0.5; 419 + transition: opacity 0.15s; 420 } 421 422 + .ios-shortcut-banner-close:hover { 423 + opacity: 1; 424 + } 425 + 426 + .ios-shortcut-banner-content { 427 + display: flex; 428 + flex-direction: column; 429 + align-items: center; 430 + gap: 12px; 431 + } 432 + 433 + .ios-shortcut-banner-icon { 434 + display: none; 435 } 436 437 + .ios-shortcut-banner-text { 438 + text-align: center; 439 + } 440 441 + .ios-shortcut-banner-text strong { 442 + display: none; 443 } 444 445 + .ios-shortcut-banner-text p { 446 + font-size: 0.8rem; 447 + color: var(--text-tertiary); 448 + margin: 0; 449 + line-height: 1.4; 450 + } 451 452 + .ios-shortcut-banner-btn { 453 + display: inline-flex; 454 + align-items: center; 455 + gap: 6px; 456 + padding: 10px 20px; 457 + background: transparent; 458 + color: var(--text-secondary); 459 + font-size: 0.85rem; 460 + font-weight: 500; 461 + border: 1px solid var(--border); 462 + border-radius: 100px; 463 + text-decoration: none; 464 + transition: all 0.15s; 465 + } 466 467 + .ios-shortcut-banner-btn:hover { 468 + background: var(--bg-hover); 469 + color: var(--text-primary); 470 + } 471 472 + @media (max-width: 768px) { 473 + .ios-shortcut-banner { 474 display: block; 475 } 476 + } 477 478 + @media (max-width: 768px) { 479 + .top-nav { 480 + display: none; 481 } 482 483 + .mobile-bottom-nav { 484 + display: flex; 485 } 486 487 + .main-content { 488 + padding: 16px 12px 100px; 489 } 490 491 + .feed-container { 492 + border-radius: var(--radius-md); 493 + padding: 4px; 494 } 495 + } 496 497 + @media (max-width: 480px) { 498 + .main-content { 499 + padding: 16px 12px 100px; 500 } 501 502 + .page-title { 503 + font-size: 1.25rem; 504 } 505 506 + .page-description { 507 + font-size: 0.85rem; 508 } 509 }
+170 -174
web/src/css/modals.css
··· 1 .modal-overlay { 2 position: fixed; 3 inset: 0; 4 - background: rgba(0, 0, 0, 0.5); 5 display: flex; 6 align-items: center; 7 justify-content: center; 8 - padding: 16px; 9 - z-index: 50; 10 - animation: fadeIn 0.2s ease-out; 11 } 12 13 .modal-container { 14 background: var(--bg-secondary); 15 border-radius: var(--radius-lg); 16 width: 100%; 17 - max-width: 28rem; 18 border: 1px solid var(--border); 19 box-shadow: var(--shadow-lg); 20 - animation: zoomIn 0.2s ease-out; 21 } 22 23 .modal-header { 24 display: flex; 25 align-items: center; 26 justify-content: space-between; 27 - padding: 16px; 28 border-bottom: 1px solid var(--border); 29 } 30 31 .modal-title { 32 - font-size: 1.25rem; 33 - font-weight: 700; 34 color: var(--text-primary); 35 } 36 37 .modal-close-btn { 38 - padding: 8px; 39 color: var(--text-tertiary); 40 - border-radius: var(--radius-md); 41 - transition: color 0.15s; 42 } 43 44 .modal-close-btn:hover { 45 color: var(--text-primary); 46 - background: var(--bg-hover); 47 } 48 49 .modal-form { 50 - padding: 16px; 51 display: flex; 52 flex-direction: column; 53 - gap: 16px; 54 - } 55 - 56 - .icon-picker-tabs { 57 - display: flex; 58 - gap: 4px; 59 - margin-bottom: 12px; 60 - } 61 - 62 - .icon-picker-tab { 63 - flex: 1; 64 - padding: 8px 12px; 65 - background: var(--bg-primary); 66 - border: 1px solid var(--border); 67 - border-radius: var(--radius-md); 68 - color: var(--text-secondary); 69 - font-size: 0.85rem; 70 - font-weight: 500; 71 - cursor: pointer; 72 - transition: all 0.15s ease; 73 - } 74 - 75 - .icon-picker-tab:hover { 76 - background: var(--bg-tertiary); 77 } 78 79 - .icon-picker-tab.active { 80 - background: var(--accent); 81 - border-color: var(--accent); 82 - color: white; 83 - } 84 - 85 - .emoji-picker-wrapper { 86 display: flex; 87 flex-direction: column; 88 - gap: 10px; 89 - } 90 - 91 - .emoji-custom-input input { 92 - width: 100%; 93 - } 94 - 95 - .emoji-picker, 96 - .icon-picker { 97 - display: flex; 98 - flex-wrap: wrap; 99 - gap: 4px; 100 - max-height: 120px; 101 - overflow-y: auto; 102 - padding: 8px; 103 - background: var(--bg-primary); 104 - border: 1px solid var(--border); 105 - border-radius: var(--radius-md); 106 - } 107 - 108 - .emoji-option, 109 - .icon-option { 110 - width: 36px; 111 - height: 36px; 112 - display: flex; 113 - align-items: center; 114 - justify-content: center; 115 - font-size: 1.2rem; 116 - background: transparent; 117 - border: 2px solid transparent; 118 - border-radius: var(--radius-sm); 119 - cursor: pointer; 120 - transition: all 0.15s ease; 121 - color: var(--text-secondary); 122 - } 123 - 124 - .emoji-option:hover, 125 - .icon-option:hover { 126 - background: var(--bg-tertiary); 127 - transform: scale(1.1); 128 - color: var(--text-primary); 129 - } 130 - 131 - .emoji-option.selected, 132 - .icon-option.selected { 133 - border-color: var(--accent); 134 - background: var(--accent-subtle); 135 - color: var(--accent); 136 } 137 138 .modal-actions { 139 display: flex; 140 justify-content: flex-end; 141 - gap: 12px; 142 - padding-top: 8px; 143 } 144 145 @keyframes fadeIn { 146 from { 147 opacity: 0; 148 } 149 - 150 to { 151 opacity: 1; 152 } 153 } 154 155 - @keyframes zoomIn { 156 from { 157 opacity: 0; 158 - transform: scale(0.95); 159 } 160 - 161 to { 162 opacity: 1; 163 - transform: scale(1); 164 } 165 } 166 ··· 170 171 .form-label { 172 display: block; 173 - font-size: 0.85rem; 174 - font-weight: 600; 175 color: var(--text-secondary); 176 margin-bottom: 6px; 177 } ··· 180 .form-textarea, 181 .form-select { 182 width: 100%; 183 - padding: 8px 12px; 184 background: var(--bg-primary); 185 border: 1px solid var(--border); 186 border-radius: var(--radius-md); 187 color: var(--text-primary); 188 transition: all 0.15s; 189 } 190 ··· 198 199 .form-textarea { 200 resize: none; 201 } 202 203 .input { 204 width: 100%; 205 - padding: 12px 14px; 206 - font-size: 0.95rem; 207 color: var(--text-primary); 208 - background: var(--bg-secondary); 209 border: 1px solid var(--border); 210 border-radius: var(--radius-md); 211 outline: none; ··· 214 215 .input:focus { 216 border-color: var(--accent); 217 - box-shadow: 0 0 0 3px var(--accent-subtle); 218 } 219 220 .input::placeholder { 221 color: var(--text-tertiary); 222 } 223 224 .color-input-container { 225 display: flex; 226 align-items: center; 227 - gap: 12px; 228 background: var(--bg-tertiary); 229 padding: 8px 12px; 230 border-radius: var(--radius-md); ··· 234 235 .color-input-wrapper { 236 position: relative; 237 - width: 32px; 238 - height: 32px; 239 border-radius: var(--radius-full); 240 overflow: hidden; 241 border: 2px solid var(--border); ··· 262 } 263 264 .signup-modal { 265 - background: var(--bg-card); 266 width: 100%; 267 - max-width: 480px; 268 - border-radius: 16px; 269 - padding: 24px; 270 border: 1px solid var(--border); 271 position: relative; 272 max-height: 85vh; 273 overflow-y: auto; 274 - overscroll-behavior: contain; 275 - box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5); 276 } 277 278 .modal-close { 279 position: absolute; 280 - top: 16px; 281 - right: 16px; 282 background: none; 283 border: none; 284 color: var(--text-secondary); 285 cursor: pointer; 286 padding: 4px; 287 - border-radius: 50%; 288 } 289 290 .modal-close:hover { 291 - background: var(--bg-hover); 292 color: var(--text-primary); 293 } 294 295 .signup-step h2 { 296 - font-size: 24px; 297 margin-bottom: 8px; 298 - font-weight: 700; 299 } 300 301 .signup-subtitle { 302 color: var(--text-secondary); 303 - margin-bottom: 24px; 304 } 305 306 .provider-grid { 307 display: grid; 308 grid-template-columns: 1fr; 309 - gap: 12px; 310 } 311 312 .provider-card { 313 display: flex; 314 align-items: center; 315 - gap: 16px; 316 - padding: 16px; 317 border: 1px solid var(--border); 318 - border-radius: 12px; 319 - background: var(--bg-element); 320 cursor: pointer; 321 text-align: left; 322 - transition: all 0.2s ease; 323 } 324 325 .provider-card:hover { 326 border-color: var(--accent); 327 - background: var(--bg-hover); 328 - transform: translateY(-1px); 329 } 330 331 .provider-icon { 332 - width: 48px; 333 - height: 48px; 334 - border-radius: 10px; 335 - background: var(--bg-card); 336 display: flex; 337 align-items: center; 338 justify-content: center; ··· 343 344 .provider-icon.wide { 345 width: auto; 346 - padding: 0 12px; 347 border: none; 348 background: transparent; 349 } 350 351 .provider-icon.wide img { 352 - max-height: 40px !important; 353 - height: 40px !important; 354 width: auto !important; 355 } 356 357 .provider-initial { 358 - font-size: 20px; 359 - font-weight: 700; 360 } 361 362 .provider-info { ··· 365 366 .provider-info h3 { 367 font-weight: 600; 368 - font-size: 16px; 369 margin-bottom: 2px; 370 } 371 372 .provider-info span { 373 color: var(--text-secondary); 374 - font-size: 13px; 375 } 376 377 .provider-arrow { ··· 381 .signup-form { 382 display: flex; 383 flex-direction: column; 384 - gap: 16px; 385 } 386 387 .handle-input-group { 388 display: flex; 389 align-items: center; 390 - gap: 8px; 391 } 392 393 .handle-suffix { 394 color: var(--text-tertiary); 395 - font-size: 14px; 396 white-space: nowrap; 397 } 398 399 .error-message { 400 - color: #ff4444; 401 - background: rgba(255, 68, 68, 0.1); 402 - padding: 12px; 403 - border-radius: 8px; 404 - font-size: 13px; 405 display: flex; 406 align-items: center; 407 - gap: 8px; 408 } 409 410 .step-header { 411 display: flex; 412 align-items: center; 413 - gap: 12px; 414 - margin-bottom: 24px; 415 } 416 417 .step-header h2 { 418 margin: 0; 419 - font-size: 20px; 420 } 421 422 .btn-back { ··· 424 border: none; 425 color: var(--text-secondary); 426 cursor: pointer; 427 - font-size: 14px; 428 padding: 0; 429 } 430 ··· 433 } 434 435 .legal-text { 436 - font-size: 12px; 437 color: var(--text-tertiary); 438 text-align: center; 439 - margin-top: 8px; 440 - } 441 - 442 - .modal-body { 443 - padding: 16px; 444 - display: flex; 445 - flex-direction: column; 446 - gap: 16px; 447 } 448 449 .links-input-group { 450 display: flex; 451 - gap: 8px; 452 - margin-bottom: 8px; 453 } 454 455 .links-input-group input { ··· 462 margin: 0; 463 display: flex; 464 flex-direction: column; 465 - gap: 8px; 466 } 467 468 .link-item { 469 display: flex; 470 align-items: center; 471 - justify-content: map; 472 - gap: 8px; 473 padding: 8px 12px; 474 background: var(--bg-tertiary); 475 border: 1px solid var(--border); 476 border-radius: var(--radius-md); 477 - font-size: 0.9rem; 478 color: var(--text-primary); 479 word-break: break-all; 480 } ··· 489 color: var(--text-tertiary); 490 cursor: pointer; 491 padding: 4px; 492 - border-radius: 4px; 493 display: flex; 494 align-items: center; 495 justify-content: center; 496 - font-size: 1.1rem; 497 line-height: 1; 498 } 499 500 .btn-icon-sm:hover { 501 background: var(--bg-hover); 502 - color: #ff4444; 503 } 504 505 .char-count { 506 text-align: right; 507 - font-size: 0.75rem; 508 color: var(--text-tertiary); 509 margin-top: 4px; 510 }
··· 1 .modal-overlay { 2 position: fixed; 3 inset: 0; 4 + background: rgba(0, 0, 0, 0.6); 5 display: flex; 6 align-items: center; 7 justify-content: center; 8 + padding: var(--spacing-md); 9 + z-index: 100; 10 + animation: fadeIn 0.15s ease-out; 11 } 12 13 .modal-container { 14 background: var(--bg-secondary); 15 border-radius: var(--radius-lg); 16 width: 100%; 17 + max-width: 420px; 18 border: 1px solid var(--border); 19 box-shadow: var(--shadow-lg); 20 + animation: modalIn 0.2s ease-out; 21 } 22 23 .modal-header { 24 display: flex; 25 align-items: center; 26 justify-content: space-between; 27 + padding: var(--spacing-md); 28 border-bottom: 1px solid var(--border); 29 } 30 31 .modal-title { 32 + font-size: 1rem; 33 + font-weight: 600; 34 color: var(--text-primary); 35 } 36 37 .modal-close-btn { 38 + padding: 6px; 39 color: var(--text-tertiary); 40 + border-radius: var(--radius-sm); 41 + transition: all 0.15s; 42 + background: none; 43 + border: none; 44 + cursor: pointer; 45 } 46 47 .modal-close-btn:hover { 48 color: var(--text-primary); 49 + background: var(--bg-tertiary); 50 } 51 52 .modal-form { 53 + padding: var(--spacing-md); 54 display: flex; 55 flex-direction: column; 56 + gap: var(--spacing-md); 57 } 58 59 + .modal-body { 60 + padding: var(--spacing-md); 61 display: flex; 62 flex-direction: column; 63 + gap: var(--spacing-md); 64 } 65 66 .modal-actions { 67 display: flex; 68 justify-content: flex-end; 69 + gap: var(--spacing-sm); 70 + padding-top: var(--spacing-sm); 71 } 72 73 @keyframes fadeIn { 74 from { 75 opacity: 0; 76 } 77 to { 78 opacity: 1; 79 } 80 } 81 82 + @keyframes modalIn { 83 from { 84 opacity: 0; 85 + transform: scale(0.96) translateY(-8px); 86 } 87 to { 88 opacity: 1; 89 + transform: scale(1) translateY(0); 90 } 91 } 92 ··· 96 97 .form-label { 98 display: block; 99 + font-size: 0.8rem; 100 + font-weight: 500; 101 color: var(--text-secondary); 102 margin-bottom: 6px; 103 } ··· 106 .form-textarea, 107 .form-select { 108 width: 100%; 109 + padding: 10px 12px; 110 background: var(--bg-primary); 111 border: 1px solid var(--border); 112 border-radius: var(--radius-md); 113 color: var(--text-primary); 114 + font-size: 0.875rem; 115 transition: all 0.15s; 116 } 117 ··· 125 126 .form-textarea { 127 resize: none; 128 + min-height: 80px; 129 } 130 131 .input { 132 width: 100%; 133 + padding: 10px 12px; 134 + font-size: 0.875rem; 135 color: var(--text-primary); 136 + background: var(--bg-primary); 137 border: 1px solid var(--border); 138 border-radius: var(--radius-md); 139 outline: none; ··· 142 143 .input:focus { 144 border-color: var(--accent); 145 + box-shadow: 0 0 0 2px var(--accent-subtle); 146 } 147 148 .input::placeholder { 149 color: var(--text-tertiary); 150 } 151 152 + .icon-picker-tabs { 153 + display: flex; 154 + gap: 4px; 155 + margin-bottom: var(--spacing-sm); 156 + } 157 + 158 + .icon-picker-tab { 159 + flex: 1; 160 + padding: 8px 12px; 161 + background: var(--bg-tertiary); 162 + border: none; 163 + border-radius: var(--radius-sm); 164 + color: var(--text-secondary); 165 + font-size: 0.8rem; 166 + font-weight: 500; 167 + cursor: pointer; 168 + transition: all 0.15s ease; 169 + } 170 + 171 + .icon-picker-tab:hover { 172 + background: var(--bg-hover); 173 + } 174 + 175 + .icon-picker-tab.active { 176 + background: var(--accent); 177 + color: white; 178 + } 179 + 180 + .emoji-picker-wrapper { 181 + display: flex; 182 + flex-direction: column; 183 + gap: var(--spacing-sm); 184 + } 185 + 186 + .emoji-picker, 187 + .icon-picker { 188 + display: flex; 189 + flex-wrap: wrap; 190 + gap: 4px; 191 + max-height: 120px; 192 + overflow-y: auto; 193 + padding: var(--spacing-sm); 194 + background: var(--bg-primary); 195 + border: 1px solid var(--border); 196 + border-radius: var(--radius-md); 197 + } 198 + 199 + .emoji-option, 200 + .icon-option { 201 + width: 32px; 202 + height: 32px; 203 + display: flex; 204 + align-items: center; 205 + justify-content: center; 206 + font-size: 1rem; 207 + background: transparent; 208 + border: 2px solid transparent; 209 + border-radius: var(--radius-sm); 210 + cursor: pointer; 211 + transition: all 0.15s ease; 212 + color: var(--text-secondary); 213 + } 214 + 215 + .emoji-option:hover, 216 + .icon-option:hover { 217 + background: var(--bg-tertiary); 218 + color: var(--text-primary); 219 + } 220 + 221 + .emoji-option.selected, 222 + .icon-option.selected { 223 + border-color: var(--accent); 224 + background: var(--accent-subtle); 225 + color: var(--accent); 226 + } 227 + 228 .color-input-container { 229 display: flex; 230 align-items: center; 231 + gap: var(--spacing-sm); 232 background: var(--bg-tertiary); 233 padding: 8px 12px; 234 border-radius: var(--radius-md); ··· 238 239 .color-input-wrapper { 240 position: relative; 241 + width: 28px; 242 + height: 28px; 243 border-radius: var(--radius-full); 244 overflow: hidden; 245 border: 2px solid var(--border); ··· 266 } 267 268 .signup-modal { 269 + background: var(--bg-secondary); 270 width: 100%; 271 + max-width: 440px; 272 + border-radius: var(--radius-lg); 273 + padding: var(--spacing-lg); 274 border: 1px solid var(--border); 275 position: relative; 276 max-height: 85vh; 277 overflow-y: auto; 278 + box-shadow: var(--shadow-lg); 279 } 280 281 .modal-close { 282 position: absolute; 283 + top: var(--spacing-md); 284 + right: var(--spacing-md); 285 background: none; 286 border: none; 287 color: var(--text-secondary); 288 cursor: pointer; 289 padding: 4px; 290 + border-radius: var(--radius-sm); 291 } 292 293 .modal-close:hover { 294 + background: var(--bg-tertiary); 295 color: var(--text-primary); 296 } 297 298 .signup-step h2 { 299 + font-size: 1.25rem; 300 margin-bottom: 8px; 301 + font-weight: 600; 302 } 303 304 .signup-subtitle { 305 color: var(--text-secondary); 306 + font-size: 0.875rem; 307 + margin-bottom: var(--spacing-lg); 308 } 309 310 .provider-grid { 311 display: grid; 312 grid-template-columns: 1fr; 313 + gap: var(--spacing-sm); 314 } 315 316 .provider-card { 317 display: flex; 318 align-items: center; 319 + gap: var(--spacing-md); 320 + padding: var(--spacing-md); 321 border: 1px solid var(--border); 322 + border-radius: var(--radius-md); 323 + background: var(--bg-primary); 324 cursor: pointer; 325 text-align: left; 326 + transition: all 0.15s ease; 327 } 328 329 .provider-card:hover { 330 border-color: var(--accent); 331 + background: var(--bg-tertiary); 332 } 333 334 .provider-icon { 335 + width: 40px; 336 + height: 40px; 337 + border-radius: var(--radius-md); 338 + background: var(--bg-tertiary); 339 display: flex; 340 align-items: center; 341 justify-content: center; ··· 346 347 .provider-icon.wide { 348 width: auto; 349 + padding: 0 10px; 350 border: none; 351 background: transparent; 352 } 353 354 .provider-icon.wide img { 355 + max-height: 36px !important; 356 + height: 36px !important; 357 width: auto !important; 358 } 359 360 .provider-initial { 361 + font-size: 1rem; 362 + font-weight: 600; 363 } 364 365 .provider-info { ··· 368 369 .provider-info h3 { 370 font-weight: 600; 371 + font-size: 0.9rem; 372 margin-bottom: 2px; 373 } 374 375 .provider-info span { 376 color: var(--text-secondary); 377 + font-size: 0.8rem; 378 } 379 380 .provider-arrow { ··· 384 .signup-form { 385 display: flex; 386 flex-direction: column; 387 + gap: var(--spacing-md); 388 } 389 390 .handle-input-group { 391 display: flex; 392 align-items: center; 393 + gap: var(--spacing-sm); 394 } 395 396 .handle-suffix { 397 color: var(--text-tertiary); 398 + font-size: 0.85rem; 399 white-space: nowrap; 400 } 401 402 .error-message { 403 + color: var(--error); 404 + background: rgba(255, 69, 58, 0.1); 405 + padding: 10px 12px; 406 + border-radius: var(--radius-md); 407 + font-size: 0.8rem; 408 display: flex; 409 align-items: center; 410 + gap: var(--spacing-sm); 411 } 412 413 .step-header { 414 display: flex; 415 align-items: center; 416 + gap: var(--spacing-sm); 417 + margin-bottom: var(--spacing-lg); 418 } 419 420 .step-header h2 { 421 margin: 0; 422 + font-size: 1.1rem; 423 } 424 425 .btn-back { ··· 427 border: none; 428 color: var(--text-secondary); 429 cursor: pointer; 430 + font-size: 0.85rem; 431 padding: 0; 432 } 433 ··· 436 } 437 438 .legal-text { 439 + font-size: 0.75rem; 440 color: var(--text-tertiary); 441 text-align: center; 442 + margin-top: var(--spacing-sm); 443 } 444 445 .links-input-group { 446 display: flex; 447 + gap: var(--spacing-sm); 448 + margin-bottom: var(--spacing-sm); 449 } 450 451 .links-input-group input { ··· 458 margin: 0; 459 display: flex; 460 flex-direction: column; 461 + gap: var(--spacing-sm); 462 } 463 464 .link-item { 465 display: flex; 466 align-items: center; 467 + justify-content: space-between; 468 + gap: var(--spacing-sm); 469 padding: 8px 12px; 470 background: var(--bg-tertiary); 471 border: 1px solid var(--border); 472 border-radius: var(--radius-md); 473 + font-size: 0.85rem; 474 color: var(--text-primary); 475 word-break: break-all; 476 } ··· 485 color: var(--text-tertiary); 486 cursor: pointer; 487 padding: 4px; 488 + border-radius: var(--radius-sm); 489 display: flex; 490 align-items: center; 491 justify-content: center; 492 + font-size: 1rem; 493 line-height: 1; 494 } 495 496 .btn-icon-sm:hover { 497 background: var(--bg-hover); 498 + color: var(--error); 499 } 500 501 .char-count { 502 text-align: right; 503 + font-size: 0.7rem; 504 color: var(--text-tertiary); 505 margin-top: 4px; 506 }
+29 -31
web/src/css/skeleton.css
··· 2 0% { 3 background-position: -200% 0; 4 } 5 - 6 100% { 7 background-position: 200% 0; 8 } ··· 12 background: linear-gradient( 13 90deg, 14 var(--bg-tertiary) 25%, 15 - var(--bg-secondary) 50%, 16 var(--bg-tertiary) 75% 17 ); 18 background-size: 200% 100%; ··· 21 } 22 23 .skeleton-card { 24 - padding: 24px 0; 25 - border-bottom: 1px solid var(--border); 26 display: flex; 27 flex-direction: column; 28 - gap: 16px; 29 } 30 31 .skeleton-header { 32 display: flex; 33 align-items: center; 34 - gap: 12px; 35 } 36 37 .skeleton-avatar { 38 - width: 36px; 39 - height: 36px; 40 - border-radius: 50%; 41 } 42 43 .skeleton-meta { 44 display: flex; 45 flex-direction: column; 46 - gap: 6px; 47 } 48 49 .skeleton-name { 50 - width: 120px; 51 - height: 14px; 52 } 53 54 .skeleton-handle { 55 - width: 80px; 56 - height: 12px; 57 } 58 59 .skeleton-content { 60 display: flex; 61 flex-direction: column; 62 - gap: 12px; 63 - padding-left: 48px; 64 } 65 66 .skeleton-source { 67 - width: 180px; 68 - height: 24px; 69 - border-radius: var(--radius-full); 70 } 71 72 .skeleton-highlight { 73 width: 100%; 74 - height: 60px; 75 - border-left: 2px solid var(--border); 76 } 77 78 .skeleton-text-1 { 79 - width: 90%; 80 - height: 14px; 81 } 82 83 .skeleton-text-2 { 84 - width: 60%; 85 - height: 14px; 86 } 87 88 .skeleton-actions { 89 display: flex; 90 - gap: 24px; 91 - padding-left: 48px; 92 - margin-top: 4px; 93 } 94 95 .skeleton-action { 96 - width: 24px; 97 - height: 24px; 98 border-radius: var(--radius-sm); 99 } 100 101 - @media (max-width: 600px) { 102 .skeleton-content, 103 .skeleton-actions { 104 padding-left: 0;
··· 2 0% { 3 background-position: -200% 0; 4 } 5 100% { 6 background-position: 200% 0; 7 } ··· 11 background: linear-gradient( 12 90deg, 13 var(--bg-tertiary) 25%, 14 + var(--bg-hover) 50%, 15 var(--bg-tertiary) 75% 16 ); 17 background-size: 200% 100%; ··· 20 } 21 22 .skeleton-card { 23 + padding: var(--spacing-md); 24 display: flex; 25 flex-direction: column; 26 + gap: var(--spacing-sm); 27 } 28 29 .skeleton-header { 30 display: flex; 31 align-items: center; 32 + gap: var(--spacing-sm); 33 } 34 35 .skeleton-avatar { 36 + width: 32px; 37 + height: 32px; 38 + border-radius: var(--radius-full); 39 + flex-shrink: 0; 40 } 41 42 .skeleton-meta { 43 display: flex; 44 flex-direction: column; 45 + gap: 4px; 46 } 47 48 .skeleton-name { 49 + width: 100px; 50 + height: 12px; 51 } 52 53 .skeleton-handle { 54 + width: 70px; 55 + height: 10px; 56 } 57 58 .skeleton-content { 59 display: flex; 60 flex-direction: column; 61 + gap: var(--spacing-sm); 62 + padding-left: 40px; 63 } 64 65 .skeleton-source { 66 + width: 140px; 67 + height: 10px; 68 } 69 70 .skeleton-highlight { 71 width: 100%; 72 + height: 48px; 73 + border-radius: var(--radius-sm); 74 } 75 76 .skeleton-text-1 { 77 + width: 85%; 78 + height: 12px; 79 } 80 81 .skeleton-text-2 { 82 + width: 55%; 83 + height: 12px; 84 } 85 86 .skeleton-actions { 87 display: flex; 88 + gap: var(--spacing-md); 89 + padding-left: 40px; 90 + margin-top: var(--spacing-xs); 91 } 92 93 .skeleton-action { 94 + width: 20px; 95 + height: 20px; 96 border-radius: var(--radius-sm); 97 } 98 99 + @media (max-width: 768px) { 100 .skeleton-content, 101 .skeleton-actions { 102 padding-left: 0;
+9 -7
web/src/css/utilities.css
··· 539 540 .share-menu-container { 541 position: relative; 542 } 543 544 .share-menu { ··· 546 top: 100%; 547 right: 0; 548 margin-top: 8px; 549 - background: var(--bg-primary); 550 border: 1px solid var(--border); 551 border-radius: var(--radius-lg); 552 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 553 - min-width: 180px; 554 - padding: 8px 0; 555 - z-index: 100; 556 animation: fadeInUp 0.15s ease; 557 } 558 ··· 589 padding: 10px 14px; 590 background: none; 591 border: none; 592 width: 100%; 593 text-align: left; 594 - font-size: 0.9rem; 595 color: var(--text-primary); 596 cursor: pointer; 597 transition: all 0.1s ease; 598 } 599 600 .share-menu-item:hover { 601 - background: var(--bg-tertiary); 602 } 603 604 .share-menu-icon {
··· 539 540 .share-menu-container { 541 position: relative; 542 + z-index: 10; 543 } 544 545 .share-menu { ··· 547 top: 100%; 548 right: 0; 549 margin-top: 8px; 550 + background: var(--bg-elevated); 551 border: 1px solid var(--border); 552 border-radius: var(--radius-lg); 553 + box-shadow: var(--shadow-lg); 554 + min-width: 200px; 555 + padding: 8px; 556 + z-index: 1000; 557 animation: fadeInUp 0.15s ease; 558 } 559 ··· 590 padding: 10px 14px; 591 background: none; 592 border: none; 593 + border-radius: var(--radius-md); 594 width: 100%; 595 text-align: left; 596 + font-size: 0.875rem; 597 color: var(--text-primary); 598 cursor: pointer; 599 transition: all 0.1s ease; 600 } 601 602 .share-menu-item:hover { 603 + background: var(--bg-hover); 604 } 605 606 .share-menu-icon {
+1 -1
web/src/index.css
··· 1 @import "./css/layout.css"; 2 @import "./css/base.css"; 3 @import "./css/buttons.css"; 4 - @import "./css/buttons.css"; 5 @import "./css/feed.css"; 6 @import "./css/profile.css"; 7 @import "./css/login.css";
··· 1 @import "./css/layout.css"; 2 @import "./css/base.css"; 3 @import "./css/buttons.css"; 4 + @import "./css/cards.css"; 5 @import "./css/feed.css"; 6 @import "./css/profile.css"; 7 @import "./css/login.css";
+48 -27
web/src/pages/Bookmarks.jsx
··· 10 } from "../api/client"; 11 import { BookmarkIcon } from "../components/Icons"; 12 import BookmarkCard from "../components/BookmarkCard"; 13 import AddToCollectionModal from "../components/AddToCollectionModal"; 14 15 export default function Bookmarks() { ··· 251 )} 252 253 {loadingBookmarks ? ( 254 - <div className="feed"> 255 - {[1, 2, 3].map((i) => ( 256 - <div key={i} className="card"> 257 - <div 258 - className="skeleton skeleton-text" 259 - style={{ width: "40%" }} 260 - ></div> 261 - <div className="skeleton skeleton-text"></div> 262 - <div 263 - className="skeleton skeleton-text" 264 - style={{ width: "60%" }} 265 - ></div> 266 - </div> 267 - ))} 268 </div> 269 ) : error ? ( 270 <div className="empty-state"> ··· 284 </p> 285 </div> 286 ) : ( 287 - <div className="feed"> 288 - {bookmarks.map((bookmark) => ( 289 - <BookmarkCard 290 - key={bookmark.id} 291 - bookmark={bookmark} 292 - onDelete={handleDelete} 293 - onAddToCollection={() => 294 - setCollectionModalState({ 295 - isOpen: true, 296 - uri: bookmark.uri || bookmark.id, 297 - }) 298 } 299 - /> 300 - ))} 301 </div> 302 )} 303 {collectionModalState.isOpen && (
··· 10 } from "../api/client"; 11 import { BookmarkIcon } from "../components/Icons"; 12 import BookmarkCard from "../components/BookmarkCard"; 13 + import CollectionItemCard from "../components/CollectionItemCard"; 14 import AddToCollectionModal from "../components/AddToCollectionModal"; 15 16 export default function Bookmarks() { ··· 252 )} 253 254 {loadingBookmarks ? ( 255 + <div className="feed-container"> 256 + <div className="feed"> 257 + {[1, 2, 3].map((i) => ( 258 + <div key={i} className="card"> 259 + <div 260 + className="skeleton skeleton-text" 261 + style={{ width: "40%" }} 262 + ></div> 263 + <div className="skeleton skeleton-text"></div> 264 + <div 265 + className="skeleton skeleton-text" 266 + style={{ width: "60%" }} 267 + ></div> 268 + </div> 269 + ))} 270 + </div> 271 </div> 272 ) : error ? ( 273 <div className="empty-state"> ··· 287 </p> 288 </div> 289 ) : ( 290 + <div className="feed-container"> 291 + <div className="feed"> 292 + {bookmarks.map((bookmark) => { 293 + if (bookmark.type === "CollectionItem") { 294 + return ( 295 + <CollectionItemCard 296 + key={bookmark.id} 297 + item={bookmark} 298 + onAddToCollection={(uri) => 299 + setCollectionModalState({ 300 + isOpen: true, 301 + uri: uri, 302 + }) 303 + } 304 + /> 305 + ); 306 } 307 + return ( 308 + <BookmarkCard 309 + key={bookmark.id} 310 + bookmark={bookmark} 311 + onDelete={handleDelete} 312 + onAddToCollection={() => 313 + setCollectionModalState({ 314 + isOpen: true, 315 + uri: bookmark.uri || bookmark.id, 316 + }) 317 + } 318 + /> 319 + ); 320 + })} 321 + </div> 322 </div> 323 )} 324 {collectionModalState.isOpen && (
+41 -39
web/src/pages/CollectionDetail.jsx
··· 256 </div> 257 </div> 258 259 - <div className="feed"> 260 - {items.length === 0 ? ( 261 - <div className="empty-state card" style={{ borderStyle: "dashed" }}> 262 - <div className="empty-state-icon"> 263 - <Plus size={32} /> 264 </div> 265 - <h3 className="empty-state-title">Collection is empty</h3> 266 - <p className="empty-state-text"> 267 - {isOwner 268 - ? 'Add items to this collection from your feed or bookmarks using the "Collect" button.' 269 - : "This collection has no items yet."} 270 - </p> 271 - </div> 272 - ) : ( 273 - items.map((item) => ( 274 - <div key={item.uri} className="collection-item-wrapper"> 275 - {isOwner && 276 - !collection.uri.includes("network.cosmik.collection") && ( 277 - <button 278 - onClick={() => handleDeleteItem(item.uri)} 279 - className="collection-item-remove" 280 - title="Remove from collection" 281 - > 282 - <Trash2 size={14} /> 283 - </button> 284 - )} 285 286 - {item.annotation ? ( 287 - <AnnotationCard annotation={item.annotation} /> 288 - ) : item.highlight ? ( 289 - <HighlightCard highlight={item.highlight} /> 290 - ) : item.bookmark ? ( 291 - <BookmarkCard bookmark={item.bookmark} /> 292 - ) : ( 293 - <div className="card" style={{ padding: "16px" }}> 294 - <p className="text-secondary">Item could not be loaded</p> 295 - </div> 296 - )} 297 - </div> 298 - )) 299 - )} 300 </div> 301 302 {isOwner && (
··· 256 </div> 257 </div> 258 259 + <div className="feed-container"> 260 + <div className="feed"> 261 + {items.length === 0 ? ( 262 + <div className="empty-state card" style={{ borderStyle: "dashed" }}> 263 + <div className="empty-state-icon"> 264 + <Plus size={32} /> 265 + </div> 266 + <h3 className="empty-state-title">Collection is empty</h3> 267 + <p className="empty-state-text"> 268 + {isOwner 269 + ? 'Add items to this collection from your feed or bookmarks using the "Collect" button.' 270 + : "This collection has no items yet."} 271 + </p> 272 </div> 273 + ) : ( 274 + items.map((item) => ( 275 + <div key={item.uri} className="collection-item-wrapper"> 276 + {isOwner && 277 + !collection.uri.includes("network.cosmik.collection") && ( 278 + <button 279 + onClick={() => handleDeleteItem(item.uri)} 280 + className="collection-item-remove" 281 + title="Remove from collection" 282 + > 283 + <Trash2 size={14} /> 284 + </button> 285 + )} 286 287 + {item.annotation ? ( 288 + <AnnotationCard annotation={item.annotation} /> 289 + ) : item.highlight ? ( 290 + <HighlightCard highlight={item.highlight} /> 291 + ) : item.bookmark ? ( 292 + <BookmarkCard bookmark={item.bookmark} /> 293 + ) : ( 294 + <div className="card" style={{ padding: "16px" }}> 295 + <p className="text-secondary">Item could not be loaded</p> 296 + </div> 297 + )} 298 + </div> 299 + )) 300 + )} 301 + </div> 302 </div> 303 304 {isOwner && (
+6
web/src/pages/Collections.jsx
··· 38 setEditingCollection(null); 39 }; 40 41 if (loading) { 42 return ( 43 <div className="feed-page"> ··· 121 setEditingCollection(null); 122 }} 123 onSuccess={handleCreateSuccess} 124 collectionToEdit={editingCollection} 125 /> 126 </div>
··· 38 setEditingCollection(null); 39 }; 40 41 + const handleDelete = () => { 42 + fetchCollections(); 43 + setEditingCollection(null); 44 + }; 45 + 46 if (loading) { 47 return ( 48 <div className="feed-page"> ··· 126 setEditingCollection(null); 127 }} 128 onSuccess={handleCreateSuccess} 129 + onDelete={handleDelete} 130 collectionToEdit={editingCollection} 131 /> 132 </div>
+173 -221
web/src/pages/Feed.jsx
··· 1 - import { useState, useEffect } from "react"; 2 import { useSearchParams } from "react-router-dom"; 3 import AnnotationCard, { HighlightCard } from "../components/AnnotationCard"; 4 import BookmarkCard from "../components/BookmarkCard"; 5 import CollectionItemCard from "../components/CollectionItemCard"; 6 import AnnotationSkeleton from "../components/AnnotationSkeleton"; 7 import { getAnnotationFeed, deleteHighlight } from "../api/client"; 8 import { AlertIcon, InboxIcon } from "../components/Icons"; 9 import { useAuth } from "../context/AuthContext"; 10 11 import AddToCollectionModal from "../components/AddToCollectionModal"; 12 ··· 39 uri: null, 40 }); 41 42 - const [showIosBanner, setShowIosBanner] = useState(false); 43 - 44 - useEffect(() => { 45 - const isIOS = 46 - /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 47 - const hasDismissed = localStorage.getItem("iosBannerDismissed"); 48 - 49 - if (isIOS && !hasDismissed) { 50 - setShowIosBanner(true); 51 - } 52 - }, []); 53 - 54 - const dismissIosBanner = () => { 55 - setShowIosBanner(false); 56 - localStorage.setItem("iosBannerDismissed", "true"); 57 - }; 58 - 59 const { user } = useAuth(); 60 61 useEffect(() => { ··· 74 } 75 } 76 77 const data = await getAnnotationFeed( 78 50, 79 0, 80 tagFilter || "", 81 creatorDid, 82 feedType, 83 - filter !== "all" ? filter : "", 84 ); 85 setAnnotations(data.items || []); 86 } catch (err) { ··· 90 } 91 } 92 fetchFeed(); 93 - }, [tagFilter, filter, feedType, user]); 94 95 const filteredAnnotations = 96 feedType === "all" || ··· 99 feedType === "margin" || 100 feedType === "my-feed" 101 ? filter === "all" 102 - ? annotations 103 - : annotations.filter((a) => { 104 if (filter === "commenting") 105 return a.motivation === "commenting" || a.type === "Annotation"; 106 if (filter === "highlighting") ··· 109 return a.motivation === "bookmarking" || a.type === "Bookmark"; 110 return a.motivation === filter; 111 }) 112 - : annotations; 113 114 return ( 115 <div className="feed-page"> 116 <div className="page-header"> 117 <h1 className="page-title">Feed</h1> 118 <p className="page-description"> 119 - See what people are annotating, highlighting, and bookmarking 120 </p> 121 - {tagFilter && ( 122 - <div 123 - style={{ 124 - marginTop: "16px", 125 - display: "flex", 126 - alignItems: "center", 127 - gap: "8px", 128 - }} 129 > 130 - <span 131 - style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }} 132 - > 133 - Filtering by tag: <strong>#{tagFilter}</strong> 134 - </span> 135 <button 136 - onClick={() => 137 - setSearchParams((prev) => { 138 - const next = new URLSearchParams(prev); 139 - next.delete("tag"); 140 - return next; 141 - }) 142 - } 143 - className="btn btn-sm" 144 - style={{ padding: "2px 8px", fontSize: "0.8rem" }} 145 > 146 - Clear 147 </button> 148 - </div> 149 - )} 150 - </div> 151 152 - {showIosBanner && ( 153 - <div 154 - className="ios-banner" 155 - style={{ 156 - background: "var(--bg-secondary)", 157 - border: "1px solid var(--border)", 158 - borderRadius: "var(--radius-md)", 159 - padding: "12px", 160 - marginBottom: "20px", 161 - display: "flex", 162 - alignItems: "center", 163 - justifyContent: "space-between", 164 - gap: "12px", 165 - }} 166 - > 167 - <div style={{ flex: 1 }}> 168 - <h3 169 - style={{ 170 - fontSize: "0.9rem", 171 - fontWeight: 600, 172 - marginBottom: "4px", 173 - }} 174 - > 175 - Get the iOS Shortcut 176 - </h3> 177 - <p style={{ fontSize: "0.8rem", color: "var(--text-secondary)" }}> 178 - Easily save links from Safari using our new shortcut. 179 - </p> 180 - </div> 181 - <div style={{ display: "flex", gap: "8px", alignItems: "center" }}> 182 - <a 183 - href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd" 184 - target="_blank" 185 - rel="noopener noreferrer" 186 - className="btn btn-primary btn-sm" 187 - style={{ whiteSpace: "nowrap" }} 188 - > 189 - Get It 190 - </a> 191 <button 192 - className="btn btn-sm" 193 - onClick={dismissIosBanner} 194 - style={{ 195 - color: "var(--text-tertiary)", 196 - padding: "4px", 197 - height: "auto", 198 - }} 199 > 200 - 201 </button> 202 - </div> 203 </div> 204 - )} 205 - 206 - {} 207 - <div 208 - className="feed-filters" 209 - style={{ 210 - marginBottom: "12px", 211 - borderBottom: "1px solid var(--border)", 212 - }} 213 - > 214 - <button 215 - className={`filter-tab ${feedType === "all" ? "active" : ""}`} 216 - onClick={() => setFeedType("all")} 217 - > 218 - All 219 - </button> 220 - <button 221 - className={`filter-tab ${feedType === "popular" ? "active" : ""}`} 222 - onClick={() => setFeedType("popular")} 223 - > 224 - Popular 225 - </button> 226 - <button 227 - className={`filter-tab ${feedType === "margin" ? "active" : ""}`} 228 - onClick={() => setFeedType("margin")} 229 - > 230 - Margin 231 - </button> 232 - <button 233 - className={`filter-tab ${feedType === "semble" ? "active" : ""}`} 234 - onClick={() => setFeedType("semble")} 235 - > 236 - Semble 237 - </button> 238 - {user && ( 239 - <button 240 - className={`filter-tab ${feedType === "my-feed" ? "active" : ""}`} 241 - onClick={() => setFeedType("my-feed")} 242 - > 243 - My Feed 244 - </button> 245 - )} 246 </div> 247 248 - <div className="feed-filters"> 249 - <button 250 - className={`filter-pill ${filter === "all" ? "active" : ""}`} 251 - onClick={() => setFilter("all")} 252 - > 253 - All Types 254 - </button> 255 - <button 256 - className={`filter-pill ${filter === "commenting" ? "active" : ""}`} 257 - onClick={() => setFilter("commenting")} 258 - > 259 - Annotations 260 - </button> 261 - <button 262 - className={`filter-pill ${filter === "highlighting" ? "active" : ""}`} 263 - onClick={() => setFilter("highlighting")} 264 - > 265 - Highlights 266 - </button> 267 - <button 268 - className={`filter-pill ${filter === "bookmarking" ? "active" : ""}`} 269 - onClick={() => setFilter("bookmarking")} 270 - > 271 - Bookmarks 272 - </button> 273 - </div> 274 275 {loading ? ( 276 - <div className="feed"> 277 - {[1, 2, 3, 4, 5].map((i) => ( 278 - <AnnotationSkeleton key={i} /> 279 - ))} 280 </div> 281 ) : ( 282 <> 283 {error && ( 284 <div className="empty-state"> 285 <div className="empty-state-icon"> 286 - <AlertIcon size={32} /> 287 </div> 288 <h3 className="empty-state-title">Something went wrong</h3> 289 <p className="empty-state-text">{error}</p> ··· 293 {!error && filteredAnnotations.length === 0 && ( 294 <div className="empty-state"> 295 <div className="empty-state-icon"> 296 - <InboxIcon size={32} /> 297 </div> 298 <h3 className="empty-state-title">No items yet</h3> 299 <p className="empty-state-text"> ··· 305 )} 306 307 {!error && filteredAnnotations.length > 0 && ( 308 - <div className="feed"> 309 - {filteredAnnotations.map((item) => { 310 - if (item.type === "CollectionItem") { 311 - return <CollectionItemCard key={item.id} item={item} />; 312 - } 313 - if ( 314 - item.type === "Highlight" || 315 - item.motivation === "highlighting" 316 - ) { 317 - return ( 318 - <HighlightCard 319 - key={item.id} 320 - highlight={item} 321 - onDelete={async (uri) => { 322 - const rkey = uri.split("/").pop(); 323 - await deleteHighlight(rkey); 324 - setAnnotations((prev) => 325 - prev.filter((a) => a.id !== item.id), 326 - ); 327 - }} 328 - onAddToCollection={() => 329 - setCollectionModalState({ 330 - isOpen: true, 331 - uri: item.uri || item.id, 332 - }) 333 - } 334 - /> 335 - ); 336 - } 337 - if ( 338 - item.type === "Bookmark" || 339 - item.motivation === "bookmarking" 340 - ) { 341 return ( 342 - <BookmarkCard 343 key={item.id} 344 - bookmark={item} 345 onAddToCollection={() => 346 setCollectionModalState({ 347 isOpen: true, ··· 350 } 351 /> 352 ); 353 - } 354 - return ( 355 - <AnnotationCard 356 - key={item.id} 357 - annotation={item} 358 - onAddToCollection={() => 359 - setCollectionModalState({ 360 - isOpen: true, 361 - uri: item.uri || item.id, 362 - }) 363 - } 364 - /> 365 - ); 366 - })} 367 </div> 368 )} 369 </>
··· 1 + import { useState, useEffect, useMemo } from "react"; 2 import { useSearchParams } from "react-router-dom"; 3 import AnnotationCard, { HighlightCard } from "../components/AnnotationCard"; 4 import BookmarkCard from "../components/BookmarkCard"; 5 import CollectionItemCard from "../components/CollectionItemCard"; 6 import AnnotationSkeleton from "../components/AnnotationSkeleton"; 7 + import IOSInstallBanner from "../components/IOSInstallBanner"; 8 import { getAnnotationFeed, deleteHighlight } from "../api/client"; 9 import { AlertIcon, InboxIcon } from "../components/Icons"; 10 import { useAuth } from "../context/AuthContext"; 11 + import { X } from "lucide-react"; 12 13 import AddToCollectionModal from "../components/AddToCollectionModal"; 14 ··· 41 uri: null, 42 }); 43 44 const { user } = useAuth(); 45 46 useEffect(() => { ··· 59 } 60 } 61 62 + const motivationMap = { 63 + commenting: "commenting", 64 + highlighting: "highlighting", 65 + bookmarking: "bookmarking", 66 + }; 67 + const motivation = motivationMap[filter] || ""; 68 + 69 const data = await getAnnotationFeed( 70 50, 71 0, 72 tagFilter || "", 73 creatorDid, 74 feedType, 75 + motivation, 76 ); 77 setAnnotations(data.items || []); 78 } catch (err) { ··· 82 } 83 } 84 fetchFeed(); 85 + }, [tagFilter, feedType, filter, user]); 86 + 87 + const deduplicatedAnnotations = useMemo(() => { 88 + const inCollectionUris = new Set(); 89 + for (const item of annotations) { 90 + if (item.type === "CollectionItem") { 91 + const inner = item.annotation || item.highlight || item.bookmark; 92 + if (inner) { 93 + if (inner.uri) inCollectionUris.add(inner.uri.trim()); 94 + if (inner.id) inCollectionUris.add(inner.id.trim()); 95 + } 96 + } 97 + } 98 + 99 + const result = []; 100 + 101 + for (const item of annotations) { 102 + if (item.type !== "CollectionItem") { 103 + const itemUri = (item.uri || "").trim(); 104 + const itemId = (item.id || "").trim(); 105 + if ( 106 + (itemUri && inCollectionUris.has(itemUri)) || 107 + (itemId && inCollectionUris.has(itemId)) 108 + ) { 109 + continue; 110 + } 111 + } 112 + 113 + result.push(item); 114 + } 115 + 116 + return result; 117 + }, [annotations]); 118 119 const filteredAnnotations = 120 feedType === "all" || ··· 123 feedType === "margin" || 124 feedType === "my-feed" 125 ? filter === "all" 126 + ? deduplicatedAnnotations 127 + : deduplicatedAnnotations.filter((a) => { 128 + if (a.type === "CollectionItem") { 129 + if (filter === "commenting") return !!a.annotation; 130 + if (filter === "highlighting") return !!a.highlight; 131 + if (filter === "bookmarking") return !!a.bookmark; 132 + } 133 if (filter === "commenting") 134 return a.motivation === "commenting" || a.type === "Annotation"; 135 if (filter === "highlighting") ··· 138 return a.motivation === "bookmarking" || a.type === "Bookmark"; 139 return a.motivation === filter; 140 }) 141 + : deduplicatedAnnotations; 142 143 return ( 144 <div className="feed-page"> 145 <div className="page-header"> 146 <h1 className="page-title">Feed</h1> 147 <p className="page-description"> 148 + See what people are annotating and bookmarking 149 </p> 150 + </div> 151 + 152 + {tagFilter && ( 153 + <div className="active-filter-banner"> 154 + <span> 155 + Filtering by <strong>#{tagFilter}</strong> 156 + </span> 157 + <button 158 + onClick={() => 159 + setSearchParams((prev) => { 160 + const next = new URLSearchParams(prev); 161 + next.delete("tag"); 162 + return next; 163 + }) 164 + } 165 + className="active-filter-clear" 166 + aria-label="Clear filter" 167 > 168 + <X size={14} /> 169 + </button> 170 + </div> 171 + )} 172 + 173 + <div className="feed-controls"> 174 + <div className="feed-filters"> 175 + {[ 176 + { key: "all", label: "All" }, 177 + { key: "popular", label: "Popular" }, 178 + { key: "margin", label: "Margin" }, 179 + { key: "semble", label: "Semble" }, 180 + ...(user ? [{ key: "my-feed", label: "Mine" }] : []), 181 + ].map(({ key, label }) => ( 182 <button 183 + key={key} 184 + className={`filter-tab ${feedType === key ? "active" : ""}`} 185 + onClick={() => setFeedType(key)} 186 > 187 + {label} 188 </button> 189 + ))} 190 + </div> 191 192 + <div className="feed-filters"> 193 + {[ 194 + { key: "all", label: "All" }, 195 + { key: "commenting", label: "Notes" }, 196 + { key: "highlighting", label: "Highlights" }, 197 + { key: "bookmarking", label: "Bookmarks" }, 198 + ].map(({ key, label }) => ( 199 <button 200 + key={key} 201 + className={`filter-pill ${filter === key ? "active" : ""}`} 202 + onClick={() => setFilter(key)} 203 > 204 + {label} 205 </button> 206 + ))} 207 </div> 208 </div> 209 210 + <IOSInstallBanner /> 211 212 {loading ? ( 213 + <div className="feed-container"> 214 + <div className="feed"> 215 + {[1, 2, 3, 4, 5].map((i) => ( 216 + <AnnotationSkeleton key={i} /> 217 + ))} 218 + </div> 219 </div> 220 ) : ( 221 <> 222 {error && ( 223 <div className="empty-state"> 224 <div className="empty-state-icon"> 225 + <AlertIcon size={24} /> 226 </div> 227 <h3 className="empty-state-title">Something went wrong</h3> 228 <p className="empty-state-text">{error}</p> ··· 232 {!error && filteredAnnotations.length === 0 && ( 233 <div className="empty-state"> 234 <div className="empty-state-icon"> 235 + <InboxIcon size={24} /> 236 </div> 237 <h3 className="empty-state-title">No items yet</h3> 238 <p className="empty-state-text"> ··· 244 )} 245 246 {!error && filteredAnnotations.length > 0 && ( 247 + <div className="feed-container"> 248 + <div className="feed"> 249 + {filteredAnnotations.map((item) => { 250 + if (item.type === "CollectionItem") { 251 + return ( 252 + <CollectionItemCard 253 + key={item.id} 254 + item={item} 255 + onAddToCollection={(uri) => 256 + setCollectionModalState({ 257 + isOpen: true, 258 + uri: uri, 259 + }) 260 + } 261 + /> 262 + ); 263 + } 264 + if ( 265 + item.type === "Highlight" || 266 + item.motivation === "highlighting" 267 + ) { 268 + return ( 269 + <HighlightCard 270 + key={item.id} 271 + highlight={item} 272 + onDelete={async (uri) => { 273 + const rkey = uri.split("/").pop(); 274 + await deleteHighlight(rkey); 275 + setAnnotations((prev) => 276 + prev.filter((a) => a.id !== item.id), 277 + ); 278 + }} 279 + onAddToCollection={() => 280 + setCollectionModalState({ 281 + isOpen: true, 282 + uri: item.uri || item.id, 283 + }) 284 + } 285 + /> 286 + ); 287 + } 288 + if ( 289 + item.type === "Bookmark" || 290 + item.motivation === "bookmarking" 291 + ) { 292 + return ( 293 + <BookmarkCard 294 + key={item.id} 295 + bookmark={item} 296 + onAddToCollection={() => 297 + setCollectionModalState({ 298 + isOpen: true, 299 + uri: item.uri || item.id, 300 + }) 301 + } 302 + /> 303 + ); 304 + } 305 return ( 306 + <AnnotationCard 307 key={item.id} 308 + annotation={item} 309 onAddToCollection={() => 310 setCollectionModalState({ 311 isOpen: true, ··· 314 } 315 /> 316 ); 317 + })} 318 + </div> 319 </div> 320 )} 321 </>
+26 -22
web/src/pages/Highlights.jsx
··· 82 </div> 83 84 {loadingHighlights ? ( 85 - <div className="feed"> 86 - {[1, 2, 3].map((i) => ( 87 - <div key={i} className="card"> 88 - <div 89 - className="skeleton skeleton-text" 90 - style={{ width: "40%" }} 91 - ></div> 92 - <div className="skeleton skeleton-text"></div> 93 - <div 94 - className="skeleton skeleton-text" 95 - style={{ width: "60%" }} 96 - ></div> 97 - </div> 98 - ))} 99 </div> 100 ) : error ? ( 101 <div className="empty-state"> ··· 114 </p> 115 </div> 116 ) : ( 117 - <div className="feed"> 118 - {highlights.map((highlight) => ( 119 - <HighlightCard 120 - key={highlight.id} 121 - highlight={highlight} 122 - onDelete={handleDelete} 123 - /> 124 - ))} 125 </div> 126 )} 127 </div>
··· 82 </div> 83 84 {loadingHighlights ? ( 85 + <div className="feed-container"> 86 + <div className="feed"> 87 + {[1, 2, 3].map((i) => ( 88 + <div key={i} className="card"> 89 + <div 90 + className="skeleton skeleton-text" 91 + style={{ width: "40%" }} 92 + ></div> 93 + <div className="skeleton skeleton-text"></div> 94 + <div 95 + className="skeleton skeleton-text" 96 + style={{ width: "60%" }} 97 + ></div> 98 + </div> 99 + ))} 100 + </div> 101 </div> 102 ) : error ? ( 103 <div className="empty-state"> ··· 116 </p> 117 </div> 118 ) : ( 119 + <div className="feed-container"> 120 + <div className="feed"> 121 + {highlights.map((highlight) => ( 122 + <HighlightCard 123 + key={highlight.id} 124 + highlight={highlight} 125 + onDelete={handleDelete} 126 + /> 127 + ))} 128 + </div> 129 </div> 130 )} 131 </div>
+37 -29
web/src/pages/Profile.jsx
··· 181 if (authLoading) { 182 return ( 183 <div className="profile-page"> 184 - <div className="feed"> 185 - {[1, 2, 3].map((i) => ( 186 - <div key={i} className="card"> 187 - <div 188 - className="skeleton skeleton-text" 189 - style={{ width: "40%" }} 190 - /> 191 - <div className="skeleton skeleton-text" /> 192 - <div 193 - className="skeleton skeleton-text" 194 - style={{ width: "60%" }} 195 - /> 196 - </div> 197 - ))} 198 </div> 199 </div> 200 ); ··· 594 </div> 595 596 {loading && ( 597 - <div className="feed"> 598 - {[1, 2, 3].map((i) => ( 599 - <div key={i} className="card"> 600 - <div 601 - className="skeleton skeleton-text" 602 - style={{ width: "40%" }} 603 - /> 604 - <div className="skeleton skeleton-text" /> 605 - <div 606 - className="skeleton skeleton-text" 607 - style={{ width: "60%" }} 608 - /> 609 - </div> 610 - ))} 611 </div> 612 )} 613 ··· 619 </div> 620 )} 621 622 - {!loading && !error && <div className="feed">{renderContent()}</div>} 623 </div> 624 ); 625 }
··· 181 if (authLoading) { 182 return ( 183 <div className="profile-page"> 184 + <div className="feed-container"> 185 + <div className="feed"> 186 + {[1, 2, 3].map((i) => ( 187 + <div key={i} className="card"> 188 + <div 189 + className="skeleton skeleton-text" 190 + style={{ width: "40%" }} 191 + /> 192 + <div className="skeleton skeleton-text" /> 193 + <div 194 + className="skeleton skeleton-text" 195 + style={{ width: "60%" }} 196 + /> 197 + </div> 198 + ))} 199 + </div> 200 </div> 201 </div> 202 ); ··· 596 </div> 597 598 {loading && ( 599 + <div className="feed-container"> 600 + <div className="feed"> 601 + {[1, 2, 3].map((i) => ( 602 + <div key={i} className="card"> 603 + <div 604 + className="skeleton skeleton-text" 605 + style={{ width: "40%" }} 606 + /> 607 + <div className="skeleton skeleton-text" /> 608 + <div 609 + className="skeleton skeleton-text" 610 + style={{ width: "60%" }} 611 + /> 612 + </div> 613 + ))} 614 + </div> 615 </div> 616 )} 617 ··· 623 </div> 624 )} 625 626 + {!loading && !error && ( 627 + <div className="feed-container"> 628 + <div className="feed">{renderContent()}</div> 629 + </div> 630 + )} 631 </div> 632 ); 633 }
+3 -1
web/src/pages/Url.jsx
··· 380 </div> 381 )} 382 383 - <div className="feed">{renderResults()}</div> 384 </> 385 )} 386 </div>
··· 380 </div> 381 )} 382 383 + <div className="feed-container"> 384 + <div className="feed">{renderResults()}</div> 385 + </div> 386 </> 387 )} 388 </div>
+19 -15
web/src/pages/UserUrl.jsx
··· 163 </div> 164 165 {loading && ( 166 - <div className="feed"> 167 - {[1, 2, 3].map((i) => ( 168 - <div key={i} className="card"> 169 - <div 170 - className="skeleton skeleton-text" 171 - style={{ width: "40%" }} 172 - /> 173 - <div className="skeleton skeleton-text" /> 174 - <div 175 - className="skeleton skeleton-text" 176 - style={{ width: "60%" }} 177 - /> 178 - </div> 179 - ))} 180 </div> 181 )} 182 ··· 227 </button> 228 </div> 229 </div> 230 - <div className="feed">{renderResults()}</div> 231 </> 232 )} 233 </div>
··· 163 </div> 164 165 {loading && ( 166 + <div className="feed-container"> 167 + <div className="feed"> 168 + {[1, 2, 3].map((i) => ( 169 + <div key={i} className="card"> 170 + <div 171 + className="skeleton skeleton-text" 172 + style={{ width: "40%" }} 173 + /> 174 + <div className="skeleton skeleton-text" /> 175 + <div 176 + className="skeleton skeleton-text" 177 + style={{ width: "60%" }} 178 + /> 179 + </div> 180 + ))} 181 + </div> 182 </div> 183 )} 184 ··· 229 </button> 230 </div> 231 </div> 232 + <div className="feed-container"> 233 + <div className="feed">{renderResults()}</div> 234 + </div> 235 </> 236 )} 237 </div>