Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

Fix multitouch button sticking on iOS in notepat

- Add pointer transfer when controlling pointer lifts but other pens still touch button
- Add force cleanup for non-controlling pointer lifts when no pens are touching
- Delete sounds[note] reference after killing sound in up callback

+308 -27
+1 -8
system/public/aesthetic.computer/disks/notepat.mjs
··· 3489 3489 applyPitchBendToNotes([previousKey], { immediate: true }); 3490 3490 } else { 3491 3491 sounds[note]?.sound.kill(quickFade ? fastFade : killFade); 3492 + delete sounds[note]; // Clean up the sound reference after killing 3492 3493 } 3493 3494 3494 3495 // console.log("🪱 Trail:", note); ··· 3502 3503 } 3503 3504 3504 3505 delete tonestack[note]; // Remove this key from the notestack. 3505 - // Note: sounds[note] already deleted in slide branch above, or killed in else branch 3506 - //} else { 3507 - // console.log(note, sounds); 3508 - // sounds[key]?.sound?.update({ 3509 - // tone: tonestack[orderedTones[orderedTones.length - 2]].tone, 3510 - // }); 3511 - // sounds[orderedTones[orderedTones.length - 2]] = sounds[key]; 3512 - //} 3513 3506 }, 3514 3507 }, 3515 3508 pens?.(),
+50 -10
system/public/aesthetic.computer/lib/ui.mjs
··· 463 463 reason: "up() returned false" 464 464 }); 465 465 } else { 466 - btn.down = false; 467 - btn.over = false; 468 - btn.downPointer = undefined; 469 - // Only remove if still in activeButtons (avoid double removal) 470 - if (activeButtons.has(btn)) { 471 - removeActiveButton(btn, "button up callback", netLog); 466 + // Check if any other pens are still touching this button 467 + // If so, transfer ownership to one of them instead of fully releasing 468 + const otherPensOnButton = (pens || []).filter(pen => 469 + pen && pen.pointer !== e.pointer && btn.box.contains(pen) 470 + ); 471 + 472 + if (otherPensOnButton.length > 0 && btn.box.contains(otherPensOnButton[0])) { 473 + // Transfer ownership to another pen that's still touching 474 + btn.downPointer = otherPensOnButton[0].pointer; 475 + // Keep button down, don't call cancel 476 + console.log("🔄 Button pointer transferred:", { 477 + buttonId: btn.id || "unnamed", 478 + fromPointer: e.pointer, 479 + toPointer: btn.downPointer, 480 + reason: "other pen still touching" 481 + }); 482 + } else { 483 + btn.down = false; 484 + btn.over = false; 485 + btn.downPointer = undefined; 486 + // Only remove if still in activeButtons (avoid double removal) 487 + if (activeButtons.has(btn)) { 488 + removeActiveButton(btn, "button up callback", netLog); 489 + } 490 + // console.log("🏁 Button fully released:", { 491 + // buttonId: btn.id || "unnamed", 492 + // activeButtonsCount: activeButtons.size 493 + // }); 472 494 } 473 - // console.log("🏁 Button fully released:", { 474 - // buttonId: btn.id || "unnamed", 475 - // activeButtonsCount: activeButtons.size 476 - // }); 477 495 } 478 496 } 479 497 ··· 557 575 reason: "lift event ignored - waiting for controlling pointer" 558 576 }); 559 577 } 578 + } 579 + } 580 + 581 + // Additional check: If this is a lift event from a non-controlling pointer, 582 + // but NO pens are touching the button anymore, force release it. 583 + // This handles the case where the controlling pointer was transferred but then 584 + // that new pointer lifted without being tracked properly. 585 + if (e.is(`lift:${t}`) && btn.down && !isControllingLiftPointer) { 586 + const anyPensOnButton = (pens || []).some(pen => pen && btn.box.contains(pen)); 587 + if (!anyPensOnButton) { 588 + console.log("🧹 Force cleanup - no pens touching button:", { 589 + buttonId: btn.id || "unnamed", 590 + pointer: e.pointer, 591 + downPointer: btn.downPointer 592 + }); 593 + btn.down = false; 594 + btn.over = false; 595 + btn.downPointer = undefined; 596 + if (activeButtons.has(btn)) { 597 + removeActiveButton(btn, "force cleanup - no pens touching", netLog); 598 + } 599 + callbacks.cancel?.(btn); 560 600 } 561 601 } 562 602
+173 -5
system/public/kidlisp.com/index.html
··· 1992 1992 padding: 16px; 1993 1993 box-sizing: border-box; 1994 1994 gap: 8px; 1995 + overflow: auto; 1996 + position: relative; 1995 1997 } 1996 1998 #boot-screen-ff1 .ff1-logo { 1997 1999 height: 48px; ··· 2000 2002 object-fit: contain; 2001 2003 filter: invert(1); 2002 2004 transition: height 0.2s ease, margin 0.2s ease; 2005 + flex-shrink: 0; 2003 2006 } 2004 2007 #boot-screen-ff1 .boot-text { 2005 2008 color: white !important; ··· 2020 2023 display: flex; 2021 2024 flex-direction: column; 2022 2025 align-items: center; 2026 + justify-content: center; 2023 2027 gap: 12px; 2024 2028 margin-top: 16px; 2025 2029 width: 100%; 2026 2030 max-width: 280px; 2031 + box-sizing: border-box; 2027 2032 } 2028 2033 .ff1-paired-row { 2029 2034 display: flex; 2030 2035 align-items: center; 2036 + justify-content: center; 2031 2037 gap: 10px; 2038 + flex-wrap: wrap; 2039 + width: 100%; 2032 2040 } 2033 2041 .ff1-btn { 2034 2042 background: rgba(255,255,255,0.15); ··· 2039 2047 font-size: 12px; 2040 2048 cursor: pointer; 2041 2049 transition: background 0.2s; 2050 + white-space: nowrap; 2042 2051 } 2043 2052 .ff1-btn:hover { background: rgba(255,255,255,0.25); } 2044 2053 .ff1-btn:disabled { opacity: 0.5; cursor: not-allowed; } 2045 2054 .ff1-status { 2046 2055 font-size: 12px; 2047 2056 color: rgba(255,255,255,0.7); 2057 + text-align: center; 2048 2058 } 2049 2059 .ff1-status.success { color: #4ade80; } 2050 2060 .ff1-status.error { color: #f87171; } 2051 2061 .ff1-pairing { 2052 2062 display: none; 2063 + position: absolute; 2064 + inset: 0; 2053 2065 flex-direction: column; 2054 2066 align-items: center; 2067 + justify-content: center; 2055 2068 gap: 10px; 2056 - width: 100%; 2069 + background: #1a1a1a; 2070 + box-sizing: border-box; 2071 + padding: 16px; 2072 + z-index: 10; 2057 2073 } 2058 2074 .ff1-pairing.open { display: flex; } 2059 2075 .ff1-pairing-hint { ··· 2061 2077 opacity: 0.7; 2062 2078 text-align: center; 2063 2079 line-height: 1.4; 2080 + max-width: 200px; 2081 + color: white; 2064 2082 } 2065 2083 .ff1-pairing-qr { 2066 - width: 160px; 2067 - height: 160px; 2084 + width: 140px; 2085 + height: 140px; 2086 + min-width: 80px; 2087 + min-height: 80px; 2068 2088 background: #fff; 2069 2089 border-radius: 6px; 2070 2090 image-rendering: pixelated; 2091 + flex-shrink: 0; 2071 2092 } 2072 2093 .ff1-pairing-status { 2073 2094 font-size: 11px; 2074 2095 opacity: 0.6; 2096 + text-align: center; 2097 + color: white; 2075 2098 } 2076 2099 .ff1-pairing-link { 2077 2100 font-size: 10px; ··· 2079 2102 word-break: break-all; 2080 2103 text-align: center; 2081 2104 cursor: pointer; 2105 + max-width: 100%; 2106 + color: white; 2082 2107 } 2083 2108 .ff1-pairing-link:hover { text-decoration: underline; } 2084 2109 ··· 2199 2224 2200 2225 /* Responsive boot screens - using @container for modern browsers */ 2201 2226 @supports (container-type: size) { 2202 - /* FF1 responsive - reflow on small panels */ 2227 + /* FF1 responsive - reflow on small panels (HEIGHT) */ 2203 2228 @container preview (max-height: 180px) { 2204 2229 #boot-screen-ff1 .ff1-logo { 2205 2230 height: 32px; ··· 2225 2250 } 2226 2251 } 2227 2252 2253 + /* FF1 responsive - WIDTH-based adjustments for narrow panels */ 2254 + @container preview (max-width: 380px) { 2255 + #boot-screen-ff1 .ff1-settings { 2256 + max-width: 100%; 2257 + padding: 0 8px; 2258 + } 2259 + #boot-screen-ff1 .ff1-pairing-qr { 2260 + width: 120px; 2261 + height: 120px; 2262 + } 2263 + #boot-screen-ff1 .ff1-pairing-hint { 2264 + font-size: 10px; 2265 + } 2266 + } 2267 + @container preview (max-width: 300px) { 2268 + #boot-screen-ff1 { 2269 + padding: 8px; 2270 + gap: 4px; 2271 + } 2272 + #boot-screen-ff1 .ff1-logo { 2273 + height: 28px; 2274 + } 2275 + #boot-screen-ff1 .boot-text-title { 2276 + font-size: 12px; 2277 + } 2278 + #boot-screen-ff1 .boot-text-action { 2279 + font-size: 9px; 2280 + } 2281 + #boot-screen-ff1 .ff1-settings { 2282 + margin-top: 8px; 2283 + gap: 8px; 2284 + } 2285 + #boot-screen-ff1 .ff1-pairing-qr { 2286 + width: 100px; 2287 + height: 100px; 2288 + } 2289 + #boot-screen-ff1 .ff1-btn { 2290 + padding: 6px 10px; 2291 + font-size: 10px; 2292 + } 2293 + } 2294 + @container preview (max-width: 240px) { 2295 + #boot-screen-ff1 .ff1-logo { 2296 + height: 22px; 2297 + } 2298 + #boot-screen-ff1 .boot-text-title { 2299 + font-size: 11px; 2300 + } 2301 + #boot-screen-ff1 .boot-text-action { 2302 + display: none; 2303 + } 2304 + #boot-screen-ff1 .ff1-pairing-qr { 2305 + width: 80px; 2306 + height: 80px; 2307 + } 2308 + #boot-screen-ff1 .ff1-pairing-hint { 2309 + font-size: 9px; 2310 + } 2311 + #boot-screen-ff1 .ff1-pairing-link { 2312 + font-size: 8px; 2313 + } 2314 + } 2315 + 2316 + /* FF1 responsive - COMBINED narrow+short for extreme cases */ 2317 + @container preview (max-width: 300px) and (max-height: 300px) { 2318 + #boot-screen-ff1 { 2319 + flex-direction: column !important; 2320 + justify-content: flex-start !important; 2321 + align-items: center !important; 2322 + padding: 8px; 2323 + gap: 4px; 2324 + overflow-y: auto; 2325 + } 2326 + #boot-screen-ff1 .ff1-logo { 2327 + height: 24px; 2328 + } 2329 + #boot-screen-ff1 .ff1-settings { 2330 + margin-top: 4px; 2331 + } 2332 + #boot-screen-ff1 .ff1-pairing { 2333 + gap: 6px; 2334 + } 2335 + #boot-screen-ff1 .ff1-pairing-qr { 2336 + width: 80px; 2337 + height: 80px; 2338 + } 2339 + } 2340 + 2341 + /* FF1 responsive - wide layout when there's enough width */ 2342 + @container preview (min-width: 500px) and (min-height: 280px) { 2343 + #boot-screen-ff1 { 2344 + flex-direction: row !important; 2345 + flex-wrap: wrap; 2346 + justify-content: center !important; 2347 + align-items: center !important; 2348 + gap: 24px; 2349 + padding: 16px 24px; 2350 + } 2351 + #boot-screen-ff1 .ff1-settings { 2352 + max-width: none; 2353 + flex-direction: row; 2354 + align-items: center; 2355 + gap: 20px; 2356 + } 2357 + #boot-screen-ff1 .ff1-pairing { 2358 + flex-direction: row; 2359 + align-items: center; 2360 + gap: 16px; 2361 + } 2362 + #boot-screen-ff1 .ff1-pairing-qr { 2363 + width: 140px; 2364 + height: 140px; 2365 + } 2366 + } 2367 + 2228 2368 /* Ableton responsive - reflow on small panels */ 2229 2369 @container preview (max-height: 200px) { 2230 2370 #boot-screen-ableton .ableton-boot-logo { ··· 2301 2441 } 2302 2442 #boot-screen-ableton .ableton-boot-logo { 2303 2443 width: 40px; 2444 + } 2445 + } 2446 + /* Width-based fallbacks for FF1 */ 2447 + @media (max-width: 380px) { 2448 + #boot-screen-ff1 .ff1-settings { 2449 + max-width: 100%; 2450 + padding: 0 8px; 2451 + } 2452 + #boot-screen-ff1 .ff1-pairing-qr { 2453 + width: 120px; 2454 + height: 120px; 2455 + } 2456 + } 2457 + @media (max-width: 300px) { 2458 + #boot-screen-ff1 { 2459 + padding: 8px; 2460 + gap: 4px; 2461 + } 2462 + #boot-screen-ff1 .ff1-logo { 2463 + height: 28px; 2464 + } 2465 + #boot-screen-ff1 .ff1-pairing-qr { 2466 + width: 100px; 2467 + height: 100px; 2468 + } 2469 + #boot-screen-ff1 .ff1-btn { 2470 + padding: 6px 10px; 2471 + font-size: 10px; 2304 2472 } 2305 2473 } 2306 2474 } ··· 7934 8102 </div> 7935 8103 <div class="ff1-pairing" id="ff1-pairing"> 7936 8104 <canvas id="ff1-qr-canvas" class="ff1-pairing-qr"></canvas> 7937 - <div class="ff1-pairing-hint">Scan with phone → then scan your TV's QR</div> 8105 + <div class="ff1-pairing-hint">1. Show FF1 QR on your TV in the Feral File App<br>2. Scan this QR with your iPhone<br>3. Scan your TV</div> 7938 8106 <div class="ff1-pairing-status" id="ff1-pairing-status">Waiting…</div> 7939 8107 <div class="ff1-pairing-link" id="ff1-pairing-link" title="Click to copy"></div> 7940 8108 </div>
+56 -3
system/public/news.aesthetic.computer/client.js
··· 984 984 return; 985 985 } 986 986 987 + const currentWrapper = document.querySelector('.news-wrapper'); 988 + 989 + // Helper to wait for transition end 990 + const waitForTransition = (el, duration = 150) => new Promise(resolve => { 991 + const timeout = setTimeout(resolve, duration + 50); // fallback 992 + el.addEventListener('transitionend', function handler(e) { 993 + if (e.target === el) { 994 + clearTimeout(timeout); 995 + el.removeEventListener('transitionend', handler); 996 + resolve(); 997 + } 998 + }); 999 + }); 1000 + 987 1001 try { 988 1002 closeModal(); 1003 + 1004 + // Start fade-out transition 1005 + if (currentWrapper) { 1006 + currentWrapper.classList.add('page-transitioning-out'); 1007 + } 1008 + 989 1009 console.log('[news] Fetching:', nextPath); 990 1010 const res = await fetch(nextPath, { headers: { 'X-Requested-With': 'spa' } }); 991 1011 console.log('[news] Fetch response:', res.status); ··· 993 1013 console.log('[news] HTML length:', html.length, 'preview:', html.substring(0, 200)); 994 1014 const doc = new DOMParser().parseFromString(html, 'text/html'); 995 1015 const nextWrapper = doc.querySelector('.news-wrapper'); 996 - const currentWrapper = document.querySelector('.news-wrapper'); 997 1016 console.log('[news] Wrappers found:', !!nextWrapper, !!currentWrapper); 998 1017 console.log('[news] nextWrapper innerHTML length:', nextWrapper?.innerHTML?.length); 999 1018 if (!nextWrapper || !currentWrapper) { ··· 1001 1020 window.location.href = nextUrl; 1002 1021 return; 1003 1022 } 1023 + 1024 + // Wait for fade-out to complete (or timeout if fetch was fast) 1025 + if (currentWrapper.classList.contains('page-transitioning-out')) { 1026 + await waitForTransition(currentWrapper); 1027 + } 1028 + 1029 + // Swap content while hidden 1004 1030 console.log('[news] Replacing content, old length:', currentWrapper.innerHTML.length, 'new length:', nextWrapper.innerHTML.length); 1005 1031 currentWrapper.innerHTML = nextWrapper.innerHTML; 1006 1032 console.log('[news] After replace, current length:', currentWrapper.innerHTML.length); 1033 + 1034 + // Prepare for fade-in 1035 + currentWrapper.classList.remove('page-transitioning-out'); 1036 + currentWrapper.classList.add('page-transitioning-in'); 1037 + 1038 + // Force reflow to ensure transition triggers 1039 + currentWrapper.offsetHeight; 1040 + 1041 + // Fade in 1042 + currentWrapper.classList.remove('page-transitioning-in'); 1043 + 1007 1044 const newTitle = doc.title || 'Aesthetic News'; 1008 1045 document.title = newTitle; 1009 1046 if (push) { ··· 1014 1051 reinitPage(); 1015 1052 } catch (error) { 1016 1053 console.error('[news] Navigation error:', error); 1054 + // Clean up transition state on error 1055 + if (currentWrapper) { 1056 + currentWrapper.classList.remove('page-transitioning-out', 'page-transitioning-in'); 1057 + } 1017 1058 window.location.href = nextUrl; 1018 1059 } 1019 1060 } ··· 1055 1096 navigateTo(link.href, { push: true }); 1056 1097 }); 1057 1098 1058 - window.addEventListener('popstate', (e) => { 1099 + window.addEventListener('popstate', async (e) => { 1059 1100 // Restore title from state 1060 1101 if (e.state && e.state.title) { 1061 1102 document.title = e.state.title; 1062 1103 } 1063 1104 1064 - // If we have cached HTML, use it instantly 1105 + // If we have cached HTML, use it with transition 1065 1106 if (e.state && e.state.html) { 1066 1107 const currentWrapper = document.querySelector('.news-wrapper'); 1067 1108 if (currentWrapper) { 1109 + // Fade out 1110 + currentWrapper.classList.add('page-transitioning-out'); 1111 + await new Promise(r => setTimeout(r, 150)); 1112 + 1113 + // Swap content 1068 1114 currentWrapper.innerHTML = e.state.html; 1115 + 1116 + // Prepare for fade-in 1117 + currentWrapper.classList.remove('page-transitioning-out'); 1118 + currentWrapper.classList.add('page-transitioning-in'); 1119 + currentWrapper.offsetHeight; // force reflow 1120 + currentWrapper.classList.remove('page-transitioning-in'); 1121 + 1069 1122 window.scrollTo(0, 0); 1070 1123 reinitPage(); 1071 1124 return;
+28 -1
system/public/news.aesthetic.computer/main.css
··· 118 118 a:visited { color: var(--text-link-visited); } 119 119 a:hover { text-decoration: underline; } 120 120 121 - /* ===== Layout Container (HN-style centered) ===== */ 121 + /* ===== SPA Page Transition ===== */ 122 122 .news-wrapper { 123 123 width: 85%; 124 124 max-width: 1200px; ··· 128 128 display: flex; 129 129 flex-direction: column; 130 130 box-sizing: border-box; 131 + 132 + /* Transition properties */ 133 + opacity: 1; 134 + transform: translateY(0); 135 + transition: opacity 150ms ease-out, transform 150ms ease-out; 136 + } 137 + 138 + .news-wrapper.page-transitioning-out { 139 + opacity: 0; 140 + transform: translateY(-8px); 141 + pointer-events: none; 142 + } 143 + 144 + .news-wrapper.page-transitioning-in { 145 + opacity: 0; 146 + transform: translateY(8px); 147 + } 148 + 149 + /* Reduce motion for users who prefer it */ 150 + @media (prefers-reduced-motion: reduce) { 151 + .news-wrapper { 152 + transition: opacity 100ms ease-out; 153 + } 154 + .news-wrapper.page-transitioning-out, 155 + .news-wrapper.page-transitioning-in { 156 + transform: none; 157 + } 131 158 } 132 159 133 160 /* Full-width backdrop behind centered content */