slack status without the slack status.zzstoatzz.io/
quickslice

feat: improve ui and codebase design

- add theme indicator to feed page for consistency
- make custom emojis searchable in emoji picker
- remove unused CSS file and references (public directory)
- clean up dockerfile to remove public directory copies

+76 -565
+1 -3
Dockerfile
··· 17 17 COPY templates ./templates 18 18 COPY lexicons ./lexicons 19 19 COPY static ./static 20 - COPY public ./public 21 20 22 21 # Build for release 23 22 RUN cargo build --release ··· 39 38 # Copy templates and lexicons 40 39 COPY templates ./templates 41 40 COPY lexicons ./lexicons 42 - # Copy static files and CSS 41 + # Copy static files 43 42 COPY static ./static 44 - COPY public ./public 45 43 46 44 # Create directory for SQLite database 47 45 RUN mkdir -p /data
-552
public/css/style.css
··· 1 - body { 2 - font-family: Arial, Helvetica, sans-serif; 3 - 4 - --border-color: #ddd; 5 - --gray-100: #fafafa; 6 - --gray-500: #666; 7 - --gray-700: #333; 8 - --primary-100: #d2e7ff; 9 - --primary-200: #b1d3fa; 10 - --primary-400: #2e8fff; 11 - --primary-500: #0078ff; 12 - --primary-600: #0066db; 13 - --error-500: #f00; 14 - --error-100: #fee; 15 - } 16 - 17 - /* 18 - Josh's Custom CSS Reset 19 - https://www.joshwcomeau.com/css/custom-css-reset/ 20 - */ 21 - *, 22 - *::before, 23 - *::after { 24 - box-sizing: border-box; 25 - } 26 - 27 - * { 28 - margin: 0; 29 - } 30 - 31 - body { 32 - line-height: 1.5; 33 - -webkit-font-smoothing: antialiased; 34 - } 35 - 36 - img, 37 - picture, 38 - video, 39 - canvas, 40 - svg { 41 - display: block; 42 - max-width: 100%; 43 - } 44 - 45 - input, 46 - button, 47 - textarea, 48 - select { 49 - font: inherit; 50 - } 51 - 52 - p, 53 - h1, 54 - h2, 55 - h3, 56 - h4, 57 - h5, 58 - h6 { 59 - overflow-wrap: break-word; 60 - } 61 - 62 - #root, 63 - #__next { 64 - isolation: isolate; 65 - } 66 - 67 - /* 68 - Common components 69 - */ 70 - button, 71 - .button { 72 - display: inline-block; 73 - border: 0; 74 - background-color: var(--primary-500); 75 - border-radius: 50px; 76 - color: #fff; 77 - padding: 2px 10px; 78 - cursor: pointer; 79 - text-decoration: none; 80 - } 81 - 82 - button:hover, 83 - .button:hover { 84 - background: var(--primary-400); 85 - } 86 - 87 - /* 88 - Custom components 89 - */ 90 - .error { 91 - background-color: var(--error-100); 92 - color: var(--error-500); 93 - text-align: center; 94 - padding: 1rem; 95 - display: none; 96 - } 97 - 98 - .error.visible { 99 - display: block; 100 - } 101 - 102 - #header { 103 - background-color: #fff; 104 - text-align: center; 105 - padding: 0.5rem 0 1.5rem; 106 - } 107 - 108 - #header h1 { 109 - font-size: 5rem; 110 - } 111 - 112 - .container { 113 - display: flex; 114 - flex-direction: column; 115 - gap: 4px; 116 - margin: 0 auto; 117 - max-width: 600px; 118 - padding: 20px; 119 - } 120 - 121 - .card { 122 - /* border: 1px solid var(--border-color); */ 123 - border-radius: 6px; 124 - padding: 10px 16px; 125 - background-color: #fff; 126 - } 127 - 128 - .card> :first-child { 129 - margin-top: 0; 130 - } 131 - 132 - .card> :last-child { 133 - margin-bottom: 0; 134 - } 135 - 136 - .session-form { 137 - display: flex; 138 - flex-direction: row; 139 - align-items: center; 140 - justify-content: space-between; 141 - } 142 - 143 - .login-form { 144 - display: flex; 145 - flex-direction: row; 146 - gap: 6px; 147 - border: 1px solid var(--border-color); 148 - border-radius: 6px; 149 - padding: 10px 16px; 150 - background-color: #fff; 151 - } 152 - 153 - .login-form input { 154 - flex: 1; 155 - border: 0; 156 - } 157 - 158 - .status-options { 159 - display: flex; 160 - flex-direction: row; 161 - flex-wrap: wrap; 162 - gap: 8px; 163 - margin: 10px 0; 164 - } 165 - 166 - .status-option { 167 - font-size: 2rem; 168 - width: 3rem; 169 - height: 3rem; 170 - padding: 0; 171 - background-color: #fff; 172 - border: 1px solid var(--border-color); 173 - border-radius: 3rem; 174 - text-align: center; 175 - box-shadow: 0 1px 4px #0001; 176 - cursor: pointer; 177 - } 178 - 179 - .status-option:hover { 180 - background-color: var(--primary-100); 181 - box-shadow: 0 0 0 1px var(--primary-400); 182 - } 183 - 184 - .status-option.selected { 185 - box-shadow: 0 0 0 1px var(--primary-500); 186 - background-color: var(--primary-100); 187 - } 188 - 189 - .status-option.selected:hover { 190 - background-color: var(--primary-200); 191 - } 192 - 193 - .status-line { 194 - display: flex; 195 - flex-direction: row; 196 - align-items: center; 197 - gap: 10px; 198 - position: relative; 199 - margin-top: 15px; 200 - } 201 - 202 - .status-line:not(.no-line)::before { 203 - content: ''; 204 - position: absolute; 205 - width: 2px; 206 - background-color: var(--border-color); 207 - left: 1.45rem; 208 - bottom: calc(100% + 2px); 209 - height: 15px; 210 - } 211 - 212 - .status-line .status { 213 - font-size: 2rem; 214 - background-color: #fff; 215 - width: 3rem; 216 - height: 3rem; 217 - border-radius: 1.5rem; 218 - text-align: center; 219 - border: 1px solid var(--border-color); 220 - } 221 - 222 - .status-line .desc { 223 - color: var(--gray-500); 224 - } 225 - 226 - .status-line .author { 227 - color: var(--gray-700); 228 - font-weight: 600; 229 - text-decoration: none; 230 - } 231 - 232 - .status-line .author:hover { 233 - text-decoration: underline; 234 - } 235 - 236 - .timestamp { 237 - font-size: 0.85rem; 238 - color: var(--gray-500); 239 - margin-top: 0.25rem; 240 - } 241 - 242 - .signup-cta { 243 - text-align: center; 244 - text-wrap: balance; 245 - margin-top: 1rem; 246 - } 247 - 248 - /* Status form */ 249 - .status-form-container { 250 - margin-top: 2rem; 251 - padding: 1.5rem; 252 - background: var(--gray-50); 253 - border-radius: 0.75rem; 254 - border: 1px solid var(--border-color); 255 - } 256 - 257 - .status-form-container h3 { 258 - margin: 0 0 1rem 0; 259 - font-size: 1rem; 260 - color: var(--gray-700); 261 - font-weight: 600; 262 - } 263 - 264 - .status-form { 265 - display: flex; 266 - flex-direction: column; 267 - gap: 1.25rem; 268 - } 269 - 270 - .emoji-selector label { 271 - display: block; 272 - font-size: 0.9rem; 273 - color: var(--gray-600); 274 - margin-bottom: 0.5rem; 275 - } 276 - 277 - .emoji-grid { 278 - display: grid; 279 - grid-template-columns: repeat(auto-fill, minmax(3.5rem, 1fr)); 280 - gap: 0.5rem; 281 - } 282 - 283 - .emoji-option { 284 - position: relative; 285 - cursor: pointer; 286 - } 287 - 288 - .emoji-option input[type="radio"] { 289 - position: absolute; 290 - opacity: 0; 291 - width: 100%; 292 - height: 100%; 293 - cursor: pointer; 294 - } 295 - 296 - .emoji-display { 297 - display: flex; 298 - align-items: center; 299 - justify-content: center; 300 - font-size: 1.75rem; 301 - width: 3.5rem; 302 - height: 3.5rem; 303 - background: white; 304 - border: 2px solid var(--border-color); 305 - border-radius: 0.5rem; 306 - transition: all 0.2s; 307 - } 308 - 309 - .emoji-option input:checked + .emoji-display { 310 - border-color: var(--primary-color); 311 - background: rgba(74, 158, 255, 0.1); 312 - transform: scale(1.05); 313 - } 314 - 315 - .emoji-option:hover .emoji-display { 316 - border-color: var(--gray-400); 317 - } 318 - 319 - .text-input label, 320 - .expiration-selector label { 321 - display: block; 322 - font-size: 0.9rem; 323 - color: var(--gray-600); 324 - margin-bottom: 0.5rem; 325 - } 326 - 327 - .text-input input[type="text"] { 328 - width: 100%; 329 - padding: 0.5rem 0.75rem; 330 - border: 1px solid var(--border-color); 331 - border-radius: 0.375rem; 332 - font-size: 0.95rem; 333 - transition: border-color 0.2s; 334 - } 335 - 336 - .text-input input[type="text"]:focus { 337 - outline: none; 338 - border-color: var(--primary-color); 339 - box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.1); 340 - } 341 - 342 - .expiration-selector select { 343 - padding: 0.5rem 0.75rem; 344 - border: 1px solid var(--border-color); 345 - border-radius: 0.375rem; 346 - background: white; 347 - font-size: 0.95rem; 348 - color: var(--gray-700); 349 - cursor: pointer; 350 - transition: border-color 0.2s; 351 - } 352 - 353 - .expiration-selector select:hover { 354 - border-color: var(--gray-400); 355 - } 356 - 357 - .expiration-selector select:focus { 358 - outline: none; 359 - border-color: var(--primary-color); 360 - box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.1); 361 - } 362 - 363 - .submit-button { 364 - padding: 0.75rem 1.5rem; 365 - background: var(--primary-color); 366 - color: white; 367 - border: none; 368 - border-radius: 0.5rem; 369 - font-size: 1rem; 370 - font-weight: 500; 371 - cursor: pointer; 372 - transition: background 0.2s; 373 - } 374 - 375 - .submit-button:hover { 376 - background: #3a8eee; 377 - } 378 - 379 - .submit-button:active { 380 - transform: translateY(1px); 381 - } 382 - 383 - .submit-button:disabled { 384 - background: var(--gray-400); 385 - cursor: not-allowed; 386 - opacity: 0.6; 387 - } 388 - 389 - .current-status-info { 390 - padding: 0.75rem; 391 - background: white; 392 - border: 1px solid var(--border-color); 393 - border-radius: 0.5rem; 394 - margin-bottom: 1rem; 395 - } 396 - 397 - .current-status-info p { 398 - margin: 0; 399 - color: var(--gray-700); 400 - } 401 - 402 - .current-emoji { 403 - font-size: 1.25rem; 404 - margin: 0 0.25rem; 405 - } 406 - 407 - .form-message { 408 - padding: 0.75rem; 409 - border-radius: 0.375rem; 410 - margin-top: 0.5rem; 411 - font-size: 0.9rem; 412 - empty-cells: hide; 413 - } 414 - 415 - .form-message.error { 416 - background: #fee; 417 - border: 1px solid #fcc; 418 - color: #c00; 419 - } 420 - 421 - .form-message:empty { 422 - display: none; 423 - } 424 - 425 - /* Walkthrough Styles */ 426 - .walkthrough-overlay { 427 - position: fixed; 428 - top: 0; 429 - left: 0; 430 - right: 0; 431 - bottom: 0; 432 - background: rgba(0, 0, 0, 0.7); 433 - z-index: 9998; 434 - display: none; 435 - opacity: 0; 436 - transition: opacity 0.3s ease; 437 - } 438 - 439 - .walkthrough-overlay.active { 440 - display: block; 441 - opacity: 1; 442 - } 443 - 444 - .walkthrough-spotlight { 445 - position: fixed; 446 - border: 3px solid #4a9eff; 447 - border-radius: 8px; 448 - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7); 449 - z-index: 9999; 450 - pointer-events: none; 451 - transition: all 0.4s ease; 452 - } 453 - 454 - .walkthrough-tooltip { 455 - position: fixed; 456 - background: white; 457 - border-radius: 12px; 458 - padding: 1.5rem; 459 - max-width: 320px; 460 - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); 461 - z-index: 10000; 462 - opacity: 0; 463 - transform: translateY(10px); 464 - transition: all 0.3s ease; 465 - } 466 - 467 - .walkthrough-tooltip.active { 468 - opacity: 1; 469 - transform: translateY(0); 470 - } 471 - 472 - .walkthrough-tooltip h3 { 473 - margin: 0 0 0.5rem 0; 474 - font-size: 1.1rem; 475 - color: #333; 476 - } 477 - 478 - .walkthrough-tooltip p { 479 - margin: 0 0 1rem 0; 480 - font-size: 0.95rem; 481 - color: #666; 482 - line-height: 1.5; 483 - } 484 - 485 - .walkthrough-tooltip .walkthrough-actions { 486 - display: flex; 487 - gap: 0.5rem; 488 - justify-content: flex-end; 489 - } 490 - 491 - .walkthrough-tooltip button { 492 - padding: 0.5rem 1rem; 493 - border: none; 494 - border-radius: 6px; 495 - font-size: 0.9rem; 496 - cursor: pointer; 497 - transition: all 0.2s; 498 - } 499 - 500 - .walkthrough-tooltip .walkthrough-skip { 501 - background: #f5f5f5; 502 - color: #666; 503 - } 504 - 505 - .walkthrough-tooltip .walkthrough-skip:hover { 506 - background: #e8e8e8; 507 - } 508 - 509 - .walkthrough-tooltip .walkthrough-next { 510 - background: #4a9eff; 511 - color: white; 512 - } 513 - 514 - .walkthrough-tooltip .walkthrough-next:hover { 515 - background: #3a8eee; 516 - } 517 - 518 - .walkthrough-tooltip .walkthrough-progress { 519 - display: flex; 520 - gap: 6px; 521 - margin-bottom: 1rem; 522 - justify-content: center; 523 - } 524 - 525 - .walkthrough-tooltip .walkthrough-dot { 526 - width: 8px; 527 - height: 8px; 528 - border-radius: 50%; 529 - background: #ddd; 530 - transition: background 0.3s; 531 - } 532 - 533 - .walkthrough-tooltip .walkthrough-dot.active { 534 - background: #4a9eff; 535 - } 536 - 537 - /* Pulse animation for spotlight */ 538 - @keyframes pulse { 539 - 0% { 540 - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7), 0 0 0 0 rgba(74, 158, 255, 0.7); 541 - } 542 - 70% { 543 - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7), 0 0 0 10px rgba(74, 158, 255, 0); 544 - } 545 - 100% { 546 - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7), 0 0 0 0 rgba(74, 158, 255, 0); 547 - } 548 - } 549 - 550 - .walkthrough-spotlight.pulse { 551 - animation: pulse 2s infinite; 552 - }
-1
src/main.rs
··· 1322 1322 ) 1323 1323 .build(), 1324 1324 ) 1325 - .service(Files::new("/css", "public/css").show_files_listing()) 1326 1325 .service(Files::new("/static", "static").show_files_listing()) 1327 1326 .service(Files::new("/emojis", "static/emojis").show_files_listing()) 1328 1327 .service(client_metadata)
-2
templates/base.html
··· 22 22 <meta property="twitter:title" content="{% block twitter_title %}status{% endblock %}"> 23 23 <meta property="twitter:description" content="{% block twitter_description %}like slack status, but decoupled from any platform{% endblock %}"> 24 24 <meta property="twitter:image" content="{% block twitter_image %}https://status.zzstoatzz.io/og-image.png{% endblock %}"> 25 - 26 - <link rel="stylesheet" href="/css/style.css"> 27 25 </head> 28 26 <body> 29 27 {% block content %}{% endblock %}
+33
templates/feed.html
··· 21 21 <svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 22 22 <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> 23 23 </svg> 24 + <span class="theme-indicator" id="theme-indicator"></span> 24 25 </button> 25 26 </header> 26 27 ··· 176 177 } 177 178 178 179 .theme-toggle { 180 + position: relative; 179 181 background: var(--bg-secondary); 180 182 border: 1px solid var(--border-color); 181 183 border-radius: var(--radius-sm); ··· 207 209 [data-theme="dark"] .moon-icon { 208 210 display: block; 209 211 stroke: #8e44ad; 212 + } 213 + 214 + .theme-indicator { 215 + position: absolute; 216 + top: calc(100% + 0.5rem); 217 + right: 0; 218 + background: var(--bg-secondary); 219 + border: 1px solid var(--border-color); 220 + border-radius: var(--radius-sm); 221 + padding: 0.25rem 0.5rem; 222 + font-size: 0.75rem; 223 + color: var(--text-secondary); 224 + white-space: nowrap; 225 + opacity: 0; 226 + pointer-events: none; 227 + transition: opacity 0.2s; 228 + z-index: 1000; 229 + } 230 + 231 + .theme-indicator.visible { 232 + opacity: 1; 210 233 } 211 234 212 235 /* Session Card */ ··· 436 459 document.body.setAttribute('data-theme', prefersDark ? 'dark' : 'light'); 437 460 } else { 438 461 document.body.setAttribute('data-theme', next); 462 + } 463 + 464 + // Show theme indicator 465 + const indicator = document.getElementById('theme-indicator'); 466 + if (indicator) { 467 + indicator.textContent = next; 468 + indicator.classList.add('visible'); 469 + setTimeout(() => { 470 + indicator.classList.remove('visible'); 471 + }, 1500); 439 472 } 440 473 }; 441 474
+42 -7
templates/status.html
··· 1444 1444 1445 1445 const lowerQuery = query.toLowerCase(); 1446 1446 const results = []; 1447 + const customResults = []; 1447 1448 1448 1449 // Search through emoji keywords 1449 1450 for (const [emoji, keywords] of Object.entries(emojiKeywords)) { ··· 1473 1474 } 1474 1475 } 1475 1476 1477 + // Search through custom emojis by name 1478 + for (const customEmoji of customEmojis) { 1479 + if (customEmoji.name.toLowerCase().includes(lowerQuery)) { 1480 + customResults.push(customEmoji); 1481 + } 1482 + } 1483 + 1476 1484 // Remove duplicates and limit results 1477 - const uniqueResults = [...new Set(results)].slice(0, 100); 1485 + const uniqueResults = [...new Set(results)].slice(0, 50); 1486 + const uniqueCustomResults = customResults.slice(0, 50); 1478 1487 1479 - // Display results 1488 + // Display results - combine regular and custom emojis 1489 + let resultsHtml = ''; 1490 + 1491 + // Add custom emoji results first 1492 + if (uniqueCustomResults.length > 0) { 1493 + resultsHtml += uniqueCustomResults.map(emoji => 1494 + `<button type="button" class="emoji-option custom-emoji" data-emoji="custom:${emoji.name}" data-name="${emoji.name}"> 1495 + <img src="/emojis/${emoji.filename}" alt="${emoji.name}" title="${emoji.name}"> 1496 + </button>` 1497 + ).join(''); 1498 + } 1499 + 1500 + // Add regular emoji results 1480 1501 if (uniqueResults.length > 0) { 1481 - emojiGrid.innerHTML = uniqueResults.map(emoji => 1502 + resultsHtml += uniqueResults.map(emoji => 1482 1503 `<button type="button" class="emoji-option" data-emoji="${emoji}">${emoji}</button>` 1483 1504 ).join(''); 1505 + } 1506 + 1507 + if (resultsHtml) { 1508 + emojiGrid.innerHTML = resultsHtml; 1484 1509 } else { 1485 1510 emojiGrid.innerHTML = '<div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">No emojis found</div>'; 1486 1511 } 1487 1512 1488 - // Add click handlers 1513 + // Add click handlers for both regular and custom emojis 1489 1514 emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { 1490 1515 btn.addEventListener('click', (e) => { 1491 - const emoji = e.target.getAttribute('data-emoji'); 1492 - selectedEmoji.textContent = emoji; 1493 - statusInput.value = emoji; 1516 + e.preventDefault(); 1517 + const emojiValue = btn.getAttribute('data-emoji'); 1518 + 1519 + if (btn.classList.contains('custom-emoji')) { 1520 + // Handle custom emoji 1521 + const img = btn.querySelector('img'); 1522 + selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 1.5em; height: 1.5em; vertical-align: middle;">`; 1523 + } else { 1524 + // Handle regular emoji 1525 + selectedEmoji.textContent = emojiValue; 1526 + } 1527 + 1528 + statusInput.value = emojiValue; 1494 1529 emojiPicker.style.display = 'none'; 1495 1530 // Clear search when emoji is selected 1496 1531 document.getElementById('emoji-search').value = '';