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

Polish emoji picker interactions and styling

Changed files
+116 -89
templates
+116 -89
templates/status.html
··· 470 470 471 471 body.modal-open { 472 472 overflow: hidden; 473 + position: fixed; 474 + width: 100%; 473 475 } 474 476 475 477 :root { ··· 1066 1068 /* Emoji Picker */ 1067 1069 .emoji-picker-overlay { 1068 1070 position: fixed; 1071 + top: 0; 1072 + right: 0; 1073 + bottom: 0; 1074 + left: 0; 1069 1075 inset: 0; 1070 1076 background: rgba(6, 6, 8, 0.75); 1071 1077 backdrop-filter: blur(6px); ··· 1175 1181 overflow-x: auto; 1176 1182 padding-bottom: 0.5rem; 1177 1183 border-bottom: 1px solid rgba(255, 255, 255, 0.06); 1184 + scrollbar-width: none; 1178 1185 } 1179 1186 1180 1187 .emoji-categories.hidden { 1188 + display: none; 1189 + } 1190 + 1191 + .emoji-categories::-webkit-scrollbar { 1181 1192 display: none; 1182 1193 } 1183 1194 ··· 1209 1220 .emoji-grid { 1210 1221 flex: 1; 1211 1222 display: grid; 1212 - grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); 1223 + grid-template-columns: repeat(auto-fill, minmax(min(84px, 18vw), 1fr)); 1213 1224 gap: 0.75rem; 1214 1225 padding-right: 0.25rem; 1215 1226 overflow-y: auto; 1227 + scrollbar-width: none; 1228 + } 1229 + 1230 + .emoji-grid::-webkit-scrollbar { 1231 + display: none; 1216 1232 } 1217 1233 1218 1234 .emoji-option { ··· 1244 1260 object-fit: contain; 1245 1261 } 1246 1262 1263 + .emoji-picker-overlay::before { 1264 + content: ''; 1265 + position: fixed; 1266 + top: 0; 1267 + right: 0; 1268 + bottom: 0; 1269 + left: 0; 1270 + } 1271 + 1247 1272 @media (max-width: 640px) { 1248 1273 .emoji-picker-overlay { 1249 1274 padding: 0; ··· 1262 1287 .emoji-grid { 1263 1288 grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); 1264 1289 gap: 0.5rem; 1290 + } 1291 + } 1292 + 1293 + @media (prefers-reduced-motion: reduce) { 1294 + .emoji-picker-overlay { 1295 + backdrop-filter: none; 1296 + background: rgba(6, 6, 8, 0.85); 1297 + } 1298 + 1299 + .emoji-picker, 1300 + .emoji-option { 1301 + transition: none; 1265 1302 } 1266 1303 } 1267 1304 ··· 2051 2088 const emojiGrid = document.getElementById('emoji-grid'); 2052 2089 const selectedEmoji = document.getElementById('selected-emoji'); 2053 2090 const statusInput = document.getElementById('status-input'); 2091 + const emojiSearch = document.getElementById('emoji-search'); 2092 + const emojiCategories = document.getElementById('emoji-categories'); 2054 2093 let lastFocusBeforeEmojiPicker = null; 2055 - 2094 + let scrollPosition = 0; 2095 + let isScrollLocked = false; 2096 + let keydownBound = false; 2097 + 2056 2098 // Clear time picker 2057 2099 const clearAfterBtn = document.getElementById('clear-after-btn'); 2058 2100 const clearPicker = document.getElementById('clear-picker'); 2059 2101 const clearText = document.getElementById('clear-text'); 2060 2102 const expiresSelect = document.getElementById('expires_in'); 2061 - 2103 + 2062 2104 const isEmojiPickerOpen = () => emojiPickerOverlay && !emojiPickerOverlay.classList.contains('hidden'); 2063 2105 2106 + const handleKeydown = (e) => { 2107 + if (e.key === 'Escape' && isEmojiPickerOpen()) { 2108 + closeEmojiPicker(); 2109 + } 2110 + }; 2111 + 2064 2112 const openEmojiPicker = () => { 2065 2113 if (!emojiPickerOverlay || !emojiPicker) return; 2066 2114 2067 2115 lastFocusBeforeEmojiPicker = document.activeElement; 2116 + scrollPosition = window.scrollY || document.documentElement.scrollTop || 0; 2068 2117 2069 2118 emojiPickerOverlay.classList.remove('hidden'); 2070 2119 emojiPickerOverlay.setAttribute('aria-hidden', 'false'); 2071 2120 document.body.classList.add('modal-open'); 2121 + document.body.style.top = `-${scrollPosition}px`; 2122 + isScrollLocked = true; 2072 2123 if (clearPicker) clearPicker.style.display = 'none'; 2073 2124 2074 - const emojiSearch = document.getElementById('emoji-search'); 2075 2125 if (emojiSearch) { 2076 2126 emojiSearch.value = ''; 2077 - const categories = document.getElementById('emoji-categories'); 2078 - if (categories) categories.classList.remove('hidden'); 2079 - setTimeout(() => { 2080 - if (emojiSearch) emojiSearch.focus(); 2081 - }, 60); 2127 + if (emojiCategories) emojiCategories.classList.remove('hidden'); 2128 + requestAnimationFrame(() => { 2129 + try { emojiSearch.focus(); } catch (_) {} 2130 + }); 2082 2131 } 2083 2132 2084 2133 loadCustomEmojis().then(() => { 2085 2134 loadEmojiCategory('frequent'); 2086 2135 }); 2136 + 2137 + if (!keydownBound) { 2138 + document.addEventListener('keydown', handleKeydown); 2139 + keydownBound = true; 2140 + } 2087 2141 }; 2088 2142 2089 2143 const closeEmojiPicker = () => { ··· 2091 2145 emojiPickerOverlay.classList.add('hidden'); 2092 2146 emojiPickerOverlay.setAttribute('aria-hidden', 'true'); 2093 2147 document.body.classList.remove('modal-open'); 2148 + document.body.style.top = ''; 2149 + if (isScrollLocked) { 2150 + window.scrollTo(0, scrollPosition); 2151 + } 2152 + scrollPosition = 0; 2153 + isScrollLocked = false; 2094 2154 if (lastFocusBeforeEmojiPicker && typeof lastFocusBeforeEmojiPicker.focus === 'function') { 2095 2155 const focusTarget = lastFocusBeforeEmojiPicker; 2096 - setTimeout(() => { 2156 + requestAnimationFrame(() => { 2097 2157 try { focusTarget.focus(); } catch (_) {} 2098 - }, 50); 2158 + }); 2099 2159 } 2100 2160 lastFocusBeforeEmojiPicker = null; 2161 + 2162 + if (keydownBound) { 2163 + document.removeEventListener('keydown', handleKeydown); 2164 + keydownBound = false; 2165 + } 2101 2166 }; 2102 2167 2103 2168 // Show emoji picker ··· 2127 2192 } 2128 2193 }); 2129 2194 } 2130 - 2131 - document.addEventListener('keydown', (e) => { 2132 - if (e.key === 'Escape' && isEmojiPickerOpen()) { 2133 - closeEmojiPicker(); 2134 - } 2135 - }); 2136 2195 2137 2196 // Store custom emojis 2138 2197 let customEmojis = []; ··· 2187 2246 } 2188 2247 2189 2248 emojiGrid.innerHTML = html; 2190 - 2191 - // Add click handlers for both types 2192 - emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { 2193 - btn.addEventListener('click', (e) => { 2194 - e.preventDefault(); 2195 - const emoji = btn.getAttribute('data-emoji'); 2196 - 2197 - if (emoji.startsWith('custom:')) { 2198 - const img = btn.querySelector('img'); 2199 - selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`; 2200 - } else { 2201 - selectedEmoji.textContent = emoji; 2202 - } 2203 - 2204 - statusInput.value = emoji; 2205 - closeEmojiPicker(); 2206 - checkForChanges(); 2207 - }); 2208 - }); 2209 2249 } else if (category === 'custom') { 2210 2250 // Load custom image emojis 2211 2251 if (customEmojis.length === 0) { ··· 2217 2257 <img src="/emojis/${emoji.filename}" alt="${emoji.name}" title="${emoji.name}"> 2218 2258 </button>` 2219 2259 ).join(''); 2220 - 2221 - // Add click handlers for custom emojis 2222 - emojiGrid.querySelectorAll('.custom-emoji').forEach(btn => { 2223 - btn.addEventListener('click', (e) => { 2224 - e.preventDefault(); 2225 - const emojiName = btn.getAttribute('data-name'); 2226 - const emojiValue = btn.getAttribute('data-emoji'); 2227 - const img = btn.querySelector('img'); 2228 - 2229 - // Display the image in the selected emoji area 2230 - selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`; 2231 - statusInput.value = emojiValue; 2232 - closeEmojiPicker(); 2233 - checkForChanges(); 2234 - }); 2235 - }); 2236 2260 } else { 2237 2261 // Load standard Unicode emojis 2238 2262 const emojis = emojiData[category] || []; ··· 2240 2264 const slug = (window.__emojiSlugs && window.__emojiSlugs[emoji]) || ''; 2241 2265 return `<button type="button" class="emoji-option" data-emoji="${emoji}" data-name="${slug}" title="${slug}">${emoji}</button>`; 2242 2266 }).join(''); 2243 - 2244 - // Add click handlers for standard emojis 2245 - emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { 2246 - btn.addEventListener('click', (e) => { 2247 - const emoji = e.target.getAttribute('data-emoji'); 2248 - selectedEmoji.textContent = emoji; 2249 - statusInput.value = emoji; 2250 - closeEmojiPicker(); 2251 - checkForChanges(); 2252 - }); 2253 - }); 2254 2267 } 2255 2268 2256 2269 // Update active category ··· 2259 2272 }); 2260 2273 }; 2261 2274 2275 + const applyEmojiSelection = (button) => { 2276 + if (!button || !selectedEmoji || !statusInput) return; 2277 + const emojiValue = button.getAttribute('data-emoji'); 2278 + if (!emojiValue) return; 2279 + 2280 + if (button.classList.contains('custom-emoji')) { 2281 + const img = button.querySelector('img'); 2282 + if (img) { 2283 + selectedEmoji.innerHTML = ''; 2284 + const previewImg = document.createElement('img'); 2285 + previewImg.src = img.src; 2286 + previewImg.alt = img.alt; 2287 + previewImg.title = img.title || img.alt || ''; 2288 + previewImg.style.width = '100%'; 2289 + previewImg.style.height = '100%'; 2290 + previewImg.style.objectFit = 'contain'; 2291 + selectedEmoji.appendChild(previewImg); 2292 + } 2293 + } else { 2294 + selectedEmoji.textContent = emojiValue; 2295 + } 2296 + 2297 + statusInput.value = emojiValue; 2298 + if (emojiSearch) emojiSearch.value = ''; 2299 + if (emojiCategories) emojiCategories.classList.remove('hidden'); 2300 + closeEmojiPicker(); 2301 + checkForChanges(); 2302 + }; 2303 + 2304 + if (emojiGrid) { 2305 + emojiGrid.addEventListener('click', (e) => { 2306 + const button = e.target.closest('.emoji-option'); 2307 + if (!button || !emojiGrid.contains(button)) return; 2308 + e.preventDefault(); 2309 + applyEmojiSelection(button); 2310 + }); 2311 + } 2312 + 2262 2313 // Category buttons 2263 2314 document.querySelectorAll('.category-btn').forEach(btn => { 2264 2315 btn.addEventListener('click', () => { ··· 2388 2439 // Search emoji function 2389 2440 const searchEmojis = (query) => { 2390 2441 if (!query) { 2391 - document.getElementById('emoji-categories').classList.remove('hidden'); 2442 + if (emojiCategories) emojiCategories.classList.remove('hidden'); 2392 2443 loadEmojiCategory('frequent'); 2393 2444 return; 2394 2445 } 2395 2446 2396 2447 // Hide categories when searching 2397 - document.getElementById('emoji-categories').classList.add('hidden'); 2448 + if (emojiCategories) emojiCategories.classList.add('hidden'); 2398 2449 2399 2450 const lowerQuery = query.toLowerCase(); 2400 2451 const results = []; ··· 2463 2514 } else { 2464 2515 emojiGrid.innerHTML = '<div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">No emojis found</div>'; 2465 2516 } 2466 - 2467 - // Add click handlers for both regular and custom emojis 2468 - emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { 2469 - btn.addEventListener('click', (e) => { 2470 - e.preventDefault(); 2471 - const emojiValue = btn.getAttribute('data-emoji'); 2472 - 2473 - if (btn.classList.contains('custom-emoji')) { 2474 - // Handle custom emoji 2475 - const img = btn.querySelector('img'); 2476 - selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`; 2477 - } else { 2478 - // Handle regular emoji 2479 - selectedEmoji.textContent = emojiValue; 2480 - } 2481 - 2482 - statusInput.value = emojiValue; 2483 - closeEmojiPicker(); 2484 - checkForChanges(); 2485 - // Clear search when emoji is selected 2486 - document.getElementById('emoji-search').value = ''; 2487 - }); 2488 - }); 2489 2517 }; 2490 2518 2491 2519 // Emoji search input handler 2492 - const emojiSearch = document.getElementById('emoji-search'); 2493 2520 if (emojiSearch) { 2494 2521 emojiSearch.addEventListener('input', (e) => { 2495 2522 searchEmojis(e.target.value);