(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

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

UI redesign + cleanup

scanash00 3920646b a71a8a08

+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>