BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

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

feat: deck/column backend with persistence layer

+1484 -7
+463
docs/designs/messages.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Messages - Lazurite</title> 7 + <script src="https://cdn.tailwindcss.com"></script> 8 + <link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet"> 9 + <style> 10 + :root { 11 + --surface-container-lowest: #000000; 12 + --surface: #0e0e0e; 13 + --surface-container: #191919; 14 + --surface-container-high: #1f1f1f; 15 + --surface-container-highest: rgba(36, 36, 36, 0.7); 16 + --surface-bright: rgba(255, 255, 255, 0.05); 17 + --primary: #7dafff; 18 + --primary-dim: #0073de; 19 + --on-primary-fixed: #05080f; 20 + --on-surface: #f4f6fb; 21 + --on-surface-variant: #ababab; 22 + --on-secondary-container: #c9d1dd; 23 + } 24 + 25 + * { box-sizing: border-box; } 26 + 27 + body { 28 + margin: 0; 29 + min-height: 100vh; 30 + font-family: "Google Sans", "Segoe UI", sans-serif; 31 + background: 32 + radial-gradient(circle at 14% 12%, rgba(125, 175, 255, 0.22), transparent 32%), 33 + radial-gradient(circle at 88% 22%, rgba(0, 115, 222, 0.18), transparent 28%), 34 + radial-gradient(circle at 72% 88%, rgba(125, 175, 255, 0.12), transparent 30%), 35 + var(--surface-container-lowest); 36 + color: var(--on-surface); 37 + } 38 + 39 + .panel { 40 + background: rgba(25, 25, 25, 0.6); 41 + border: 1px solid rgba(255, 255, 255, 0.05); 42 + } 43 + 44 + .gradient-btn { 45 + background: linear-gradient(135deg, #7dafff 0%, #0073de 100%); 46 + color: #05080f; 47 + } 48 + 49 + .app-rail { 50 + width: 64px; 51 + background: var(--surface-container-lowest); 52 + } 53 + 54 + .rail-icon { 55 + width: 40px; 56 + height: 40px; 57 + display: flex; 58 + align-items: center; 59 + justify-content: center; 60 + border-radius: 12px; 61 + transition: all 0.15s ease; 62 + } 63 + 64 + .rail-icon.active { 65 + color: var(--primary); 66 + background: rgba(125, 175, 255, 0.1); 67 + } 68 + 69 + .rail-icon:not(.active) { 70 + color: var(--on-surface-variant); 71 + } 72 + 73 + .rail-icon:not(.active):hover { 74 + color: var(--on-surface); 75 + background: rgba(255, 255, 255, 0.05); 76 + } 77 + 78 + .conversation-item { 79 + transition: all 0.15s ease; 80 + } 81 + 82 + .conversation-item:hover { 83 + background: rgba(255, 255, 255, 0.03); 84 + } 85 + 86 + .conversation-item.active { 87 + background: rgba(125, 175, 255, 0.1); 88 + } 89 + 90 + .message-bubble { 91 + max-width: 70%; 92 + word-wrap: break-word; 93 + } 94 + 95 + .message-bubble.sent { 96 + background: linear-gradient(135deg, rgba(125, 175, 255, 0.9) 0%, rgba(0, 115, 222, 0.9) 100%); 97 + color: #05080f; 98 + } 99 + 100 + .message-bubble.received { 101 + background: var(--surface-container-high); 102 + color: var(--on-surface); 103 + } 104 + 105 + .icon-btn:hover { 106 + color: var(--primary); 107 + background: rgba(125, 175, 255, 0.1); 108 + } 109 + 110 + .typing-dot { 111 + animation: typing 1.4s infinite; 112 + } 113 + 114 + .typing-dot:nth-child(2) { animation-delay: 0.2s; } 115 + .typing-dot:nth-child(3) { animation-delay: 0.4s; } 116 + 117 + @keyframes typing { 118 + 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } 119 + 30% { transform: translateY(-4px); opacity: 1; } 120 + } 121 + 122 + .scrollbar-thin::-webkit-scrollbar { 123 + width: 6px; 124 + } 125 + 126 + .scrollbar-thin::-webkit-scrollbar-track { 127 + background: transparent; 128 + } 129 + 130 + .scrollbar-thin::-webkit-scrollbar-thumb { 131 + background: rgba(255, 255, 255, 0.1); 132 + border-radius: 3px; 133 + } 134 + 135 + .scrollbar-thin::-webkit-scrollbar-thumb:hover { 136 + background: rgba(255, 255, 255, 0.2); 137 + } 138 + </style> 139 + </head> 140 + <body class="flex"> 141 + <!-- App Rail --> 142 + <aside class="app-rail fixed left-0 top-0 h-full flex flex-col items-center py-4 z-50"> 143 + <div class="mb-6"> 144 + <svg width="40" height="40" viewBox="0 0 512 512" style="color: #7dafff;"> 145 + <path fill="currentColor" d="M128 16v99.3l119 118.9V120.1zm256 0L265 120.1v114.1l119-119zM16 128l104 119h114.2L115.3 128zm380.8 0l-119 119h114.1l104-119zM120 265L16 384h99.2l119-119zm157.8 0l119 119h99.1l-104-119zM247 277.8l-119 119V496l119-104.1zm18 0v114.1L384 496v-99.2z"/> 146 + </svg> 147 + </div> 148 + 149 + <nav class="flex flex-col gap-1 flex-1"> 150 + <button class="rail-icon" title="Timeline"> 151 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 152 + <rect x="3" y="3" width="7" height="7" rx="1"/> 153 + <rect x="14" y="3" width="7" height="7" rx="1"/> 154 + <rect x="14" y="14" width="7" height="7" rx="1"/> 155 + <rect x="3" y="14" width="7" height="7" rx="1"/> 156 + </svg> 157 + </button> 158 + 159 + <button class="rail-icon" title="Search"> 160 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 161 + <circle cx="11" cy="11" r="8"/> 162 + <path d="m21 21-4.35-4.35"/> 163 + </svg> 164 + </button> 165 + 166 + <button class="rail-icon" title="Notifications"> 167 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 168 + <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/> 169 + <path d="M13.73 21a2 2 0 0 1-3.46 0"/> 170 + </svg> 171 + </button> 172 + 173 + <button class="rail-icon" title="AT Explorer"> 174 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 175 + <circle cx="12" cy="12" r="3"/> 176 + <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.9 10h-6m-6 0H2.1m16.12 4.24l4.24 4.24M6.34 17.66l-4.24 4.24"/> 177 + </svg> 178 + </button> 179 + 180 + <button class="rail-icon active" title="Messages"> 181 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 182 + <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 183 + </svg> 184 + <span class="absolute top-2 right-2 w-2 h-2 rounded-full bg-red-400"></span> 185 + </button> 186 + </nav> 187 + 188 + <div class="flex flex-col gap-2"> 189 + <button class="w-10 h-10 rounded-full overflow-hidden border-2 border-transparent hover:border-white/20 transition-colors"> 190 + <img src="https://placehold.co/40x40/7dafff/05080f?text=U" alt="Current account" class="w-full h-full object-cover"> 191 + </button> 192 + </div> 193 + </aside> 194 + 195 + <!-- Main Content --> 196 + <main class="flex-1 ml-16 flex h-screen"> 197 + <!-- Conversation List Sidebar --> 198 + <aside class="w-80 border-r border-white/5 flex flex-col" style="background: rgba(25, 25, 25, 0.4);"> 199 + <!-- Header --> 200 + <header class="sticky top-0 z-40 panel backdrop-blur-xl border-b border-white/5 p-4"> 201 + <div class="flex items-center justify-between mb-4"> 202 + <h1 class="text-xl font-medium" style="letter-spacing: -0.02em;">Messages</h1> 203 + <button class="p-2 rounded-lg icon-btn text-white/60 transition-all" title="New conversation"> 204 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 205 + <line x1="12" y1="5" x2="12" y2="19"/> 206 + <line x1="5" y1="12" x2="19" y2="12"/> 207 + </svg> 208 + </button> 209 + </div> 210 + 211 + <!-- Search --> 212 + <div class="relative"> 213 + <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--on-surface-variant);"> 214 + <circle cx="11" cy="11" r="8"/> 215 + <path d="m21 21-4.35-4.35"/> 216 + </svg> 217 + <input type="text" 218 + placeholder="Search messages" 219 + class="w-full pl-10 pr-4 py-2 rounded-xl text-sm outline-none transition-all" 220 + style="background: rgba(0,0,0,0.4); color: var(--on-surface);"> 221 + </div> 222 + </header> 223 + 224 + <!-- Conversations --> 225 + <div class="flex-1 overflow-y-auto scrollbar-thin"> 226 + <!-- Conversation 1 - Active --> 227 + <div class="conversation-item active p-4 cursor-pointer border-b border-white/5"> 228 + <div class="flex items-start gap-3"> 229 + <div class="relative"> 230 + <div class="w-12 h-12 rounded-full overflow-hidden"> 231 + <img src="https://placehold.co/48x48/7dafff/05080f?text=AC" alt="Alice" class="w-full h-full object-cover"> 232 + </div> 233 + <span class="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-green-400 border-2 border-black"></span> 234 + </div> 235 + <div class="flex-1 min-w-0"> 236 + <div class="flex items-center justify-between mb-1"> 237 + <span class="font-medium text-sm truncate">Alice Chen</span> 238 + <span class="text-xs" style="color: var(--primary);">2m</span> 239 + </div> 240 + <p class="text-xs truncate" style="color: var(--on-surface);">Just shipped the new timeline view! Check it out when you have a chance</p> 241 + </div> 242 + </div> 243 + </div> 244 + 245 + <!-- Conversation 2 - Unread --> 246 + <div class="conversation-item p-4 cursor-pointer border-b border-white/5"> 247 + <div class="flex items-start gap-3"> 248 + <div class="w-12 h-12 rounded-full overflow-hidden"> 249 + <img src="https://placehold.co/48x48/555/fff?text=BM" alt="Bob" class="w-full h-full object-cover"> 250 + </div> 251 + <div class="flex-1 min-w-0"> 252 + <div class="flex items-center justify-between mb-1"> 253 + <span class="font-medium text-sm truncate">Bob Martinez</span> 254 + <span class="text-xs" style="color: var(--on-surface-variant);">1h</span> 255 + </div> 256 + <p class="text-xs truncate" style="color: var(--on-surface);">The Rust AT Protocol client is coming along nicely!</p> 257 + <span class="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-medium mt-1" style="background: var(--primary); color: var(--on-primary-fixed);">2</span> 258 + </div> 259 + </div> 260 + </div> 261 + 262 + <!-- Conversation 3 --> 263 + <div class="conversation-item p-4 cursor-pointer border-b border-white/5"> 264 + <div class="flex items-start gap-3"> 265 + <div class="w-12 h-12 rounded-full overflow-hidden"> 266 + <img src="https://placehold.co/48x48/666/fff?text=DK" alt="David" class="w-full h-full object-cover"> 267 + </div> 268 + <div class="flex-1 min-w-0"> 269 + <div class="flex items-center justify-between mb-1"> 270 + <span class="font-medium text-sm truncate">David Kim</span> 271 + <span class="text-xs" style="color: var(--on-surface-variant);">3h</span> 272 + </div> 273 + <p class="text-xs truncate" style="color: var(--on-surface-variant);">Let me know what you think about the PR</p> 274 + </div> 275 + </div> 276 + </div> 277 + 278 + <!-- Conversation 4 --> 279 + <div class="conversation-item p-4 cursor-pointer border-b border-white/5"> 280 + <div class="flex items-start gap-3"> 281 + <div class="w-12 h-12 rounded-full overflow-hidden"> 282 + <img src="https://placehold.co/48x48/444/fff?text=EW" alt="Emma" class="w-full h-full object-cover"> 283 + </div> 284 + <div class="flex-1 min-w-0"> 285 + <div class="flex items-center justify-between mb-1"> 286 + <span class="font-medium text-sm truncate">Emma Watson</span> 287 + <span class="text-xs" style="color: var(--on-surface-variant);">Yesterday</span> 288 + </div> 289 + <p class="text-xs truncate" style="color: var(--on-surface-variant);">Thanks for the feedback on the design!</p> 290 + </div> 291 + </div> 292 + </div> 293 + 294 + <!-- Conversation 5 --> 295 + <div class="conversation-item p-4 cursor-pointer border-b border-white/5"> 296 + <div class="flex items-start gap-3"> 297 + <div class="w-12 h-12 rounded-full overflow-hidden"> 298 + <img src="https://placehold.co/48x48/333/fff?text=FO" alt="Frank" class="w-full h-full object-cover"> 299 + </div> 300 + <div class="flex-1 min-w-0"> 301 + <div class="flex items-center justify-between mb-1"> 302 + <span class="font-medium text-sm truncate">Frank Ocean</span> 303 + <span class="text-xs" style="color: var(--on-surface-variant);">2d</span> 304 + </div> 305 + <p class="text-xs truncate" style="color: var(--on-surface-variant);">Sent an attachment</p> 306 + </div> 307 + </div> 308 + </div> 309 + </div> 310 + </aside> 311 + 312 + <!-- Chat Area --> 313 + <div class="flex-1 flex flex-col" style="background: rgba(14, 14, 14, 0.6);"> 314 + <!-- Chat Header --> 315 + <header class="sticky top-0 z-40 panel backdrop-blur-xl border-b border-white/5 p-4"> 316 + <div class="flex items-center justify-between"> 317 + <div class="flex items-center gap-3"> 318 + <div class="relative"> 319 + <div class="w-10 h-10 rounded-full overflow-hidden"> 320 + <img src="https://placehold.co/40x40/7dafff/05080f?text=AC" alt="Alice" class="w-full h-full object-cover"> 321 + </div> 322 + <span class="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-green-400 border-2 border-black"></span> 323 + </div> 324 + <div> 325 + <h2 class="font-medium text-sm">Alice Chen</h2> 326 + <p class="text-xs" style="color: var(--on-surface-variant);">@alice.bsky.social · Active now</p> 327 + </div> 328 + </div> 329 + 330 + <div class="flex items-center gap-1"> 331 + <button class="p-2 rounded-lg icon-btn text-white/60 transition-all" title="Voice call"> 332 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 333 + <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/> 334 + </svg> 335 + </button> 336 + <button class="p-2 rounded-lg icon-btn text-white/60 transition-all" title="Video call"> 337 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 338 + <path d="M23 7l-7 5 7 5V7z"/> 339 + <rect x="1" y="5" width="15" height="14" rx="2"/> 340 + </svg> 341 + </button> 342 + <button class="p-2 rounded-lg icon-btn text-white/60 transition-all" title="More options"> 343 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 344 + <circle cx="12" cy="12" r="1"/> 345 + <circle cx="19" cy="12" r="1"/> 346 + <circle cx="5" cy="12" r="1"/> 347 + </svg> 348 + </button> 349 + </div> 350 + </div> 351 + </header> 352 + 353 + <!-- Messages --> 354 + <div class="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin" id="messages-container"> 355 + <!-- Date divider --> 356 + <div class="flex items-center justify-center my-4"> 357 + <span class="text-xs px-3 py-1 rounded-full" style="color: var(--on-surface-variant); background: rgba(255,255,255,0.05);">Today</span> 358 + </div> 359 + 360 + <!-- Received message --> 361 + <div class="flex items-end gap-2"> 362 + <div class="w-8 h-8 rounded-full overflow-hidden shrink-0"> 363 + <img src="https://placehold.co/32x32/7dafff/05080f?text=AC" alt="Alice" class="w-full h-full object-cover"> 364 + </div> 365 + <div class="message-bubble received px-4 py-2.5 rounded-2xl rounded-bl-md text-sm"> 366 + <p>Hey! How's the new feature coming along?</p> 367 + </div> 368 + <span class="text-xs" style="color: var(--on-surface-variant);">10:30 AM</span> 369 + </div> 370 + 371 + <!-- Sent message --> 372 + <div class="flex items-end gap-2 justify-end"> 373 + <span class="text-xs" style="color: var(--on-surface-variant);">10:32 AM</span> 374 + <div class="message-bubble sent px-4 py-2.5 rounded-2xl rounded-br-md text-sm"> 375 + <p>Going great! Just shipped the new timeline view 🚀</p> 376 + </div> 377 + </div> 378 + 379 + <!-- Sent message with image --> 380 + <div class="flex items-end gap-2 justify-end"> 381 + <span class="text-xs" style="color: var(--on-surface-variant);">10:32 AM</span> 382 + <div class="max-w-[70%]"> 383 + <div class="message-bubble sent p-1 rounded-2xl rounded-br-md overflow-hidden"> 384 + <img src="https://placehold.co/400x200/191919/7dafff?text=Timeline+Screenshot" alt="Screenshot" class="w-full rounded-xl"> 385 + </div> 386 + </div> 387 + </div> 388 + 389 + <!-- Received message --> 390 + <div class="flex items-end gap-2"> 391 + <div class="w-8 h-8 rounded-full overflow-hidden shrink-0"> 392 + <img src="https://placehold.co/32x32/7dafff/05080f?text=AC" alt="Alice" class="w-full h-full object-cover"> 393 + </div> 394 + <div class="message-bubble received px-4 py-2.5 rounded-2xl rounded-bl-md text-sm"> 395 + <p>That looks amazing! The dark theme works so well with the blue accents.</p> 396 + </div> 397 + <span class="text-xs" style="color: var(--on-surface-variant);">10:35 AM</span> 398 + </div> 399 + 400 + <!-- Sent message --> 401 + <div class="flex items-end gap-2 justify-end"> 402 + <span class="text-xs" style="color: var(--on-surface-variant);">10:36 AM</span> 403 + <div class="message-bubble sent px-4 py-2.5 rounded-2xl rounded-br-md text-sm"> 404 + <p>Thanks! Took a lot of iteration to get the contrast right.</p> 405 + </div> 406 + </div> 407 + 408 + <!-- Received message --> 409 + <div class="flex items-end gap-2"> 410 + <div class="w-8 h-8 rounded-full overflow-hidden shrink-0"> 411 + <img src="https://placehold.co/32x32/7dafff/05080f?text=AC" alt="Alice" class="w-full h-full object-cover"> 412 + </div> 413 + <div class="message-bubble received px-4 py-2.5 rounded-2xl rounded-bl-md text-sm"> 414 + <p>Just shipped the new timeline view! Check it out when you have a chance</p> 415 + </div> 416 + <span class="text-xs" style="color: var(--on-surface-variant);">2m</span> 417 + </div> 418 + 419 + <!-- Typing indicator --> 420 + <div class="flex items-end gap-2"> 421 + <div class="w-8 h-8 rounded-full overflow-hidden shrink-0"> 422 + <img src="https://placehold.co/32x32/7dafff/05080f?text=AC" alt="Alice" class="w-full h-full object-cover"> 423 + </div> 424 + <div class="message-bubble received px-4 py-3 rounded-2xl rounded-bl-md flex items-center gap-1"> 425 + <span class="w-2 h-2 rounded-full bg-white/40 typing-dot"></span> 426 + <span class="w-2 h-2 rounded-full bg-white/40 typing-dot"></span> 427 + <span class="w-2 h-2 rounded-full bg-white/40 typing-dot"></span> 428 + </div> 429 + </div> 430 + </div> 431 + 432 + <!-- Message Input --> 433 + <div class="p-4 border-t border-white/5" style="background: rgba(25, 25, 25, 0.6);"> 434 + <div class="flex items-end gap-2"> 435 + <button class="p-3 rounded-full icon-btn text-white/60 transition-all" title="Add attachment"> 436 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 437 + <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/> 438 + </svg> 439 + </button> 440 + 441 + <div class="flex-1 relative"> 442 + <textarea 443 + placeholder="Type a message..." 444 + class="w-full px-4 py-3 rounded-2xl text-sm outline-none resize-none transition-all" 445 + style="background: rgba(0,0,0,0.4); color: var(--on-surface); min-height: 48px; max-height: 120px;" 446 + rows="1" 447 + oninput="this.style.height = ''; this.style.height = Math.min(this.scrollHeight, 120) + 'px'" 448 + ></textarea> 449 + </div> 450 + 451 + <button class="p-3 rounded-full gradient-btn transition-all" title="Send message"> 452 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 453 + <line x1="22" y1="2" x2="11" y2="13"/> 454 + <polygon points="22 2 15 22 11 13 2 9 22 2"/> 455 + </svg> 456 + </button> 457 + </div> 458 + </div> 459 + </div> 460 + </main> 461 + 462 + </body> 463 + </html>
+749
docs/designs/multicol.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Multicolumn - Lazurite</title> 7 + <script src="https://cdn.tailwindcss.com"></script> 8 + <link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet"> 9 + <style> 10 + :root { 11 + --surface-container-lowest: #000000; 12 + --surface: #0e0e0e; 13 + --surface-container: #191919; 14 + --surface-container-high: #1f1f1f; 15 + --surface-container-highest: rgba(36, 36, 36, 0.7); 16 + --surface-bright: rgba(255, 255, 255, 0.05); 17 + --primary: #7dafff; 18 + --primary-dim: #0073de; 19 + --on-primary-fixed: #05080f; 20 + --on-surface: #f4f6fb; 21 + --on-surface-variant: #ababab; 22 + --on-secondary-container: #c9d1dd; 23 + } 24 + 25 + * { box-sizing: border-box; } 26 + 27 + body { 28 + margin: 0; 29 + min-height: 100vh; 30 + font-family: "Google Sans", "Segoe UI", sans-serif; 31 + background: 32 + radial-gradient(circle at 14% 12%, rgba(125, 175, 255, 0.22), transparent 32%), 33 + radial-gradient(circle at 88% 22%, rgba(0, 115, 222, 0.18), transparent 28%), 34 + radial-gradient(circle at 72% 88%, rgba(125, 175, 255, 0.12), transparent 30%), 35 + var(--surface-container-lowest); 36 + color: var(--on-surface); 37 + overflow-x: hidden; 38 + } 39 + 40 + .panel { 41 + background: rgba(25, 25, 25, 0.6); 42 + border: 1px solid rgba(255, 255, 255, 0.05); 43 + } 44 + 45 + .gradient-btn { 46 + background: linear-gradient(135deg, #7dafff 0%, #0073de 100%); 47 + color: #05080f; 48 + } 49 + 50 + .app-rail { 51 + width: 64px; 52 + background: var(--surface-container-lowest); 53 + } 54 + 55 + .rail-icon { 56 + width: 40px; 57 + height: 40px; 58 + display: flex; 59 + align-items: center; 60 + justify-content: center; 61 + border-radius: 12px; 62 + transition: all 0.15s ease; 63 + } 64 + 65 + .rail-icon.active { 66 + color: var(--primary); 67 + background: rgba(125, 175, 255, 0.1); 68 + } 69 + 70 + .rail-icon:not(.active) { 71 + color: var(--on-surface-variant); 72 + } 73 + 74 + .rail-icon:not(.active):hover { 75 + color: var(--on-surface); 76 + background: rgba(255, 255, 255, 0.05); 77 + } 78 + 79 + .column { 80 + flex-shrink: 0; 81 + display: flex; 82 + flex-direction: column; 83 + border-right: 1px solid rgba(255, 255, 255, 0.05); 84 + transition: box-shadow 0.2s ease; 85 + } 86 + 87 + .column.focused { 88 + box-shadow: 0 0 20px rgba(125, 175, 255, 0.15), 0 0 40px rgba(125, 175, 255, 0.08); 89 + border-color: rgba(125, 175, 255, 0.3); 90 + } 91 + 92 + .column.narrow { width: 320px; } 93 + .column.standard { width: 420px; } 94 + .column.wide { width: 560px; } 95 + 96 + .column-header { 97 + cursor: grab; 98 + user-select: none; 99 + } 100 + 101 + .column-header:active { 102 + cursor: grabbing; 103 + } 104 + 105 + .column-content { 106 + flex: 1; 107 + overflow-y: auto; 108 + overflow-x: hidden; 109 + } 110 + 111 + .column-content::-webkit-scrollbar { 112 + width: 4px; 113 + } 114 + 115 + .column-content::-webkit-scrollbar-track { 116 + background: transparent; 117 + } 118 + 119 + .column-content::-webkit-scrollbar-thumb { 120 + background: rgba(255, 255, 255, 0.1); 121 + border-radius: 2px; 122 + } 123 + 124 + .post-card { 125 + transition: background 0.15s ease; 126 + } 127 + 128 + .post-card:hover { 129 + background: rgba(255, 255, 255, 0.03); 130 + } 131 + 132 + .icon-btn:hover { 133 + color: var(--primary); 134 + background: rgba(125, 175, 255, 0.1); 135 + } 136 + 137 + .explorer-node { 138 + transition: all 0.15s ease; 139 + } 140 + 141 + .explorer-node:hover { 142 + background: rgba(255, 255, 255, 0.03); 143 + } 144 + 145 + .json-key { color: #7dafff; } 146 + .json-string { color: #4cd964; } 147 + .json-number { color: #ff9500; } 148 + 149 + .add-column-btn { 150 + flex-shrink: 0; 151 + transition: all 0.2s ease; 152 + } 153 + 154 + .add-column-btn:hover { 155 + background: rgba(125, 175, 255, 0.1); 156 + border-color: rgba(125, 175, 255, 0.3); 157 + } 158 + 159 + .column-picker { 160 + animation: scaleIn 0.2s ease; 161 + } 162 + 163 + @keyframes scaleIn { 164 + from { opacity: 0; transform: scale(0.95); } 165 + to { opacity: 1; transform: scale(1); } 166 + } 167 + 168 + .diagnostics-tab { 169 + transition: all 0.15s ease; 170 + } 171 + 172 + .diagnostics-tab.active { 173 + color: var(--primary); 174 + border-bottom: 2px solid var(--primary); 175 + } 176 + 177 + .diagnostics-tab:not(.active):hover { 178 + color: var(--on-surface); 179 + } 180 + </style> 181 + </head> 182 + <body class="flex"> 183 + <!-- App Rail --> 184 + <aside class="app-rail fixed left-0 top-0 h-full flex flex-col items-center py-4 z-50"> 185 + <div class="mb-6"> 186 + <svg width="40" height="40" viewBox="0 0 512 512" style="color: #7dafff;"> 187 + <path fill="currentColor" d="M128 16v99.3l119 118.9V120.1zm256 0L265 120.1v114.1l119-119zM16 128l104 119h114.2L115.3 128zm380.8 0l-119 119h114.1l104-119zM120 265L16 384h99.2l119-119zm157.8 0l119 119h99.1l-104-119zM247 277.8l-119 119V496l119-104.1zm18 0v114.1L384 496v-99.2z"/> 188 + </svg> 189 + </div> 190 + 191 + <nav class="flex flex-col gap-1 flex-1"> 192 + <button class="rail-icon" title="Timeline"> 193 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 194 + <rect x="3" y="3" width="7" height="7" rx="1"/> 195 + <rect x="14" y="3" width="7" height="7" rx="1"/> 196 + <rect x="14" y="14" width="7" height="7" rx="1"/> 197 + <rect x="3" y="14" width="7" height="7" rx="1"/> 198 + </svg> 199 + </button> 200 + 201 + <button class="rail-icon" title="Search"> 202 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 203 + <circle cx="11" cy="11" r="8"/> 204 + <path d="m21 21-4.35-4.35"/> 205 + </svg> 206 + </button> 207 + 208 + <button class="rail-icon" title="Notifications"> 209 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 210 + <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/> 211 + <path d="M13.73 21a2 2 0 0 1-3.46 0"/> 212 + </svg> 213 + </button> 214 + 215 + <button class="rail-icon" title="AT Explorer"> 216 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 217 + <circle cx="12" cy="12" r="3"/> 218 + <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.9 10h-6m-6 0H2.1m16.12 4.24l4.24 4.24M6.34 17.66l-4.24 4.24"/> 219 + </svg> 220 + </button> 221 + 222 + <button class="rail-icon active" title="Multicolumn"> 223 + <svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 224 + <rect x="3" y="3" width="7" height="18" rx="1"/> 225 + <rect x="14" y="3" width="7" height="18" rx="1"/> 226 + </svg> 227 + </button> 228 + </nav> 229 + 230 + <div class="flex flex-col gap-2"> 231 + <button class="w-10 h-10 rounded-full overflow-hidden border-2 border-transparent hover:border-white/20 transition-colors"> 232 + <img src="https://placehold.co/40x40/7dafff/05080f?text=U" alt="Current account" class="w-full h-full object-cover"> 233 + </button> 234 + </div> 235 + </aside> 236 + 237 + <!-- Main Content --> 238 + <main class="flex-1 ml-16 flex flex-col h-screen"> 239 + <!-- Toolbar --> 240 + <header class="sticky top-0 z-40 panel backdrop-blur-xl border-b border-white/5 px-4 py-3"> 241 + <div class="flex items-center justify-between"> 242 + <div class="flex items-center gap-3"> 243 + <h1 class="text-lg font-medium">Deck</h1> 244 + <span class="text-xs px-2 py-1 rounded-full" style="background: rgba(125, 175, 255, 0.15); color: var(--primary);">4 columns</span> 245 + </div> 246 + 247 + <div class="flex items-center gap-2"> 248 + <button class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm transition-all text-white/60 hover:text-white hover:bg-white/5"> 249 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 250 + <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/> 251 + </svg> 252 + <span>Save Layout</span> 253 + </button> 254 + <button class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm transition-all text-white/60 hover:text-white hover:bg-white/5"> 255 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 256 + <path d="M4 7V4h3M4 17v3h3M20 7V4h-3M20 17v3h-3M9 9h6v6H9z"/> 257 + </svg> 258 + <span>Clear All</span> 259 + </button> 260 + </div> 261 + </div> 262 + </header> 263 + 264 + <!-- Columns Container --> 265 + <div class="flex-1 flex overflow-x-auto overflow-y-hidden" id="columns-container"> 266 + <!-- Column 1: Following Feed --> 267 + <div class="column standard focused"> 268 + <!-- Column Header --> 269 + <div class="column-header panel border-b border-white/5 p-3 flex items-center justify-between"> 270 + <div class="flex items-center gap-2"> 271 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5 cursor-grab"> 272 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 273 + <circle cx="9" cy="12" r="1"/> 274 + <circle cx="9" cy="5" r="1"/> 275 + <circle cx="9" cy="19" r="1"/> 276 + <circle cx="15" cy="12" r="1"/> 277 + <circle cx="15" cy="5" r="1"/> 278 + <circle cx="15" cy="19" r="1"/> 279 + </svg> 280 + </button> 281 + <div class="flex items-center gap-2"> 282 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 283 + <rect x="3" y="3" width="7" height="7" rx="1"/> 284 + <rect x="14" y="3" width="7" height="7" rx="1"/> 285 + <rect x="14" y="14" width="7" height="7" rx="1"/> 286 + <rect x="3" y="14" width="7" height="7" rx="1"/> 287 + </svg> 288 + <span class="font-medium text-sm">Following</span> 289 + </div> 290 + </div> 291 + <div class="flex items-center gap-1"> 292 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5" title="Resize"> 293 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 294 + <rect x="3" y="3" width="18" height="18" rx="2"/> 295 + <path d="M9 3v18"/> 296 + </svg> 297 + </button> 298 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5" title="Settings"> 299 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 300 + <circle cx="12" cy="12" r="1"/> 301 + <circle cx="19" cy="12" r="1"/> 302 + <circle cx="5" cy="12" r="1"/> 303 + </svg> 304 + </button> 305 + <button class="p-1.5 rounded-lg text-white/40 hover:text-red-400 hover:bg-red-500/10" title="Close"> 306 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 307 + <line x1="18" y1="6" x2="6" y2="18"/> 308 + <line x1="6" y1="6" x2="18" y2="18"/> 309 + </svg> 310 + </button> 311 + </div> 312 + </div> 313 + 314 + <!-- Column Content --> 315 + <div class="column-content"> 316 + <!-- Post 1 --> 317 + <article class="post-card p-4 border-b border-white/5"> 318 + <div class="flex gap-3"> 319 + <div class="w-9 h-9 rounded-full overflow-hidden shrink-0"> 320 + <img src="https://placehold.co/36x36/7dafff/05080f?text=AC" alt="Author" class="w-full h-full object-cover"> 321 + </div> 322 + <div class="flex-1 min-w-0"> 323 + <div class="flex items-center gap-2 mb-1"> 324 + <span class="font-medium text-sm">Alice Chen</span> 325 + <span class="text-xs" style="color: var(--on-surface-variant);">@alice.bsky.social</span> 326 + <span class="text-xs" style="color: var(--on-surface-variant);">· 2h</span> 327 + </div> 328 + <p class="text-sm leading-relaxed" style="color: var(--on-secondary-container);"> 329 + Just published a new article about AT Protocol architecture! 🧵 330 + </p> 331 + <div class="flex items-center gap-4 mt-3"> 332 + <button class="flex items-center gap-1 text-xs" style="color: var(--on-surface-variant);"> 333 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 334 + <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 335 + </svg> 336 + 12 337 + </button> 338 + <button class="flex items-center gap-1 text-xs" style="color: var(--primary);"> 339 + <svg width="14" height="14" fill="currentColor" viewBox="0 0 24 24"> 340 + <path d="M7 10v12l4-4 4 4V10"/> 341 + <path d="M5 10a7 7 0 0 1 14 0"/> 342 + </svg> 343 + 48 344 + </button> 345 + </div> 346 + </div> 347 + </div> 348 + </article> 349 + 350 + <!-- Post 2 --> 351 + <article class="post-card p-4 border-b border-white/5"> 352 + <div class="flex gap-3"> 353 + <div class="w-9 h-9 rounded-full overflow-hidden shrink-0"> 354 + <img src="https://placehold.co/36x36/555/fff?text=BM" alt="Author" class="w-full h-full object-cover"> 355 + </div> 356 + <div class="flex-1 min-w-0"> 357 + <div class="flex items-center gap-2 mb-1"> 358 + <span class="font-medium text-sm">Bob Martinez</span> 359 + <span class="text-xs" style="color: var(--on-surface-variant);">@bob.dev</span> 360 + <span class="text-xs" style="color: var(--on-surface-variant);">· 4h</span> 361 + </div> 362 + <p class="text-sm leading-relaxed" style="color: var(--on-secondary-container);"> 363 + Working on a new Rust project for handling AT Protocol lexicons 🔧 364 + </p> 365 + <div class="rounded-xl overflow-hidden mt-3 border border-white/5"> 366 + <img src="https://placehold.co/380x180/1a1a1a/7dafff?text=Code+Preview" alt="Code" class="w-full object-cover"> 367 + </div> 368 + </div> 369 + </div> 370 + </article> 371 + 372 + <!-- Post 3 --> 373 + <article class="post-card p-4 border-b border-white/5"> 374 + <div class="flex gap-3"> 375 + <div class="w-9 h-9 rounded-full overflow-hidden shrink-0"> 376 + <img src="https://placehold.co/36x36/666/fff?text=DK" alt="Author" class="w-full h-full object-cover"> 377 + </div> 378 + <div class="flex-1 min-w-0"> 379 + <div class="flex items-center gap-2 mb-1"> 380 + <span class="font-medium text-sm">David Kim</span> 381 + <span class="text-xs" style="color: var(--on-surface-variant);">@david.kim</span> 382 + <span class="text-xs" style="color: var(--on-surface-variant);">· 6h</span> 383 + </div> 384 + <p class="text-sm leading-relaxed" style="color: var(--on-secondary-container);"> 385 + Protocols over platforms every time. The future is interoperable. 386 + </p> 387 + </div> 388 + </div> 389 + </article> 390 + </div> 391 + </div> 392 + 393 + <!-- Column 2: Discover Feed --> 394 + <div class="column standard"> 395 + <div class="column-header panel border-b border-white/5 p-3 flex items-center justify-between"> 396 + <div class="flex items-center gap-2"> 397 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5 cursor-grab"> 398 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 399 + <circle cx="9" cy="12" r="1"/> 400 + <circle cx="9" cy="5" r="1"/> 401 + <circle cx="9" cy="19" r="1"/> 402 + <circle cx="15" cy="12" r="1"/> 403 + <circle cx="15" cy="5" r="1"/> 404 + <circle cx="15" cy="19" r="1"/> 405 + </svg> 406 + </button> 407 + <div class="flex items-center gap-2"> 408 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 409 + <circle cx="11" cy="11" r="8"/> 410 + <path d="m21 21-4.35-4.35"/> 411 + </svg> 412 + <span class="font-medium text-sm">Discover</span> 413 + </div> 414 + </div> 415 + <div class="flex items-center gap-1"> 416 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5"> 417 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 418 + <rect x="3" y="3" width="18" height="18" rx="2"/> 419 + <path d="M9 3v18"/> 420 + </svg> 421 + </button> 422 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5"> 423 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 424 + <circle cx="12" cy="12" r="1"/> 425 + <circle cx="19" cy="12" r="1"/> 426 + <circle cx="5" cy="12" r="1"/> 427 + </svg> 428 + </button> 429 + <button class="p-1.5 rounded-lg text-white/40 hover:text-red-400 hover:bg-red-500/10"> 430 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 431 + <line x1="18" y1="6" x2="6" y2="18"/> 432 + <line x1="6" y1="6" x2="18" y2="18"/> 433 + </svg> 434 + </button> 435 + </div> 436 + </div> 437 + 438 + <div class="column-content"> 439 + <article class="post-card p-4 border-b border-white/5"> 440 + <div class="flex gap-3"> 441 + <div class="w-9 h-9 rounded-full overflow-hidden shrink-0"> 442 + <img src="https://placehold.co/36x36/444/fff?text=TC" alt="Author" class="w-full h-full object-cover"> 443 + </div> 444 + <div class="flex-1 min-w-0"> 445 + <div class="flex items-center gap-2 mb-1"> 446 + <span class="font-medium text-sm">Tech Crunch</span> 447 + <span class="text-xs" style="color: var(--on-surface-variant);">@techcrunch.bsky.social</span> 448 + <span class="text-xs" style="color: var(--on-surface-variant);">· 1h</span> 449 + </div> 450 + <p class="text-sm leading-relaxed" style="color: var(--on-secondary-container);"> 451 + Bluesky reaches 20 million users milestone 🎉 452 + </p> 453 + <div class="flex items-center gap-4 mt-3"> 454 + <button class="flex items-center gap-1 text-xs" style="color: var(--on-surface-variant);"> 455 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 456 + <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 457 + </svg> 458 + 234 459 + </button> 460 + <button class="flex items-center gap-1 text-xs" style="color: var(--on-surface-variant);"> 461 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 462 + <path d="M7 10v12l4-4 4 4V10"/> 463 + <path d="M5 10a7 7 0 0 1 14 0"/> 464 + </svg> 465 + 1.2K 466 + </button> 467 + </div> 468 + </div> 469 + </div> 470 + </article> 471 + 472 + <article class="post-card p-4 border-b border-white/5"> 473 + <div class="flex gap-3"> 474 + <div class="w-9 h-9 rounded-full overflow-hidden shrink-0"> 475 + <img src="https://placehold.co/36x36/333/fff?text=RS" alt="Author" class="w-full h-full object-cover"> 476 + </div> 477 + <div class="flex-1 min-w-0"> 478 + <div class="flex items-center gap-2 mb-1"> 479 + <span class="font-medium text-sm">Rust Society</span> 480 + <span class="text-xs" style="color: var(--on-surface-variant);">@rust-lang.org</span> 481 + <span class="text-xs" style="color: var(--on-surface-variant);">· 3h</span> 482 + </div> 483 + <p class="text-sm leading-relaxed" style="color: var(--on-secondary-container);"> 484 + Rust 1.75 is out with major async improvements! 🦀 485 + </p> 486 + </div> 487 + </div> 488 + </article> 489 + </div> 490 + </div> 491 + 492 + <!-- Column 3: AT Explorer --> 493 + <div class="column wide"> 494 + <div class="column-header panel border-b border-white/5 p-3 flex items-center justify-between"> 495 + <div class="flex items-center gap-2"> 496 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5 cursor-grab"> 497 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 498 + <circle cx="9" cy="12" r="1"/> 499 + <circle cx="9" cy="5" r="1"/> 500 + <circle cx="9" cy="19" r="1"/> 501 + <circle cx="15" cy="12" r="1"/> 502 + <circle cx="15" cy="5" r="1"/> 503 + <circle cx="15" cy="19" r="1"/> 504 + </svg> 505 + </button> 506 + <div class="flex items-center gap-2"> 507 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 508 + <circle cx="12" cy="12" r="3"/> 509 + <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.9 10h-6m-6 0H2.1m16.12 4.24l4.24 4.24M6.34 17.66l-4.24 4.24"/> 510 + </svg> 511 + <span class="font-medium text-sm">AT Explorer</span> 512 + </div> 513 + </div> 514 + <div class="flex items-center gap-1"> 515 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5"> 516 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 517 + <rect x="3" y="3" width="18" height="18" rx="2"/> 518 + <path d="M9 3v18"/> 519 + </svg> 520 + </button> 521 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5"> 522 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 523 + <circle cx="12" cy="12" r="1"/> 524 + <circle cx="19" cy="12" r="1"/> 525 + <circle cx="5" cy="12" r="1"/> 526 + </svg> 527 + </button> 528 + <button class="p-1.5 rounded-lg text-white/40 hover:text-red-400 hover:bg-red-500/10"> 529 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 530 + <line x1="18" y1="6" x2="6" y2="18"/> 531 + <line x1="6" y1="6" x2="18" y2="18"/> 532 + </svg> 533 + </button> 534 + </div> 535 + </div> 536 + 537 + <div class="column-content p-4"> 538 + <!-- Breadcrumb --> 539 + <div class="flex items-center gap-2 text-sm mb-4 pb-3 border-b border-white/5"> 540 + <button class="flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-white/5 transition-colors" style="color: var(--primary);"> 541 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 542 + <rect x="2" y="2" width="20" height="8" rx="2"/> 543 + <rect x="2" y="14" width="20" height="8" rx="2"/> 544 + </svg> 545 + PDS 546 + </button> 547 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--on-surface-variant);"> 548 + <path d="m9 18 6-6-6-6"/> 549 + </svg> 550 + <button class="flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-white/5 transition-colors" style="color: var(--primary);"> 551 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 552 + <circle cx="12" cy="12" r="10"/> 553 + <circle cx="12" cy="10" r="3"/> 554 + <path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/> 555 + </svg> 556 + Repo 557 + </button> 558 + <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--on-surface-variant);"> 559 + <path d="m9 18 6-6-6-6"/> 560 + </svg> 561 + <span class="px-2 py-1" style="color: var(--on-surface);">app.bsky.feed.post</span> 562 + </div> 563 + 564 + <!-- URL Bar --> 565 + <div class="flex items-center gap-2 px-3 py-2 rounded-lg mb-4" style="background: rgba(0,0,0,0.4);"> 566 + <span class="text-xs font-mono" style="color: var(--primary);">at://</span> 567 + <input type="text" value="did:plc:abc123/app.bsky.feed.post" class="flex-1 bg-transparent text-xs font-mono outline-none" style="color: var(--on-surface);"> 568 + </div> 569 + 570 + <!-- Collections List --> 571 + <div class="space-y-1"> 572 + <div class="explorer-node flex items-center gap-3 p-3 rounded-xl cursor-pointer"> 573 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 574 + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> 575 + <polyline points="14 2 14 8 20 8"/> 576 + </svg> 577 + <div class="flex-1"> 578 + <p class="text-sm font-medium">app.bsky.actor.profile</p> 579 + <p class="text-xs" style="color: var(--on-surface-variant);">1 record</p> 580 + </div> 581 + </div> 582 + 583 + <div class="explorer-node flex items-center gap-3 p-3 rounded-xl cursor-pointer bg-white/5"> 584 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 585 + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> 586 + <polyline points="14 2 14 8 20 8"/> 587 + </svg> 588 + <div class="flex-1"> 589 + <p class="text-sm font-medium">app.bsky.feed.post</p> 590 + <p class="text-xs" style="color: var(--on-surface-variant);">1,247 records</p> 591 + </div> 592 + </div> 593 + 594 + <div class="explorer-node flex items-center gap-3 p-3 rounded-xl cursor-pointer"> 595 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 596 + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> 597 + <polyline points="14 2 14 8 20 8"/> 598 + </svg> 599 + <div class="flex-1"> 600 + <p class="text-sm font-medium">app.bsky.feed.like</p> 601 + <p class="text-xs" style="color: var(--on-surface-variant);">8,532 records</p> 602 + </div> 603 + </div> 604 + </div> 605 + 606 + <!-- JSON Preview --> 607 + <div class="mt-4 rounded-xl border border-white/10 overflow-hidden"> 608 + <div class="px-3 py-2 border-b border-white/5" style="background: rgba(0,0,0,0.3);"> 609 + <span class="text-xs font-medium">Record Preview</span> 610 + </div> 611 + <div class="p-3 overflow-x-auto"> 612 + <pre class="text-xs font-mono leading-relaxed"><span class="json-key">"$type"</span>: <span class="json-string">"app.bsky.feed.post"</span>, 613 + <span class="json-key">"text"</span>: <span class="json-string">"Hello AT Protocol!"</span>, 614 + <span class="json-key">"createdAt"</span>: <span class="json-string">"2024-03-15T10:30:00Z"</span></pre> 615 + </div> 616 + </div> 617 + </div> 618 + </div> 619 + 620 + <!-- Column 4: Diagnostics --> 621 + <div class="column standard"> 622 + <div class="column-header panel border-b border-white/5 p-3 flex items-center justify-between"> 623 + <div class="flex items-center gap-2"> 624 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5 cursor-grab"> 625 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 626 + <circle cx="9" cy="12" r="1"/> 627 + <circle cx="9" cy="5" r="1"/> 628 + <circle cx="9" cy="19" r="1"/> 629 + <circle cx="15" cy="12" r="1"/> 630 + <circle cx="15" cy="5" r="1"/> 631 + <circle cx="15" cy="19" r="1"/> 632 + </svg> 633 + </button> 634 + <div class="flex items-center gap-2"> 635 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 636 + <circle cx="12" cy="12" r="10"/> 637 + <line x1="12" y1="8" x2="12" y2="12"/> 638 + <line x1="12" y1="16" x2="12.01" y2="16"/> 639 + </svg> 640 + <span class="font-medium text-sm">Diagnostics</span> 641 + </div> 642 + </div> 643 + <div class="flex items-center gap-1"> 644 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5"> 645 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 646 + <rect x="3" y="3" width="18" height="18" rx="2"/> 647 + <path d="M9 3v18"/> 648 + </svg> 649 + </button> 650 + <button class="p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/5"> 651 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 652 + <circle cx="12" cy="12" r="1"/> 653 + <circle cx="19" cy="12" r="1"/> 654 + <circle cx="5" cy="12" r="1"/> 655 + </svg> 656 + </button> 657 + <button class="p-1.5 rounded-lg text-white/40 hover:text-red-400 hover:bg-red-500/10"> 658 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 659 + <line x1="18" y1="6" x2="6" y2="18"/> 660 + <line x1="6" y1="6" x2="18" y2="18"/> 661 + </svg> 662 + </button> 663 + </div> 664 + </div> 665 + 666 + <div class="column-content flex flex-col"> 667 + <!-- Target Input --> 668 + <div class="p-4 border-b border-white/5"> 669 + <div class="flex items-center gap-2 px-3 py-2 rounded-lg" style="background: rgba(0,0,0,0.4);"> 670 + <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--on-surface-variant);"> 671 + <circle cx="12" cy="12" r="10"/> 672 + <circle cx="12" cy="10" r="3"/> 673 + <path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/> 674 + </svg> 675 + <input type="text" value="alice.bsky.social" class="flex-1 bg-transparent text-sm outline-none" style="color: var(--on-surface);"> 676 + </div> 677 + </div> 678 + 679 + <!-- Tabs --> 680 + <div class="flex border-b border-white/5 px-2"> 681 + <button class="diagnostics-tab active px-3 py-2 text-xs font-medium">Lists</button> 682 + <button class="diagnostics-tab px-3 py-2 text-xs font-medium" style="color: var(--on-surface-variant);">Labels</button> 683 + <button class="diagnostics-tab px-3 py-2 text-xs font-medium" style="color: var(--on-surface-variant);">Blocks</button> 684 + <button class="diagnostics-tab px-3 py-2 text-xs font-medium" style="color: var(--on-surface-variant);">Packs</button> 685 + </div> 686 + 687 + <!-- Lists Content --> 688 + <div class="flex-1 p-4 space-y-3"> 689 + <div class="flex items-center gap-3 p-3 rounded-xl bg-white/5"> 690 + <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background: rgba(125, 175, 255, 0.15);"> 691 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 692 + <path d="M9 11l3 3L22 4"/> 693 + <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> 694 + </svg> 695 + </div> 696 + <div class="flex-1"> 697 + <p class="text-sm font-medium">Tech Community</p> 698 + <p class="text-xs" style="color: var(--on-surface-variant);">Moderated by @techmods.bsky</p> 699 + </div> 700 + <span class="text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-400">Member</span> 701 + </div> 702 + 703 + <div class="flex items-center gap-3 p-3 rounded-xl bg-white/5"> 704 + <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background: rgba(125, 175, 255, 0.15);"> 705 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 706 + <path d="M9 11l3 3L22 4"/> 707 + <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> 708 + </svg> 709 + </div> 710 + <div class="flex-1"> 711 + <p class="text-sm font-medium">Rust Developers</p> 712 + <p class="text-xs" style="color: var(--on-surface-variant);">Moderated by @rustteam.bsky</p> 713 + </div> 714 + <span class="text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-400">Member</span> 715 + </div> 716 + 717 + <div class="flex items-center gap-3 p-3 rounded-xl bg-white/5"> 718 + <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background: rgba(125, 175, 255, 0.15);"> 719 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--primary);"> 720 + <circle cx="12" cy="12" r="10"/> 721 + <line x1="15" y1="9" x2="9" y2="15"/> 722 + <line x1="9" y1="9" x2="15" y2="15"/> 723 + </svg> 724 + </div> 725 + <div class="flex-1"> 726 + <p class="text-sm font-medium">Crypto Spammers</p> 727 + <p class="text-xs" style="color: var(--on-surface-variant);">Moderated by @moderators.bsky</p> 728 + </div> 729 + <span class="text-xs px-2 py-1 rounded-full bg-red-500/20 text-red-400">Blocked</span> 730 + </div> 731 + </div> 732 + </div> 733 + </div> 734 + 735 + <!-- Add Column Button --> 736 + <div class="add-column-btn w-16 shrink-0 flex flex-col items-center justify-center border-r border-white/5 cursor-pointer" style="background: rgba(25, 25, 25, 0.3);"> 737 + <div class="w-10 h-10 rounded-xl border border-dashed border-white/20 flex items-center justify-center mb-2"> 738 + <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="color: var(--on-surface-variant);"> 739 + <line x1="12" y1="5" x2="12" y2="19"/> 740 + <line x1="5" y1="12" x2="19" y2="12"/> 741 + </svg> 742 + </div> 743 + <span class="text-xs" style="color: var(--on-surface-variant);">Add</span> 744 + </div> 745 + </div> 746 + </main> 747 + 748 + </body> 749 + </html>
+6 -6
docs/tasks/08-multicolumn.md
··· 10 10 11 11 ### Backend - `src-tauri/src/columns.rs` + `src-tauri/src/commands/columns.rs` 12 12 13 - - [ ] SQLite migration: `columns` table (`id TEXT PRIMARY KEY, account_did TEXT, kind TEXT, config TEXT, position INTEGER, width TEXT, created_at TEXT`) 13 + - [x] SQLite migration: `columns` table (`id TEXT PRIMARY KEY, account_did TEXT, kind TEXT, config TEXT, position INTEGER, width TEXT, created_at TEXT`) 14 14 - `kind`: `feed` | `explorer` | `diagnostics` - determines the column type 15 15 - `config`: JSON blob - for feeds: `{ feed_uri, feed_type }`, for explorer: `{ target_uri }`, for diagnostics: `{ did }` 16 16 - `width`: `narrow` | `standard` | `wide` 17 - - [ ] `get_columns(account_did: String)` - return ordered column list for the active account 18 - - [ ] `add_column(account_did: String, kind: String, config: String, position: Option<u32>)` - insert at position or append 19 - - [ ] `remove_column(id: String)` - delete column by ID 20 - - [ ] `reorder_columns(ids: Vec<String>)` - bulk update positions 21 - - [ ] `update_column(id: String, config: Option<String>, width: Option<String>)` - modify column settings 17 + - [x] `get_columns(account_did: String)` - return ordered column list for the active account 18 + - [x] `add_column(account_did: String, kind: String, config: String, position: Option<u32>)` - insert at position or append 19 + - [x] `remove_column(id: String)` - delete column by ID 20 + - [x] `reorder_columns(ids: Vec<String>)` - bulk update positions 21 + - [x] `update_column(id: String, config: Option<String>, width: Option<String>)` - modify column settings 22 22 23 23 ### Frontend - Column Layout 24 24
+1
src-tauri/Cargo.lock
··· 3867 3867 "tauri-plugin-opener", 3868 3868 "thiserror 2.0.18", 3869 3869 "tokio", 3870 + "uuid", 3870 3871 ] 3871 3872 3872 3873 [[package]]
+1
src-tauri/Cargo.toml
··· 35 35 tauri-plugin-log = "2" 36 36 tauri-plugin-notification = "2" 37 37 thiserror = "2.0.18" 38 + uuid = { version = "1", features = ["v4"] } 38 39 39 40 # TODO: add this later 40 41 # [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
+208
src-tauri/src/columns.rs
··· 1 + use super::error::{AppError, Result}; 2 + use super::state::AppState; 3 + use rusqlite::params; 4 + use serde::Serialize; 5 + use uuid::Uuid; 6 + 7 + #[derive(Debug, Clone, Serialize)] 8 + #[serde(rename_all = "camelCase")] 9 + pub struct Column { 10 + pub id: String, 11 + pub account_did: String, 12 + pub kind: String, 13 + pub config: String, 14 + pub position: i64, 15 + pub width: String, 16 + pub created_at: String, 17 + } 18 + 19 + pub fn get_columns(account_did: &str, state: &AppState) -> Result<Vec<Column>> { 20 + let conn = state.auth_store.lock_connection()?; 21 + let mut stmt = conn.prepare( 22 + "SELECT id, account_did, kind, config, position, width, created_at 23 + FROM columns 24 + WHERE account_did = ?1 25 + ORDER BY position ASC", 26 + )?; 27 + 28 + let rows = stmt.query_map(params![account_did], |row| { 29 + Ok(Column { 30 + id: row.get(0)?, 31 + account_did: row.get(1)?, 32 + kind: row.get(2)?, 33 + config: row.get(3)?, 34 + position: row.get(4)?, 35 + width: row.get(5)?, 36 + created_at: row.get(6)?, 37 + }) 38 + })?; 39 + 40 + let mut columns = Vec::new(); 41 + for row in rows { 42 + columns.push(row?); 43 + } 44 + Ok(columns) 45 + } 46 + 47 + pub fn add_column( 48 + account_did: &str, kind: &str, config: &str, position: Option<u32>, state: &AppState, 49 + ) -> Result<Column> { 50 + validate_kind(kind)?; 51 + validate_config_json(config)?; 52 + 53 + let conn = state.auth_store.lock_connection()?; 54 + 55 + let insert_position = match position { 56 + Some(pos) => { 57 + // Shift existing columns at or after this position down by one 58 + conn.execute( 59 + "UPDATE columns SET position = position + 1 60 + WHERE account_did = ?1 AND position >= ?2", 61 + params![account_did, pos], 62 + )?; 63 + pos as i64 64 + } 65 + None => { 66 + // Append: find the current max position 67 + let max: Option<i64> = conn 68 + .query_row( 69 + "SELECT MAX(position) FROM columns WHERE account_did = ?1", 70 + params![account_did], 71 + |row| row.get(0), 72 + ) 73 + .unwrap_or(None); 74 + max.map(|m| m + 1).unwrap_or(0) 75 + } 76 + }; 77 + 78 + let id = Uuid::new_v4().to_string(); 79 + conn.execute( 80 + "INSERT INTO columns(id, account_did, kind, config, position, width) 81 + VALUES (?1, ?2, ?3, ?4, ?5, 'standard')", 82 + params![id, account_did, kind, config, insert_position], 83 + )?; 84 + 85 + let column = conn.query_row( 86 + "SELECT id, account_did, kind, config, position, width, created_at 87 + FROM columns WHERE id = ?1", 88 + params![id], 89 + |row| { 90 + Ok(Column { 91 + id: row.get(0)?, 92 + account_did: row.get(1)?, 93 + kind: row.get(2)?, 94 + config: row.get(3)?, 95 + position: row.get(4)?, 96 + width: row.get(5)?, 97 + created_at: row.get(6)?, 98 + }) 99 + }, 100 + )?; 101 + 102 + Ok(column) 103 + } 104 + 105 + pub fn remove_column(id: &str, state: &AppState) -> Result<()> { 106 + let conn = state.auth_store.lock_connection()?; 107 + 108 + let affected = conn.execute("DELETE FROM columns WHERE id = ?1", params![id])?; 109 + 110 + if affected == 0 { 111 + return Err(AppError::validation(format!("column '{id}' not found"))); 112 + } 113 + 114 + Ok(()) 115 + } 116 + 117 + pub fn reorder_columns(ids: &[String], state: &AppState) -> Result<()> { 118 + if ids.is_empty() { 119 + return Ok(()); 120 + } 121 + 122 + let conn = state.auth_store.lock_connection()?; 123 + 124 + for (position, id) in ids.iter().enumerate() { 125 + conn.execute( 126 + "UPDATE columns SET position = ?1 WHERE id = ?2", 127 + params![position as i64, id], 128 + )?; 129 + } 130 + 131 + Ok(()) 132 + } 133 + 134 + pub fn update_column(id: &str, config: Option<&str>, width: Option<&str>, state: &AppState) -> Result<Column> { 135 + if config.is_none() && width.is_none() { 136 + return Err(AppError::validation("at least one of config or width must be provided")); 137 + } 138 + 139 + if let Some(c) = config { 140 + validate_config_json(c)?; 141 + } 142 + 143 + if let Some(w) = width { 144 + validate_width(w)?; 145 + } 146 + 147 + let conn = state.auth_store.lock_connection()?; 148 + 149 + // Verify the column exists first 150 + let exists: bool = conn 151 + .query_row("SELECT 1 FROM columns WHERE id = ?1", params![id], |_| Ok(true)) 152 + .unwrap_or(false); 153 + 154 + if !exists { 155 + return Err(AppError::validation(format!("column '{id}' not found"))); 156 + } 157 + 158 + if let Some(c) = config { 159 + conn.execute("UPDATE columns SET config = ?1 WHERE id = ?2", params![c, id])?; 160 + } 161 + 162 + if let Some(w) = width { 163 + conn.execute("UPDATE columns SET width = ?1 WHERE id = ?2", params![w, id])?; 164 + } 165 + 166 + let column = conn.query_row( 167 + "SELECT id, account_did, kind, config, position, width, created_at 168 + FROM columns WHERE id = ?1", 169 + params![id], 170 + |row| { 171 + Ok(Column { 172 + id: row.get(0)?, 173 + account_did: row.get(1)?, 174 + kind: row.get(2)?, 175 + config: row.get(3)?, 176 + position: row.get(4)?, 177 + width: row.get(5)?, 178 + created_at: row.get(6)?, 179 + }) 180 + }, 181 + )?; 182 + 183 + Ok(column) 184 + } 185 + 186 + fn validate_kind(kind: &str) -> Result<()> { 187 + match kind { 188 + "feed" | "explorer" | "diagnostics" => Ok(()), 189 + _ => Err(AppError::validation(format!( 190 + "invalid column kind '{kind}': must be 'feed', 'explorer', or 'diagnostics'" 191 + ))), 192 + } 193 + } 194 + 195 + fn validate_width(width: &str) -> Result<()> { 196 + match width { 197 + "narrow" | "standard" | "wide" => Ok(()), 198 + _ => Err(AppError::validation(format!( 199 + "invalid column width '{width}': must be 'narrow', 'standard', or 'wide'" 200 + ))), 201 + } 202 + } 203 + 204 + fn validate_config_json(config: &str) -> Result<()> { 205 + serde_json::from_str::<serde_json::Value>(config) 206 + .map(|_| ()) 207 + .map_err(|e| AppError::validation(format!("config must be valid JSON: {e}"))) 208 + }
+35
src-tauri/src/commands/columns.rs
··· 1 + #![allow(clippy::needless_pass_by_value)] 2 + 3 + use crate::columns::{self, Column}; 4 + use crate::error::AppError; 5 + use crate::state::AppState; 6 + use tauri::State; 7 + 8 + #[tauri::command] 9 + pub fn get_columns(account_did: String, state: State<'_, AppState>) -> Result<Vec<Column>, AppError> { 10 + columns::get_columns(&account_did, &state) 11 + } 12 + 13 + #[tauri::command] 14 + pub fn add_column( 15 + account_did: String, kind: String, config: String, position: Option<u32>, state: State<'_, AppState>, 16 + ) -> Result<Column, AppError> { 17 + columns::add_column(&account_did, &kind, &config, position, &state) 18 + } 19 + 20 + #[tauri::command] 21 + pub fn remove_column(id: String, state: State<'_, AppState>) -> Result<(), AppError> { 22 + columns::remove_column(&id, &state) 23 + } 24 + 25 + #[tauri::command] 26 + pub fn reorder_columns(ids: Vec<String>, state: State<'_, AppState>) -> Result<(), AppError> { 27 + columns::reorder_columns(&ids, &state) 28 + } 29 + 30 + #[tauri::command] 31 + pub fn update_column( 32 + id: String, config: Option<String>, width: Option<String>, state: State<'_, AppState>, 33 + ) -> Result<Column, AppError> { 34 + columns::update_column(&id, config.as_deref(), width.as_deref(), &state) 35 + }
+1
src-tauri/src/commands/mod.rs
··· 1 1 #![allow(clippy::needless_pass_by_value)] 2 2 3 + pub mod columns; 3 4 pub mod explorer; 4 5 pub mod search; 5 6 pub mod settings;
+2
src-tauri/src/db.rs
··· 47 47 "search_owner_scope", 48 48 include_str!("migrations/007_search_owner_scope.sql"), 49 49 ), 50 + Migration::new(8, "columns", include_str!("migrations/008_columns.sql")), 50 51 ]; 51 52 52 53 pub fn initialize_database(app: &AppHandle) -> Result<DbPool, AppError> { ··· 149 150 DROP TABLE IF EXISTS oauth_auth_requests; 150 151 DROP TABLE IF EXISTS accounts; 151 152 DROP TABLE IF EXISTS app_settings; 153 + DROP TABLE IF EXISTS columns; 152 154 DROP TABLE IF EXISTS schema_migrations; 153 155 154 156 PRAGMA wal_checkpoint(TRUNCATE);
+7 -1
src-tauri/src/lib.rs
··· 1 1 mod auth; 2 + mod columns; 2 3 mod commands; 3 4 mod db; 4 5 mod error; ··· 121 122 cmd::settings::clear_cache, 122 123 cmd::settings::export_data, 123 124 cmd::settings::reset_app, 124 - cmd::settings::get_log_entries 125 + cmd::settings::get_log_entries, 126 + cmd::columns::get_columns, 127 + cmd::columns::add_column, 128 + cmd::columns::remove_column, 129 + cmd::columns::reorder_columns, 130 + cmd::columns::update_column 125 131 ]) 126 132 .run(tauri::generate_context!()) 127 133 .expect("error while running tauri application");
+11
src-tauri/src/migrations/008_columns.sql
··· 1 + CREATE TABLE IF NOT EXISTS columns ( 2 + id TEXT PRIMARY KEY, 3 + account_did TEXT NOT NULL, 4 + kind TEXT NOT NULL CHECK(kind IN ('feed', 'explorer', 'diagnostics')), 5 + config TEXT NOT NULL, 6 + position INTEGER NOT NULL, 7 + width TEXT NOT NULL DEFAULT 'standard' CHECK(width IN ('narrow', 'standard', 'wide')), 8 + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP 9 + ); 10 + 11 + CREATE INDEX IF NOT EXISTS columns_account_did ON columns(account_did, position);