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