Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
HTML
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132<!--ATProto User Page for *.at.aesthetic.computerShows all records for a specific user's handleUses ONLY ATProto APIs - no aesthetic.computer backend dependencies--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title id="page-title">Loading...</title> <meta name="description" content="Personal ATProto data page"> <link rel="icon" type="image/png" href="https://pals-aesthetic-computer.sfo3.cdn.digitaloceanspaces.com/painting-2023.7.29.20.39.png"> <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script> <style> * { box-sizing: border-box; }
::-webkit-scrollbar { display: none; } body { margin: 0; font-size: 14px; font-family: monospace; -webkit-text-size-adjust: none; background: #f5f5f5; color: #000; line-height: 1.4; }
.container { max-width: 1400px; margin: 0 auto; padding: 1em 0.5em; }
header { text-align: center; padding: 1em 0; border-bottom: 2px solid rgb(205, 92, 155); margin-bottom: 1em; }
h1 { font-size: 1.5em; font-weight: normal; margin: 0 0 0.3em 0; color: rgb(205, 92, 155); }
.subtitle { font-size: 0.85em; opacity: 0.7; margin: 0.3em 0; }
.stats { display: flex; gap: 1em; justify-content: center; flex-wrap: wrap; margin: 0.5em 0; font-size: 0.75em; }
.stat { padding: 0.3em 0.6em; background: rgba(205, 92, 155, 0.1); border-radius: 3px; }
.stat strong { color: rgb(205, 92, 155); }
.loading { text-align: center; padding: 4em 2em; font-size: 1.2em; opacity: 0.6; }
.error { text-align: center; padding: 4em 2em; color: #d32f2f; font-size: 1.1em; }
.section { margin: 1.5em 0; }
.section-title { font-size: 1.2em; font-weight: normal; margin: 0 0 0.5em 0; padding-bottom: 0.3em; border-bottom: 1px solid rgba(205, 92, 155, 0.3); display: flex; justify-content: space-between; align-items: center; }
.section-count { font-size: 0.7em; color: rgb(205, 92, 155); font-weight: bold; }
.records-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.5em; }
.record-card { background: white; padding: 0.75em; border-radius: 4px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; }
.record-card:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); }
.kidlisp-preview-card { position: relative; border-radius: 8px; overflow: hidden; background: #0d0d1a; aspect-ratio: 1; display: flex; align-items: stretch; justify-content: stretch; margin: 0.5em 0; }
.kidlisp-preview-card img.kidlisp-webp { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; opacity: 0.6; transform: scale(1.05); }
.kidlisp-preview-overlay { position: absolute; inset: 0; padding: 0.7em 0.8em; color: #fff; font-family: monospace; font-size: 0.75em; line-height: 1.35; text-shadow: 0 1px 2px rgba(0,0,0,0.9); pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.5) 80%); }
.kidlisp-preview-code { overflow: hidden; max-height: 60%; white-space: pre-wrap; word-break: break-word; }
.kidlisp-preview-qr { align-self: flex-end; display: flex; flex-direction: column; align-items: flex-end; line-height: 0; }
.kidlisp-preview-qr img { display: block; image-rendering: pixelated; width: 44px; height: 44px; background: #fff; padding: 2px; }
.kidlisp-preview-label { font-family: monospace; font-size: 1em; color: #fff; background: #000; padding: 0.15em 0.4em; margin-bottom: 4px; line-height: 1; }
.record-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5em; gap: 0.5em; }
.lexicon-badge { font-size: 0.65em; padding: 0.3em 0.6em; background: transparent; border: none; color: rgb(150, 150, 150); border-radius: 3px; cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.lexicon-badge:hover { color: rgb(200, 200, 200); text-decoration: underline; }
.color-cycle-a { animation: colorCycleA 1.8s steps(1) infinite; }
.color-cycle-r { animation: colorCycleR 1.8s steps(1) infinite; }
.color-cycle-t { animation: colorCycleT 1.8s steps(1) infinite; }
@keyframes colorCycleA { 0% { color: rgb(205, 92, 155); } 16.6% { color: rgb(255, 120, 100); } 33.3% { color: rgb(255, 200, 100); } 50% { color: rgb(150, 230, 150); } 66.6% { color: rgb(100, 180, 255); } 83.3% { color: rgb(180, 130, 230); } 100% { color: rgb(205, 92, 155); } }
@keyframes colorCycleR { 0% { color: rgb(255, 120, 100); } 16.6% { color: rgb(255, 200, 100); } 33.3% { color: rgb(150, 230, 150); } 50% { color: rgb(100, 180, 255); } 66.6% { color: rgb(180, 130, 230); } 83.3% { color: rgb(205, 92, 155); } 100% { color: rgb(255, 120, 100); } }
@keyframes colorCycleT { 0% { color: rgb(255, 200, 100); } 16.6% { color: rgb(150, 230, 150); } 33.3% { color: rgb(100, 180, 255); } 50% { color: rgb(180, 130, 230); } 66.6% { color: rgb(205, 92, 155); } 83.3% { color: rgb(255, 120, 100); } 100% { color: rgb(255, 200, 100); } }
/* Generic color cycle for handle characters */ .color-cycle { animation: colorCycleGeneric 1.8s steps(1) infinite; }
@keyframes colorCycleGeneric { 0% { color: rgb(205, 92, 155); } 16.6% { color: rgb(255, 120, 100); } 33.3% { color: rgb(255, 200, 100); } 50% { color: rgb(150, 230, 150); } 66.6% { color: rgb(100, 180, 255); } 83.3% { color: rgb(180, 130, 230); } 100% { color: rgb(205, 92, 155); } }
.record-type { font-size: 0.65em; padding: 0.2em 0.5em; background: rgb(205, 92, 155); color: white; border-radius: 2px; text-transform: uppercase; letter-spacing: 0.05em; }
.record-date { font-size: 0.7em; opacity: 0.6; white-space: nowrap; }
.record-content { margin: 0.5em 0; }
.record-image { width: 100%; max-width: 200px; margin: 0.5em 0; border-radius: 2px; border: none; image-rendering: pixelated; image-rendering: crisp-edges; }
.record-text { font-size: 0.85em; line-height: 1.3; white-space: pre-wrap; word-wrap: break-word; }
.record-meta { margin-top: 0.5em; padding-top: 0.5em; border-top: 1px solid #e0e0e0; font-size: 0.7em; opacity: 0.7; display: flex; gap: 0.5em; flex-wrap: wrap; }
.record-link { color: rgb(205, 92, 155); text-decoration: none; font-size: 0.75em; display: inline-block; margin-top: 0.3em; transition: color 0.2s; }
.record-link:hover { text-decoration: underline; color: rgb(255, 120, 200); }
/* Style all links */ a { color: rgb(205, 92, 155); text-decoration: none; transition: all 0.2s; }
a:hover { color: rgb(255, 120, 200); text-decoration: underline; }
a:visited { color: rgb(180, 70, 135); }
#pals { position: fixed; bottom: 5px; right: 16px; user-select: none; }
/* Preview box scrolling animation */ .source-preview { position: relative; max-height: 120px; overflow: hidden; background: linear-gradient(135deg, rgba(205, 92, 155, 0.05), rgba(100, 180, 255, 0.05)); border: 1px solid rgba(205, 92, 155, 0.2); } .source-preview:hover .preview-scroll-container { animation-play-state: paused !important; } .preview-scroll-container { display: flex; animation: previewScrollLoop 40s linear infinite; will-change: transform; } .line-numbers { flex-shrink: 0; width: 3em; text-align: right; padding-right: 0.5em; user-select: none; font-size: 0.9em; line-height: inherit; background: rgba(0, 0, 0, 0.1); border-right: 1px solid rgba(0, 0, 0, 0.15); } .line-numbers div { padding: 0.1em 0; } /* Striped color palette for line numbers */ .line-numbers div:nth-child(6n+1) { color: rgb(205, 92, 155); opacity: 0.8; } .line-numbers div:nth-child(6n+2) { color: rgb(255, 120, 100); opacity: 0.8; } .line-numbers div:nth-child(6n+3) { color: rgb(255, 200, 100); opacity: 0.8; } .line-numbers div:nth-child(6n+4) { color: rgb(150, 230, 150); opacity: 0.8; } .line-numbers div:nth-child(6n+5) { color: rgb(100, 180, 255); opacity: 0.8; } .line-numbers div:nth-child(6n+6) { color: rgb(180, 130, 230); opacity: 0.8; } .preview-content { display: block; flex: 1; padding-left: 0.5em; } /* Infinite scroll - content is duplicated so it wraps seamlessly */ @keyframes previewScrollLoop { 0% { transform: translateY(0); } 100% { transform: translateY(-50%); } }
.empty-state { text-align: center; padding: 2em 1em; opacity: 0.5; font-style: italic; }
.tabs { display: flex; gap: 0.3em; margin-bottom: 1em; border-bottom: 1px solid #e0e0e0; flex-wrap: wrap; }
.tab { padding: 0.5em 1em; background: none; border: none; font-family: monospace; font-size: 0.85em; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s; color: #666; }
.tab:hover { color: rgb(205, 92, 155); background: rgba(205, 92, 155, 0.05); }
.tab.active { color: rgb(205, 92, 155); border-bottom-color: rgb(205, 92, 155); font-weight: bold; }
.tab.disabled { opacity: 0.3; cursor: not-allowed; }
#scroll-top:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.4); }
#scroll-top:active { transform: translateY(0); }
@media (prefers-color-scheme: dark) { body { background: rgb(40, 35, 45); color: rgba(255, 255, 255, 0.9); }
.record-card { background: rgba(255, 255, 255, 0.05); color: rgba(255, 255, 255, 0.9); }
.stat { background: rgba(205, 92, 155, 0.2); }
.tabs { border-bottom-color: rgba(255, 255, 255, 0.1); }
.tab { color: rgba(255, 255, 255, 0.6); }
.record-meta { border-top-color: rgba(255, 255, 255, 0.1); } }
@media (max-width: 600px) { body { font-size: 16px; }
h1 { font-size: 1.5em; }
.stats { flex-direction: column; align-items: center; }
.tab { padding: 0.6em 1em; } } </style></head>
<body> <div class="container"> <header> <a href="https://at.aesthetic.computer" style="text-decoration: none; color: inherit;"><div style="font-size: 0.75em; color: rgb(205, 92, 155); margin-bottom: 0.3em;">← at.aesthetic.computer</div></a> <h1 id="handle-display">Loading...</h1> <div class="subtitle" id="did-display"></div> <div class="stats" id="stats"></div> </header>
<div id="loading" class="loading"> 🔍 Loading ATProto records... </div>
<div id="error" class="error" style="display: none;"></div>
<div id="content" style="display: none;"> <div class="tabs" id="tabs"></div> <div id="records-container"></div> </div>
<!-- Floating AC logo --> <svg id="pals" width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" onclick="window.scrollTo({ top: 0, behavior: 'smooth' })" style="cursor: pointer;" > <path fill-rule="evenodd" clip-rule="evenodd" d="M14.8982 5.10335C15.5333 4.92226 16.0802 4.97918 16.6196 5.27392L16.6294 5.27925L16.6386 5.28543C16.8852 5.45034 17.0637 5.6336 17.1768 5.8569C17.2893 6.07886 17.3264 6.31921 17.3264 6.57986C17.3264 6.8465 17.3041 7.09444 17.2269 7.3334C17.2091 7.38846 17.1886 7.44241 17.1652 7.4955C17.23 7.47358 17.2711 7.4571 17.2859 7.44936C17.3941 7.3926 17.5907 7.24475 17.8166 7.06782C17.8604 7.03348 17.9051 6.99825 17.9497 6.96304C18.1213 6.82774 18.2924 6.69283 18.4123 6.61077C18.6212 6.46789 18.9896 6.23185 19.3662 6.01043C19.7366 5.79269 20.1374 5.57551 20.4022 5.48361C20.7689 5.35632 21.2081 5.38009 21.5334 5.72086C21.7339 5.93084 21.8023 6.15795 21.7913 6.36941C21.7808 6.57 21.7001 6.73725 21.6399 6.84298L21.6299 6.86056L21.6171 6.87629C21.4157 7.12388 21.1869 7.28577 20.956 7.41907C20.851 7.47973 20.743 7.53584 20.6386 7.59007L20.6124 7.60369C20.4982 7.66307 20.3871 7.72144 20.2759 7.78716C20.0167 7.94039 19.4561 8.36643 19.1861 8.59807C18.854 8.88299 18.5291 9.22697 18.2969 9.64591C18.2562 9.71926 18.2357 9.85967 18.246 10.0459C18.2558 10.2216 18.2902 10.397 18.3192 10.5068C18.3474 10.6137 18.369 10.7073 18.39 10.7987C18.4496 11.0574 18.5052 11.2984 18.696 11.7739C18.8086 12.0543 18.9341 12.2641 19.0783 12.5052C19.1295 12.5907 19.183 12.6802 19.2391 12.7781C19.2555 12.8067 19.2708 12.8346 19.2856 12.8619C19.3428 12.9667 19.3941 13.0606 19.4805 13.1372C19.5653 13.2123 19.6973 13.2788 19.9584 13.218L19.9665 13.2161L19.9748 13.2147C20.347 13.1537 20.6475 13.0147 20.955 12.8725C20.9664 12.8672 20.9778 12.862 20.9891 12.8567C21.298 12.714 21.636 12.5602 22.0258 12.5602C22.3606 12.5602 22.6158 12.7005 22.7802 12.9167C22.9376 13.1238 23 13.384 23 13.6229C23 13.9455 22.7996 14.1926 22.6034 14.3582C22.4028 14.5276 22.1652 14.6481 21.9994 14.718C21.48 14.9369 20.4859 15.2891 19.7384 15.3685C19.7042 15.3721 19.661 15.3789 19.6105 15.3867C19.1991 15.4509 18.3 15.591 17.7518 14.7545C17.6407 14.585 17.4006 14.2594 17.1498 13.9515C17.0248 13.7981 16.8997 13.6521 16.7888 13.5341C16.6729 13.4106 16.5881 13.3346 16.5416 13.305C16.38 13.2022 16.262 13.2217 16.1713 13.2721C16.0628 13.3325 15.9743 13.4514 15.9373 13.5606C15.8308 13.8749 15.691 14.2874 15.5776 14.6879C15.4273 15.2186 15.2045 16.0282 15.0393 16.6782C15.0149 16.7741 14.9905 16.8863 14.963 17.0127C14.957 17.0402 14.9509 17.0685 14.9445 17.0974C14.9098 17.2562 14.8705 17.4305 14.8234 17.6033C14.7321 17.9384 14.6013 18.3097 14.3836 18.556C14.0202 18.9669 13.4846 19.0727 13.0429 18.9548C12.6009 18.8368 12.2125 18.4785 12.2125 17.943C12.2125 17.6301 12.3162 17.124 12.4343 16.6504C12.5535 16.1718 12.6958 15.6946 12.7899 15.416C12.9757 14.749 13.2116 13.8949 13.3866 13.1212C13.3979 13.0613 13.4099 12.9976 13.4226 12.9309C13.4978 12.5343 13.5932 12.031 13.6699 11.5717C13.7149 11.3024 13.7529 11.0514 13.7766 10.8478C13.8015 10.633 13.8065 10.5012 13.7992 10.4515C13.7844 10.3497 13.751 10.315 13.729 10.2987C13.699 10.2766 13.6441 10.2559 13.5426 10.2494C13.4417 10.2429 13.3238 10.2517 13.1852 10.2649C13.1692 10.2664 13.1529 10.268 13.1363 10.2696C13.0172 10.2811 12.8839 10.294 12.7597 10.294C12.6223 10.294 12.472 10.2977 12.3149 10.3015C11.9756 10.3098 11.605 10.3188 11.2661 10.2933C11.1149 10.2819 10.9352 10.258 10.7544 10.2337L10.7295 10.2304C10.5535 10.2067 10.3742 10.1825 10.2029 10.1659C10.0225 10.1485 9.86134 10.1404 9.7317 10.1484C9.59167 10.157 9.53289 10.1823 9.51826 10.1932C9.40563 10.2772 9.36293 10.3344 9.34618 10.3683C9.33318 10.3946 9.32868 10.4202 9.3367 10.4673C9.34595 10.5217 9.36711 10.5816 9.40163 10.6792C9.40461 10.6877 9.40769 10.6964 9.41087 10.7054C9.44816 10.8111 9.49265 10.9424 9.52178 11.0999C9.55533 11.2814 9.5886 11.4647 9.62198 11.6487C9.7153 12.1629 9.80952 12.6821 9.91334 13.1798C9.9259 13.24 9.93878 13.3014 9.95186 13.3637C10.1143 14.1372 10.3081 15.0602 10.3081 15.8244C10.3081 15.9889 10.3121 16.1554 10.3162 16.3235C10.3297 16.8792 10.3436 17.4525 10.2136 18.0277C10.0889 18.5797 9.55124 18.8059 9.10572 18.8008C8.66723 18.7957 8.13248 18.5567 8.08837 17.9935C8.06444 17.6878 8.09368 17.4281 8.12633 17.2004C8.13244 17.1578 8.13853 17.1171 8.1444 17.0777C8.17087 16.9005 8.19298 16.7524 8.19298 16.5993C8.19298 15.8848 8.00978 15.083 7.83775 14.4909C7.75439 14.204 7.63364 14.1146 7.55668 14.0888C7.47435 14.0612 7.35495 14.0767 7.21784 14.1711C7.12371 14.2358 7.02 14.3956 6.91136 14.6516C6.83797 14.8246 6.77436 15.01 6.71076 15.1953C6.68322 15.2755 6.65569 15.3557 6.62737 15.4349C6.44732 15.9385 6.18242 16.5995 5.96374 17.0833C5.90321 17.2173 5.85247 17.3496 5.80243 17.4829C5.79639 17.4991 5.79033 17.5152 5.78426 17.5315C5.74091 17.6474 5.69659 17.7658 5.64795 17.8792C5.53571 18.1408 5.39094 18.3993 5.13767 18.6146L5.12889 18.6221L5.11943 18.6287C4.88967 18.7898 4.54244 18.8806 4.21559 18.8642C3.88604 18.8477 3.51569 18.7163 3.3221 18.3609C3.22342 18.1798 3.20996 17.9754 3.22908 17.7902C3.24839 17.6032 3.3033 17.4128 3.36603 17.2414C3.42919 17.0687 3.50363 16.9062 3.56667 16.7745C3.58947 16.7269 3.60969 16.6854 3.62724 16.6494C3.6617 16.5786 3.68592 16.5289 3.69936 16.495C3.8768 16.0476 4.01924 15.64 4.17636 15.1904C4.26827 14.9274 4.3652 14.65 4.47711 14.3419C4.60715 13.9838 4.95824 12.8655 5.14491 12.0888C5.19511 11.8799 5.24781 11.7216 5.28981 11.5955C5.30443 11.5516 5.31775 11.5115 5.32922 11.4746C5.37289 11.3342 5.40057 11.2104 5.40057 11.0093C5.40057 10.9784 5.3803 10.9448 5.35211 10.9295C5.34057 10.9232 5.32949 10.921 5.31788 10.9221C5.30669 10.9232 5.28313 10.9285 5.24908 10.9554C5.15797 11.0275 5.02885 11.1337 4.88463 11.2523C4.62733 11.4639 4.32194 11.715 4.09846 11.8826C3.78743 12.1158 3.44918 12.4103 3.10959 12.706C2.99451 12.8062 2.87926 12.9066 2.76487 13.0047C2.58673 13.1575 2.3447 13.326 2.06932 13.376C1.92662 13.4019 1.77378 13.3961 1.62075 13.3392C1.46845 13.2826 1.33057 13.1809 1.20826 13.0366C0.991479 12.781 0.95847 12.4944 1.04269 12.2282C1.12188 11.9778 1.30054 11.7539 1.49196 11.5725C1.60625 11.4642 1.75653 11.3228 1.92542 11.164C2.46962 10.6521 3.20715 9.9583 3.55739 9.60275C3.70303 9.4549 3.82293 9.3268 3.93164 9.21066C4.21275 8.91035 4.41901 8.68999 4.80176 8.415C4.86144 8.35575 4.92154 8.30978 4.97317 8.27381C4.96626 8.26895 4.95902 8.26391 4.95145 8.2587C4.53475 7.97188 4.10253 7.21461 4.52248 6.34306C4.68856 5.9984 4.89668 5.74448 5.14374 5.56752C5.39101 5.39041 5.6638 5.30006 5.94419 5.26279C6.4907 5.19016 7.04612 5.30816 7.42088 5.58929C7.72296 5.8159 7.96946 6.15062 8.06084 6.52803C8.16526 6.95934 8.07218 7.30168 7.98025 7.53605C7.96274 7.58069 7.94515 7.6217 7.92956 7.65735C8.38866 7.7876 8.68645 7.8138 9.01858 7.84303C9.0943 7.8497 9.17182 7.85652 9.25343 7.86477C9.31219 7.8707 9.39816 7.87955 9.50086 7.89011C9.89664 7.93083 10.5409 7.99711 10.8338 8.02111C11.262 8.05621 12.0343 8.11934 12.7225 8.02302C13.095 7.96395 13.3919 7.92769 13.6272 7.90177C13.7148 7.89213 13.7918 7.8841 13.8602 7.87697C13.9759 7.86491 14.067 7.85542 14.1427 7.84501C14.088 7.79376 14.027 7.72972 13.9687 7.65108C13.7716 7.38488 13.6315 6.98552 13.759 6.39462C13.8933 5.77205 14.2887 5.27713 14.8982 5.10335ZM14.3079 7.98743C14.3079 7.98742 14.3077 7.98718 14.3072 7.98672C14.3077 7.9872 14.3079 7.98743 14.3079 7.98743ZM16.3689 5.69496C15.9552 5.4716 15.5479 5.42594 15.0363 5.57182C14.6356 5.68607 14.3482 6.01348 14.2442 6.49583C14.1451 6.9551 14.2576 7.21261 14.3697 7.36393C14.4296 7.44486 14.4963 7.50454 14.5562 7.55437C14.562 7.55923 14.5685 7.56462 14.5755 7.57035C14.5985 7.58926 14.6259 7.61178 14.6458 7.63026C14.6596 7.64302 14.6809 7.66378 14.7001 7.69016C14.7164 7.7125 14.7547 7.77006 14.7547 7.85171C14.7547 7.96533 14.7273 8.10948 14.587 8.20991C14.482 8.28514 14.3415 8.30979 14.2213 8.32669C14.1353 8.33878 14.0269 8.3501 13.8989 8.36346C13.8319 8.37045 13.7596 8.37799 13.6824 8.3865C13.4524 8.41184 13.163 8.44719 12.7992 8.50491L12.797 8.50526L12.7948 8.50558C12.0497 8.61026 11.2306 8.5431 10.8053 8.50823L10.7926 8.50719C10.4937 8.48269 9.83874 8.4153 9.4439 8.37468C9.34318 8.36432 9.25938 8.35569 9.20274 8.34997C9.12713 8.34233 9.05328 8.33587 8.97943 8.32941C8.59427 8.29573 8.20917 8.26205 7.57537 8.06072L7.54617 8.05145L7.52016 8.03546C7.41937 7.97351 7.37992 7.87416 7.37749 7.78687C7.37557 7.71796 7.39598 7.6556 7.40927 7.6188C7.4236 7.57911 7.44274 7.5356 7.45971 7.49703L7.46158 7.4928C7.48022 7.45041 7.49907 7.4075 7.5175 7.36051C7.58929 7.1775 7.65106 6.94132 7.57835 6.641C7.51721 6.38847 7.34484 6.14571 7.12007 5.9771C6.8714 5.79055 6.45604 5.68696 6.01061 5.74616C5.79502 5.77481 5.60406 5.84123 5.43573 5.96179C5.26721 6.0825 5.10792 6.26713 4.97069 6.55193C4.67103 7.17382 4.98354 7.68542 5.23585 7.85909C5.34648 7.93524 5.44869 8.01423 5.50733 8.10184C5.54069 8.1517 5.57312 8.22342 5.56708 8.3111C5.56098 8.39945 5.51857 8.46412 5.48267 8.50442C5.44885 8.54238 5.40977 8.57112 5.38263 8.59003C5.36351 8.60336 5.34006 8.61864 5.31952 8.63202C5.31133 8.63736 5.30361 8.64239 5.2968 8.64688C5.24196 8.68306 5.19124 8.71952 5.14493 8.76759L5.12909 8.78404L5.11044 8.79733C4.75876 9.04799 4.5912 9.22704 4.32176 9.51494C4.21003 9.63433 4.08078 9.77243 3.91362 9.94213C3.55595 10.3052 2.80487 11.0117 2.26019 11.5241C2.09389 11.6805 1.94683 11.8188 1.83608 11.9238L1.83607 11.9238C1.67225 12.079 1.56004 12.2347 1.51628 12.373C1.47755 12.4955 1.4895 12.6067 1.58913 12.7242L1.58914 12.7242C1.66719 12.8163 1.73782 12.8613 1.79609 12.8829C1.85363 12.9043 1.91343 12.9083 1.97935 12.8963C2.12123 12.8706 2.28041 12.7731 2.43882 12.6372C2.5461 12.5451 2.6567 12.4488 2.76895 12.3511C3.11216 12.0522 3.47086 11.7398 3.79775 11.4947C4.01563 11.3313 4.29585 11.1007 4.54485 10.8957C4.69383 10.7731 4.83163 10.6597 4.93822 10.5753C5.14798 10.4094 5.39517 10.3956 5.59199 10.5026C5.77395 10.6014 5.89654 10.7962 5.89654 11.0093C5.89654 11.2684 5.85837 11.4408 5.80353 11.6172C5.78813 11.6668 5.77232 11.7142 5.75615 11.7628C5.71555 11.8848 5.67258 12.0138 5.62758 12.201C5.4365 12.9961 5.0801 14.1317 4.94419 14.5059C4.83695 14.8012 4.7417 15.0737 4.65024 15.3353C4.49033 15.7927 4.34202 16.2169 4.16143 16.6723C4.14095 16.7239 4.10332 16.8012 4.06288 16.8842C4.0472 16.9164 4.0311 16.9495 4.01541 16.9823C3.95473 17.109 3.88802 17.2553 3.83273 17.4065C3.77699 17.5588 3.73614 17.7075 3.72252 17.8395C3.7087 17.9733 3.72513 18.0679 3.75929 18.1306C3.84089 18.2804 4.01052 18.3656 4.24086 18.3771C4.46841 18.3885 4.69552 18.3225 4.82254 18.2377C4.98849 18.0935 5.09436 17.9148 5.19099 17.6896C5.23478 17.5875 5.27479 17.4806 5.31861 17.3635C5.3247 17.3472 5.33086 17.3308 5.33712 17.3141C5.38755 17.1797 5.44298 17.0347 5.51051 16.8852C5.72338 16.4142 5.98345 15.7655 6.15948 15.2732C6.18339 15.2063 6.20838 15.1335 6.23446 15.0575C6.30034 14.8656 6.37323 14.6533 6.45363 14.4638C6.56239 14.2075 6.71075 13.9247 6.93342 13.7715C7.15058 13.622 7.43534 13.5329 7.71667 13.6272C8.00337 13.7232 8.20577 13.9822 8.31465 14.3569C8.49077 14.9631 8.68895 15.8166 8.68895 16.5993C8.68895 16.7915 8.66032 16.9819 8.63345 17.1605C8.62795 17.197 8.62253 17.2331 8.61745 17.2685C8.5865 17.4843 8.56307 17.703 8.58288 17.956C8.5982 18.1516 8.79173 18.3094 9.11151 18.313C9.42426 18.3166 9.67486 18.1635 9.72945 17.9218C9.84509 17.4101 9.83345 16.9177 9.82064 16.376C9.81644 16.1982 9.81212 16.0152 9.81212 15.8244C9.81212 15.1129 9.62868 14.2379 9.46366 13.4507C9.45148 13.3926 9.43939 13.3349 9.42748 13.2778C9.32227 12.7734 9.22637 12.245 9.13272 11.7289C9.09957 11.5463 9.0667 11.3651 9.0338 11.1872C9.01181 11.0682 8.97786 10.9661 8.94228 10.8652C8.93868 10.855 8.93495 10.8445 8.93114 10.8339C8.90026 10.7471 8.8642 10.6458 8.84752 10.5478C8.82676 10.4258 8.83197 10.2929 8.90009 10.1551C8.96445 10.0248 9.07427 9.9122 9.21849 9.8046C9.35612 9.70192 9.54184 9.6714 9.70072 9.66162C9.86998 9.6512 10.0621 9.66217 10.2516 9.68053C10.4325 9.69806 10.6203 9.72334 10.7939 9.74672L10.8216 9.75045C11.006 9.77525 11.1703 9.79689 11.3039 9.80695C11.6184 9.83063 11.9425 9.82254 12.2673 9.81444C12.4316 9.81035 12.5961 9.80624 12.7597 9.80624C12.8578 9.80624 12.9647 9.79597 13.0871 9.7842C13.1036 9.78261 13.1205 9.78099 13.1376 9.77937C13.2736 9.76649 13.4288 9.75328 13.5749 9.76267C13.7205 9.77202 13.8865 9.80514 14.0267 9.90866C14.1749 10.0181 14.2609 10.1809 14.2902 10.3825C14.308 10.5047 14.2931 10.6992 14.2693 10.9032C14.2443 11.1184 14.2048 11.3785 14.1593 11.6507C14.0817 12.1157 13.9851 12.625 13.91 13.0213C13.8971 13.0893 13.8848 13.1539 13.8734 13.2144L13.8726 13.2187L13.8717 13.2228C13.693 14.0134 13.4526 14.8832 13.2665 15.5512L13.2647 15.5576L13.2626 15.5639C13.1732 15.8277 13.0333 16.2957 12.916 16.7665C12.7965 17.2461 12.7085 17.6974 12.7085 17.943C12.7085 18.2088 12.894 18.4096 13.1729 18.4841C13.4521 18.5586 13.7844 18.4902 14.0093 18.2359C14.1458 18.0815 14.2541 17.8083 14.3444 17.477C14.3881 17.3167 14.4252 17.1524 14.4596 16.9949C14.4656 16.9678 14.4714 16.9407 14.4773 16.9139C14.5048 16.7873 14.5314 16.6648 14.5581 16.5599C14.7248 15.904 14.9488 15.0899 15.0998 14.557C15.2169 14.1438 15.3601 13.721 15.4661 13.4085L15.4668 13.4064C15.5361 13.202 15.695 12.9767 15.9271 12.8476C16.1771 12.7085 16.4931 12.6932 16.811 12.8955C16.9151 12.9617 17.0368 13.0792 17.1532 13.2032C17.2747 13.3326 17.4078 13.4881 17.5368 13.6466C17.7942 13.9624 18.0454 14.3021 18.1687 14.4903C18.529 15.0401 19.0678 14.9663 19.5043 14.9065C19.567 14.898 19.6276 14.8897 19.6852 14.8836C20.3572 14.8122 21.2962 14.4837 21.8041 14.2697C21.9448 14.2104 22.1323 14.1132 22.2803 13.9882C22.4326 13.8596 22.504 13.7357 22.504 13.6229C22.504 13.4595 22.4602 13.3104 22.3829 13.2087C22.3125 13.1161 22.2047 13.048 22.0258 13.048C21.7624 13.048 21.5204 13.1501 21.2 13.2982C21.1848 13.3052 21.1693 13.3124 21.1538 13.3196C20.8554 13.4578 20.5021 13.6215 20.0644 13.6945C19.6566 13.7872 19.3585 13.6856 19.1484 13.4995C18.9898 13.3588 18.889 13.1701 18.8331 13.0654C18.823 13.0465 18.8143 13.0303 18.8071 13.0176C18.7602 12.9358 18.7123 12.8558 18.6642 12.7754C18.5145 12.5254 18.3627 12.2719 18.2347 11.953C18.0289 11.4402 17.964 11.1584 17.9028 10.8925C17.8827 10.8056 17.8631 10.7204 17.8391 10.6294C17.8037 10.4951 17.7627 10.2871 17.7508 10.0724C17.7395 9.86822 17.7512 9.61118 17.8614 9.41243C18.1313 8.92552 18.5018 8.53787 18.8601 8.23051C19.1397 7.99063 19.7252 7.54356 20.0204 7.3691C20.1445 7.29577 20.2664 7.23185 20.3806 7.1725L20.4049 7.15984C20.5114 7.1045 20.6099 7.05333 20.7049 6.99846C20.9004 6.88556 21.0698 6.76333 21.2168 6.58743C21.2585 6.51101 21.2916 6.42806 21.2959 6.34449C21.3001 6.26538 21.2799 6.16786 21.1719 6.05471C21.0165 5.89202 20.8027 5.86182 20.5672 5.94356C20.3557 6.01698 19.9948 6.20913 19.6207 6.42908C19.2529 6.64535 18.8941 6.87534 18.6955 7.01117C18.5909 7.08275 18.4392 7.20237 18.2698 7.33588C18.2229 7.37287 18.1746 7.41091 18.1256 7.4493C17.9134 7.61561 17.6767 7.79714 17.5193 7.87974C17.4582 7.91177 17.362 7.94576 17.2726 7.97451C17.1769 8.00529 17.0675 8.03675 16.9677 8.06233C16.8714 8.08699 16.7721 8.10934 16.7016 8.11789C16.6834 8.1201 16.6606 8.12222 16.6371 8.12214H16.6358C16.6227 8.12216 16.5634 8.12225 16.5035 8.09117C16.465 8.07123 16.3993 8.02361 16.3756 7.93227C16.3529 7.84493 16.3841 7.77575 16.4022 7.74494C16.4208 7.71319 16.4444 7.6894 16.4629 7.67362C16.613 7.50624 16.7012 7.34969 16.7542 7.18571C16.8097 7.01388 16.8305 6.82185 16.8305 6.57986C16.8305 6.36355 16.7994 6.20559 16.7329 6.07444C16.6682 5.94668 16.5594 5.82392 16.3689 5.69496Z" fill="rgb(205, 92, 155)"/> </svg> </div>
<script src="/media-modal.js"></script> <script> // Determine base URLs based on current environment function getBaseURL() { const hostname = window.location.hostname; // Dev mode: localhost or 127.0.0.1 if (hostname === '127.0.0.1' || hostname === 'localhost') { // Always use localhost:8888 for dev (not 127.0.0.1 due to SSL cert) return 'https://localhost:8888'; } // Production return 'https://aesthetic.computer'; } const API_BASE_URL = getBaseURL(); const PDS_URL = 'https://at.aesthetic.computer'; const RECORDS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes const FORCE_REFRESH = (() => { const params = new URLSearchParams(window.location.search); return params.has('refresh') || params.has('nocache'); })(); console.log('🌐 API Base URL:', API_BASE_URL); // Shared modal functions function openModal(url) { if (!url) return; if (!window.ACMediaModal?.open) { window.open(url, '_blank', 'noopener,noreferrer'); return; } window.ACMediaModal.open({ title: 'Aesthetic Computer', subtitle: 'Embedded view', iframeUrl: url, bodyHtml: '', actions: [{ label: 'Open In New Tab', url }], }); }
function closeModal() { if (!window.ACMediaModal?.close) return; window.ACMediaModal.close(); }
// Intercept aesthetic.computer and pdsls.dev links (left-click only) document.addEventListener('click', (e) => { const link = e.target.closest('a'); if (link && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.button === 0) { const href = link.getAttribute('href'); // Intercept aesthetic.computer URLs (not at.aesthetic.computer) and pdsls.dev URLs if (href && ((href.includes('aesthetic.computer') && !href.includes('at.aesthetic.computer')) || href.includes('pdsls.dev'))) { e.preventDefault(); openModal(href); } } }); function normalizeHandle(handle) { if (!handle) return null; if (handle.includes('.')) return handle; return `${handle}.at.aesthetic.computer`; }
function getHandleFromQuery() { const params = new URLSearchParams(window.location.search); const handle = params.get('handle') || params.get('h'); return normalizeHandle(handle); }
// Extract handle from subdomain function getHandleFromSubdomain() { const hostname = window.location.hostname;
// Query param override (local testing) const queryHandle = getHandleFromQuery(); if (queryHandle) { console.log(`🔧 Using handle from query param: ${queryHandle}`); return queryHandle; } // Dev mode: default to 'art' handle when running on localhost if (hostname === '127.0.0.1' || hostname === 'localhost') { console.log('🔧 Dev mode: defaulting to art.at.aesthetic.computer'); return 'art.at.aesthetic.computer'; } // Match: handle.at.aesthetic.computer const match = hostname.match(/^([^.]+)\.at\.aesthetic\.computer$/); if (match) { return normalizeHandle(match[1]); } return null; }
function getCacheKey(handle, collection) { return `ac:at-records:${handle}:${collection}`; }
function loadCachedRecords(handle, collection) { if (FORCE_REFRESH) return null; try { const key = getCacheKey(handle, collection); const raw = localStorage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || !parsed.ts || !Array.isArray(parsed.records)) return null; if (Date.now() - parsed.ts > RECORDS_CACHE_TTL_MS) return null; return parsed.records; } catch (error) { return null; } }
function saveCachedRecords(handle, collection, records) { try { const key = getCacheKey(handle, collection); localStorage.setItem(key, JSON.stringify({ ts: Date.now(), records })); } catch (error) { // Ignore storage errors (quota, etc.) } }
function clearRecordsCache(handle) { try { const prefix = `ac:at-records:${handle}:`; Object.keys(localStorage) .filter(key => key.startsWith(prefix)) .forEach(key => localStorage.removeItem(key)); } catch (error) { // Ignore storage errors } }
// Resolve handle to DID async function resolveDID(handle) { try { const response = await fetch(`${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); if (!response.ok) throw new Error(`Failed to resolve handle: ${response.status}`); const data = await response.json(); return data.did; } catch (error) { console.error('Error resolving DID:', error); throw error; } }
// List all records for a collection async function listRecords(did, collection, handle, limit = 100) { const cached = handle ? loadCachedRecords(handle, collection) : null; if (cached) return cached;
const records = []; let cursor = undefined;
try { do { const url = new URL(`${PDS_URL}/xrpc/com.atproto.repo.listRecords`); url.searchParams.append('repo', did); url.searchParams.append('collection', collection); url.searchParams.append('limit', limit); if (cursor) url.searchParams.append('cursor', cursor);
const response = await fetch(url); if (!response.ok) { if (response.status === 400) { // Collection might not exist for this user return []; } throw new Error(`Failed to list records: ${response.status}`); }
const data = await response.json(); records.push(...(data.records || [])); cursor = data.cursor; } while (cursor);
if (handle) saveCachedRecords(handle, collection, records); return records; } catch (error) { console.error(`Error listing ${collection}:`, error); return []; } }
// Format date function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }
// Get record key from URI function getRkey(uri) { return uri.split('/').pop(); }
function getLexicon(uri) { // URI format: at://did:plc:xyz/computer.aesthetic.painting/rkey const parts = uri.split('//')[1].split('/'); return parts[1]; // computer.aesthetic.painting }
function getLexiconTypeName(lexicon) { // Get just the type name (e.g., "painting" from "computer.aesthetic.painting") return lexicon.split('.').pop(); }
// Parse kidlisp color escape codes (like \red\, \blue\, etc.) and convert to HTML function parseColoredKidlisp(coloredString) { if (!coloredString) return ''; const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; // Color mapping from kidlisp names to CSS colors const colorMap = { 'red': '#ff0000', 'darkred': '#8b0000', 'orange': '#ff6600', 'yellow': isDark ? '#ffff00' : '#d4a000', 'lime': isDark ? '#00ff00' : '#00a000', 'limegreen': '#32cd32', 'green': '#008000', 'cyan': isDark ? '#00ffff' : '#008b8b', 'blue': '#0000ff', 'purple': '#800080', 'magenta': '#ff00ff', 'hotpink': '#ff69b4', 'white': isDark ? '#ffffff' : '#333333', 'black': isDark ? '#000000' : '#ffffff', 'gray': '#808080', 'grey': '#808080', 'mediumseagreen': '#3cb371', 'pink': '#ffc0cb', 'brown': '#a52a2a', 'violet': '#ee82ee', 'teal': '#008080', 'navy': '#000080', 'maroon': '#800000', 'olive': '#808000', 'silver': '#c0c0c0' }; const result = []; let i = 0; let currentColor = isDark ? '#ffffff' : '#333333'; let textBuffer = ''; function flushBuffer() { if (textBuffer) { const escaped = textBuffer .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); result.push(`<span style="color: ${currentColor}">${escaped}</span>`); textBuffer = ''; } } while (i < coloredString.length) { if (coloredString[i] === '\\' && i + 1 < coloredString.length) { // Find the closing \ let j = i + 1; while (j < coloredString.length && coloredString[j] !== '\\') { j++; } if (j < coloredString.length) { const colorCode = coloredString.substring(i + 1, j); // Flush any buffered text before changing color flushBuffer(); // Parse the color code if (colorMap[colorCode.toLowerCase()]) { currentColor = colorMap[colorCode.toLowerCase()]; } else if (/^\d+,\d+,\d+(,[\d.]+)?$/.test(colorCode)) { // RGB or RGBA value like "255,20,147" or "255,255,255,0.5" currentColor = `rgb(${colorCode})`; } else { // Unknown color code - try as CSS color name currentColor = colorCode; } i = j + 1; continue; } } // Regular character - add to buffer textBuffer += coloredString[i]; i++; } // Flush any remaining text flushBuffer(); return result.join(''); }
function buildQrDataUrl(text, cellSize = 4, margin = 0) { try { if (typeof qrcode === 'undefined') return ''; const qr = qrcode(0, 'M'); qr.addData(text); qr.make(); return qr.createDataURL(cellSize, margin); } catch (error) { return ''; } } // Load kidlisp.mjs and generate colored string let kidlispModule = null; let highlightCache = new Map(); // Cache highlighted results let sourceMap = window.sourceMap || (window.sourceMap = {}); // Cache source code for modal const kidlispModuleUrls = [ 'https://aesthetic.computer/aesthetic.computer/lib/kidlisp.mjs', 'https://aesthetic.computer/lib/kidlisp.mjs', `${window.location.origin}/aesthetic.computer/lib/kidlisp.mjs`, `${window.location.origin}/lib/kidlisp.mjs`, ];
async function loadKidlispModule() { if (kidlispModule) return kidlispModule;
for (const url of kidlispModuleUrls) { try { const module = await import(url); if (module && module.KidLisp) { kidlispModule = module; console.log(`✅ Kidlisp module loaded successfully from ${url}`); return kidlispModule; } } catch (error) { console.warn(`⚠️ Failed to load kidlisp module from ${url}:`, error); } }
console.error('❌ Failed to load kidlisp module from all known locations'); return null; } async function highlightKidlisp(source) { if (!source) return ''; // Check cache first if (highlightCache.has(source)) { return highlightCache.get(source); } try { const module = await loadKidlispModule(); if (module && module.KidLisp) { // Create a kidlisp instance for syntax highlighting const kid = new module.KidLisp(); kid.initializeSyntaxHighlighting(source); const coloredString = kid.buildColoredKidlispString();
if (coloredString && coloredString.trim()) { console.log('🎨 KidLisp colored string sample:', coloredString.substring(0, 100));
const result = parseColoredKidlisp(coloredString);
// Cache the result highlightCache.set(source, result); return result; } } else { console.warn('⚠️ Kidlisp module or KidLisp class not found'); } } catch (error) { console.error('❌ Syntax highlighting failed:', error); }
// Fallback: simple syntax highlighting const fallback = highlightKidLispSimple(source); highlightCache.set(source, fallback); return fallback; }
async function openLexiconModal(lexicon) { // Load the lexicon JSON file const lexiconPath = lexicon.replace(/\./g, '/'); try { const response = await fetch(`/lexicons/${lexiconPath}.json`); if (!response.ok) { throw new Error(`Failed to load lexicon: ${response.status}`); } const lexiconData = await response.json(); // Create a formatted HTML view of the lexicon const formattedJson = JSON.stringify(lexiconData, null, 2); const htmlContent = ` <!DOCTYPE html> <html> <head> <style> body { margin: 0; padding: 2em; background: black; color: rgb(200, 200, 200); font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: 11px; line-height: 1.4; } h1 { color: rgb(205, 92, 155); font-size: 1.2em; margin-bottom: 0.8em; font-weight: normal; } pre { margin: 0; white-space: pre-wrap; word-break: break-word; } .json-key { color: rgb(100, 150, 200); } .json-string { color: rgb(152, 195, 121); } .json-number { color: rgb(209, 154, 102); } .json-boolean { color: rgb(198, 120, 221); } .json-null { color: rgb(229, 192, 123); } </style> </head> <body> <h1>${lexicon}</h1> <pre>${syntaxHighlight(formattedJson)}</pre> </body> </html> `; // Create blob URL and open in modal const blob = new Blob([htmlContent], { type: 'text/html' }); const blobUrl = URL.createObjectURL(blob); openModal(blobUrl); // Clean up blob URL after modal opens setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); } catch (error) { console.error('Failed to load lexicon:', error); alert(`Failed to load lexicon: ${error.message}`); } }
function syntaxHighlight(json) { return json .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'json-number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'json-key'; } else { cls = 'json-string'; } } else if (/true|false/.test(match)) { cls = 'json-boolean'; } else if (/null/.test(match)) { cls = 'json-null'; } return '<span class="' + cls + '">' + match + '</span>'; }); }
// Simple JavaScript syntax highlighter function highlightJavaScript(code) { // Escape HTML first const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); // Parse and highlight in order of precedence to avoid conflicts let result = ''; let i = 0; while (i < escaped.length) { let matched = false; // Multi-line comments if (escaped.substr(i, 2) === '/*') { let end = escaped.indexOf('*/', i + 2); if (end === -1) end = escaped.length; else end += 2; result += '<span style="color: rgb(120, 120, 120);">' + escaped.substring(i, end) + '</span>'; i = end; continue; } // Single-line comments if (escaped.substr(i, 2) === '//') { let end = escaped.indexOf('\n', i); if (end === -1) end = escaped.length; else end += 1; result += '<span style="color: rgb(120, 120, 120);">' + escaped.substring(i, end) + '</span>'; i = end; continue; } // Strings if (escaped[i] === '"' || escaped[i] === "'" || escaped[i] === '`') { const quote = escaped[i]; let end = i + 1; while (end < escaped.length && escaped[end] !== quote) { if (escaped[end] === '\\') end += 2; else end++; } end++; // Include closing quote result += '<span style="color: rgb(150, 255, 150);">' + escaped.substring(i, end) + '</span>'; i = end; continue; } // Numbers if (/\d/.test(escaped[i])) { let end = i; while (end < escaped.length && /[\d.]/.test(escaped[end])) end++; result += '<span style="color: rgb(255, 200, 100);">' + escaped.substring(i, end) + '</span>'; i = end; continue; } // Keywords and identifiers if (/[a-zA-Z_$]/.test(escaped[i])) { let end = i; while (end < escaped.length && /[a-zA-Z0-9_$]/.test(escaped[end])) end++; const word = escaped.substring(i, end); const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'async', 'await', 'class', 'extends', 'import', 'export', 'from', 'default', 'new', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'delete', 'in', 'of', 'void', 'yield', 'static', 'get', 'set', 'super', 'this']; if (keywords.includes(word)) { result += '<span style="color: rgb(100, 150, 255);">' + word + '</span>'; } else { // Check if it's a function call (followed by opening paren) let j = end; while (j < escaped.length && /\s/.test(escaped[j])) j++; if (escaped[j] === '(') { result += '<span style="color: rgb(255, 220, 100);">' + word + '</span>'; } else { result += word; } } i = end; continue; } // Default: just add the character result += escaped[i]; i++; } return result; }
// KidLisp syntax highlighter - simple fallback function highlightKidLispSimple(code) { // Escape HTML first code = code.replace(/</g, '<').replace(/>/g, '>'); // Comments (semicolons) code = code.replace(/(;.*$)/gm, '<span style="color: rgb(120, 120, 120);">$1</span>'); // Strings code = code.replace(/("(?:\\.|[^"\\])*")/g, '<span style="color: rgb(150, 255, 150);">$1</span>'); // Numbers code = code.replace(/\b(-?\d+\.?\d*)\b/g, '<span style="color: rgb(255, 200, 100);">$1</span>'); // Common KidLisp forms/functions (after opening paren) code = code.replace(/\(([a-zA-Z_][a-zA-Z0-9_-]*)/g, '(<span style="color: rgb(147, 51, 234);">$1</span>'); // Parentheses code = code.replace(/([()])/g, '<span style="color: rgb(200, 200, 200);">$1</span>'); return code; }
async function openSourceModal(uri, source, slug) { // Detect file type from slug const isJavaScript = slug && slug.endsWith('.mjs'); const isKidLisp = slug && slug.endsWith('.lisp'); // Apply syntax highlighting let highlightedSource; if (isJavaScript) { highlightedSource = highlightJavaScript(source); } else if (isKidLisp) { // Use the proper KidLisp highlighter with color codes highlightedSource = await highlightKidlisp(source); if (!highlightedSource || highlightedSource.trim() === '') { // Fallback to simple highlighter if the full one fails highlightedSource = highlightKidLispSimple(source); } } else { highlightedSource = source.replace(/</g, '<').replace(/>/g, '>'); } // Create a formatted HTML view of the source code with animated scrolling const htmlContent = ` <!DOCTYPE html> <html> <head> <style> * { box-sizing: border-box; } body { margin: 0; padding: 0; background: black; color: rgb(200, 200, 200); font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: 11px; line-height: 1.4; overflow: hidden; height: 100vh; display: flex; flex-direction: column; } .header { padding: 1.5em 2em; background: rgba(0, 0, 0, 0.8); border-bottom: 1px solid rgb(205, 92, 155); flex-shrink: 0; } h1 { color: rgb(205, 92, 155); font-size: 1.2em; margin: 0 0 0.5em 0; font-weight: normal; } .controls { display: flex; gap: 1em; align-items: center; margin-top: 0.8em; } button { background: rgba(205, 92, 155, 0.2); border: 1px solid rgb(205, 92, 155); color: rgb(205, 92, 155); padding: 0.4em 0.8em; cursor: pointer; font-family: inherit; font-size: 0.9em; border-radius: 3px; } button:hover { background: rgba(205, 92, 155, 0.3); } button.active { background: rgb(205, 92, 155); color: black; } .scroll-speed { display: flex; gap: 0.5em; align-items: center; } .scroll-speed label { font-size: 0.85em; color: rgb(150, 150, 150); } .scroll-speed input { width: 80px; } a { color: rgb(205, 92, 155); text-decoration: none; } a:hover { text-decoration: underline; } .code-container { flex: 1; overflow: hidden; position: relative; } .code-scroll { padding: 2em; overflow-y: auto; height: 100%; position: relative; } .code-scroll.auto-scrolling { overflow-y: hidden; /* Hide scrollbar during animation */ animation: autoScroll var(--scroll-duration, 60s) linear infinite; } .code-scroll.auto-scrolling pre { animation: smoothScroll var(--scroll-duration, 60s) linear infinite; } pre { margin: 0; white-space: pre-wrap; word-break: break-word; } @keyframes autoScroll { 0% { scroll-behavior: smooth; } 100% { scroll-behavior: smooth; } } @keyframes smoothScroll { from { transform: translateY(0); } to { transform: translateY(calc(-100% + 100vh - 250px)); } } .badge { display: inline-block; padding: 0.2em 0.6em; background: rgba(100, 150, 255, 0.2); border: 1px solid rgb(100, 150, 255); color: rgb(100, 150, 255); border-radius: 3px; font-size: 0.75em; margin-left: 0.5em; } </style> </head> <body> <div class="header"> <h1> ${slug ? `piece: ${slug}` : 'piece source'} ${isJavaScript ? '<span class="badge">.mjs</span>' : ''} ${isKidLisp ? '<span class="badge">.lisp</span>' : ''} <span class="badge" style="background: rgba(100, 100, 100, 0.2); border-color: rgb(150, 150, 150); color: rgb(150, 150, 150);">${source.split('\\n').length} lines</span> </h1> ${slug ? `<p style="margin: 0;"><a href="https://aesthetic.computer/${slug}" target="_blank">→ open on aesthetic.computer</a></p>` : ''} <div class="controls"> <button id="scrollBtn" onclick="toggleScroll()">▶ Auto-scroll</button> <div class="scroll-speed"> <label for="speedRange">Speed:</label> <input type="range" id="speedRange" min="10" max="120" value="60" oninput="updateSpeed(this.value)"> <span id="speedLabel">60s</span> </div> </div> </div> <div class="code-container"> <div class="code-scroll" id="codeScroll"> <pre>${highlightedSource}</pre> </div> </div> <script> let isScrolling = true; // Start with auto-scroll enabled const scrollContainer = document.getElementById('codeScroll'); const scrollBtn = document.getElementById('scrollBtn'); const speedLabel = document.getElementById('speedLabel'); // Initialize auto-scroll on load window.addEventListener('load', () => { scrollContainer.classList.add('auto-scrolling'); scrollBtn.classList.add('active'); scrollBtn.textContent = '⏸ Pause'; }); function toggleScroll() { isScrolling = !isScrolling; if (isScrolling) { scrollContainer.classList.add('auto-scrolling'); scrollBtn.classList.add('active'); scrollBtn.textContent = '⏸ Pause'; } else { scrollContainer.classList.remove('auto-scrolling'); scrollBtn.classList.remove('active'); scrollBtn.textContent = '▶ Auto-scroll'; } } function updateSpeed(seconds) { document.documentElement.style.setProperty('--scroll-duration', seconds + 's'); speedLabel.textContent = seconds + 's'; } // Pause on hover scrollContainer.addEventListener('mouseenter', () => { if (isScrolling) { scrollContainer.style.animationPlayState = 'paused'; } }); scrollContainer.addEventListener('mouseleave', () => { if (isScrolling) { scrollContainer.style.animationPlayState = 'running'; } }); // Manual scroll disables auto-scroll scrollContainer.addEventListener('wheel', () => { if (isScrolling) { toggleScroll(); } }); <\/script> </body> </html> `; // Create blob URL and open in modal const blob = new Blob([htmlContent], { type: 'text/html' }); const blobUrl = URL.createObjectURL(blob); openModal(blobUrl); // Clean up blob URL after modal opens setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); }
// Helper function to open source modal from DOM element async function openSourceModalFromElement(element) { const sourceId = element.dataset.sourceId; const uri = element.dataset.uri; const slug = element.dataset.slug; const language = element.dataset.language || 'mjs'; const sourceHolder = document.getElementById(sourceId); if (sourceHolder) { const source = sourceHolder.textContent; // Add language extension to slug for proper detection const slugWithExtension = slug.includes('.') ? slug : `${slug}.${language}`; await openSourceModal(uri, source, slugWithExtension); } else { console.error('Source not found for ID:', sourceId); } }
// Render a painting record function renderPainting(record) { const { value, uri } = record; const rkey = getRkey(uri); const lexicon = getLexicon(uri); const typeName = getLexiconTypeName(lexicon); const card = document.createElement('div'); card.className = 'record-card'; let imageHtml = ''; if (value.thumbnail?.ref) { const did = uri.split('/')[2]; const thumbnailUrl = `${PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${value.thumbnail.ref.$link}`; imageHtml = `<img src="${thumbnailUrl}" alt="Painting thumbnail" class="record-image">`; }
card.innerHTML = ` <div class="record-header"> <span class="record-date">${formatDate(value.createdAt || value.when)}</span> <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> </div> <div class="record-content"> ${imageHtml} <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> ${value.code ? `<a href="${value.acUrl || `https://aesthetic.computer/#${value.code}`}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>#</strong><strong>${value.code}</strong></a>` : ''} <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> </div> </div> `; return card; }
// Render a mood record function renderMood(record) { const { value, uri } = record; const rkey = getRkey(uri); const lexicon = getLexicon(uri); const typeName = getLexiconTypeName(lexicon); const card = document.createElement('div'); card.className = 'record-card'; card.innerHTML = ` <div class="record-header"> <span class="record-date">${formatDate(value.when || value.createdAt)}</span> <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> </div> <div class="record-content"> <div class="record-text">${value.mood || value.text || ''}</div> </div> `; return card; }
// Render a tape record function renderTape(record) { const { value, uri } = record; const rkey = getRkey(uri); const lexicon = getLexicon(uri); const typeName = getLexiconTypeName(lexicon); const card = document.createElement('div'); card.className = 'record-card'; let videoHtml = ''; if (value.video?.ref) { const did = uri.split('/')[2]; const videoUrl = `${PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${value.video.ref.$link}`; videoHtml = `<video controls loop autoplay muted playsinline webkit-playsinline class="record-image" style="max-width: 100%; height: auto; border: none;"> <source src="${videoUrl}" type="video/mp4"> Your browser does not support the video tag. </video>`; }
card.innerHTML = ` <div class="record-header"> <span class="record-date">${formatDate(value.when || value.createdAt)}</span> <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> </div> <div class="record-content"> ${videoHtml} <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> ${value.code ? `<a href="${value.acUrl || `https://aesthetic.computer/!${value.code}`}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>!</strong><strong>${value.code}</strong></a>` : ''} <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> </div> </div> `; return card; }
// Render a kidlisp record async function renderKidlisp(record) { const { value, uri } = record; const rkey = getRkey(uri); const lexicon = getLexicon(uri); const typeName = getLexiconTypeName(lexicon); const card = document.createElement('div'); card.className = 'record-card'; // Create initial card HTML with preview container const previewCode = value.code ? value.code.replace(/[^a-zA-Z0-9_-]/g, '') : ''; const kidlispPreviewHtml = previewCode ? `<a href="https://aesthetic.computer/$${previewCode}" target="_blank" rel="noreferrer" class="kidlisp-preview-card"> <img class="kidlisp-webp" src="https://oven.aesthetic.computer/grab/webp/200/200/$${previewCode}?duration=4000&fps=8&quality=80&density=1&nowait=true" alt="KidLisp preview" loading="lazy" /> <div class="kidlisp-preview-overlay" data-kidlisp-code="${previewCode}"> <div class="kidlisp-preview-code">${value.source ? value.source.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') : ''}</div> <div class="kidlisp-preview-qr"> <div class="kidlisp-preview-label">$${previewCode}</div> <img alt="KidLisp QR" /> </div> </div> </a>` : '';
card.innerHTML = ` <div class="record-header"> <span class="record-date">${formatDate(value.when || value.createdAt)}</span> <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> </div> <div class="record-content"> ${kidlispPreviewHtml} <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> ${value.code ? `<a href="${value.acUrl || `https://aesthetic.computer/$${value.code}`}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>$</strong><strong>${value.code}</strong></a>` : ''} <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> </div> </div> `; // Highlight and animate the source code asynchronously if (value.source) { highlightKidlisp(value.source).then(highlightedSource => { // Store source for modal const sourceId = `kidlisp-${rkey}`; sourceMap = window.sourceMap || (window.sourceMap = sourceMap || {}); sourceMap[sourceId] = value.source;
// Update overlay code in WebP preview const overlay = card.querySelector('.kidlisp-preview-overlay'); if (overlay) { const overlayCode = overlay.querySelector('.kidlisp-preview-code'); if (overlayCode) { overlayCode.innerHTML = highlightedSource; } const qrImg = overlay.querySelector('.kidlisp-preview-qr img'); if (qrImg && value.code) { const qrUrl = buildQrDataUrl(`https://aesthetic.computer/$${value.code}`, 3, 0); if (qrUrl) qrImg.src = qrUrl; } } }).catch(error => { console.error('Highlighting error:', error); }); }
if (value.code) { const overlay = card.querySelector('.kidlisp-preview-overlay'); if (overlay) { const qrImg = overlay.querySelector('.kidlisp-preview-qr img'); if (qrImg) { const qrUrl = buildQrDataUrl(`https://aesthetic.computer/$${value.code}`, 3, 0); if (qrUrl) qrImg.src = qrUrl; } } } return card; }
// Render a piece record async function renderPiece(record, handle) { const { value, uri } = record; const rkey = getRkey(uri); const lexicon = getLexicon(uri); const typeName = getLexiconTypeName(lexicon); const card = document.createElement('div'); card.className = 'record-card'; // Create initial card HTML card.innerHTML = ` <div class="record-header"> <span class="record-date">${formatDate(value.when || value.createdAt)}</span> <span class="lexicon-badge" onclick="openLexiconModal('${lexicon}')">${typeName}</span> </div> <div class="record-content"> <div class="source-preview-container" style="margin: 0.5em 0;"> <div style="font-size: 0.75em; color: rgb(150, 150, 150);">loading source...</div> </div> <div style="margin-top: 0.5em; display: flex; gap: 0.5em; flex-wrap: wrap;"> ${value.slug ? `<a href="https://prompt.ac/@${handle ? handle.split('.at.aesthetic.computer')[0] : 'art'}/${value.slug}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(205, 92, 155, 0.1); border: 1px solid rgb(205, 92, 155); border-radius: 3px; font-size: 0.85em;">█ prompt.ac/<strong>@${handle ? handle.split('.at.aesthetic.computer')[0] : 'art'}</strong>/<strong>${value.slug}</strong></a>` : ''} <a href="https://pdsls.dev/at://${uri.split('//')[1]}" class="record-link" style="display: inline-block; padding: 0.4em 0.8em; background: rgba(100, 150, 200, 0.1); border: 1px solid rgb(100, 150, 200); border-radius: 3px; font-size: 0.85em; color: rgb(100, 150, 200);">${rkey}</a> </div> </div> `; // Fetch source code from record or media URL if available if (value.slug && handle) { try { let source = null; let language = 'mjs'; // Default to JavaScript
// Use embedded source if present if (typeof value.source === 'string' && value.source.trim()) { source = value.source; language = value.language || (value.source.trim().startsWith('(') ? 'lisp' : 'mjs'); } else if (typeof value.sourceCode === 'string' && value.sourceCode.trim()) { source = value.sourceCode; language = value.language || (value.sourceCode.trim().startsWith('(') ? 'lisp' : 'mjs'); } if (!source) { // Prefer same-origin media proxy to avoid CORS issues const baseHandle = handle.replace(/^@/, ''); const shortHandle = baseHandle.split('.at.aesthetic.computer')[0]; const mediaHandles = Array.from(new Set([baseHandle, shortHandle].filter(Boolean))); const rawSlug = value.slug || ''; const slug = rawSlug.replace(/\.(mjs|js|lisp)$/i, '');
async function tryFetchPieceSource(mediaHandle) { const bases = [ `/media/@${mediaHandle}/piece/${slug}`, `/media/@${mediaHandle}/code/${slug}` ]; const extensions = ['.lisp', '.mjs', '.js', ''];
for (const base of bases) { for (const ext of extensions) { const url = `${base}${ext}`; const response = await fetch(url); if (response.ok) { const text = await response.text(); const isLisp = ext === '.lisp' || text.trim().startsWith('('); return { source: text, language: isLisp ? 'lisp' : 'mjs' }; } } }
return null; }
for (const mediaHandle of mediaHandles) { const result = await tryFetchPieceSource(mediaHandle); if (result) { source = result.source; language = result.language; break; } }
// Fallback for art handle legacy URLs if (!source && handle === 'art.at.aesthetic.computer') { const legacyLispUrl = `https://art.aesthetic.computer/${slug}.lisp`; let response = await fetch(legacyLispUrl); if (response.ok) { source = await response.text(); language = 'lisp'; } else { const legacyMjsUrl = `https://art.aesthetic.computer/${slug}.mjs`; response = await fetch(legacyMjsUrl); if (response.ok) { source = await response.text(); language = 'mjs'; } } } } // Render source if found if (source) { const previewContainer = card.querySelector('.source-preview-container'); if (previewContainer) { // Create a unique ID for this source const sourceId = `source-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Store source in a hidden element to avoid escaping issues const sourceHolder = document.createElement('div'); sourceHolder.id = sourceId; sourceHolder.style.display = 'none'; sourceHolder.textContent = source; sourceHolder.dataset.language = language; document.body.appendChild(sourceHolder); // Apply syntax highlighting to full source (not just preview) let highlightedSource; if (language === 'lisp') { // Use async highlighter for KidLisp highlightKidlisp(source).then(highlighted => { if (highlighted && highlighted.trim()) { // Update the preview with highlighted code (duplicated for seamless loop) const previewContent = previewContainer.querySelector('.preview-content'); if (previewContent) { previewContent.innerHTML = ` ${highlighted} <div style="margin: 2em 0; border-top: 1px dashed rgba(100,100,100,0.3); padding-top: 2em;"></div> ${highlighted} `; } } }); // Start with plain text while loading highlightedSource = source.replace(/</g, '<').replace(/>/g, '>'); } else if (language === 'mjs') { highlightedSource = highlightJavaScript(source); } else { highlightedSource = source.replace(/</g, '<').replace(/>/g, '>'); } // Language badge colors const languageBadge = language === 'lisp' ? '<span style="display: inline-block; padding: 0.15em 0.4em; background: rgba(147, 51, 234, 0.1); color: rgb(147, 51, 234); border-radius: 3px; font-size: 0.85em; font-weight: 600; margin-left: 0.5em;">KidLisp</span>' : '<span style="display: inline-block; padding: 0.15em 0.4em; background: rgba(234, 179, 8, 0.1); color: rgb(234, 179, 8); border-radius: 3px; font-size: 0.85em; font-weight: 600; margin-left: 0.5em;">JavaScript</span>'; // Calculate dynamic font size and animation speed based on content const lineCount = source.split('\n').length; const fontSize = lineCount > 50 ? '0.55em' : lineCount > 30 ? '0.6em' : lineCount > 15 ? '0.7em' : '0.8em'; // Shorter programs scroll faster - scale duration based on line count const animationDuration = Math.max(15, Math.min(60, lineCount * 0.8)); // Generate line numbers (doubled for the duplicated content) const lineNumbers = Array.from({length: lineCount * 2}, (_, i) => `<div>${(i % lineCount) + 1}</div>` ).join(''); previewContainer.innerHTML = ` <div class="source-preview" data-source-id="${sourceId}" data-uri="${uri.replace(/"/g, '"')}" data-slug="${value.slug}" data-language="${language}" onclick="openSourceModalFromElement(this)" style="padding: 0.5em; border-radius: 2px; cursor: pointer; font-family: monospace; font-size: ${fontSize}; color: rgb(100, 100, 100);"> <div class="preview-scroll-container" style="animation: previewScrollLoop ${animationDuration}s linear infinite;"> <div class="line-numbers">${lineNumbers}</div> <div class="preview-content" style="white-space: pre; overflow-wrap: normal;"> ${highlightedSource} <div style="margin: 2em 0; border-top: 1px dashed rgba(100,100,100,0.3); padding-top: 2em;"></div> ${highlightedSource} </div> </div> </div> `; } } else { const previewContainer = card.querySelector('.source-preview-container'); if (previewContainer) previewContainer.innerHTML = ''; } } catch (error) { console.error('Failed to fetch piece source:', error); const previewContainer = card.querySelector('.source-preview-container'); if (previewContainer) previewContainer.innerHTML = ''; } } else { const previewContainer = card.querySelector('.source-preview-container'); if (previewContainer) previewContainer.innerHTML = ''; } return card; }
// Render a generic record function renderGenericRecord(record, typeName) { const { value, uri } = record; const rkey = getRkey(uri); const card = document.createElement('div'); card.className = 'record-card'; card.innerHTML = ` <div class="record-header"> <span class="record-type">${typeName}</span> <span class="record-date">${formatDate(value.createdAt || value.when || new Date().toISOString())}</span> </div> <div class="record-content"> <pre class="record-text" style="font-size: 0.85em; overflow-x: auto;">${JSON.stringify(value, null, 2)}</pre> </div> <div class="record-meta"> <span>rkey: ${rkey}</span> </div> <a href="https://pdsls.dev/at://${uri.split('//')[1]}" target="_blank" class="record-link"> View on pdsls.dev → </a> `; return card; }
// Global state for lazy loading let currentRecords = []; let currentCollection = ''; let currentHandle = ''; let currentIndex = 0; const BATCH_SIZE = 50; // Load 50 records at a time let isLoading = false; let loadingSentinel = null;
// Render a batch of records async function renderBatch(startIndex, endIndex) { if (isLoading || startIndex >= currentRecords.length) return; isLoading = true; const grid = document.querySelector('.records-grid'); if (!grid) return;
const batch = currentRecords.slice(startIndex, endIndex); for (const record of batch) { let card; const collectionKey = currentCollection === 'all' ? (record._collection || '') : currentCollection;
if (collectionKey.includes('painting')) { card = renderPainting(record); } else if (collectionKey.includes('mood')) { card = renderMood(record); } else if (collectionKey.includes('tape')) { card = renderTape(record); } else if (collectionKey.includes('kidlisp')) { card = await renderKidlisp(record); } else if (collectionKey.includes('piece')) { card = await renderPiece(record, currentHandle); } else { card = renderGenericRecord(record, collectionKey.split('.').pop()); } // Insert before the sentinel if (loadingSentinel && loadingSentinel.parentNode) { grid.insertBefore(card, loadingSentinel); } else { grid.appendChild(card); } }
currentIndex = endIndex; isLoading = false;
// Update sentinel text if (loadingSentinel) { if (currentIndex >= currentRecords.length) { loadingSentinel.textContent = `✓ All ${currentRecords.length} records loaded`; loadingSentinel.style.opacity = '0.5'; } else { loadingSentinel.textContent = `Loaded ${currentIndex} of ${currentRecords.length} records... (scroll to load more)`; } } }
// Render records for a collection with lazy loading async function renderRecords(records, collection, handle) { const container = document.getElementById('records-container'); container.innerHTML = '';
if (records.length === 0) { container.innerHTML = '<div class="empty-state">No records found</div>'; return; }
// Sort by date (newest first) const sorted = [...records].sort((a, b) => { const dateA = new Date(a.value.createdAt || a.value.when || 0); const dateB = new Date(b.value.createdAt || b.value.when || 0); return dateB - dateA; });
// Update global state currentRecords = sorted; currentCollection = collection; currentHandle = handle; currentIndex = 0; isLoading = false;
// Create grid const grid = document.createElement('div'); grid.className = 'records-grid';
// Create loading sentinel loadingSentinel = document.createElement('div'); loadingSentinel.className = 'loading-sentinel'; loadingSentinel.style.cssText = 'text-align: center; padding: 2em 1em; opacity: 0.6; font-size: 0.9em; grid-column: 1 / -1;'; loadingSentinel.textContent = `Loading records...`; grid.appendChild(loadingSentinel);
container.appendChild(grid);
// Set up Intersection Observer for lazy loading const observer = new IntersectionObserver( async (entries) => { const entry = entries[0]; if (entry.isIntersecting && !isLoading && currentIndex < currentRecords.length) { await renderBatch(currentIndex, currentIndex + BATCH_SIZE); } }, { rootMargin: '400px', // Start loading 400px before sentinel is visible threshold: 0.1 } );
observer.observe(loadingSentinel);
// Load initial batch await renderBatch(0, BATCH_SIZE); }
// Initialize the page async function init() { try { const handle = getHandleFromSubdomain(); if (!handle) { throw new Error('Invalid subdomain format. Expected: handle.at.aesthetic.computer'); }
document.getElementById('handle-display').textContent = `@${handle}`; document.title = `@${handle} · ATProto`;
// Add subtitle for art.at.aesthetic.computer const didDisplay = document.getElementById('did-display'); const handleDisplay = document.getElementById('handle-display'); if (handle === 'art.at.aesthetic.computer') { didDisplay.innerHTML = '<div style="font-size: 0.9em; margin-top: 0.5em; opacity: 0.8; line-height: 1.5; max-width: 600px; margin-left: auto; margin-right: auto;">Anonymously recorded tapes and other media on <a href="https://aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">Aesthetic Computer</a>, synced to <a href="https://atproto.com" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">ATProto</a> for open syndication via the <a href="https://at.aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">at.aesthetic.computer</a> PDS instance.</div>'; // Make handle clickable and add color cycling to "art" handleDisplay.innerHTML = '<a href="https://at.aesthetic.computer" style="color: inherit; text-decoration: none;"><span class="color-cycle-a">a</span><span class="color-cycle-r">r</span><span class="color-cycle-t">t</span>.at.aesthetic.computer</a>'; handleDisplay.style.cursor = 'pointer'; } else { // For user pages, extract username part (before .at.aesthetic.computer) const username = handle.split('.at.aesthetic.computer')[0]; // Add color cycling only to username part const usernameParts = username.split(''); const colorCycledUsername = usernameParts.map((char, index) => { const delay = (index * 0.3).toFixed(1); return `<span class="color-cycle" style="animation-delay: -${delay}s">${char}</span>`; }).join(''); handleDisplay.innerHTML = `${colorCycledUsername}.at.aesthetic.computer`; // Simple subtitle with username and Aesthetic Computer link didDisplay.innerHTML = `<div style="font-size: 0.9em; margin-top: 0.5em; opacity: 0.8; line-height: 1.5; max-width: 600px; margin-left: auto; margin-right: auto;">All media by <span style="font-weight: bold;">@${username}</span> on <a href="https://aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">Aesthetic Computer</a>, synced to <a href="https://atproto.com" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">ATProto</a> for open syndication via the <a href="https://at.aesthetic.computer" target="_blank" style="color: rgb(205, 92, 155); text-decoration: none;">at.aesthetic.computer</a> PDS instance.</div>`; }
// Resolve DID (but don't display it - we're showing subtitles instead) const did = await resolveDID(handle);
// Fetch all collections (tapes first) const collections = [ 'computer.aesthetic.tape', 'computer.aesthetic.painting', 'computer.aesthetic.mood', 'computer.aesthetic.piece', 'computer.aesthetic.kidlisp' ];
const recordsByCollection = {}; const allRecords = []; let totalRecords = 0; let firstTab = null; let firstRecords = null; let firstCollection = null; let loadingHidden = false;
function hideLoading() { if (loadingHidden) return; document.getElementById('loading').style.display = 'none'; document.getElementById('content').style.display = 'block'; loadingHidden = true; }
// Stats section removed - counts now in tabs document.getElementById('stats').innerHTML = '';
// Create tabs const tabsContainer = document.getElementById('tabs');
// Helper to get display name for collection function getDisplayName(collection) { const name = collection.split('.').pop(); if (name === 'kidlisp') return 'KidLisp'; if (name === 'painting') return 'Paintings'; if (name === 'mood') return 'Moods'; if (name === 'piece') return 'Pieces'; if (name === 'tape') return 'Tapes'; return name.charAt(0).toUpperCase() + name.slice(1); }
const tabs = new Map();
function setTabLabel(tab, label, count, loading) { tab.innerHTML = `${label} (${count}${loading ? '…' : ''})`; }
function activateTab(tab, records, collection) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); renderRecords(records, collection, handle); }
const allTab = document.createElement('button'); allTab.className = 'tab disabled'; allTab.disabled = true; setTabLabel(allTab, 'All Media', 0, true); allTab.onclick = () => activateTab(allTab, allRecords, 'all'); tabsContainer.appendChild(allTab); tabs.set('all', allTab);
// Collection tabs (initialize with loading state) collections.forEach(collection => { const displayName = getDisplayName(collection); const tab = document.createElement('button'); tab.className = 'tab disabled'; tab.disabled = true; setTabLabel(tab, displayName, 0, true); tab.onclick = () => activateTab(tab, recordsByCollection[collection] || [], collection); tabsContainer.appendChild(tab); tabs.set(collection, tab); });
const refreshButton = document.createElement('button'); refreshButton.className = 'tab'; refreshButton.style.opacity = '0.7'; refreshButton.innerHTML = '↻ Refresh'; refreshButton.onclick = () => { clearRecordsCache(handle); window.location.search = window.location.search.includes('refresh') ? window.location.search : window.location.search + (window.location.search ? '&refresh=1' : '?refresh=1'); }; tabsContainer.appendChild(refreshButton);
// Fetch collections concurrently and update tabs as they resolve const pending = new Set(collections); await Promise.all(collections.map(async (collection) => { const records = await listRecords(did, collection, handle); recordsByCollection[collection] = records; totalRecords += records.length;
const withCollection = records.map(record => ({ ...record, _collection: collection })); allRecords.push(...withCollection); allRecords.sort((a, b) => { const dateA = new Date(a.value.createdAt || a.value.when || 0); const dateB = new Date(b.value.createdAt || b.value.when || 0); return dateB - dateA; });
const tab = tabs.get(collection); if (tab) { setTabLabel(tab, getDisplayName(collection), records.length, false); if (records.length > 0) { tab.disabled = false; tab.classList.remove('disabled'); } }
pending.delete(collection);
const allTabRef = tabs.get('all'); if (allTabRef) { setTabLabel(allTabRef, 'All Media', totalRecords, pending.size > 0); if (totalRecords > 0) { allTabRef.disabled = false; allTabRef.classList.remove('disabled'); } }
if (!firstTab && allRecords.length > 0) { firstTab = allTabRef; firstRecords = allRecords; firstCollection = 'all'; hideLoading(); activateTab(firstTab, firstRecords, firstCollection); } else if (currentCollection === 'all') { // Update active All Media view when new records arrive renderRecords(allRecords, 'all', handle); } }));
if (!loadingHidden) { hideLoading(); }
if (!firstTab) { document.getElementById('records-container').innerHTML = '<div class="empty-state">No records found</div>'; }
} catch (error) { console.error('Error loading page:', error); document.getElementById('loading').style.display = 'none'; document.getElementById('error').style.display = 'block'; document.getElementById('error').textContent = `❌ Error: ${error.message}`; } }
// Auto-refresh placeholder images (baking indicator) document.addEventListener('load', (e) => { if (e.target.tagName === 'IMG' && e.target.src.includes('nowait=true')) { const img = e.target; if (!img.dataset.retried) { // Schedule a retry after 10-15 seconds to fetch the real baked image const retryDelay = 10000 + Math.random() * 5000; setTimeout(() => { img.dataset.retried = 'true'; const url = new URL(img.src); url.searchParams.set('t', Date.now()); img.src = url.toString(); }, retryDelay); } } }, true);
// Start init(); </script></body></html>