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

Redesign emoji picker as immersive modal

+365 -100
+365 -100
templates/status.html
··· 190 190 </form> 191 191 </div> 192 192 193 - <!-- Emoji Picker (hidden by default) --> 194 - <div class="emoji-picker" id="emoji-picker" style="display: none;"> 195 - <div class="emoji-search-container"> 196 - <input type="text" 197 - id="emoji-search" 198 - class="emoji-search" 199 - placeholder="Search emojis..." 200 - autocomplete="off"> 201 - </div> 202 - <div class="emoji-categories" id="emoji-categories"> 203 - <button class="category-btn active" data-category="frequent">Frequent</button> 204 - <button class="category-btn" data-category="custom">Custom</button> 205 - <button class="category-btn" data-category="people">People</button> 206 - <button class="category-btn" data-category="nature">Nature</button> 207 - <button class="category-btn" data-category="food">Food</button> 208 - <button class="category-btn" data-category="activity">Activity</button> 209 - <button class="category-btn" data-category="travel">Travel</button> 210 - <button class="category-btn" data-category="objects">Objects</button> 211 - <button class="category-btn" data-category="symbols">Symbols</button> 212 - <button class="category-btn" data-category="flags">Flags</button> 213 - </div> 214 - <div class="emoji-grid" id="emoji-grid"> 215 - <!-- Will be populated by JavaScript --> 193 + <!-- Emoji Picker Modal --> 194 + <div class="emoji-picker-overlay hidden" id="emoji-picker-overlay" aria-hidden="true"> 195 + <div class="emoji-picker" id="emoji-picker" role="dialog" aria-modal="true" aria-labelledby="emoji-picker-title"> 196 + <div class="emoji-picker-header"> 197 + <div> 198 + <h2 id="emoji-picker-title">pick an emoji</h2> 199 + <p class="emoji-picker-subtitle">custom and unicode options side by side</p> 200 + </div> 201 + <button type="button" class="emoji-picker-close" id="emoji-picker-close" aria-label="close emoji picker">✕</button> 202 + </div> 203 + <div class="emoji-picker-preview"> 204 + <div class="emoji-preview-block"> 205 + <span class="emoji-preview-label">current</span> 206 + <div class="emoji-preview-emoji" id="emoji-preview-current"> 207 + {% if let Some(current) = current_status.as_ref() %} 208 + {% if current.status.starts_with("custom:") %} 209 + {% let emoji_name = current.status.strip_prefix("custom:").unwrap() %} 210 + <img src="/emojis/{{emoji_name}}.png" alt="{{emoji_name}}" title="{{emoji_name}}" 211 + onerror="this.onerror=null; this.src='/emojis/{{emoji_name}}.gif';"> 212 + {% else %} 213 + <span title="{{ current.status }}">{{ current.status }}</span> 214 + {% endif %} 215 + {% else %} 216 + <span title="happy">😊</span> 217 + {% endif %} 218 + </div> 219 + </div> 220 + <div class="emoji-preview-block"> 221 + <span class="emoji-preview-label">next</span> 222 + <div class="emoji-preview-emoji" id="emoji-preview-next"> 223 + {% if let Some(current) = current_status.as_ref() %} 224 + {% if current.status.starts_with("custom:") %} 225 + {% let emoji_name = current.status.strip_prefix("custom:").unwrap() %} 226 + <img src="/emojis/{{emoji_name}}.png" alt="{{emoji_name}}" title="{{emoji_name}}" 227 + onerror="this.onerror=null; this.src='/emojis/{{emoji_name}}.gif';"> 228 + {% else %} 229 + <span title="{{ current.status }}">{{ current.status }}</span> 230 + {% endif %} 231 + {% else %} 232 + <span title="happy">😊</span> 233 + {% endif %} 234 + </div> 235 + </div> 236 + </div> 237 + <div class="emoji-search-container"> 238 + <input type="text" 239 + id="emoji-search" 240 + class="emoji-search" 241 + placeholder="Search emojis..." 242 + autocomplete="off"> 243 + </div> 244 + <div class="emoji-picker-body"> 245 + <div class="emoji-categories" id="emoji-categories"> 246 + <button class="category-btn active" data-category="frequent">Frequent</button> 247 + <button class="category-btn" data-category="custom">Custom</button> 248 + <button class="category-btn" data-category="people">People</button> 249 + <button class="category-btn" data-category="nature">Nature</button> 250 + <button class="category-btn" data-category="food">Food</button> 251 + <button class="category-btn" data-category="activity">Activity</button> 252 + <button class="category-btn" data-category="travel">Travel</button> 253 + <button class="category-btn" data-category="objects">Objects</button> 254 + <button class="category-btn" data-category="symbols">Symbols</button> 255 + <button class="category-btn" data-category="flags">Flags</button> 256 + </div> 257 + <div class="emoji-grid" id="emoji-grid"> 258 + <!-- Will be populated by JavaScript --> 259 + </div> 260 + </div> 216 261 </div> 217 262 </div> 218 263 ··· 455 500 <style> 456 501 body { 457 502 font-family: var(--font-family) !important; 503 + } 504 + 505 + body.modal-open { 506 + overflow: hidden; 458 507 } 459 508 460 509 :root { ··· 1049 1098 } 1050 1099 1051 1100 /* Emoji Picker */ 1101 + .emoji-picker-overlay { 1102 + position: fixed; 1103 + inset: 0; 1104 + background: rgba(6, 6, 8, 0.75); 1105 + backdrop-filter: blur(6px); 1106 + display: flex; 1107 + align-items: center; 1108 + justify-content: center; 1109 + padding: clamp(1rem, 6vw, 2.5rem); 1110 + z-index: 1400; 1111 + } 1112 + 1113 + .emoji-picker-overlay.hidden { 1114 + display: none; 1115 + } 1116 + 1052 1117 .emoji-picker { 1053 - position: absolute; 1118 + width: min(720px, 100%); 1119 + max-height: min(90vh, 720px); 1054 1120 background: var(--bg-secondary); 1055 1121 border: 1px solid var(--border-color); 1122 + border-radius: clamp(var(--radius), 2vw, 24px); 1123 + box-shadow: 0 32px 80px rgba(0, 0, 0, 0.45); 1124 + display: flex; 1125 + flex-direction: column; 1126 + gap: 1rem; 1127 + padding: clamp(1.25rem, 5vw, 2rem); 1128 + overflow: hidden; 1129 + } 1130 + 1131 + .emoji-picker-header { 1132 + display: flex; 1133 + align-items: flex-start; 1134 + justify-content: space-between; 1135 + gap: 1rem; 1136 + } 1137 + 1138 + .emoji-picker-header h2 { 1139 + margin: 0; 1140 + font-size: 1.5rem; 1141 + color: var(--text-primary); 1142 + } 1143 + 1144 + .emoji-picker-subtitle { 1145 + margin: 0.25rem 0 0; 1146 + font-size: 0.875rem; 1147 + color: var(--text-tertiary); 1148 + } 1149 + 1150 + .emoji-picker-close { 1151 + border: 1px solid var(--border-color); 1152 + background: var(--bg-tertiary); 1153 + color: var(--text-secondary); 1154 + border-radius: 999px; 1155 + width: 2.25rem; 1156 + height: 2.25rem; 1157 + display: flex; 1158 + align-items: center; 1159 + justify-content: center; 1160 + cursor: pointer; 1161 + transition: all 0.2s ease; 1162 + } 1163 + 1164 + .emoji-picker-close:hover, 1165 + .emoji-picker-close:focus-visible { 1166 + color: white; 1167 + background: var(--accent); 1168 + border-color: transparent; 1169 + outline: none; 1170 + } 1171 + 1172 + .emoji-picker-preview { 1173 + display: flex; 1174 + gap: 1rem; 1175 + background: var(--bg-primary); 1056 1176 border-radius: var(--radius); 1177 + border: 1px solid var(--border-color); 1057 1178 padding: 1rem; 1058 - box-shadow: var(--shadow-md); 1059 - width: 320px; 1060 - max-height: 400px; 1061 - overflow-y: auto; 1062 - z-index: 1000; 1179 + } 1180 + 1181 + .emoji-preview-block { 1182 + flex: 1; 1183 + display: flex; 1184 + flex-direction: column; 1185 + align-items: center; 1186 + gap: 0.5rem; 1187 + } 1188 + 1189 + .emoji-preview-label { 1190 + font-size: 0.75rem; 1191 + text-transform: uppercase; 1192 + letter-spacing: 0.08em; 1193 + color: var(--text-tertiary); 1194 + } 1195 + 1196 + .emoji-preview-emoji { 1197 + width: clamp(3.25rem, 16vw, 4.5rem); 1198 + height: clamp(3.25rem, 16vw, 4.5rem); 1199 + display: flex; 1200 + align-items: center; 1201 + justify-content: center; 1202 + font-size: clamp(2rem, 10vw, 3rem); 1203 + border-radius: var(--radius); 1204 + background: var(--bg-secondary); 1205 + border: 1px solid rgba(255, 255, 255, 0.05); 1206 + overflow: hidden; 1207 + } 1208 + 1209 + .emoji-preview-emoji img { 1210 + width: 100%; 1211 + height: 100%; 1212 + object-fit: contain; 1213 + } 1214 + 1215 + .emoji-preview-emoji span { 1216 + display: block; 1063 1217 } 1064 1218 1065 1219 .emoji-search-container { 1066 - margin-bottom: 1rem; 1220 + margin: 0; 1067 1221 } 1068 1222 1069 1223 .emoji-search { 1070 1224 width: 100%; 1071 - padding: 0.5rem 0.75rem; 1225 + padding: 0.75rem 1rem; 1072 1226 background: var(--bg-primary); 1073 1227 border: 1px solid var(--border-color); 1074 - border-radius: var(--radius-sm); 1228 + border-radius: var(--radius); 1075 1229 color: var(--text-primary); 1076 - font-size: 0.875rem; 1230 + font-size: 0.95rem; 1077 1231 outline: none; 1078 - transition: border-color 0.2s; 1232 + transition: border-color 0.2s ease, box-shadow 0.2s ease; 1079 1233 } 1080 1234 1081 1235 .emoji-search:focus { 1082 1236 border-color: var(--accent); 1083 - box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.1); 1237 + box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.12); 1084 1238 } 1085 1239 1086 1240 .emoji-search::placeholder { 1087 1241 color: var(--text-tertiary); 1088 1242 } 1089 1243 1244 + .emoji-picker-body { 1245 + flex: 1; 1246 + display: flex; 1247 + flex-direction: column; 1248 + gap: 0.75rem; 1249 + min-height: 0; 1250 + } 1251 + 1090 1252 .emoji-categories { 1091 1253 display: flex; 1092 - gap: 0.25rem; 1093 - margin-bottom: 1rem; 1254 + gap: 0.5rem; 1094 1255 overflow-x: auto; 1095 1256 padding-bottom: 0.5rem; 1257 + border-bottom: 1px solid rgba(255, 255, 255, 0.06); 1096 1258 } 1097 1259 1098 1260 .emoji-categories.hidden { ··· 1101 1263 1102 1264 .category-btn { 1103 1265 background: transparent; 1104 - border: none; 1266 + border: 1px solid transparent; 1105 1267 color: var(--text-secondary); 1106 - font-size: 0.75rem; 1107 - padding: 0.25rem 0.5rem; 1268 + font-size: 0.8rem; 1269 + padding: 0.4rem 0.75rem; 1108 1270 cursor: pointer; 1109 1271 white-space: nowrap; 1110 - border-radius: var(--radius-sm); 1111 - transition: all 0.2s; 1272 + border-radius: 999px; 1273 + transition: all 0.2s ease; 1112 1274 } 1113 1275 1114 - .category-btn:hover { 1115 - background: var(--bg-tertiary); 1276 + .category-btn:hover, 1277 + .category-btn:focus-visible { 1278 + border-color: var(--accent); 1279 + color: var(--accent); 1280 + outline: none; 1116 1281 } 1117 1282 1118 1283 .category-btn.active { 1119 1284 background: var(--accent); 1120 1285 color: white; 1286 + border-color: transparent; 1121 1287 } 1122 1288 1123 1289 .emoji-grid { 1290 + flex: 1; 1124 1291 display: grid; 1125 - grid-template-columns: repeat(auto-fill, 2rem); 1126 - gap: 0.25rem; 1127 - justify-content: start; 1292 + grid-template-columns: repeat(auto-fill, minmax(56px, 1fr)); 1293 + gap: 0.5rem; 1294 + padding-right: 0.25rem; 1295 + overflow-y: auto; 1128 1296 } 1129 1297 1130 1298 .emoji-option { 1131 - background: transparent; 1132 - border: none; 1133 - font-size: 1.5rem; 1134 - padding: 0.25rem; 1299 + background: var(--bg-primary); 1300 + border: 1px solid transparent; 1301 + font-size: 2rem; 1135 1302 cursor: pointer; 1136 - border-radius: var(--radius-sm); 1137 - transition: background 0.2s; 1303 + border-radius: 16px; 1304 + transition: transform 0.15s ease, border-color 0.2s ease, background 0.2s ease; 1138 1305 display: flex; 1139 1306 align-items: center; 1140 1307 justify-content: center; 1141 - width: 2rem; 1142 - height: 2rem; 1308 + width: 100%; 1309 + aspect-ratio: 1; 1310 + position: relative; 1143 1311 } 1144 1312 1145 - .emoji-option:hover { 1313 + .emoji-option:hover, 1314 + .emoji-option:focus-visible { 1146 1315 background: var(--bg-tertiary); 1316 + border-color: var(--accent); 1317 + transform: translateY(-2px); 1318 + outline: none; 1147 1319 } 1148 1320 1149 - /* Custom emoji styles */ 1150 - .emoji-option.custom-emoji { 1151 - padding: 0.125rem; 1321 + .emoji-option.custom-emoji img { 1322 + width: 70%; 1323 + height: 70%; 1324 + object-fit: contain; 1152 1325 } 1153 1326 1154 - .emoji-option.custom-emoji img { 1155 - width: 1.75rem; 1156 - height: 1.75rem; 1157 - object-fit: contain; 1327 + .emoji-option.custom-emoji::after { 1328 + content: ''; 1329 + position: absolute; 1330 + inset: 8%; 1331 + border-radius: 12px; 1332 + pointer-events: none; 1333 + } 1334 + 1335 + @media (max-width: 640px) { 1336 + .emoji-picker-overlay { 1337 + padding: 0; 1338 + align-items: stretch; 1339 + } 1340 + 1341 + .emoji-picker { 1342 + width: 100%; 1343 + height: 100%; 1344 + max-height: none; 1345 + border-radius: 0; 1346 + padding: 1.25rem 1rem 1rem; 1347 + gap: 0.75rem; 1348 + } 1349 + 1350 + .emoji-picker-preview { 1351 + padding: 0.75rem; 1352 + } 1353 + 1354 + .emoji-grid { 1355 + grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); 1356 + } 1158 1357 } 1159 1358 1160 1359 /* Clear Picker */ ··· 1937 2136 1938 2137 // Emoji picker 1939 2138 const emojiTrigger = document.getElementById('emoji-trigger'); 2139 + const emojiPickerOverlay = document.getElementById('emoji-picker-overlay'); 1940 2140 const emojiPicker = document.getElementById('emoji-picker'); 2141 + const emojiPickerClose = document.getElementById('emoji-picker-close'); 1941 2142 const emojiGrid = document.getElementById('emoji-grid'); 1942 2143 const selectedEmoji = document.getElementById('selected-emoji'); 1943 2144 const statusInput = document.getElementById('status-input'); 2145 + const emojiPreviewCurrent = document.getElementById('emoji-preview-current'); 2146 + const emojiPreviewNext = document.getElementById('emoji-preview-next'); 2147 + let lastFocusBeforeEmojiPicker = null; 1944 2148 1945 2149 // Clear time picker 1946 2150 const clearAfterBtn = document.getElementById('clear-after-btn'); ··· 1948 2152 const clearText = document.getElementById('clear-text'); 1949 2153 const expiresSelect = document.getElementById('expires_in'); 1950 2154 2155 + const isEmojiPickerOpen = () => emojiPickerOverlay && !emojiPickerOverlay.classList.contains('hidden'); 2156 + 2157 + const syncPreviewWithSelection = () => { 2158 + if (emojiPreviewNext && selectedEmoji) { 2159 + emojiPreviewNext.innerHTML = selectedEmoji.innerHTML; 2160 + } 2161 + }; 2162 + 2163 + syncPreviewWithSelection(); 2164 + 2165 + const openEmojiPicker = () => { 2166 + if (!emojiPickerOverlay || !emojiPicker) return; 2167 + 2168 + lastFocusBeforeEmojiPicker = document.activeElement; 2169 + 2170 + emojiPickerOverlay.classList.remove('hidden'); 2171 + emojiPickerOverlay.setAttribute('aria-hidden', 'false'); 2172 + document.body.classList.add('modal-open'); 2173 + if (clearPicker) clearPicker.style.display = 'none'; 2174 + 2175 + const emojiSearch = document.getElementById('emoji-search'); 2176 + if (emojiSearch) { 2177 + emojiSearch.value = ''; 2178 + const categories = document.getElementById('emoji-categories'); 2179 + if (categories) categories.classList.remove('hidden'); 2180 + setTimeout(() => { 2181 + if (emojiSearch) emojiSearch.focus(); 2182 + }, 60); 2183 + } 2184 + 2185 + if (emojiPreviewCurrent) { 2186 + const currentDisplay = document.querySelector('.current-status .status-emoji'); 2187 + if (currentDisplay) { 2188 + emojiPreviewCurrent.innerHTML = currentDisplay.innerHTML; 2189 + } else if (selectedEmoji) { 2190 + emojiPreviewCurrent.innerHTML = selectedEmoji.innerHTML; 2191 + } 2192 + } 2193 + 2194 + syncPreviewWithSelection(); 2195 + 2196 + loadCustomEmojis().then(() => { 2197 + loadEmojiCategory('frequent'); 2198 + }); 2199 + }; 2200 + 2201 + const closeEmojiPicker = () => { 2202 + if (!emojiPickerOverlay) return; 2203 + emojiPickerOverlay.classList.add('hidden'); 2204 + emojiPickerOverlay.setAttribute('aria-hidden', 'true'); 2205 + document.body.classList.remove('modal-open'); 2206 + if (lastFocusBeforeEmojiPicker && typeof lastFocusBeforeEmojiPicker.focus === 'function') { 2207 + const focusTarget = lastFocusBeforeEmojiPicker; 2208 + setTimeout(() => { 2209 + try { focusTarget.focus(); } catch (_) {} 2210 + }, 50); 2211 + } 2212 + lastFocusBeforeEmojiPicker = null; 2213 + }; 2214 + 1951 2215 // Show emoji picker 1952 - if (emojiTrigger && emojiPicker) { 2216 + if (emojiTrigger && emojiPickerOverlay) { 1953 2217 emojiTrigger.addEventListener('click', (e) => { 2218 + e.preventDefault(); 1954 2219 e.stopPropagation(); 1955 - const wasHidden = emojiPicker.style.display === 'none'; 1956 - emojiPicker.style.display = wasHidden ? 'block' : 'none'; 1957 - clearPicker.style.display = 'none'; 1958 - 1959 - if (wasHidden) { 1960 - // Position picker 1961 - const rect = emojiTrigger.getBoundingClientRect(); 1962 - emojiPicker.style.top = rect.bottom + 'px'; 1963 - emojiPicker.style.left = rect.left + 'px'; 1964 - 1965 - // Clear search and show categories 1966 - const emojiSearch = document.getElementById('emoji-search'); 1967 - if (emojiSearch) { 1968 - emojiSearch.value = ''; 1969 - document.getElementById('emoji-categories').classList.remove('hidden'); 1970 - } 1971 - 1972 - // Load frequent emojis by default (wait for custom emojis to be loaded first) 1973 - loadCustomEmojis().then(() => { 1974 - loadEmojiCategory('frequent'); 1975 - }); 1976 - 1977 - // Focus search for easy typing 1978 - setTimeout(() => { 1979 - if (emojiSearch) emojiSearch.focus(); 1980 - }, 50); 2220 + if (isEmojiPickerOpen()) { 2221 + closeEmojiPicker(); 2222 + } else { 2223 + openEmojiPicker(); 1981 2224 } 1982 2225 }); 1983 2226 } 2227 + 2228 + if (emojiPickerClose) { 2229 + emojiPickerClose.addEventListener('click', (e) => { 2230 + e.preventDefault(); 2231 + closeEmojiPicker(); 2232 + }); 2233 + } 2234 + 2235 + if (emojiPickerOverlay) { 2236 + emojiPickerOverlay.addEventListener('click', (e) => { 2237 + if (e.target === emojiPickerOverlay) { 2238 + closeEmojiPicker(); 2239 + } 2240 + }); 2241 + } 2242 + 2243 + document.addEventListener('keydown', (e) => { 2244 + if (e.key === 'Escape' && isEmojiPickerOpen()) { 2245 + closeEmojiPicker(); 2246 + } 2247 + }); 1984 2248 1985 2249 // Store custom emojis 1986 2250 let customEmojis = []; ··· 2003 2267 document.addEventListener('custom-emoji-uploaded', async (e) => { 2004 2268 await loadCustomEmojis(); 2005 2269 // If picker visible, refresh current category 2006 - if (emojiPicker && emojiPicker.style.display !== 'none') { 2270 + if (isEmojiPickerOpen()) { 2007 2271 const active = document.querySelector('.emoji-categories .category-btn.active'); 2008 2272 const cat = active ? active.getAttribute('data-category') : 'frequent'; 2009 2273 loadEmojiCategory(cat || 'frequent'); ··· 2050 2314 } 2051 2315 2052 2316 statusInput.value = emoji; 2053 - emojiPicker.style.display = 'none'; 2317 + syncPreviewWithSelection(); 2318 + closeEmojiPicker(); 2054 2319 checkForChanges(); 2055 2320 }); 2056 2321 }); ··· 2077 2342 // Display the image in the selected emoji area 2078 2343 selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`; 2079 2344 statusInput.value = emojiValue; 2080 - emojiPicker.style.display = 'none'; 2345 + syncPreviewWithSelection(); 2346 + closeEmojiPicker(); 2081 2347 checkForChanges(); 2082 2348 }); 2083 2349 }); ··· 2095 2361 const emoji = e.target.getAttribute('data-emoji'); 2096 2362 selectedEmoji.textContent = emoji; 2097 2363 statusInput.value = emoji; 2098 - emojiPicker.style.display = 'none'; 2364 + syncPreviewWithSelection(); 2365 + closeEmojiPicker(); 2099 2366 checkForChanges(); 2100 2367 }); 2101 2368 }); ··· 2119 2386 clearAfterBtn.addEventListener('click', (e) => { 2120 2387 e.stopPropagation(); 2121 2388 clearPicker.style.display = clearPicker.style.display === 'none' ? 'block' : 'none'; 2122 - emojiPicker.style.display = 'none'; 2389 + closeEmojiPicker(); 2123 2390 2124 2391 // Position picker 2125 2392 const rect = clearAfterBtn.getBoundingClientRect(); ··· 2228 2495 2229 2496 // Close pickers on outside click 2230 2497 document.addEventListener('click', (e) => { 2231 - if (!emojiPicker.contains(e.target) && e.target !== emojiTrigger) { 2232 - emojiPicker.style.display = 'none'; 2233 - } 2234 - if (!clearPicker.contains(e.target) && e.target !== clearAfterBtn) { 2498 + if (clearPicker && !clearPicker.contains(e.target) && e.target !== clearAfterBtn) { 2235 2499 clearPicker.style.display = 'none'; 2236 2500 } 2237 2501 }); ··· 2331 2595 } 2332 2596 2333 2597 statusInput.value = emojiValue; 2334 - emojiPicker.style.display = 'none'; 2598 + syncPreviewWithSelection(); 2599 + closeEmojiPicker(); 2335 2600 checkForChanges(); 2336 2601 // Clear search when emoji is selected 2337 2602 document.getElementById('emoji-search').value = '';