Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
954 kB · 26065 lines
HTML
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014150151501615017150181501915020150211502215023150241502515026150271502815029150301503115032150331503415035150361503715038150391504015041150421504315044150451504615047150481504915050150511505215053150541505515056150571505815059150601506115062150631506415065150661506715068150691507015071150721507315074150751507615077150781507915080150811508215083150841508515086150871508815089150901509115092150931509415095150961509715098150991510015101151021510315104151051510615107151081510915110151111511215113151141511515116151171511815119151201512115122151231512415125151261512715128151291513015131151321513315134151351513615137151381513915140151411514215143151441514515146151471514815149151501515115152151531515415155151561515715158151591516015161151621516315164151651516615167151681516915170151711517215173151741517515176151771517815179151801518115182151831518415185151861518715188151891519015191151921519315194151951519615197151981519915200152011520215203152041520515206152071520815209152101521115212152131521415215152161521715218152191522015221152221522315224152251522615227152281522915230152311523215233152341523515236152371523815239152401524115242152431524415245152461524715248152491525015251152521525315254152551525615257152581525915260152611526215263152641526515266152671526815269152701527115272152731527415275152761527715278152791528015281152821528315284152851528615287152881528915290152911529215293152941529515296152971529815299153001530115302153031530415305153061530715308153091531015311153121531315314153151531615317153181531915320153211532215323153241532515326153271532815329153301533115332153331533415335153361533715338153391534015341153421534315344153451534615347153481534915350153511535215353153541535515356153571535815359153601536115362153631536415365153661536715368153691537015371153721537315374153751537615377153781537915380153811538215383153841538515386153871538815389153901539115392153931539415395153961539715398153991540015401154021540315404154051540615407154081540915410154111541215413154141541515416154171541815419154201542115422154231542415425154261542715428154291543015431154321543315434154351543615437154381543915440154411544215443154441544515446154471544815449154501545115452154531545415455154561545715458154591546015461154621546315464154651546615467154681546915470154711547215473154741547515476154771547815479154801548115482154831548415485154861548715488154891549015491154921549315494154951549615497154981549915500155011550215503155041550515506155071550815509155101551115512155131551415515155161551715518155191552015521155221552315524155251552615527155281552915530155311553215533155341553515536155371553815539155401554115542155431554415545155461554715548155491555015551155521555315554155551555615557155581555915560155611556215563155641556515566155671556815569155701557115572155731557415575155761557715578155791558015581155821558315584155851558615587155881558915590155911559215593155941559515596155971559815599156001560115602156031560415605156061560715608156091561015611156121561315614156151561615617156181561915620156211562215623156241562515626156271562815629156301563115632156331563415635156361563715638156391564015641156421564315644156451564615647156481564915650156511565215653156541565515656156571565815659156601566115662156631566415665156661566715668156691567015671156721567315674156751567615677156781567915680156811568215683156841568515686156871568815689156901569115692156931569415695156961569715698156991570015701157021570315704157051570615707157081570915710157111571215713157141571515716157171571815719157201572115722157231572415725157261572715728157291573015731157321573315734157351573615737157381573915740157411574215743157441574515746157471574815749157501575115752157531575415755157561575715758157591576015761157621576315764157651576615767157681576915770157711577215773157741577515776157771577815779157801578115782157831578415785157861578715788157891579015791157921579315794157951579615797157981579915800158011580215803158041580515806158071580815809158101581115812158131581415815158161581715818158191582015821158221582315824158251582615827158281582915830158311583215833158341583515836158371583815839158401584115842158431584415845158461584715848158491585015851158521585315854158551585615857158581585915860158611586215863158641586515866158671586815869158701587115872158731587415875158761587715878158791588015881158821588315884158851588615887158881588915890158911589215893158941589515896158971589815899159001590115902159031590415905159061590715908159091591015911159121591315914159151591615917159181591915920159211592215923159241592515926159271592815929159301593115932159331593415935159361593715938159391594015941159421594315944159451594615947159481594915950159511595215953159541595515956159571595815959159601596115962159631596415965159661596715968159691597015971159721597315974159751597615977159781597915980159811598215983159841598515986159871598815989159901599115992159931599415995159961599715998159991600016001160021600316004160051600616007160081600916010160111601216013160141601516016160171601816019160201602116022160231602416025160261602716028160291603016031160321603316034160351603616037160381603916040160411604216043160441604516046160471604816049160501605116052160531605416055160561605716058160591606016061160621606316064160651606616067160681606916070160711607216073160741607516076160771607816079160801608116082160831608416085160861608716088160891609016091160921609316094160951609616097160981609916100161011610216103161041610516106161071610816109161101611116112161131611416115161161611716118161191612016121161221612316124161251612616127161281612916130161311613216133161341613516136161371613816139161401614116142161431614416145161461614716148161491615016151161521615316154161551615616157161581615916160161611616216163161641616516166161671616816169161701617116172161731617416175161761617716178161791618016181161821618316184161851618616187161881618916190161911619216193161941619516196161971619816199162001620116202162031620416205162061620716208162091621016211162121621316214162151621616217162181621916220162211622216223162241622516226162271622816229162301623116232162331623416235162361623716238162391624016241162421624316244162451624616247162481624916250162511625216253162541625516256162571625816259162601626116262162631626416265162661626716268162691627016271162721627316274162751627616277162781627916280162811628216283162841628516286162871628816289162901629116292162931629416295162961629716298162991630016301163021630316304163051630616307163081630916310163111631216313163141631516316163171631816319163201632116322163231632416325163261632716328163291633016331163321633316334163351633616337163381633916340163411634216343163441634516346163471634816349163501635116352163531635416355163561635716358163591636016361163621636316364163651636616367163681636916370163711637216373163741637516376163771637816379163801638116382163831638416385163861638716388163891639016391163921639316394163951639616397163981639916400164011640216403164041640516406164071640816409164101641116412164131641416415164161641716418164191642016421164221642316424164251642616427164281642916430164311643216433164341643516436164371643816439164401644116442164431644416445164461644716448164491645016451164521645316454164551645616457164581645916460164611646216463164641646516466164671646816469164701647116472164731647416475164761647716478164791648016481164821648316484164851648616487164881648916490164911649216493164941649516496164971649816499165001650116502165031650416505165061650716508165091651016511165121651316514165151651616517165181651916520165211652216523165241652516526165271652816529165301653116532165331653416535165361653716538165391654016541165421654316544165451654616547165481654916550165511655216553165541655516556165571655816559165601656116562165631656416565165661656716568165691657016571165721657316574165751657616577165781657916580165811658216583165841658516586165871658816589165901659116592165931659416595165961659716598165991660016601166021660316604166051660616607166081660916610166111661216613166141661516616166171661816619166201662116622166231662416625166261662716628166291663016631166321663316634166351663616637166381663916640166411664216643166441664516646166471664816649166501665116652166531665416655166561665716658166591666016661166621666316664166651666616667166681666916670166711667216673166741667516676166771667816679166801668116682166831668416685166861668716688166891669016691166921669316694166951669616697166981669916700167011670216703167041670516706167071670816709167101671116712167131671416715167161671716718167191672016721167221672316724167251672616727167281672916730167311673216733167341673516736167371673816739167401674116742167431674416745167461674716748167491675016751167521675316754167551675616757167581675916760167611676216763167641676516766167671676816769167701677116772167731677416775167761677716778167791678016781167821678316784167851678616787167881678916790167911679216793167941679516796167971679816799168001680116802168031680416805168061680716808168091681016811168121681316814168151681616817168181681916820168211682216823168241682516826168271682816829168301683116832168331683416835168361683716838168391684016841168421684316844168451684616847168481684916850168511685216853168541685516856168571685816859168601686116862168631686416865168661686716868168691687016871168721687316874168751687616877168781687916880168811688216883168841688516886168871688816889168901689116892168931689416895168961689716898168991690016901169021690316904169051690616907169081690916910169111691216913169141691516916169171691816919169201692116922169231692416925169261692716928169291693016931169321693316934169351693616937169381693916940169411694216943169441694516946169471694816949169501695116952169531695416955169561695716958169591696016961169621696316964169651696616967169681696916970169711697216973169741697516976169771697816979169801698116982169831698416985169861698716988169891699016991169921699316994169951699616997169981699917000170011700217003170041700517006170071700817009170101701117012170131701417015170161701717018170191702017021170221702317024170251702617027170281702917030170311703217033170341703517036170371703817039170401704117042170431704417045170461704717048170491705017051170521705317054170551705617057170581705917060170611706217063170641706517066170671706817069170701707117072170731707417075170761707717078170791708017081170821708317084170851708617087170881708917090170911709217093170941709517096170971709817099171001710117102171031710417105171061710717108171091711017111171121711317114171151711617117171181711917120171211712217123171241712517126171271712817129171301713117132171331713417135171361713717138171391714017141171421714317144171451714617147171481714917150171511715217153171541715517156171571715817159171601716117162171631716417165171661716717168171691717017171171721717317174171751717617177171781717917180171811718217183171841718517186171871718817189171901719117192171931719417195171961719717198171991720017201172021720317204172051720617207172081720917210172111721217213172141721517216172171721817219172201722117222172231722417225172261722717228172291723017231172321723317234172351723617237172381723917240172411724217243172441724517246172471724817249172501725117252172531725417255172561725717258172591726017261172621726317264172651726617267172681726917270172711727217273172741727517276172771727817279172801728117282172831728417285172861728717288172891729017291172921729317294172951729617297172981729917300173011730217303173041730517306173071730817309173101731117312173131731417315173161731717318173191732017321173221732317324173251732617327173281732917330173311733217333173341733517336173371733817339173401734117342173431734417345173461734717348173491735017351173521735317354173551735617357173581735917360173611736217363173641736517366173671736817369173701737117372173731737417375173761737717378173791738017381173821738317384173851738617387173881738917390173911739217393173941739517396173971739817399174001740117402174031740417405174061740717408174091741017411174121741317414174151741617417174181741917420174211742217423174241742517426174271742817429174301743117432174331743417435174361743717438174391744017441174421744317444174451744617447174481744917450174511745217453174541745517456174571745817459174601746117462174631746417465174661746717468174691747017471174721747317474174751747617477174781747917480174811748217483174841748517486174871748817489174901749117492174931749417495174961749717498174991750017501175021750317504175051750617507175081750917510175111751217513175141751517516175171751817519175201752117522175231752417525175261752717528175291753017531175321753317534175351753617537175381753917540175411754217543175441754517546175471754817549175501755117552175531755417555175561755717558175591756017561175621756317564175651756617567175681756917570175711757217573175741757517576175771757817579175801758117582175831758417585175861758717588175891759017591175921759317594175951759617597175981759917600176011760217603176041760517606176071760817609176101761117612176131761417615176161761717618176191762017621176221762317624176251762617627176281762917630176311763217633176341763517636176371763817639176401764117642176431764417645176461764717648176491765017651176521765317654176551765617657176581765917660176611766217663176641766517666176671766817669176701767117672176731767417675176761767717678176791768017681176821768317684176851768617687176881768917690176911769217693176941769517696176971769817699177001770117702177031770417705177061770717708177091771017711177121771317714177151771617717177181771917720177211772217723177241772517726177271772817729177301773117732177331773417735177361773717738177391774017741177421774317744177451774617747177481774917750177511775217753177541775517756177571775817759177601776117762177631776417765177661776717768177691777017771177721777317774177751777617777177781777917780177811778217783177841778517786177871778817789177901779117792177931779417795177961779717798177991780017801178021780317804178051780617807178081780917810178111781217813178141781517816178171781817819178201782117822178231782417825178261782717828178291783017831178321783317834178351783617837178381783917840178411784217843178441784517846178471784817849178501785117852178531785417855178561785717858178591786017861178621786317864178651786617867178681786917870178711787217873178741787517876178771787817879178801788117882178831788417885178861788717888178891789017891178921789317894178951789617897178981789917900179011790217903179041790517906179071790817909179101791117912179131791417915179161791717918179191792017921179221792317924179251792617927179281792917930179311793217933179341793517936179371793817939179401794117942179431794417945179461794717948179491795017951179521795317954179551795617957179581795917960179611796217963179641796517966179671796817969179701797117972179731797417975179761797717978179791798017981179821798317984179851798617987179881798917990179911799217993179941799517996179971799817999180001800118002180031800418005180061800718008180091801018011180121801318014180151801618017180181801918020180211802218023180241802518026180271802818029180301803118032180331803418035180361803718038180391804018041180421804318044180451804618047180481804918050180511805218053180541805518056180571805818059180601806118062180631806418065180661806718068180691807018071180721807318074180751807618077180781807918080180811808218083180841808518086180871808818089180901809118092180931809418095180961809718098180991810018101181021810318104181051810618107181081810918110181111811218113181141811518116181171811818119181201812118122181231812418125181261812718128181291813018131181321813318134181351813618137181381813918140181411814218143181441814518146181471814818149181501815118152181531815418155181561815718158181591816018161181621816318164181651816618167181681816918170181711817218173181741817518176181771817818179181801818118182181831818418185181861818718188181891819018191181921819318194181951819618197181981819918200182011820218203182041820518206182071820818209182101821118212182131821418215182161821718218182191822018221182221822318224182251822618227182281822918230182311823218233182341823518236182371823818239182401824118242182431824418245182461824718248182491825018251182521825318254182551825618257182581825918260182611826218263182641826518266182671826818269182701827118272182731827418275182761827718278182791828018281182821828318284182851828618287182881828918290182911829218293182941829518296182971829818299183001830118302183031830418305183061830718308183091831018311183121831318314183151831618317183181831918320183211832218323183241832518326183271832818329183301833118332183331833418335183361833718338183391834018341183421834318344183451834618347183481834918350183511835218353183541835518356183571835818359183601836118362183631836418365183661836718368183691837018371183721837318374183751837618377183781837918380183811838218383183841838518386183871838818389183901839118392183931839418395183961839718398183991840018401184021840318404184051840618407184081840918410184111841218413184141841518416184171841818419184201842118422184231842418425184261842718428184291843018431184321843318434184351843618437184381843918440184411844218443184441844518446184471844818449184501845118452184531845418455184561845718458184591846018461184621846318464184651846618467184681846918470184711847218473184741847518476184771847818479184801848118482184831848418485184861848718488184891849018491184921849318494184951849618497184981849918500185011850218503185041850518506185071850818509185101851118512185131851418515185161851718518185191852018521185221852318524185251852618527185281852918530185311853218533185341853518536185371853818539185401854118542185431854418545185461854718548185491855018551185521855318554185551855618557185581855918560185611856218563185641856518566185671856818569185701857118572185731857418575185761857718578185791858018581185821858318584185851858618587185881858918590185911859218593185941859518596185971859818599186001860118602186031860418605186061860718608186091861018611186121861318614186151861618617186181861918620186211862218623186241862518626186271862818629186301863118632186331863418635186361863718638186391864018641186421864318644186451864618647186481864918650186511865218653186541865518656186571865818659186601866118662186631866418665186661866718668186691867018671186721867318674186751867618677186781867918680186811868218683186841868518686186871868818689186901869118692186931869418695186961869718698186991870018701187021870318704187051870618707187081870918710187111871218713187141871518716187171871818719187201872118722187231872418725187261872718728187291873018731187321873318734187351873618737187381873918740187411874218743187441874518746187471874818749187501875118752187531875418755187561875718758187591876018761187621876318764187651876618767187681876918770187711877218773187741877518776187771877818779187801878118782187831878418785187861878718788187891879018791187921879318794187951879618797187981879918800188011880218803188041880518806188071880818809188101881118812188131881418815188161881718818188191882018821188221882318824188251882618827188281882918830188311883218833188341883518836188371883818839188401884118842188431884418845188461884718848188491885018851188521885318854188551885618857188581885918860188611886218863188641886518866188671886818869188701887118872188731887418875188761887718878188791888018881188821888318884188851888618887188881888918890188911889218893188941889518896188971889818899189001890118902189031890418905189061890718908189091891018911189121891318914189151891618917189181891918920189211892218923189241892518926189271892818929189301893118932189331893418935189361893718938189391894018941189421894318944189451894618947189481894918950189511895218953189541895518956189571895818959189601896118962189631896418965189661896718968189691897018971189721897318974189751897618977189781897918980189811898218983189841898518986189871898818989189901899118992189931899418995189961899718998189991900019001190021900319004190051900619007190081900919010190111901219013190141901519016190171901819019190201902119022190231902419025190261902719028190291903019031190321903319034190351903619037190381903919040190411904219043190441904519046190471904819049190501905119052190531905419055190561905719058190591906019061190621906319064190651906619067190681906919070190711907219073190741907519076190771907819079190801908119082190831908419085190861908719088190891909019091190921909319094190951909619097190981909919100191011910219103191041910519106191071910819109191101911119112191131911419115191161911719118191191912019121191221912319124191251912619127191281912919130191311913219133191341913519136191371913819139191401914119142191431914419145191461914719148191491915019151191521915319154191551915619157191581915919160191611916219163191641916519166191671916819169191701917119172191731917419175191761917719178191791918019181191821918319184191851918619187191881918919190191911919219193191941919519196191971919819199192001920119202192031920419205192061920719208192091921019211192121921319214192151921619217192181921919220192211922219223192241922519226192271922819229192301923119232192331923419235192361923719238192391924019241192421924319244192451924619247192481924919250192511925219253192541925519256192571925819259192601926119262192631926419265192661926719268192691927019271192721927319274192751927619277192781927919280192811928219283192841928519286192871928819289192901929119292192931929419295192961929719298192991930019301193021930319304193051930619307193081930919310193111931219313193141931519316193171931819319193201932119322193231932419325193261932719328193291933019331193321933319334193351933619337193381933919340193411934219343193441934519346193471934819349193501935119352193531935419355193561935719358193591936019361193621936319364193651936619367193681936919370193711937219373193741937519376193771937819379193801938119382193831938419385193861938719388193891939019391193921939319394193951939619397193981939919400194011940219403194041940519406194071940819409194101941119412194131941419415194161941719418194191942019421194221942319424194251942619427194281942919430194311943219433194341943519436194371943819439194401944119442194431944419445194461944719448194491945019451194521945319454194551945619457194581945919460194611946219463194641946519466194671946819469194701947119472194731947419475194761947719478194791948019481194821948319484194851948619487194881948919490194911949219493194941949519496194971949819499195001950119502195031950419505195061950719508195091951019511195121951319514195151951619517195181951919520195211952219523195241952519526195271952819529195301953119532195331953419535195361953719538195391954019541195421954319544195451954619547195481954919550195511955219553195541955519556195571955819559195601956119562195631956419565195661956719568195691957019571195721957319574195751957619577195781957919580195811958219583195841958519586195871958819589195901959119592195931959419595195961959719598195991960019601196021960319604196051960619607196081960919610196111961219613196141961519616196171961819619196201962119622196231962419625196261962719628196291963019631196321963319634196351963619637196381963919640196411964219643196441964519646196471964819649196501965119652196531965419655196561965719658196591966019661196621966319664196651966619667196681966919670196711967219673196741967519676196771967819679196801968119682196831968419685196861968719688196891969019691196921969319694196951969619697196981969919700197011970219703197041970519706197071970819709197101971119712197131971419715197161971719718197191972019721197221972319724197251972619727197281972919730197311973219733197341973519736197371973819739197401974119742197431974419745197461974719748197491975019751197521975319754197551975619757197581975919760197611976219763197641976519766197671976819769197701977119772197731977419775197761977719778197791978019781197821978319784197851978619787197881978919790197911979219793197941979519796197971979819799198001980119802198031980419805198061980719808198091981019811198121981319814198151981619817198181981919820198211982219823198241982519826198271982819829198301983119832198331983419835198361983719838198391984019841198421984319844198451984619847198481984919850198511985219853198541985519856198571985819859198601986119862198631986419865198661986719868198691987019871198721987319874198751987619877198781987919880198811988219883198841988519886198871988819889198901989119892198931989419895198961989719898198991990019901199021990319904199051990619907199081990919910199111991219913199141991519916199171991819919199201992119922199231992419925199261992719928199291993019931199321993319934199351993619937199381993919940199411994219943199441994519946199471994819949199501995119952199531995419955199561995719958199591996019961199621996319964199651996619967199681996919970199711997219973199741997519976199771997819979199801998119982199831998419985199861998719988199891999019991199921999319994199951999619997199981999920000200012000220003200042000520006200072000820009200102001120012200132001420015200162001720018200192002020021200222002320024200252002620027200282002920030200312003220033200342003520036200372003820039200402004120042200432004420045200462004720048200492005020051200522005320054200552005620057200582005920060200612006220063200642006520066200672006820069200702007120072200732007420075200762007720078200792008020081200822008320084200852008620087200882008920090200912009220093200942009520096200972009820099201002010120102201032010420105201062010720108201092011020111201122011320114201152011620117201182011920120201212012220123201242012520126201272012820129201302013120132201332013420135201362013720138201392014020141201422014320144201452014620147201482014920150201512015220153201542015520156201572015820159201602016120162201632016420165201662016720168201692017020171201722017320174201752017620177201782017920180201812018220183201842018520186201872018820189201902019120192201932019420195201962019720198201992020020201202022020320204202052020620207202082020920210202112021220213202142021520216202172021820219202202022120222202232022420225202262022720228202292023020231202322023320234202352023620237202382023920240202412024220243202442024520246202472024820249202502025120252202532025420255202562025720258202592026020261202622026320264202652026620267202682026920270202712027220273202742027520276202772027820279202802028120282202832028420285202862028720288202892029020291202922029320294202952029620297202982029920300203012030220303203042030520306203072030820309203102031120312203132031420315203162031720318203192032020321203222032320324203252032620327203282032920330203312033220333203342033520336203372033820339203402034120342203432034420345203462034720348203492035020351203522035320354203552035620357203582035920360203612036220363203642036520366203672036820369203702037120372203732037420375203762037720378203792038020381203822038320384203852038620387203882038920390203912039220393203942039520396203972039820399204002040120402204032040420405204062040720408204092041020411204122041320414204152041620417204182041920420204212042220423204242042520426204272042820429204302043120432204332043420435204362043720438204392044020441204422044320444204452044620447204482044920450204512045220453204542045520456204572045820459204602046120462204632046420465204662046720468204692047020471204722047320474204752047620477204782047920480204812048220483204842048520486204872048820489204902049120492204932049420495204962049720498204992050020501205022050320504205052050620507205082050920510205112051220513205142051520516205172051820519205202052120522205232052420525205262052720528205292053020531205322053320534205352053620537205382053920540205412054220543205442054520546205472054820549205502055120552205532055420555205562055720558205592056020561205622056320564205652056620567205682056920570205712057220573205742057520576205772057820579205802058120582205832058420585205862058720588205892059020591205922059320594205952059620597205982059920600206012060220603206042060520606206072060820609206102061120612206132061420615206162061720618206192062020621206222062320624206252062620627206282062920630206312063220633206342063520636206372063820639206402064120642206432064420645206462064720648206492065020651206522065320654206552065620657206582065920660206612066220663206642066520666206672066820669206702067120672206732067420675206762067720678206792068020681206822068320684206852068620687206882068920690206912069220693206942069520696206972069820699207002070120702207032070420705207062070720708207092071020711207122071320714207152071620717207182071920720207212072220723207242072520726207272072820729207302073120732207332073420735207362073720738207392074020741207422074320744207452074620747207482074920750207512075220753207542075520756207572075820759207602076120762207632076420765207662076720768207692077020771207722077320774207752077620777207782077920780207812078220783207842078520786207872078820789207902079120792207932079420795207962079720798207992080020801208022080320804208052080620807208082080920810208112081220813208142081520816208172081820819208202082120822208232082420825208262082720828208292083020831208322083320834208352083620837208382083920840208412084220843208442084520846208472084820849208502085120852208532085420855208562085720858208592086020861208622086320864208652086620867208682086920870208712087220873208742087520876208772087820879208802088120882208832088420885208862088720888208892089020891208922089320894208952089620897208982089920900209012090220903209042090520906209072090820909209102091120912209132091420915209162091720918209192092020921209222092320924209252092620927209282092920930209312093220933209342093520936209372093820939209402094120942209432094420945209462094720948209492095020951209522095320954209552095620957209582095920960209612096220963209642096520966209672096820969209702097120972209732097420975209762097720978209792098020981209822098320984209852098620987209882098920990209912099220993209942099520996209972099820999210002100121002210032100421005210062100721008210092101021011210122101321014210152101621017210182101921020210212102221023210242102521026210272102821029210302103121032210332103421035210362103721038210392104021041210422104321044210452104621047210482104921050210512105221053210542105521056210572105821059210602106121062210632106421065210662106721068210692107021071210722107321074210752107621077210782107921080210812108221083210842108521086210872108821089210902109121092210932109421095210962109721098210992110021101211022110321104211052110621107211082110921110211112111221113211142111521116211172111821119211202112121122211232112421125211262112721128211292113021131211322113321134211352113621137211382113921140211412114221143211442114521146211472114821149211502115121152211532115421155211562115721158211592116021161211622116321164211652116621167211682116921170211712117221173211742117521176211772117821179211802118121182211832118421185211862118721188211892119021191211922119321194211952119621197211982119921200212012120221203212042120521206212072120821209212102121121212212132121421215212162121721218212192122021221212222122321224212252122621227212282122921230212312123221233212342123521236212372123821239212402124121242212432124421245212462124721248212492125021251212522125321254212552125621257212582125921260212612126221263212642126521266212672126821269212702127121272212732127421275212762127721278212792128021281212822128321284212852128621287212882128921290212912129221293212942129521296212972129821299213002130121302213032130421305213062130721308213092131021311213122131321314213152131621317213182131921320213212132221323213242132521326213272132821329213302133121332213332133421335213362133721338213392134021341213422134321344213452134621347213482134921350213512135221353213542135521356213572135821359213602136121362213632136421365213662136721368213692137021371213722137321374213752137621377213782137921380213812138221383213842138521386213872138821389213902139121392213932139421395213962139721398213992140021401214022140321404214052140621407214082140921410214112141221413214142141521416214172141821419214202142121422214232142421425214262142721428214292143021431214322143321434214352143621437214382143921440214412144221443214442144521446214472144821449214502145121452214532145421455214562145721458214592146021461214622146321464214652146621467214682146921470214712147221473214742147521476214772147821479214802148121482214832148421485214862148721488214892149021491214922149321494214952149621497214982149921500215012150221503215042150521506215072150821509215102151121512215132151421515215162151721518215192152021521215222152321524215252152621527215282152921530215312153221533215342153521536215372153821539215402154121542215432154421545215462154721548215492155021551215522155321554215552155621557215582155921560215612156221563215642156521566215672156821569215702157121572215732157421575215762157721578215792158021581215822158321584215852158621587215882158921590215912159221593215942159521596215972159821599216002160121602216032160421605216062160721608216092161021611216122161321614216152161621617216182161921620216212162221623216242162521626216272162821629216302163121632216332163421635216362163721638216392164021641216422164321644216452164621647216482164921650216512165221653216542165521656216572165821659216602166121662216632166421665216662166721668216692167021671216722167321674216752167621677216782167921680216812168221683216842168521686216872168821689216902169121692216932169421695216962169721698216992170021701217022170321704217052170621707217082170921710217112171221713217142171521716217172171821719217202172121722217232172421725217262172721728217292173021731217322173321734217352173621737217382173921740217412174221743217442174521746217472174821749217502175121752217532175421755217562175721758217592176021761217622176321764217652176621767217682176921770217712177221773217742177521776217772177821779217802178121782217832178421785217862178721788217892179021791217922179321794217952179621797217982179921800218012180221803218042180521806218072180821809218102181121812218132181421815218162181721818218192182021821218222182321824218252182621827218282182921830218312183221833218342183521836218372183821839218402184121842218432184421845218462184721848218492185021851218522185321854218552185621857218582185921860218612186221863218642186521866218672186821869218702187121872218732187421875218762187721878218792188021881218822188321884218852188621887218882188921890218912189221893218942189521896218972189821899219002190121902219032190421905219062190721908219092191021911219122191321914219152191621917219182191921920219212192221923219242192521926219272192821929219302193121932219332193421935219362193721938219392194021941219422194321944219452194621947219482194921950219512195221953219542195521956219572195821959219602196121962219632196421965219662196721968219692197021971219722197321974219752197621977219782197921980219812198221983219842198521986219872198821989219902199121992219932199421995219962199721998219992200022001220022200322004220052200622007220082200922010220112201222013220142201522016220172201822019220202202122022220232202422025220262202722028220292203022031220322203322034220352203622037220382203922040220412204222043220442204522046220472204822049220502205122052220532205422055220562205722058220592206022061220622206322064220652206622067220682206922070220712207222073220742207522076220772207822079220802208122082220832208422085220862208722088220892209022091220922209322094220952209622097220982209922100221012210222103221042210522106221072210822109221102211122112221132211422115221162211722118221192212022121221222212322124221252212622127221282212922130221312213222133221342213522136221372213822139221402214122142221432214422145221462214722148221492215022151221522215322154221552215622157221582215922160221612216222163221642216522166221672216822169221702217122172221732217422175221762217722178221792218022181221822218322184221852218622187221882218922190221912219222193221942219522196221972219822199222002220122202222032220422205222062220722208222092221022211222122221322214222152221622217222182221922220222212222222223222242222522226222272222822229222302223122232222332223422235222362223722238222392224022241222422224322244222452224622247222482224922250222512225222253222542225522256222572225822259222602226122262222632226422265222662226722268222692227022271222722227322274222752227622277222782227922280222812228222283222842228522286222872228822289222902229122292222932229422295222962229722298222992230022301223022230322304223052230622307223082230922310223112231222313223142231522316223172231822319223202232122322223232232422325223262232722328223292233022331223322233322334223352233622337223382233922340223412234222343223442234522346223472234822349223502235122352223532235422355223562235722358223592236022361223622236322364223652236622367223682236922370223712237222373223742237522376223772237822379223802238122382223832238422385223862238722388223892239022391223922239322394223952239622397223982239922400224012240222403224042240522406224072240822409224102241122412224132241422415224162241722418224192242022421224222242322424224252242622427224282242922430224312243222433224342243522436224372243822439224402244122442224432244422445224462244722448224492245022451224522245322454224552245622457224582245922460224612246222463224642246522466224672246822469224702247122472224732247422475224762247722478224792248022481224822248322484224852248622487224882248922490224912249222493224942249522496224972249822499225002250122502225032250422505225062250722508225092251022511225122251322514225152251622517225182251922520225212252222523225242252522526225272252822529225302253122532225332253422535225362253722538225392254022541225422254322544225452254622547225482254922550225512255222553225542255522556225572255822559225602256122562225632256422565225662256722568225692257022571225722257322574225752257622577225782257922580225812258222583225842258522586225872258822589225902259122592225932259422595225962259722598225992260022601226022260322604226052260622607226082260922610226112261222613226142261522616226172261822619226202262122622226232262422625226262262722628226292263022631226322263322634226352263622637226382263922640226412264222643226442264522646226472264822649226502265122652226532265422655226562265722658226592266022661226622266322664226652266622667226682266922670226712267222673226742267522676226772267822679226802268122682226832268422685226862268722688226892269022691226922269322694226952269622697226982269922700227012270222703227042270522706227072270822709227102271122712227132271422715227162271722718227192272022721227222272322724227252272622727227282272922730227312273222733227342273522736227372273822739227402274122742227432274422745227462274722748227492275022751227522275322754227552275622757227582275922760227612276222763227642276522766227672276822769227702277122772227732277422775227762277722778227792278022781227822278322784227852278622787227882278922790227912279222793227942279522796227972279822799228002280122802228032280422805228062280722808228092281022811228122281322814228152281622817228182281922820228212282222823228242282522826228272282822829228302283122832228332283422835228362283722838228392284022841228422284322844228452284622847228482284922850228512285222853228542285522856228572285822859228602286122862228632286422865228662286722868228692287022871228722287322874228752287622877228782287922880228812288222883228842288522886228872288822889228902289122892228932289422895228962289722898228992290022901229022290322904229052290622907229082290922910229112291222913229142291522916229172291822919229202292122922229232292422925229262292722928229292293022931229322293322934229352293622937229382293922940229412294222943229442294522946229472294822949229502295122952229532295422955229562295722958229592296022961229622296322964229652296622967229682296922970229712297222973229742297522976229772297822979229802298122982229832298422985229862298722988229892299022991229922299322994229952299622997229982299923000230012300223003230042300523006230072300823009230102301123012230132301423015230162301723018230192302023021230222302323024230252302623027230282302923030230312303223033230342303523036230372303823039230402304123042230432304423045230462304723048230492305023051230522305323054230552305623057230582305923060230612306223063230642306523066230672306823069230702307123072230732307423075230762307723078230792308023081230822308323084230852308623087230882308923090230912309223093230942309523096230972309823099231002310123102231032310423105231062310723108231092311023111231122311323114231152311623117231182311923120231212312223123231242312523126231272312823129231302313123132231332313423135231362313723138231392314023141231422314323144231452314623147231482314923150231512315223153231542315523156231572315823159231602316123162231632316423165231662316723168231692317023171231722317323174231752317623177231782317923180231812318223183231842318523186231872318823189231902319123192231932319423195231962319723198231992320023201232022320323204232052320623207232082320923210232112321223213232142321523216232172321823219232202322123222232232322423225232262322723228232292323023231232322323323234232352323623237232382323923240232412324223243232442324523246232472324823249232502325123252232532325423255232562325723258232592326023261232622326323264232652326623267232682326923270232712327223273232742327523276232772327823279232802328123282232832328423285232862328723288232892329023291232922329323294232952329623297232982329923300233012330223303233042330523306233072330823309233102331123312233132331423315233162331723318233192332023321233222332323324233252332623327233282332923330233312333223333233342333523336233372333823339233402334123342233432334423345233462334723348233492335023351233522335323354233552335623357233582335923360233612336223363233642336523366233672336823369233702337123372233732337423375233762337723378233792338023381233822338323384233852338623387233882338923390233912339223393233942339523396233972339823399234002340123402234032340423405234062340723408234092341023411234122341323414234152341623417234182341923420234212342223423234242342523426234272342823429234302343123432234332343423435234362343723438234392344023441234422344323444234452344623447234482344923450234512345223453234542345523456234572345823459234602346123462234632346423465234662346723468234692347023471234722347323474234752347623477234782347923480234812348223483234842348523486234872348823489234902349123492234932349423495234962349723498234992350023501235022350323504235052350623507235082350923510235112351223513235142351523516235172351823519235202352123522235232352423525235262352723528235292353023531235322353323534235352353623537235382353923540235412354223543235442354523546235472354823549235502355123552235532355423555235562355723558235592356023561235622356323564235652356623567235682356923570235712357223573235742357523576235772357823579235802358123582235832358423585235862358723588235892359023591235922359323594235952359623597235982359923600236012360223603236042360523606236072360823609236102361123612236132361423615236162361723618236192362023621236222362323624236252362623627236282362923630236312363223633236342363523636236372363823639236402364123642236432364423645236462364723648236492365023651236522365323654236552365623657236582365923660236612366223663236642366523666236672366823669236702367123672236732367423675236762367723678236792368023681236822368323684236852368623687236882368923690236912369223693236942369523696236972369823699237002370123702237032370423705237062370723708237092371023711237122371323714237152371623717237182371923720237212372223723237242372523726237272372823729237302373123732237332373423735237362373723738237392374023741237422374323744237452374623747237482374923750237512375223753237542375523756237572375823759237602376123762237632376423765237662376723768237692377023771237722377323774237752377623777237782377923780237812378223783237842378523786237872378823789237902379123792237932379423795237962379723798237992380023801238022380323804238052380623807238082380923810238112381223813238142381523816238172381823819238202382123822238232382423825238262382723828238292383023831238322383323834238352383623837238382383923840238412384223843238442384523846238472384823849238502385123852238532385423855238562385723858238592386023861238622386323864238652386623867238682386923870238712387223873238742387523876238772387823879238802388123882238832388423885238862388723888238892389023891238922389323894238952389623897238982389923900239012390223903239042390523906239072390823909239102391123912239132391423915239162391723918239192392023921239222392323924239252392623927239282392923930239312393223933239342393523936239372393823939239402394123942239432394423945239462394723948239492395023951239522395323954239552395623957239582395923960239612396223963239642396523966239672396823969239702397123972239732397423975239762397723978239792398023981239822398323984239852398623987239882398923990239912399223993239942399523996239972399823999240002400124002240032400424005240062400724008240092401024011240122401324014240152401624017240182401924020240212402224023240242402524026240272402824029240302403124032240332403424035240362403724038240392404024041240422404324044240452404624047240482404924050240512405224053240542405524056240572405824059240602406124062240632406424065240662406724068240692407024071240722407324074240752407624077240782407924080240812408224083240842408524086240872408824089240902409124092240932409424095240962409724098240992410024101241022410324104241052410624107241082410924110241112411224113241142411524116241172411824119241202412124122241232412424125241262412724128241292413024131241322413324134241352413624137241382413924140241412414224143241442414524146241472414824149241502415124152241532415424155241562415724158241592416024161241622416324164241652416624167241682416924170241712417224173241742417524176241772417824179241802418124182241832418424185241862418724188241892419024191241922419324194241952419624197241982419924200242012420224203242042420524206242072420824209242102421124212242132421424215242162421724218242192422024221242222422324224242252422624227242282422924230242312423224233242342423524236242372423824239242402424124242242432424424245242462424724248242492425024251242522425324254242552425624257242582425924260242612426224263242642426524266242672426824269242702427124272242732427424275242762427724278242792428024281242822428324284242852428624287242882428924290242912429224293242942429524296242972429824299243002430124302243032430424305243062430724308243092431024311243122431324314243152431624317243182431924320243212432224323243242432524326243272432824329243302433124332243332433424335243362433724338243392434024341243422434324344243452434624347243482434924350243512435224353243542435524356243572435824359243602436124362243632436424365243662436724368243692437024371243722437324374243752437624377243782437924380243812438224383243842438524386243872438824389243902439124392243932439424395243962439724398243992440024401244022440324404244052440624407244082440924410244112441224413244142441524416244172441824419244202442124422244232442424425244262442724428244292443024431244322443324434244352443624437244382443924440244412444224443244442444524446244472444824449244502445124452244532445424455244562445724458244592446024461244622446324464244652446624467244682446924470244712447224473244742447524476244772447824479244802448124482244832448424485244862448724488244892449024491244922449324494244952449624497244982449924500245012450224503245042450524506245072450824509245102451124512245132451424515245162451724518245192452024521245222452324524245252452624527245282452924530245312453224533245342453524536245372453824539245402454124542245432454424545245462454724548245492455024551245522455324554245552455624557245582455924560245612456224563245642456524566245672456824569245702457124572245732457424575245762457724578245792458024581245822458324584245852458624587245882458924590245912459224593245942459524596245972459824599246002460124602246032460424605246062460724608246092461024611246122461324614246152461624617246182461924620246212462224623246242462524626246272462824629246302463124632246332463424635246362463724638246392464024641246422464324644246452464624647246482464924650246512465224653246542465524656246572465824659246602466124662246632466424665246662466724668246692467024671246722467324674246752467624677246782467924680246812468224683246842468524686246872468824689246902469124692246932469424695246962469724698246992470024701247022470324704247052470624707247082470924710247112471224713247142471524716247172471824719247202472124722247232472424725247262472724728247292473024731247322473324734247352473624737247382473924740247412474224743247442474524746247472474824749247502475124752247532475424755247562475724758247592476024761247622476324764247652476624767247682476924770247712477224773247742477524776247772477824779247802478124782247832478424785247862478724788247892479024791247922479324794247952479624797247982479924800248012480224803248042480524806248072480824809248102481124812248132481424815248162481724818248192482024821248222482324824248252482624827248282482924830248312483224833248342483524836248372483824839248402484124842248432484424845248462484724848248492485024851248522485324854248552485624857248582485924860248612486224863248642486524866248672486824869248702487124872248732487424875248762487724878248792488024881248822488324884248852488624887248882488924890248912489224893248942489524896248972489824899249002490124902249032490424905249062490724908249092491024911249122491324914249152491624917249182491924920249212492224923249242492524926249272492824929249302493124932249332493424935249362493724938249392494024941249422494324944249452494624947249482494924950249512495224953249542495524956249572495824959249602496124962249632496424965249662496724968249692497024971249722497324974249752497624977249782497924980249812498224983249842498524986249872498824989249902499124992249932499424995249962499724998249992500025001250022500325004250052500625007250082500925010250112501225013250142501525016250172501825019250202502125022250232502425025250262502725028250292503025031250322503325034250352503625037250382503925040250412504225043250442504525046250472504825049250502505125052250532505425055250562505725058250592506025061250622506325064250652506625067250682506925070250712507225073250742507525076250772507825079250802508125082250832508425085250862508725088250892509025091250922509325094250952509625097250982509925100251012510225103251042510525106251072510825109251102511125112251132511425115251162511725118251192512025121251222512325124251252512625127251282512925130251312513225133251342513525136251372513825139251402514125142251432514425145251462514725148251492515025151251522515325154251552515625157251582515925160251612516225163251642516525166251672516825169251702517125172251732517425175251762517725178251792518025181251822518325184251852518625187251882518925190251912519225193251942519525196251972519825199252002520125202252032520425205252062520725208252092521025211252122521325214252152521625217252182521925220252212522225223252242522525226252272522825229252302523125232252332523425235252362523725238252392524025241252422524325244252452524625247252482524925250252512525225253252542525525256252572525825259252602526125262252632526425265252662526725268252692527025271252722527325274252752527625277252782527925280252812528225283252842528525286252872528825289252902529125292252932529425295252962529725298252992530025301253022530325304253052530625307253082530925310253112531225313253142531525316253172531825319253202532125322253232532425325253262532725328253292533025331253322533325334253352533625337253382533925340253412534225343253442534525346253472534825349253502535125352253532535425355253562535725358253592536025361253622536325364253652536625367253682536925370253712537225373253742537525376253772537825379253802538125382253832538425385253862538725388253892539025391253922539325394253952539625397253982539925400254012540225403254042540525406254072540825409254102541125412254132541425415254162541725418254192542025421254222542325424254252542625427254282542925430254312543225433254342543525436254372543825439254402544125442254432544425445254462544725448254492545025451254522545325454254552545625457254582545925460254612546225463254642546525466254672546825469254702547125472254732547425475254762547725478254792548025481254822548325484254852548625487254882548925490254912549225493254942549525496254972549825499255002550125502255032550425505255062550725508255092551025511255122551325514255152551625517255182551925520255212552225523255242552525526255272552825529255302553125532255332553425535255362553725538255392554025541255422554325544255452554625547255482554925550255512555225553255542555525556255572555825559255602556125562255632556425565255662556725568255692557025571255722557325574255752557625577255782557925580255812558225583255842558525586255872558825589255902559125592255932559425595255962559725598255992560025601256022560325604256052560625607256082560925610256112561225613256142561525616256172561825619256202562125622256232562425625256262562725628256292563025631256322563325634256352563625637256382563925640256412564225643256442564525646256472564825649256502565125652256532565425655256562565725658256592566025661256622566325664256652566625667256682566925670256712567225673256742567525676256772567825679256802568125682256832568425685256862568725688256892569025691256922569325694256952569625697256982569925700257012570225703257042570525706257072570825709257102571125712257132571425715257162571725718257192572025721257222572325724257252572625727257282572925730257312573225733257342573525736257372573825739257402574125742257432574425745257462574725748257492575025751257522575325754257552575625757257582575925760257612576225763257642576525766257672576825769257702577125772257732577425775257762577725778257792578025781257822578325784257852578625787257882578925790257912579225793257942579525796257972579825799258002580125802258032580425805258062580725808258092581025811258122581325814258152581625817258182581925820258212582225823258242582525826258272582825829258302583125832258332583425835258362583725838258392584025841258422584325844258452584625847258482584925850258512585225853258542585525856258572585825859258602586125862258632586425865258662586725868258692587025871258722587325874258752587625877258782587925880258812588225883258842588525886258872588825889258902589125892258932589425895258962589725898258992590025901259022590325904259052590625907259082590925910259112591225913259142591525916259172591825919259202592125922259232592425925259262592725928259292593025931259322593325934259352593625937259382593925940259412594225943259442594525946259472594825949259502595125952259532595425955259562595725958259592596025961259622596325964259652596625967259682596925970259712597225973259742597525976259772597825979259802598125982259832598425985259862598725988259892599025991259922599325994259952599625997259982599926000260012600226003260042600526006260072600826009260102601126012260132601426015260162601726018260192602026021260222602326024260252602626027260282602926030260312603226033260342603526036260372603826039260402604126042260432604426045260462604726048260492605026051260522605326054260552605626057260582605926060260612606226063260642606526066<!-- KidLisp, 2025.10.17 --><html>
<head> <script>console.clear();</script> <meta charset="utf-8"> <title>KidLisp.com</title> <!-- Open Graph / Social Preview --> <meta property="og:type" content="website" /> <meta property="og:url" content="https://kidlisp.com" /> <meta property="og:title" content="KidLisp" /> <meta property="og:image" content="https://oven.aesthetic.computer/kidlisp-og.png" /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" /> <!-- Twitter Card --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="KidLisp" /> <meta name="twitter:image" content="https://oven.aesthetic.computer/kidlisp-og.png" /> <link rel="icon" type="image/gif" href="https://assets.aesthetic.computer/kidlisp-favicon.gif" /> <!-- Preload fonts for iOS Safari --> <link rel="preload" href="https://aesthetic.computer/type/webfonts/ywft-processing-regular.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <link rel="preload" href="https://aesthetic.computer/type/webfonts/ywft-processing-bold.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <!-- Note: We use inline @font-face instead of external CSS which has broken relative URLs --> <link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css" media="print" onload="this.media='all'" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <!-- Auth0 SDK is loaded on demand by loadAuth0SDK() with Monaco's AMD define() hidden — a static UMD <script> races the AMD loader and never sets window.auth0, hanging the login button. -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Comic+Relief:ital,wght@0,400;0,700;1,400;1,700&family=Noto+Sans+Mono:wght@400;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" /> <!-- Flag icons for language selector (lazy-loaded — not needed at startup) --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" media="print" onload="this.media='all'" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <!-- Modular CSS Architecture (parallel <link> tags instead of chained @import) --> <link rel="stylesheet" href="/kidlisp.com/css/variables.css" /> <link rel="stylesheet" href="/kidlisp.com/css/layout.css" /> <link rel="stylesheet" href="/kidlisp.com/css/components.css" /> <link rel="stylesheet" href="/kidlisp.com/css/splits.css" /> <link rel="stylesheet" href="/kidlisp.com/css/branding.css" /> <link rel="stylesheet" href="/kidlisp.com/css/editor.css" /> <link rel="stylesheet" href="/kidlisp.com/css/keeps.css" /> <link rel="stylesheet" href="/kidlisp.com/css/mobile.css" /> <style> /* iOS Safari font fix - explicit absolute URLs with font-display: swap */ @font-face { font-family: 'YWFTProcessing-Regular'; src: url('https://aesthetic.computer/type/webfonts/ywft-processing-regular.woff2') format('woff2'), url('https://aesthetic.computer/type/webfonts/ywft-processing-regular.woff') format('woff'); font-weight: normal; font-style: normal; font-display: swap; } @font-face { font-family: 'YWFTProcessing-Bold'; src: url('https://aesthetic.computer/type/webfonts/ywft-processing-bold.woff2') format('woff2'), url('https://aesthetic.computer/type/webfonts/ywft-processing-bold.woff') format('woff'); font-weight: bold; font-style: normal; font-display: swap; } /* Platform-specific fonts */ @font-face { font-family: 'PP Mori'; src: url('https://assets.aesthetic.computer/fonts/PPMori-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'PP Mori'; src: url('https://assets.aesthetic.computer/fonts/PPMori-SemiBold.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; } @font-face { font-family: 'Roobert'; src: url('https://assets.aesthetic.computer/fonts/Roobert-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'Roobert'; src: url('https://assets.aesthetic.computer/fonts/Roobert-Bold.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; } @font-face { font-family: 'Futura PT'; src: url('https://assets.aesthetic.computer/fonts/FuturaPT-Book.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
/* Remaining inline styles not yet extracted - TODO: migrate these */ :root { /* Light mode (default) */ --bg-primary: #f7f7f7; --bg-secondary: #e8e8e8; --bg-tertiary: white; --text-primary: #111; --text-secondary: #333; --text-tertiary: #666; --border-color: #ddd; --border-subtle: #e0e0e0; --code-bg: #e8e8e8; --code-text: #a31515; --editor-bg: #fafafa; --editor-text: #111; --editor-border: #ddd; }
@media (prefers-color-scheme: dark) { :root { --bg-primary: #1e1e1e; --bg-secondary: #252526; --bg-tertiary: #2d2d30; --text-primary: #d4d4d4; --text-secondary: #d4d4d4; --text-tertiary: #858585; --border-color: #3e3e42; --border-subtle: #3e3e42; --code-bg: #252526; --code-text: #ce9178; --editor-bg: #1e1e1e; --editor-text: #d4d4d4; --editor-border: #333; } } /* Manual theme overrides via data-theme attribute */ [data-theme="light"] { --bg-primary: #f7f7f7; --bg-secondary: #e8e8e8; --bg-tertiary: white; --text-primary: #111; --text-secondary: #333; --text-tertiary: #666; --border-color: #ddd; --border-subtle: #e0e0e0; --code-bg: #e8e8e8; --code-text: #a31515; --editor-bg: #fafafa; --editor-text: #111; --editor-border: #ddd; } [data-theme="dark"] { --bg-primary: #1e1e1e; --bg-secondary: #252526; --bg-tertiary: #2d2d30; --text-primary: #d4d4d4; --text-secondary: #d4d4d4; --text-tertiary: #858585; --border-color: #3e3e42; --border-subtle: #3e3e42; --code-bg: #252526; --code-text: #ce9178; --editor-bg: #1e1e1e; --editor-text: #d4d4d4; --editor-border: #333; }
/* Device mode: canvas-only view for M4L embedding */ body.device-mode { background: #000 !important; overflow: hidden !important; } body.device-mode #splash-overlay, body.device-mode .editor-panel, body.device-mode .bottom-row, body.device-mode #center-square, body.device-mode .preview-playback-controls, body.device-mode .preview-rulers, body.device-mode .console-panel, body.device-mode .boot-screen, body.device-mode .preview-mouse-overlay, body.device-mode .docs-header { display: none !important; } body.device-mode #main-split { opacity: 1 !important; transition: none !important; display: flex !important; flex-direction: column !important; width: 100vw !important; height: 100vh !important; } body.device-mode .top-row { display: flex !important; width: 100% !important; height: 100% !important; flex: 1 !important; } body.device-mode .preview-panel { width: 100% !important; height: 100% !important; border: none !important; border-radius: 0 !important; flex: 1 !important; } body.device-mode .preview-iframe-container { width: 100% !important; height: 100% !important; border: none !important; background: #000 !important; } body.device-mode #preview-iframe { width: 100% !important; height: 100% !important; border: none !important; } /* Hide gutter splitters in device mode */ body.device-mode .gutter { display: none !important; }
/* Stage mode: single-pane performance layout (preview + editor overlay) */ body.stage-mode:not(.device-mode) #center-square, body.stage-mode:not(.device-mode) #bottom-row, body.stage-mode:not(.device-mode) #mobile-checkerboard, body.stage-mode:not(.device-mode) .gutter { display: none !important; }
body.stage-mode:not(.device-mode) #main-split { display: flex !important; flex-direction: column !important; width: 100% !important; height: 100% !important; overflow: hidden !important; }
body.stage-mode:not(.device-mode) #top-row { position: relative !important; width: 100% !important; height: 100% !important; flex: 1 1 auto !important; min-height: 0 !important; display: block !important; }
body.stage-mode:not(.device-mode) #preview-panel { position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important; border: none !important; z-index: 1 !important; display: flex !important; flex-direction: column !important; flex: none !important; }
body.stage-mode:not(.device-mode) #editor-panel { position: absolute !important; top: 0 !important; left: 0 !important; bottom: 0 !important; width: 100% !important; min-width: 0; z-index: 25 !important; border-radius: 0; border: none; box-shadow: none; overflow: hidden; backdrop-filter: none; background: transparent !important; }
[data-theme="dark"] body.stage-mode:not(.device-mode) #editor-panel { background: transparent !important; border-color: transparent; }
body.stage-mode:not(.device-mode) #editor-panel .editor-header { display: none !important; }
/* Hide preview panel title bar (Aesthetic.Computer / platform selector) */ body.stage-mode:not(.device-mode) #preview-panel .docs-header { display: none !important; }
body.stage-mode:not(.device-mode) #kidlisp-editor { padding: 0 !important; }
/* Added by JS when context menu is open — boosts above all siblings */ .editor-panel.context-menu-open, .context-menu-open .viz-container { z-index: 100000 !important; } /* Hide play/stop buttons while context menu is open so they don't overlay it */ .context-menu-open #playback-controls, .context-menu-open #mini-playback-controls { visibility: hidden; }
/* Flush Monaco text to top-left edge in stage mode */ body.stage-mode:not(.device-mode) .monaco-editor .lines-content { padding-left: 8px !important; } body.stage-mode:not(.device-mode) .monaco-editor .view-lines { padding-top: 0 !important; } body.stage-mode:not(.device-mode) .monaco-editor .monaco-scrollable-element { left: 0 !important; }
/* Make Monaco editor background transparent in stage mode */ body.stage-mode:not(.device-mode) .monaco-editor, body.stage-mode:not(.device-mode) .monaco-editor .overflow-guard, body.stage-mode:not(.device-mode) .monaco-editor .monaco-editor-background, body.stage-mode:not(.device-mode) .monaco-editor .margin { background: transparent !important; }
/* Stage mode text shadows are applied per-token via JS (dynamic contrast shadows) */
/* Hide line numbers in stage mode */ body.stage-mode:not(.device-mode) .monaco-editor .margin { display: none !important; }
/* Remove Monaco focus outline/border in stage mode */ body.stage-mode:not(.device-mode) .monaco-editor .focused .selected-text, body.stage-mode:not(.device-mode) .monaco-editor.focused { outline: none !important; border: none !important; }
/* Remove current line highlight background in stage mode */ body.stage-mode:not(.device-mode) .monaco-editor .current-line, body.stage-mode:not(.device-mode) .monaco-editor .view-overlays .current-line { background: transparent !important; border: none !important; }
/* Hide live-running/paused/stopped green/yellow/purple outlines in stage mode */ body.stage-mode:not(.device-mode) .editor-panel.live-running, body.stage-mode:not(.device-mode) .editor-panel.live-paused, body.stage-mode:not(.device-mode) .editor-panel.live-stopped, body.stage-mode:not(.device-mode) #kidlisp-editor.live-running, body.stage-mode:not(.device-mode) #kidlisp-editor.live-paused, body.stage-mode:not(.device-mode) #kidlisp-editor.live-stopped { box-shadow: none !important; }
/* Hide playback controls in stage mode */ body.stage-mode:not(.device-mode) #playback-controls, body.stage-mode:not(.device-mode) #mini-playback-controls, body.stage-mode:not(.device-mode) #ai-assist-bar { display: none !important; }
@media (max-width: 768px) { body.stage-mode:not(.device-mode) #editor-panel { top: 0 !important; left: 0 !important; right: 0 !important; bottom: auto !important; width: auto !important; min-width: 0 !important; height: 52vh !important; } }
::-webkit-scrollbar { display: none; } /* Custom scrollbars for console and code list panels */ .console-output::-webkit-scrollbar, .code-list-items::-webkit-scrollbar, .docs-content::-webkit-scrollbar { display: block; width: 12px; height: 12px; } .console-output::-webkit-scrollbar-track, .code-list-items::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); } .console-output::-webkit-scrollbar-thumb, .code-list-items::-webkit-scrollbar-thumb { background: rgba(255, 159, 67, 0.4); } .console-output::-webkit-scrollbar-thumb:hover, .code-list-items::-webkit-scrollbar-thumb:hover { background: rgba(255, 159, 67, 0.6); } /* Dark mode scrollbar colors */ @media (prefers-color-scheme: dark) { .console-output::-webkit-scrollbar-track, .code-list-items::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2); } .console-output::-webkit-scrollbar-thumb, .code-list-items::-webkit-scrollbar-thumb { background: rgba(255, 159, 67, 0.5); } } [data-theme="dark"] .console-output::-webkit-scrollbar-track, [data-theme="dark"] .code-list-items::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2); } [data-theme="dark"] .console-output::-webkit-scrollbar-thumb { background: rgba(255, 159, 67, 0.5); } [data-theme="dark"] .docs-content::-webkit-scrollbar-thumb { background: rgba(100, 149, 237, 0.5); } [data-theme="light"] .console-output::-webkit-scrollbar-track, [data-theme="light"] .docs-content::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); } [data-theme="light"] .console-output::-webkit-scrollbar-thumb { background: rgba(255, 159, 67, 0.4); } [data-theme="light"] .docs-content::-webkit-scrollbar-thumb { background: rgba(100, 149, 237, 0.4); }
* { box-sizing: border-box; touch-action: manipulation; }
html, body { margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden; }
body { font-size: 18px; font-family: monospace; -webkit-text-size-adjust: none; display: flex; flex-direction: column; background: var(--bg-primary); color: var(--text-primary); cursor: url('https://aesthetic.computer/aesthetic.computer/cursors/precise.svg') 12 12, auto; }
header { padding: 16px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); }
h1 { font-weight: normal; font-size: 20px; margin: 0; }
.tagline { font-size: 12px; opacity: 0.6; margin-top: 4px; }
main { flex: 1; display: flex; flex-direction: column; gap: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; /* Don't show body bg through gaps */ }
.top-row, .bottom-row { display: flex; flex-direction: row; overflow: hidden; width: 100%; height: 50%; /* Fill available space */ min-height: 0; /* Allow flex items to shrink below content size */ }
.editor-panel, .preview-panel, .console-section, .reference-section { min-width: 0; /* Allow flex items to shrink below content size */ min-height: 0; height: 100%; /* Ensure panels fill completely with no body background showing through */ /* overflow: hidden; - Removed to allow playback controls to be visible */ } /* Keep overflow hidden on viz-container for monaco editor scrolling */ .viz-container { overflow: hidden; position: relative; /* Needed for #playback-controls positioning */ }
/* Remove any visual borders on outer edges of right-side panels */ .preview-panel, .console-section { border-right: none; border-bottom: none; flex: 1; /* Fill remaining horizontal space */ }
.editor-panel { /* flex: 1; Removed for Split.js */ display: flex; flex-direction: column; /* border-right: 2px solid var(--border-color); Removed for Split.js */ background: #fffacd; /* Legal pad yellow */ position: relative; }
@media (prefers-color-scheme: dark) { .editor-panel { background: #2a2520; /* Dark mode: warm dark brown */ } } [data-theme="light"] .editor-panel { background: #fffacd; /* Legal pad yellow */ } [data-theme="dark"] .editor-panel { background: #2a2520; /* Dark mode: warm dark brown */ }
.editor-header { padding: 0 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--editor-border); display: flex; justify-content: flex-start; /* Align items to the left */ gap: 12px; /* Space between logo and language switcher */ align-items: center; color: var(--editor-text); height: 44px; box-sizing: border-box; overflow: visible; /* Allow dropdown to overflow */ flex-shrink: 0; /* Prevent header from shrinking */ position: relative; /* For z-index context */ z-index: 500; /* Must be ABOVE #playback-controls (z-index: 50) */ }
/* Logo landing zone with gear reveal */ #logo-landing-zone { position: relative; overflow: visible; display: flex; align-items: center; gap: 6px; } #settings-gear { position: absolute; right: 32px; /* Default: left of collapse arrow (single-column / mobile) */ top: 50%; transform: translateY(-50%); width: 16px; height: 16px; opacity: 0.6; transition: opacity 0.2s; cursor: pointer; color: #a8a29e; flex-shrink: 0; z-index: 501; }
#settings-gear:hover { opacity: 1; animation: gear-spin 2s linear infinite; }
#splash-logo.landed { order: 1; /* Logo text comes first */ } @keyframes gear-spin { from { transform: translateY(-50%) rotate(0deg); } to { transform: translateY(-50%) rotate(360deg); } } [data-theme="dark"] #settings-gear { color: #d6d3d1; }
#clear-button { position: relative; left: auto; right: auto; top: auto; bottom: auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; flex-shrink: 0; box-sizing: border-box; background: rgb(244, 67, 54); /* Red */ color: white; border: none; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2); transition: background 0.2s, box-shadow 0.2s, transform 0.1s; display: flex; align-items: center; justify-content: center; padding: 0; margin: 0; }
@media (hover: hover) { #clear-button:hover { background: rgb(239, 83, 80); transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.3); color: white; text-decoration: none; } } #clear-button:active { background: rgb(211, 47, 47); transform: scale(0.95); } [data-theme="dark"] #clear-button { background: rgb(239, 83, 80); } /* ==================== SLIDE MODE ==================== */ /* Slide button - toggle numeric parameter sliding */ #slide-button { position: relative; width: 44px; height: 44px; min-width: 44px; min-height: 44px; flex-shrink: 0; box-sizing: border-box; background: rgb(100, 100, 120); /* Muted purple-gray */ color: white; border: none; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2); transition: background 0.2s, box-shadow 0.2s, transform 0.1s, opacity 0.2s; display: none; /* Hidden by default - shown when code has numbers and is playing */ align-items: center; justify-content: center; padding: 0; margin: 0; font-family: 'Berkeley Mono Variable', 'SF Mono', monospace; font-size: 11px; font-weight: bold; letter-spacing: 0; opacity: 0; } #slide-button.available { display: flex; opacity: 1; } /* Slider icon inside button */ #slide-button svg { width: 22px; height: 22px; } @media (hover: hover) { #slide-button:hover { background: rgb(120, 120, 145); transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.3); } } #slide-button:active { background: rgb(80, 80, 100); transform: scale(0.95); } #slide-button.active { background: rgb(156, 39, 176); /* Purple when active */ box-shadow: 0 0 0 3px rgba(156, 39, 176, 0.4), 0 2px 4px rgba(0,0,0,0.3); animation: slide-button-pulse 2s ease-in-out infinite; } @keyframes slide-button-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(156, 39, 176, 0.4), 0 2px 4px rgba(0,0,0,0.3); } 50% { box-shadow: 0 0 0 3px rgba(156, 39, 176, 0.7), 0 2px 4px rgba(0,0,0,0.3); } } [data-theme="dark"] #slide-button { background: rgb(120, 120, 145); } [data-theme="dark"] #slide-button.active { background: rgb(186, 104, 200); } /* Slide mode styling */ #kidlisp-editor.slide-mode-active { position: relative; } /* Real overlay div inserted into Monaco editor - covers everything including gutter */ #slide-curtain-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.65); pointer-events: none; z-index: 4; transition: opacity 0.2s ease; } [data-theme="dark"] #slide-curtain-overlay { background: rgba(0, 0, 0, 0.55); } /* Also dim the gutter/margin in slide mode */ #kidlisp-editor.slide-mode-active .margin { opacity: 0.3; } #kidlisp-editor.slide-mode-active .view-line { font-size: 0.9em; transition: font-size 0.2s ease; } /* Hide playback controls in slide mode (except slide button itself and set button) */ #kidlisp-editor.slide-mode-active ~ #playback-controls #clear-button, #kidlisp-editor.slide-mode-active ~ #playback-controls #stop-button, #kidlisp-editor.slide-mode-active ~ #playback-controls #send-button, .slide-mode-controls-hidden #clear-button, .slide-mode-controls-hidden #stop-button, .slide-mode-controls-hidden #send-button { display: none !important; } /* Slide mode number highlights - perfect circles */ .slide-number-highlight { position: relative; z-index: 10 !important; background: rgb(156, 39, 176) !important; border-radius: 50% !important; cursor: ew-resize !important; transition: background 0.15s, transform 0.15s, box-shadow 0.15s; display: inline-flex !important; align-items: center; justify-content: center; font-size: 1.1em !important; font-weight: bold !important; color: white !important; min-width: 28px; height: 28px; padding: 0 6px; margin: -4px -2px; box-shadow: 0 2px 8px rgba(156, 39, 176, 0.5); line-height: 1; } [data-theme="dark"] .slide-number-highlight { background: rgb(186, 104, 200) !important; color: white !important; box-shadow: 0 2px 12px rgba(186, 104, 200, 0.6); } .slide-number-highlight:hover { background: rgb(180, 60, 200) !important; transform: scale(1.1); box-shadow: 0 4px 12px rgba(156, 39, 176, 0.7); } .slide-number-dragging { background: rgb(200, 80, 220) !important; transform: scale(1.2); box-shadow: 0 4px 16px rgba(156, 39, 176, 0.8); } /* Hide cursor and selection in slide mode */ #kidlisp-editor.slide-mode-active .cursor { display: none !important; } #kidlisp-editor.slide-mode-active .cursors-layer { display: none !important; } #kidlisp-editor.slide-mode-active .selected-text { display: none !important; } /* Prevent text selection cursor appearance */ #kidlisp-editor.slide-mode-active .monaco-editor .view-lines { cursor: default; } #kidlisp-editor.slide-mode-active .monaco-editor .view-line { cursor: default; } /* Slide mode overlay for drag feedback */ #slide-drag-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; cursor: ew-resize; } #slide-value-tooltip { display: none; position: fixed; background: rgba(0, 0, 0, 0.85); color: white; padding: 4px 8px; border-radius: 4px; font-family: 'Berkeley Mono Variable', monospace; font-size: 14px; font-weight: bold; pointer-events: none; z-index: 10000; white-space: nowrap; } /* Set button - shown next to slide button when slide mode is active */ #slide-set-btn { display: none; position: relative; width: 44px; height: 44px; min-width: 44px; min-height: 44px; flex-shrink: 0; box-sizing: border-box; background: rgb(255, 193, 7); /* Yellow */ color: rgb(50, 40, 0); border: none; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.3); transition: background 0.2s, box-shadow 0.2s, transform 0.1s; align-items: center; justify-content: center; padding: 0; margin: 0; font-family: 'Berkeley Mono Variable', 'SF Mono', monospace; font-size: 11px; font-weight: bold; } #slide-set-btn.visible { display: flex; } @media (hover: hover) { #slide-set-btn:hover { background: rgb(255, 213, 79); transform: scale(1.05); } } #slide-set-btn:active { background: rgb(255, 160, 0); transform: scale(0.95); } [data-theme="dark"] #slide-set-btn { background: rgb(255, 213, 79); color: rgb(50, 40, 0); } /* Legacy slide-actions (hidden, keeping for compatibility) */ #slide-actions { display: none !important; } /* ==================== END SLIDE MODE ==================== */ /* Mini playback controls in header (mobile only) */ #mini-playback-controls { display: none; /* Hidden by default - shown via JS when main controls obscured */ position: absolute; right: 32px; /* Left of the collapse arrow which is at right: 12px */ top: 50%; transform: translateY(-50%); flex-direction: row; align-items: center; gap: 6px; z-index: 501; /* Above editor-header content */ } #mini-playback-controls button { width: 28px; height: 28px; min-width: 28px; min-height: 28px; border: none; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; transition: transform 0.1s, opacity 0.15s, background 0.2s, box-shadow 0.2s; } #mini-send-button { background: rgb(76, 175, 80); color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.3); } /* Empty state - outline only, hollow look to indicate "write something" */ #mini-send-button.empty { background: transparent; color: rgb(76, 175, 80); box-shadow: inset 0 0 0 2px rgb(76, 175, 80), 0 1px 3px rgba(0,0,0,0.2); } #mini-send-button.playing { background: rgb(76, 175, 80); } /* Paused state - yellow outline on green play button */ #mini-send-button.paused { background: rgb(76, 175, 80); box-shadow: 0 0 0 2px rgba(255, 217, 61, 0.8), 0 1px 3px rgba(0,0,0,0.3); animation: mini-pause-pulse 2s ease-in-out infinite; } @keyframes mini-pause-pulse { 0%, 100% { box-shadow: 0 0 0 2px rgba(255, 217, 61, 0.8), 0 1px 3px rgba(0,0,0,0.3); } 50% { box-shadow: 0 0 0 2px rgba(255, 217, 61, 0.4), 0 1px 3px rgba(0,0,0,0.3); } } #mini-send-button .play-icon { display: block; } #mini-send-button .pause-icon { display: none; } #mini-send-button.playing .play-icon { display: none; } #mini-send-button.playing .pause-icon { display: block; } #mini-stop-button { background: rgb(239, 83, 80); color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.3); display: none; /* Hidden until playing */ } #mini-playback-controls button:active { transform: scale(0.92); } /* Dipping animation for mini buttons */ #mini-send-button.dipping { animation: mini-dip 0.15s ease-out; } @keyframes mini-dip { 0% { transform: scale(1); } 50% { transform: scale(0.9); } 100% { transform: scale(1); } } @media (prefers-color-scheme: dark) { #mini-send-button { background: rgb(102, 187, 106); } #mini-send-button.empty { background: transparent; color: rgb(102, 187, 106); box-shadow: inset 0 0 0 2px rgb(102, 187, 106), 0 1px 3px rgba(0,0,0,0.2); } } [data-theme="dark"] #mini-send-button { background: rgb(102, 187, 106); } [data-theme="dark"] #mini-send-button.empty { background: transparent; color: rgb(102, 187, 106); box-shadow: inset 0 0 0 2px rgb(102, 187, 106), 0 1px 3px rgba(0,0,0,0.2); } /* AI Assist Bar - describe what you want (hidden until Ctrl+G or context menu) */ #ai-assist-bar { display: none; /* Hidden by default, toggled via context menu */ position: absolute; bottom: 14px; left: 14px; right: 90px; /* Leave room for playback controls */ z-index: 1; flex-direction: row; align-items: center; gap: 6px; opacity: 0.7; transition: opacity 0.2s ease; } #ai-assist-bar:focus-within { opacity: 1; } #ai-assist-input { flex: 1; height: 32px; padding: 0 12px; font-family: 'Berkeley Mono Variable', monospace; font-size: 12px; background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 16px; color: var(--text-primary); outline: none; transition: all 0.2s ease; } #ai-assist-input::placeholder { color: var(--text-tertiary); font-style: italic; } #ai-assist-input:focus { border-color: rgb(205, 92, 155); box-shadow: 0 0 0 2px rgba(205, 92, 155, 0.2); } #ai-assist-send { width: 32px; height: 32px; padding: 0; display: flex; align-items: center; justify-content: center; background: rgb(205, 92, 155); border: none; border-radius: 50%; color: white; cursor: pointer; transition: all 0.15s ease; flex-shrink: 0; } #ai-assist-send:hover { background: rgb(185, 72, 135); transform: scale(1.05); } #ai-assist-send:active { transform: scale(0.95); } #ai-assist-send:disabled { background: var(--bg-tertiary); color: var(--text-tertiary); cursor: not-allowed; transform: none; } #ai-assist-send.loading { animation: ai-assist-pulse 1s ease-in-out infinite; } @keyframes ai-assist-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* Playback controls container */ #playback-controls { position: absolute; bottom: 14px; right: 14px; z-index: 1; /* Very low - must be BELOW .editor-header (z-index: 500) */ display: flex; flex-direction: row; align-items: center; gap: 8px; } #playback-controls > button { margin-right: 0; } #playback-controls > button:last-child { margin-right: 0; }
#send-button { position: relative; left: auto; right: auto; top: auto; bottom: auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; flex-shrink: 0; box-sizing: border-box; background: rgb(76, 175, 80); /* Green by default */ color: white; border: none; border-radius: 50%; font-family: monospace; font-size: 16px; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2); transition: background 0.2s, box-shadow 0.2s, transform 0.1s; display: flex; align-items: center; justify-content: center; padding: 0; margin: 0; } @media (prefers-color-scheme: dark) { #send-button { background: rgb(102, 187, 106); /* Lighter green for dark mode */ box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.3); } } [data-theme="dark"] #send-button { background: rgb(102, 187, 106); /* Lighter green for dark mode */ box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.3); }
#send-button.playing { background: rgb(76, 175, 80); /* Stay green when playing */ cursor: pointer; /* Remain clickable */ } /* Paused state - yellow outline on green play button */ #send-button.paused { background: rgb(76, 175, 80); /* Keep green background */ box-shadow: 0 0 0 3px rgba(255, 217, 61, 0.8), 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2); animation: pause-button-pulse 2s ease-in-out infinite; } @keyframes pause-button-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(255, 217, 61, 0.8), 0 2px 4px rgba(0,0,0,0.3); } 50% { box-shadow: 0 0 0 3px rgba(255, 217, 61, 0.4), 0 2px 4px rgba(0,0,0,0.3); } } @media (prefers-color-scheme: dark) { #send-button.paused { box-shadow: 0 0 0 3px rgba(255, 217, 61, 0.9), 0 3px 6px rgba(0,0,0,0.6); } } [data-theme="dark"] #send-button.paused { box-shadow: 0 0 0 3px rgba(255, 217, 61, 0.9), 0 3px 6px rgba(0,0,0,0.6); } @media (prefers-color-scheme: dark) { #send-button.playing { background: rgb(102, 187, 106); /* Stay green for dark mode */ } } [data-theme="dark"] #send-button.playing { background: rgb(102, 187, 106); /* Stay green for dark mode */ }
#send-button.playing:active { background: rgb(56, 142, 60); /* Darker green on click */ box-shadow: 0 2px 4px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.1); transform: scale(0.95); } /* Paused state also needs dip effect when clicked */ #send-button.paused:active { background: rgb(56, 142, 60); /* Darker green on click */ box-shadow: 0 2px 4px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.1); transform: scale(0.95); animation: none; /* Stop pulse animation during click */ } /* Button press animation - plays on click for visible dip feedback */ @keyframes button-dip { 0% { transform: scale(1); } 50% { transform: scale(0.92); } 100% { transform: scale(1); } } #send-button.dipping { animation: button-dip 0.15s ease-out !important; }
@media (hover: hover) { #send-button.playing:hover { background: rgb(102, 187, 106); /* Lighter green on hover */ transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.3); } }
#send-button::before { content: none; /* SVG icons used instead */ }
/* Pause icon (double bars) */ #send-button.playing::before { content: none; /* SVG icons used instead */ } /* Show/hide SVG icons based on playing state */ #send-button .play-icon { display: block; } #send-button .pause-icon { display: none; } #send-button.playing .play-icon { display: none; } #send-button.playing .pause-icon { display: block; }
@media (hover: hover) { #send-button:hover { background: rgb(102, 187, 106); /* Lighter green on hover */ box-shadow: 0 4px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.3); transform: scale(1.05); } }
#send-button:active { background: rgb(56, 142, 60); /* Darker green on click */ box-shadow: 0 2px 4px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.1); transform: scale(0.95); } #send-button:disabled { cursor: not-allowed !important; opacity: 0.4 !important; pointer-events: auto; /* Still allow clicks for debugging */ } #send-button:disabled:hover, #send-button:disabled:active { background: rgb(76, 175, 80) !important; transform: scale(1) !important; box-shadow: 0 3px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2) !important; }
#stop-button { position: relative; left: auto; right: auto; top: auto; bottom: auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; flex-shrink: 0; box-sizing: border-box; background: rgb(147, 112, 219); /* Purple */ color: white; border: none; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2); transition: background 0.2s, box-shadow 0.2s, transform 0.1s; display: none; /* Hidden by default, JS sets to flex */ align-items: center; justify-content: center; padding: 0; margin: 0; } @media (prefers-color-scheme: dark) { #stop-button { background: rgb(167, 132, 239); /* Lighter purple for dark mode */ box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.3); } } [data-theme="dark"] #stop-button { background: rgb(167, 132, 239); /* Lighter purple for dark mode */ box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.3); } @media (prefers-color-scheme: dark) { #stop-button { background: rgb(167, 132, 239); /* Lighter purple for dark mode */ box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.3); } } [data-theme="dark"] #stop-button { background: rgb(167, 132, 239); /* Lighter purple for dark mode */ box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.3); }
#stop-button::before { content: none; /* SVG icon used instead */ }
@media (hover: hover) { #stop-button:hover { background: rgb(167, 132, 239); /* Lighter purple */ box-shadow: 0 4px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.3); transform: scale(1.05); } }
#stop-button:active { background: rgb(127, 92, 199); /* Darker purple on click */ box-shadow: 0 2px 4px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.1); transform: scale(0.95); } #stop-button:disabled { cursor: not-allowed !important; opacity: 0.4 !important; pointer-events: auto; } #stop-button:disabled:hover, #stop-button:disabled:active { background: rgb(147, 112, 219) !important; transform: scale(1) !important; box-shadow: 0 3px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2) !important; }
#kidlisp-editor { flex: 1; overflow: hidden; position: relative; /* For absolute positioning of button */ padding-left: 12px; /* Add padding to the left of the editor */ transition: box-shadow 0.3s ease; z-index: 1; /* Base z-index */ } /* Decrypting state - Monaco visible but read-only during decrypt animation */ #kidlisp-editor.decrypting { pointer-events: none; position: relative; } /* Darken the editor during decryption to show loading state */ #kidlisp-editor.decrypting::after { content: ''; position: absolute; inset: 0; background: rgba(0, 0, 0, 0.25); pointer-events: none; z-index: 10; transition: opacity 0.2s ease; }
#kidlisp-editor.decrypting .monaco-editor { opacity: 0.7; filter: saturate(0.6); transition: opacity 0.15s ease, filter 0.15s ease; } /* Hide Monaco's mobile keyboard toggle button (overlaps our playback controls) */ .monaco-editor .monaco-editor-overlaymessage, .monaco-editor .accessibilityHelpWidget, .monaco-editor .iPadShowKeyboard, .monaco-editor .inputarea.ime-input, .monaco-editor .monaco-action-bar.vertical, .monaco-editor .codicon-keyboard { display: none !important; } /* Decrypting state animation (cryptographic reveal) */ #kidlisp-editor.decrypting .view-line { animation: decrypt-flicker 0.1s ease-in-out infinite; } @keyframes decrypt-flicker { 0%, 100% { opacity: 1; } 50% { opacity: 0.95; } } /* Flash effect for quick transitions */ @keyframes code-flash { 0% { background: rgba(255, 255, 255, 0.4); } 100% { background: transparent; } } #kidlisp-editor.code-flash { animation: code-flash 100ms ease-out; } /* Flying code animation from ticker to editor */ .flying-code { position: fixed; z-index: 100001; pointer-events: none; font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace; font-size: 12px; white-space: nowrap; overflow: hidden; max-width: 300px; text-overflow: ellipsis; padding: 4px 8px; border-radius: 4px; background: rgba(0, 0, 0, 0.8); border: 1px solid rgba(255, 255, 255, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 20px rgba(138, 43, 226, 0.4); transition: none; } .flying-code.animating { transition: transform 150ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 150ms ease-out, box-shadow 150ms ease-out; } [data-theme="light"] .flying-code { background: rgba(255, 255, 255, 0.95); border: 1px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2), 0 0 20px rgba(138, 43, 226, 0.3); } /* KidLisp Documentation Tooltip */ #kidlisp-doc-tooltip { background: var(--bg-secondary, #252526); color: var(--text-primary, #d4d4d4); border: 2px solid #FF69B4; border-radius: 8px; padding: 12px 16px; max-width: 420px; font-family: 'Berkeley Mono', 'JetBrains Mono', 'Fira Code', monospace; font-size: 13px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); line-height: 1.5; } [data-theme="light"] #kidlisp-doc-tooltip { background: #fffacd; color: #333; border-color: #FF1493; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); } #kidlisp-doc-tooltip .doc-signature { display: block; background: rgba(0, 0, 0, 0.3); padding: 6px 10px; margin-top: 6px; border-radius: 4px; color: #98c379; font-size: 14px; } [data-theme="light"] #kidlisp-doc-tooltip .doc-signature { background: rgba(0, 0, 0, 0.08); color: #2e7d32; } #kidlisp-doc-tooltip .doc-category { background: var(--bg-tertiary, #3a3530); color: #61afef; padding: 2px 8px; border-radius: 4px; font-size: 11px; display: inline-block; } [data-theme="light"] #kidlisp-doc-tooltip .doc-category { background: #e0e0e0; color: #1565c0; } #kidlisp-doc-tooltip .doc-example { display: block; background: rgba(0, 0, 0, 0.2); padding: 4px 8px; margin-top: 4px; border-radius: 3px; font-size: 12px; color: #e5c07b; } [data-theme="light"] #kidlisp-doc-tooltip .doc-example { background: rgba(0, 0, 0, 0.06); color: #8b5a00; } /* Live running state - green outline matching play button (#90EE90) */ /* Uses box-shadow inset so it renders below content (won't cover context menu) */ .editor-panel.live-running, #kidlisp-editor.live-running { box-shadow: inset 0 0 0 2px rgba(144, 238, 144, 0.4); }
/* Paused state - yellow outline matching pause button (#ffd93d) */ .editor-panel.live-paused, #kidlisp-editor.live-paused { box-shadow: inset 0 0 0 2px rgba(255, 217, 61, 0.4); }
/* Dark mode: slightly brighter for visibility */ @media (prefers-color-scheme: dark) { .editor-panel.live-running, #kidlisp-editor.live-running { box-shadow: inset 0 0 0 2px rgba(144, 238, 144, 0.5); } .editor-panel.live-paused, #kidlisp-editor.live-paused { box-shadow: inset 0 0 0 2px rgba(255, 217, 61, 0.5); } }
[data-theme="dark"] .editor-panel.live-running, [data-theme="dark"] #kidlisp-editor.live-running { box-shadow: inset 0 0 0 2px rgba(144, 238, 144, 0.5); }
[data-theme="dark"] .editor-panel.live-paused, [data-theme="dark"] #kidlisp-editor.live-paused { box-shadow: inset 0 0 0 2px rgba(255, 217, 61, 0.5); }
/* Stopped state - purple outline (matches stop button) */ .editor-panel.live-stopped, #kidlisp-editor.live-stopped { box-shadow: inset 0 0 0 2px rgba(147, 112, 219, 0.5); animation: stop-pulse 2s ease-in-out infinite; }
/* Dark mode: slightly brighter for stopped state */ @media (prefers-color-scheme: dark) { .editor-panel.live-stopped, #kidlisp-editor.live-stopped { box-shadow: inset 0 0 0 2px rgba(147, 112, 219, 0.6); } }
[data-theme="dark"] .editor-panel.live-stopped, [data-theme="dark"] #kidlisp-editor.live-stopped { box-shadow: inset 0 0 0 2px rgba(147, 112, 219, 0.6); }
/* Pulse animation for stopped state - purple */ @keyframes stop-pulse { 0%, 100% { box-shadow: inset 0 0 0 2px rgba(147, 112, 219, 0.5); } 50% { box-shadow: inset 0 0 0 2px rgba(147, 112, 219, 0.25); } }
/* Visualization Tabs */ .viz-tabs { display: flex; gap: 0; padding: 0 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--editor-border); height: 28px; align-items: stretch; flex-shrink: 0; } .viz-tab { padding: 4px 12px; font-size: 11px; font-family: var(--font-mono); background: transparent; border: none; border-bottom: 2px solid transparent; color: var(--text-tertiary); cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; gap: 4px; } .viz-tab:hover { color: var(--text-primary); background: rgba(147, 112, 219, 0.1); } .viz-tab.active { color: #9370DB; border-bottom-color: #9370DB; background: rgba(147, 112, 219, 0.05); } .viz-tab .tab-icon { font-size: 12px; opacity: 0.7; } .viz-tab.active .tab-icon { opacity: 1; } /* Visualization Containers */ .viz-container { flex: 1; position: relative; overflow: hidden; display: none; } .viz-container.active { display: flex; flex-direction: column; } #viz-text.active { display: flex; flex-direction: column; } #viz-tree { background: var(--bg-primary); } #viz-tree canvas { flex: 1; width: 100%; display: block; } #viz-space { background: #0a0a0f; } #viz-space canvas { flex: 1; width: 100%; display: block; } #viz-beads { background: #141420; } #viz-beads canvas { flex: 1; width: 100%; display: block; } /* Tree view node colors */ .tree-node-function { fill: #9370DB; } .tree-node-number { fill: #FF9F43; } .tree-node-string { fill: #2ECC71; } .tree-node-symbol { fill: #4ECDC4; } .tree-node-list { fill: #CD5C9B; }
/* Zebra striping REMOVED - was blocking Monaco's selection overlay in dark mode */ /* The .view-lines backgrounds paint over .view-overlays where selection is rendered */
#preview-iframe { /* Use inset: 0 for pixel-perfect edge-to-edge positioning */ position: absolute; inset: 0; width: 100%; height: 100%; border: none; outline: none; display: block; background: transparent; /* Prevent any hairline gaps */ margin: 0; padding: 0; box-sizing: border-box; } /* Container for iframe - must have overflow hidden to prevent hairline gaps */ .preview-iframe-container { position: relative; flex: 1; overflow: hidden; box-sizing: border-box; /* Match the default kidlisp checkerboard background to hide any subpixel gaps */ background: #f0f0f0; } @media (prefers-color-scheme: dark) { .preview-iframe-container { background: #202020; } } [data-theme="light"] .preview-iframe-container { background: #f0f0f0; } [data-theme="dark"] .preview-iframe-container { background: #202020; }
/* Stopped state - gray out the preview */ .preview-iframe-container.stopped #preview-iframe { filter: grayscale(0.8) brightness(0.7); opacity: 0.6; transition: filter 0.3s ease, opacity 0.3s ease; } /* Popped-out state - dark overlay with channel info */ /* PJ dropdown popover */ .pj-dropdown { position: absolute; top: 100%; right: 0; z-index: 200; width: 200px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.25); padding: 10px; font-family: monospace; font-size: 12px; display: flex; flex-direction: column; align-items: center; gap: 8px; } .pj-dropdown.hidden { display: none; } .pj-dropdown-header { width: 100%; display: flex; justify-content: space-between; align-items: center; } .pj-dropdown-channel { font-weight: 700; color: rgb(205, 92, 155); cursor: pointer; } .pj-dropdown-channel:hover { text-decoration: underline; } .pj-dropdown-viewers { color: var(--text-secondary, #888); font-size: 11px; } .pj-dropdown-stop { width: 100%; padding: 5px 0; background: rgba(205, 92, 155, 0.12); border: 1px solid rgba(205, 92, 155, 0.4); color: rgb(205, 92, 155); border-radius: 4px; cursor: pointer; font-family: monospace; font-size: 11px; transition: background 0.15s; } .pj-dropdown-stop:hover { background: rgba(205, 92, 155, 0.25); } .pj-dropdown-popout { width: 100%; padding: 5px 0; background: rgba(100, 160, 255, 0.12); border: 1px solid rgba(100, 160, 255, 0.4); color: rgb(100, 160, 255); border-radius: 4px; cursor: pointer; font-family: monospace; font-size: 11px; transition: background 0.15s; } .pj-dropdown-popout:hover { background: rgba(100, 160, 255, 0.25); }
/* Jockey tab content */ .jockey-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 16px; height: 100%; font-family: monospace; font-size: 13px; color: var(--text-primary); text-align: center; } .jockey-channel { font-size: 18px; font-weight: 700; color: rgb(205, 92, 155); cursor: pointer; } .jockey-channel:hover { text-decoration: underline; } .jockey-viewers { font-size: 13px; color: var(--text-secondary, #888); } .jockey-url { font-size: 11px; color: var(--text-secondary, #888); word-break: break-all; cursor: pointer; } .jockey-url:hover { color: rgb(205, 92, 155); } .jockey-stop { padding: 6px 16px; background: rgba(205, 92, 155, 0.12); border: 1px solid rgba(205, 92, 155, 0.4); color: rgb(205, 92, 155); border-radius: 4px; cursor: pointer; font-family: monospace; font-size: 12px; } .jockey-stop:hover { background: rgba(205, 92, 155, 0.25); } .jockey-off { color: var(--text-secondary, #888); font-size: 12px; }
/* Pixel rulers for paused state (Photoshop-style) - all 4 sides */ .preview-rulers { display: none; pointer-events: none; } .preview-rulers.visible { display: block; } /* Ruler bar styling */ .ruler-horizontal, .ruler-horizontal-bottom { position: absolute; left: 20px; right: 20px; height: 16px; background: rgba(40, 40, 40, 0.92); z-index: 998; /* Must be BELOW center-square drag nub (z-index: 999) */ overflow: hidden; } .ruler-horizontal { top: 0; border-bottom: 1px solid rgba(100, 100, 100, 0.5); } .ruler-horizontal-bottom { bottom: 0; border-top: 1px solid rgba(100, 100, 100, 0.5); } .ruler-vertical, .ruler-vertical-right { position: absolute; top: 16px; bottom: 16px; width: 20px; background: rgba(40, 40, 40, 0.92); z-index: 998; /* Must be BELOW center-square drag nub (z-index: 999) */ overflow: hidden; } .ruler-vertical { left: 0; border-right: 1px solid rgba(100, 100, 100, 0.5); } .ruler-vertical-right { right: 0; border-left: 1px solid rgba(100, 100, 100, 0.5); } /* Corner pieces */ .ruler-corner { position: absolute; width: 20px; height: 16px; background: rgba(50, 50, 50, 0.95); z-index: 998; /* Must be BELOW center-square drag nub (z-index: 999) */ display: flex; align-items: center; justify-content: center; font-size: 7px; color: rgba(255, 255, 255, 0.5); font-family: 'JetBrains Mono', monospace; } .ruler-corner-tl { top: 0; left: 0; } .ruler-corner-tr { top: 0; right: 0; } .ruler-corner-bl { bottom: 0; left: 0; } .ruler-corner-br { bottom: 0; right: 0; } .ruler-ticks { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } .ruler-horizontal .ruler-ticks, .ruler-horizontal-bottom .ruler-ticks { display: flex; align-items: flex-end; } .ruler-horizontal-bottom .ruler-ticks { align-items: flex-start; } .ruler-tick { position: absolute; background: rgba(150, 150, 150, 0.6); } .ruler-horizontal .ruler-tick, .ruler-horizontal-bottom .ruler-tick { width: 1px; } .ruler-horizontal .ruler-tick { bottom: 0; } .ruler-horizontal-bottom .ruler-tick { top: 0; } .ruler-horizontal .ruler-tick.major, .ruler-horizontal-bottom .ruler-tick.major { height: 10px; } .ruler-horizontal .ruler-tick.minor, .ruler-horizontal-bottom .ruler-tick.minor { height: 5px; } .ruler-vertical .ruler-tick, .ruler-vertical-right .ruler-tick { height: 1px; } .ruler-vertical .ruler-tick { right: 0; } .ruler-vertical-right .ruler-tick { left: 0; } .ruler-vertical .ruler-tick.major, .ruler-vertical-right .ruler-tick.major { width: 10px; } .ruler-vertical .ruler-tick.minor, .ruler-vertical-right .ruler-tick.minor { width: 5px; } .ruler-label { position: absolute; font-size: 8px; font-family: 'JetBrains Mono', monospace; color: rgba(200, 200, 200, 0.8); pointer-events: none; } .ruler-horizontal .ruler-label { top: 1px; } .ruler-horizontal-bottom .ruler-label { bottom: 1px; } .ruler-vertical .ruler-label { left: 2px; writing-mode: vertical-lr; transform: rotate(180deg); } .ruler-vertical-right .ruler-label { right: 2px; writing-mode: vertical-rl; } /* Coordinate display overlay */ .coord-display { position: absolute; top: 20px; left: 24px; background: rgba(0, 0, 0, 0.85); color: #00ff88; font-family: 'JetBrains Mono', monospace; font-size: 11px; padding: 4px 8px; border-radius: 3px; z-index: 1003; pointer-events: none; opacity: 0; transition: opacity 0.1s ease; white-space: nowrap; border: 1px solid rgba(0, 255, 136, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .coord-display.visible { opacity: 1; } /* Crosshair lines on rulers */ .ruler-crosshair-h { position: absolute; top: 0; width: 1px; height: 100%; background: rgba(0, 0, 0, 0.7); pointer-events: none; z-index: 1002; } .ruler-crosshair-v { position: absolute; left: 0; height: 1px; width: 100%; background: rgba(0, 0, 0, 0.7); pointer-events: none; z-index: 1002; } /* Dark theme: white crosshairs */ [data-theme="dark"] .ruler-crosshair-h, [data-theme="dark"] .ruler-crosshair-v { background: rgba(255, 255, 255, 0.8); } /* Light theme rulers: light gray background with dark marks */ [data-theme="light"] .ruler-horizontal, [data-theme="light"] .ruler-horizontal-bottom, [data-theme="light"] .ruler-vertical, [data-theme="light"] .ruler-vertical-right { background: rgba(170, 170, 175, 0.92); border-color: rgba(140, 140, 145, 0.5); } [data-theme="light"] .ruler-corner { background: rgba(160, 160, 165, 0.95); color: rgba(60, 60, 60, 0.7); } [data-theme="light"] .ruler-tick { background: rgba(80, 80, 80, 0.6); } [data-theme="light"] .ruler-label { color: rgba(60, 60, 60, 0.8); } /* Full preview crosshair lines (military reticle style) */ .preview-crosshair-h, .preview-crosshair-v { position: absolute; pointer-events: none; z-index: 1003; opacity: 0; transition: opacity 0.1s ease; } .preview-crosshair-h { left: 20px; right: 20px; height: 1px; background: rgba(0, 0, 0, 0.5); } .preview-crosshair-v { top: 16px; bottom: 16px; width: 1px; background: rgba(0, 0, 0, 0.5); } /* Dark theme: white preview crosshairs */ [data-theme="dark"] .preview-crosshair-h, [data-theme="dark"] .preview-crosshair-v { background: rgba(255, 255, 255, 0.6); } .preview-rulers.visible .preview-crosshair-h, .preview-rulers.visible .preview-crosshair-v { opacity: 1; } /* Preview overlay for mouse tracking (invisible but captures events) */ .preview-mouse-overlay { position: absolute; top: 16px; left: 20px; right: 20px; bottom: 16px; z-index: 1000; cursor: crosshair; display: none; } .preview-rulers.visible + .preview-mouse-overlay { display: block; } /* Frozen mode overlay - match frozen iframe dimensions */ .preview-iframe-container.canvas-frozen .preview-mouse-overlay { width: var(--frozen-width); height: var(--frozen-height); max-width: calc(100% - 40px); max-height: calc(100% - 32px); top: var(--frozen-top, 16px); left: var(--frozen-left, 20px); right: auto; bottom: auto; } /* Iframe inset when rulers are visible (instant, no animation) */ .preview-iframe-container.rulers-active #preview-iframe { position: absolute; top: 16px; left: 20px; right: 20px; bottom: 16px; width: calc(100% - 40px); height: calc(100% - 32px); } /* Frozen canvas mode - maintain fixed size centered in container */ .preview-iframe-container.rulers-active.canvas-frozen #preview-iframe { /* Override the fill behavior - use fixed frozen dimensions */ width: var(--frozen-width); height: var(--frozen-height); max-width: calc(100% - 40px); max-height: calc(100% - 32px); /* Position set via JS-calculated CSS variables for proper centering */ top: var(--frozen-top, 16px); left: var(--frozen-left, 20px); right: auto; bottom: auto; transform: none; } /* Pixel zoom mode - magnify around cursor position */ .preview-iframe-container.rulers-active #preview-iframe { /* Keep transform-origin consistent during zoom in/out to prevent anchor jump */ transform-origin: var(--zoom-x, 50%) var(--zoom-y, 50%); transition: transform 0.1s ease-out; } .preview-iframe-container.rulers-active.pixel-zoom #preview-iframe { transform: scale(2); image-rendering: pixelated; } /* When frozen AND zoomed, use same scale */ .preview-iframe-container.rulers-active.canvas-frozen.pixel-zoom #preview-iframe { transform: scale(2); }
/* Ensure preview panel has no internal gaps */ .preview-panel { box-sizing: border-box; overflow: hidden; }
/* Boot screen / loading state */ .boot-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: row; z-index: 998; /* Below center-square drag nub (z-index: 999) */ transition: opacity 0.5s; }
.boot-screen.hidden { opacity: 0; pointer-events: none; }
.color-bar { flex: 1; height: 100%; }
.test-pattern { width: 100%; height: 60px; display: flex; margin-bottom: 24px; }
.color-bar { flex: 1; height: 100%; }
.boot-text { color: #fff; font-family: monospace; font-size: 14px; text-align: center; line-height: 1.6; opacity: 0.8; }
@media (prefers-color-scheme: light) { .boot-screen { background: #fff; } .boot-text { color: #000; } /* Inverted/muted color bars for light mode */ .bar-1 { background: #000000 !important; } .bar-2 { background: #0000aa !important; } .bar-3 { background: #aa0000 !important; } .bar-4 { background: #aa00aa !important; } .bar-5 { background: #00aa00 !important; } .bar-6 { background: #00aaaa !important; } .bar-7 { background: #aaaa00 !important; } .bar-8 { background: #ffffff !important; } } @media (prefers-color-scheme: dark) { .boot-screen { background: #1a1a1a; } .boot-text { color: #fff; } /* Standard SMPTE bright color bars for dark mode */ .bar-1 { background: #ffffff !important; } .bar-2 { background: #ffff00 !important; } .bar-3 { background: #00ffff !important; } .bar-4 { background: #00ff00 !important; } .bar-5 { background: #ff00ff !important; } .bar-6 { background: #ff0000 !important; } .bar-7 { background: #0000ff !important; } .bar-8 { background: #000000 !important; } }
/* Manual theme toggle overrides for preview iframe */ [data-theme="light"] .boot-screen { background: #fff; } [data-theme="light"] .boot-text { color: #000; } [data-theme="light"] .bar-1 { background: #000000 !important; } [data-theme="light"] .bar-2 { background: #0000aa !important; } [data-theme="light"] .bar-3 { background: #aa0000 !important; } [data-theme="light"] .bar-4 { background: #aa00aa !important; } [data-theme="light"] .bar-5 { background: #00aa00 !important; } [data-theme="light"] .bar-6 { background: #00aaaa !important; } [data-theme="light"] .bar-7 { background: #aaaa00 !important; } [data-theme="light"] .bar-8 { background: #ffffff !important; }
[data-theme="dark"] .boot-screen { background: #1a1a1a; } [data-theme="dark"] .boot-text { color: #fff; } /* Dark mode uses standard SMPTE color bars (bright on dark) */ [data-theme="dark"] .bar-1 { background: #ffffff !important; } [data-theme="dark"] .bar-2 { background: #ffff00 !important; } [data-theme="dark"] .bar-3 { background: #00ffff !important; } [data-theme="dark"] .bar-4 { background: #00ff00 !important; } [data-theme="dark"] .bar-5 { background: #ff00ff !important; } [data-theme="dark"] .bar-6 { background: #ff0000 !important; } [data-theme="dark"] .bar-7 { background: #0000ff !important; } [data-theme="dark"] .bar-8 { background: #000000 !important; }
/* FF1 Art Computer boot screen - theme-aware */ #boot-screen-ff1 { background: #1a1a1a !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; padding: 16px; box-sizing: border-box; gap: 8px; overflow: auto; position: relative; } #boot-screen-ff1 .ff1-logo { height: 48px; width: auto; max-width: 80%; object-fit: contain; filter: invert(1); transition: height 0.2s ease, margin 0.2s ease; flex-shrink: 0; } #boot-screen-ff1 .boot-text { color: white !important; font-family: 'PP Mori', 'Futura', 'Century Gothic', sans-serif; text-align: center; } #boot-screen-ff1 .boot-text-title { font-size: clamp(14px, 4vw, 18px); font-weight: bold; } #boot-screen-ff1 .boot-text-action { font-size: clamp(10px, 2.5vw, 12px); opacity: 0.6; } /* FF1 Settings UI - Inline Pairing */ .ff1-settings { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; margin-top: 16px; width: 100%; max-width: 280px; box-sizing: border-box; } .ff1-bridge-status, .ff1-no-bridge { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; } .ff1-devices-list { display: flex; flex-direction: column; gap: 6px; width: 100%; max-height: 120px; overflow-y: auto; } .ff1-device-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: rgba(255,255,255,0.1); border-radius: 4px; cursor: pointer; transition: background 0.2s; } .ff1-device-item:hover, .ff1-device-item.selected { background: rgba(74, 222, 128, 0.2); } .ff1-device-item.selected { border: 1px solid rgba(74, 222, 128, 0.5); } .ff1-device-name { font-size: 12px; color: white; } .ff1-download-btn { text-decoration: none; background: rgba(74, 222, 128, 0.2); border-color: rgba(74, 222, 128, 0.5); } .ff1-download-btn:hover { background: rgba(74, 222, 128, 0.3); } .ff1-paired-row { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; width: 100%; } .ff1-btn { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 8px 16px; color: white; font-size: 12px; cursor: pointer; transition: background 0.2s; white-space: nowrap; } .ff1-btn:hover { background: rgba(255,255,255,0.25); } .ff1-btn:disabled { opacity: 0.5; cursor: not-allowed; } .ff1-status { font-size: 12px; color: rgba(255,255,255,0.7); text-align: center; } .ff1-status.success { color: #4ade80; } .ff1-status.error { color: #f87171; } .ff1-pairing { display: none; position: absolute; inset: 0; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: #1a1a1a; box-sizing: border-box; padding: 16px; z-index: 10; overflow: auto; } .ff1-pairing.open { display: flex; } .ff1-pairing-close { position: absolute; top: 8px; right: 8px; background: rgba(255,255,255,0.15); border: none; border-radius: 4px; width: 28px; height: 28px; color: white; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .ff1-pairing-close:hover { background: rgba(255,255,255,0.25); } .ff1-pairing-content { display: flex; flex-direction: column; align-items: center; gap: 12px; max-width: 100%; } .ff1-pairing-hint { font-size: 12px; opacity: 0.85; text-align: left; line-height: 1.6; color: white; } .ff1-pairing-qr { width: 120px; height: 120px; min-width: 80px; min-height: 80px; background: #fff; border-radius: 6px; image-rendering: pixelated; flex-shrink: 0; } .ff1-pairing-status { font-size: 11px; opacity: 0.6; text-align: center; color: white; } .ff1-pairing-link { font-size: 10px; opacity: 0.5; word-break: break-all; text-align: center; cursor: pointer; max-width: 100%; color: white; } .ff1-pairing-link:hover { text-decoration: underline; } /* FF1 light mode - keep dark aesthetic for brand consistency */ [data-theme="light"] #boot-screen-ff1 { background: #1a1a1a !important; } [data-theme="light"] #boot-screen-ff1 .boot-text { color: white !important; }
/* Ableton Live 12 boot screen - theme-aware */ #boot-screen-ableton { flex-direction: column !important; align-items: center !important; justify-content: center !important; padding: 16px; box-sizing: border-box; gap: 8px; transition: background 0.3s ease; /* Default to dark for system dark preference */ background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%) !important; } #boot-screen-ableton .ableton-boot-logo { width: clamp(50px, 15vw, 80px); height: auto; transition: width 0.2s ease; color: #fff; /* Default fill via currentColor */ } #boot-screen-ableton .boot-text { font-family: 'Futura PT', 'Futura', 'Century Gothic', sans-serif; text-align: center; color: #fff; /* Default text */ } #boot-screen-ableton .boot-text-title { font-size: clamp(14px, 4vw, 18px); font-weight: bold; } #boot-screen-ableton .boot-text-action { font-size: clamp(10px, 2.5vw, 12px); opacity: 0.6; } /* Download button styling - default dark mode */ #boot-screen-ableton .m4l-download-btn { display: inline-block; padding: 8px 16px; text-decoration: none; border-radius: 4px; font-size: clamp(10px, 2.5vw, 12px); font-weight: 500; transition: transform 0.1s ease, background 0.2s ease; white-space: nowrap; background: #fff; color: #000; } #boot-screen-ableton .m4l-download-btn:hover { transform: scale(1.02); opacity: 0.9; } #boot-screen-ableton .m4l-download-btn:active { transform: scale(0.98); } /* System light preference (no data-theme set) */ @media (prefers-color-scheme: light) { #boot-screen-ableton { background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%) !important; } #boot-screen-ableton .ableton-boot-logo { color: #333; } #boot-screen-ableton .boot-text { color: #333 !important; } #boot-screen-ableton .m4l-download-btn { background: #333; color: #fff; } } /* Explicit light mode */ [data-theme="light"] #boot-screen-ableton { background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%) !important; } [data-theme="light"] #boot-screen-ableton .ableton-boot-logo { color: #333; } [data-theme="light"] #boot-screen-ableton .boot-text { color: #333 !important; } [data-theme="light"] #boot-screen-ableton .m4l-download-btn { background: #333 !important; color: #fff !important; } /* Explicit dark mode */ [data-theme="dark"] #boot-screen-ableton { background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%) !important; } [data-theme="dark"] #boot-screen-ableton .ableton-boot-logo { color: #fff; } [data-theme="dark"] #boot-screen-ableton .boot-text { color: #fff !important; } [data-theme="dark"] #boot-screen-ableton .m4l-download-btn { background: #fff !important; color: #000 !important; } /* Enable container queries on preview-iframe-container */ .preview-iframe-container { container-type: size; container-name: preview; } /* Responsive boot screens - using @container for modern browsers */ @supports (container-type: size) { /* FF1 responsive - reflow on small panels (HEIGHT) */ @container preview (max-height: 180px) { #boot-screen-ff1 .ff1-logo { height: 32px; } #boot-screen-ff1 .boot-text-title { font-size: 14px; } } @container preview (max-height: 120px) { #boot-screen-ff1 { flex-direction: row !important; gap: 16px; padding: 8px 16px; } #boot-screen-ff1 .ff1-logo { height: 28px; } #boot-screen-ff1 .boot-text-title { font-size: 12px; } #boot-screen-ff1 .boot-text-action { display: none; } } /* FF1 responsive - WIDTH-based adjustments for narrow panels */ @container preview (max-width: 380px) { #boot-screen-ff1 .ff1-settings { max-width: 100%; padding: 0 8px; } #boot-screen-ff1 .ff1-pairing-qr { width: 120px; height: 120px; } #boot-screen-ff1 .ff1-pairing-hint { font-size: 10px; } } @container preview (max-width: 300px) { #boot-screen-ff1 { padding: 8px; gap: 4px; } #boot-screen-ff1 .ff1-logo { height: 28px; } #boot-screen-ff1 .boot-text-title { font-size: 12px; } #boot-screen-ff1 .boot-text-action { font-size: 9px; } #boot-screen-ff1 .ff1-settings { margin-top: 8px; gap: 8px; } #boot-screen-ff1 .ff1-pairing-qr { width: 100px; height: 100px; } #boot-screen-ff1 .ff1-btn { padding: 6px 10px; font-size: 10px; } } @container preview (max-width: 240px) { #boot-screen-ff1 .ff1-logo { height: 22px; } #boot-screen-ff1 .boot-text-title { font-size: 11px; } #boot-screen-ff1 .boot-text-action { display: none; } #boot-screen-ff1 .ff1-pairing-qr { width: 80px; height: 80px; } #boot-screen-ff1 .ff1-pairing-hint { font-size: 9px; } #boot-screen-ff1 .ff1-pairing-link { font-size: 8px; } } /* FF1 responsive - COMBINED narrow+short for extreme cases */ @container preview (max-width: 300px) and (max-height: 300px) { #boot-screen-ff1 { flex-direction: column !important; justify-content: flex-start !important; align-items: center !important; padding: 8px; gap: 4px; overflow-y: auto; } #boot-screen-ff1 .ff1-logo { height: 24px; } #boot-screen-ff1 .ff1-settings { margin-top: 4px; } #boot-screen-ff1 .ff1-pairing { gap: 6px; } #boot-screen-ff1 .ff1-pairing-qr { width: 80px; height: 80px; } } /* FF1 responsive - wide layout when there's enough width */ @container preview (min-width: 500px) and (min-height: 280px) { #boot-screen-ff1 { flex-direction: row !important; flex-wrap: wrap; justify-content: center !important; align-items: center !important; gap: 24px; padding: 16px 24px; } #boot-screen-ff1 .ff1-settings { max-width: none; flex-direction: row; align-items: center; gap: 20px; } #boot-screen-ff1 .ff1-pairing-qr { width: 140px; height: 140px; } } /* FF1 pairing overlay - horizontal layout for wider panels */ @container preview (min-width: 420px) { .ff1-pairing-content { flex-direction: row; align-items: center; gap: 24px; } .ff1-pairing-hint { text-align: left; } .ff1-pairing-qr { width: 140px; height: 140px; } } /* FF1 pairing overlay - larger QR for spacious panels */ @container preview (min-width: 500px) and (min-height: 300px) { .ff1-pairing-qr { width: 160px; height: 160px; } .ff1-pairing-hint { font-size: 13px; } } /* FF1 pairing overlay - compact vertical for narrow */ @container preview (max-width: 300px) { .ff1-pairing { padding: 12px; gap: 8px; } .ff1-pairing-qr { width: 90px; height: 90px; } .ff1-pairing-hint { font-size: 10px; } .ff1-pairing-close { width: 24px; height: 24px; font-size: 14px; } } /* Ableton responsive - reflow on small panels */ @container preview (max-height: 200px) { #boot-screen-ableton .ableton-boot-logo { width: 50px; } #boot-screen-ableton .boot-text-title { font-size: 14px; } #boot-screen-ableton .m4l-download-btn { padding: 6px 12px; font-size: 10px; } } @container preview (max-height: 140px) { #boot-screen-ableton { flex-direction: row !important; flex-wrap: wrap; gap: 12px; padding: 8px 16px; justify-content: center !important; } #boot-screen-ableton .ableton-boot-logo { width: 40px; } #boot-screen-ableton .boot-text-title { font-size: 12px; } #boot-screen-ableton .boot-text-action { display: none; } } @container preview (max-height: 80px) { #boot-screen-ableton .m4l-download-btn { display: none; } #boot-screen-ff1 .ff1-logo { height: 24px; } } } /* Fallback for browsers without container queries - use media queries */ @supports not (container-type: size) { @media (max-height: 400px) { #boot-screen-ff1 .ff1-logo { height: 32px; } #boot-screen-ableton .ableton-boot-logo { width: 50px; } #boot-screen-ableton .m4l-download-btn { padding: 6px 12px; font-size: 10px; } } @media (max-height: 280px) { #boot-screen-ff1 { flex-direction: row !important; gap: 16px; padding: 8px 16px; } #boot-screen-ff1 .ff1-logo { height: 28px; } #boot-screen-ff1 .boot-text-action, #boot-screen-ableton .boot-text-action { display: none; } #boot-screen-ableton { flex-direction: row !important; flex-wrap: wrap; gap: 12px; padding: 8px 16px; } #boot-screen-ableton .ableton-boot-logo { width: 40px; } } /* Width-based fallbacks for FF1 */ @media (max-width: 380px) { #boot-screen-ff1 .ff1-settings { max-width: 100%; padding: 0 8px; } #boot-screen-ff1 .ff1-pairing-qr { width: 120px; height: 120px; } } @media (max-width: 300px) { #boot-screen-ff1 { padding: 8px; gap: 4px; } #boot-screen-ff1 .ff1-logo { height: 28px; } #boot-screen-ff1 .ff1-pairing-qr { width: 100px; height: 100px; } #boot-screen-ff1 .ff1-btn { padding: 6px 10px; font-size: 10px; } } }
.boot-text .blink { animation: blink 1s infinite; }
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.bounce-dot { display: inline-block; animation: bounce 1.2s ease-in-out 3; font-weight: bold; }
@keyframes babyColors { 0% { color: #FF7096; } /* Saturated Pink */ 14% { color: #FFB870; } /* Saturated Orange */ 28% { color: #FFEA70; } /* Saturated Yellow */ 42% { color: #8AFF8A; } /* Saturated Lime */ 57% { color: #70D6FF; } /* Saturated Cyan */ 71% { color: #C596FF; } /* Saturated Purple */ 85% { color: #FF96C5; } /* Saturated Rose */ 100% { color: #FF7096; } /* Saturated Pink */ }
.baby-colors { font-weight: bold; font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-size: 20px; line-height: 32px; /* Match tab bar height */ display: flex; align-items: center; height: 100%; }
.baby-colors span { display: inline-block; /* animation: babyColors 4s step-end infinite; */ text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.3); letter-spacing: 0.05em; /* Extra spacing between characters */ } .baby-colors .com-part { color: #70D6FF; /* Light Blue */ text-shadow: none; }
@media (prefers-color-scheme: dark) { .baby-colors .com-part { color: #70D6FF; /* Keep Light Blue in dark mode */ } } /* Connectivity indicator for the period in kidlisp. */ .baby-colors .connectivity-dot { transition: color 0.3s ease, text-shadow 0.3s ease; } .baby-colors .connectivity-dot.disconnected { color: #ff4444; /* Red - disconnected */ text-shadow: 1px 1px 0px rgba(255, 68, 68, 0.3); } .baby-colors .connectivity-dot.connecting { color: #ff8800; /* Orange - connecting */ text-shadow: 1px 1px 0px rgba(255, 136, 0, 0.3); animation: connectingPulse 1s ease-in-out infinite; } .baby-colors .connectivity-dot.waiting { color: #ffdd00; /* Yellow - waiting/ready */ text-shadow: 1px 1px 0px rgba(255, 221, 0, 0.3); } .baby-colors .connectivity-dot.connected { color: #00ff88; /* Green - connected */ text-shadow: 1px 1px 0px rgba(0, 255, 136, 0.3); } .baby-colors .connectivity-dot.receiving { animation: receivingBlink 0.2s ease-in-out 2; } @keyframes connectingPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } @keyframes receivingBlink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; transform: scale(1.2); } } /* Theme toggle button */ #theme-toggle { width: 32px; height: 32px; border: none; border-radius: 4px; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; transition: opacity 0.2s; padding: 0; opacity: 0.6; user-select: none; -webkit-tap-highlight-color: transparent; } @media (hover: hover) { #theme-toggle:hover { opacity: 1; } } #theme-toggle:active { opacity: 0.4; } @media (prefers-color-scheme: dark) { #theme-toggle { opacity: 0.6; } }
#preview-panel .docs-header { background: rgb(240, 235, 250); /* Light muted purple backdrop - AC default */ border-bottom-color: rgb(220, 215, 230); position: relative; z-index: 99998; /* Ensure dropdown appears above iframe */ overflow: visible; /* Allow dropdown to overflow */ transition: background 0.3s ease; } /* Platform-specific header colors - Light mode (explicit) */ [data-theme="light"] #preview-panel[data-platform="aesthetic-computer"] .docs-header { background: rgb(240, 235, 250); /* Muted purple */ border-bottom-color: rgb(220, 215, 230); } [data-theme="light"] #preview-panel[data-platform="ff1"] .docs-header { background: rgb(26, 26, 26); /* Black */ border-bottom-color: rgb(50, 50, 50); } [data-theme="light"] #preview-panel[data-platform="ff1"] .docs-header #preview-title { color: white; } [data-theme="light"] #preview-panel[data-platform="ff1"] .docs-header #preview-title .platform-logo { filter: invert(1); /* Invert black logo to white */ } [data-theme="light"] #preview-panel[data-platform="ableton"] .docs-header { background: rgb(255, 255, 255); /* White */ border-bottom-color: rgb(230, 230, 230); } [data-theme="light"] #preview-panel[data-platform="ableton"] .docs-header #preview-title { color: black; } [data-theme="light"] #preview-panel[data-platform="playdate"] .docs-header { background: rgb(255, 204, 0); /* Playdate yellow */ border-bottom-color: rgb(230, 180, 0); } [data-theme="light"] #preview-panel[data-platform="playdate"] .docs-header #preview-title { color: black; } [data-theme="light"] #preview-panel[data-platform="gameboy"] .docs-header { background: rgb(155, 188, 15); /* Game Boy green */ border-bottom-color: rgb(130, 160, 10); } [data-theme="light"] #preview-panel[data-platform="gameboy"] .docs-header #preview-title { color: rgb(15, 56, 15); } [data-theme="light"] #preview-panel[data-platform="n64"] .docs-header { background: rgb(200, 50, 50); /* N64 red */ border-bottom-color: rgb(170, 40, 40); } [data-theme="light"] #preview-panel[data-platform="n64"] .docs-header #preview-title { color: white; } /* Platform-specific header colors - Dark mode (explicit) */ [data-theme="dark"] #preview-panel .docs-header, [data-theme="dark"] #preview-panel[data-platform="aesthetic-computer"] .docs-header { background: rgb(48, 43, 58); border-bottom-color: rgb(62, 56, 72); } [data-theme="dark"] #preview-panel[data-platform="ff1"] .docs-header { background: rgb(26, 26, 26); /* Black */ border-bottom-color: rgb(50, 50, 50); } [data-theme="dark"] #preview-panel[data-platform="ff1"] .docs-header #preview-title { color: white; } [data-theme="dark"] #preview-panel[data-platform="ff1"] .docs-header #preview-title .platform-logo { filter: invert(1); /* Invert black logo to white */ } [data-theme="dark"] #preview-panel[data-platform="ableton"] .docs-header { background: rgb(255, 255, 255); /* White */ border-bottom-color: rgb(230, 230, 230); } [data-theme="dark"] #preview-panel[data-platform="ableton"] .docs-header #preview-title { color: black; } [data-theme="dark"] #preview-panel[data-platform="playdate"] .docs-header { background: rgb(255, 204, 0); /* Playdate yellow */ border-bottom-color: rgb(230, 180, 0); } [data-theme="dark"] #preview-panel[data-platform="playdate"] .docs-header #preview-title { color: black; } [data-theme="dark"] #preview-panel[data-platform="gameboy"] .docs-header { background: rgb(155, 188, 15); /* Game Boy green */ border-bottom-color: rgb(130, 160, 10); } [data-theme="dark"] #preview-panel[data-platform="gameboy"] .docs-header #preview-title { color: rgb(15, 56, 15); } [data-theme="dark"] #preview-panel[data-platform="n64"] .docs-header { background: rgb(200, 50, 50); /* N64 red */ border-bottom-color: rgb(170, 40, 40); } [data-theme="dark"] #preview-panel[data-platform="n64"] .docs-header #preview-title { color: white; }
#preview-title { font-family: 'YWFTProcessing-Regular', monospace; font-weight: bold; font-size: 17px; /* Slightly larger text */ color: rgb(88, 78, 108); /* Muted purple text */ margin-left: 0px; /* Reset margin */ display: inline-flex; align-items: center; line-height: 32px; /* Match header height */ white-space: nowrap; /* Prevent line breaking */ overflow: hidden; text-overflow: ellipsis; max-width: 100%; letter-spacing: -0.02em; /* Tighter letter spacing */ padding-right: 8px; /* Extra room for $code display */ }
.route-code-tab { display: none; align-items: center; font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-weight: 700; letter-spacing: 0.04em; white-space: nowrap; line-height: 1; margin-left: 2px; }
.route-code-tab.visible { display: inline-flex; }
.route-code-tab.fly-target { display: inline-flex; opacity: 0; }
.route-code-fly-char { position: fixed; z-index: 10000; font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-weight: 700; pointer-events: none; transition: top 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), font-size 0.5s ease, color 0.3s ease; } #preview-title .platform-logo { height: 16px; margin-right: 6px; vertical-align: middle; filter: brightness(0.6); } @media (prefers-color-scheme: dark) { #preview-title { color: rgb(188, 168, 218); /* Lighter purple for dark mode */ } #preview-title .platform-logo { filter: brightness(1.2); } } [data-theme="dark"] #preview-title { color: rgb(188, 168, 218); /* Lighter purple for dark mode */ } [data-theme="dark"] #preview-title .platform-logo { filter: brightness(1.2); } #preview-title .bounce-dot { font-weight: 900; font-size: 1.5em; /* Larger dot for visibility */ text-shadow: 1px 0 0 currentColor; /* Faux bold/doubled up effect */ margin: 0 2px; color: rgb(205, 92, 155); /* AC Purple */ transition: color 0.3s ease, text-shadow 0.3s ease; display: inline-block; line-height: 0.6; /* Adjust vertical position */ vertical-align: middle; position: relative; top: -2px; /* Moved up */ left: -0.5px; /* Nudged left */ } /* Rainbow letter animation during boot */ #preview-title .title-letter { display: inline; transition: color 0.15s ease, text-shadow 0.15s ease; } #preview-title.booting .title-letter { animation: rainbowFlicker 0.1s ease-in-out infinite; } @keyframes rainbowFlicker { 0%, 100% { opacity: 1; } 50% { opacity: 0.85; } } /* State-based bounce-dot colors: running=green, paused=yellow, stopped=red */ #preview-title.state-running .bounce-dot { color: #90EE90; /* Light green matching play button */ text-shadow: 0 0 10px rgba(144, 238, 144, 0.9), 0 0 20px rgba(144, 238, 144, 0.6), 0 0 30px rgba(144, 238, 144, 0.3); } #preview-title.state-paused .bounce-dot { color: #ffd93d; /* Yellow matching pause */ text-shadow: 0 0 10px rgba(255, 217, 61, 0.9), 0 0 20px rgba(255, 217, 61, 0.6), 0 0 30px rgba(255, 217, 61, 0.3); } #preview-title.state-stopped .bounce-dot { color: #9370DB; /* Purple - matches stop button */ text-shadow: 0 0 10px rgba(147, 112, 219, 0.9), 0 0 20px rgba(147, 112, 219, 0.6), 0 0 30px rgba(147, 112, 219, 0.3); } /* Code display with KidLisp font to match the branding */ #preview-title .code-display, .route-code-tab .code-display { font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-weight: bold; letter-spacing: 0.05em; position: relative; top: -1.5px; left: 1px; padding-right: 4px; /* Extra room for font width variance */ } /* Code display states */ #preview-title .code-display.stale, .route-code-tab .code-display.stale { pointer-events: none; } /* Stale code chars blast off and fade - POOF! */ #preview-title .code-display.stale .code-char, .route-code-tab .code-display.stale .code-char { animation: charBlastOff 0.4s ease-out forwards; } /* Stagger the blast-off for each character */ #preview-title .code-display.stale .code-char:nth-child(1), .route-code-tab .code-display.stale .code-char:nth-child(1) { animation-delay: 0ms; } #preview-title .code-display.stale .code-char:nth-child(2), .route-code-tab .code-display.stale .code-char:nth-child(2) { animation-delay: 30ms; } #preview-title .code-display.stale .code-char:nth-child(3), .route-code-tab .code-display.stale .code-char:nth-child(3) { animation-delay: 50ms; } #preview-title .code-display.stale .code-char:nth-child(4), .route-code-tab .code-display.stale .code-char:nth-child(4) { animation-delay: 70ms; } #preview-title .code-display.stale .code-char:nth-child(5), .route-code-tab .code-display.stale .code-char:nth-child(5) { animation-delay: 90ms; } #preview-title .code-display.stale .code-char:nth-child(6), .route-code-tab .code-display.stale .code-char:nth-child(6) { animation-delay: 110ms; } #preview-title .code-display.stale .code-char:nth-child(7), .route-code-tab .code-display.stale .code-char:nth-child(7) { animation-delay: 130ms; } #preview-title .code-display.stale .code-char:nth-child(8), .route-code-tab .code-display.stale .code-char:nth-child(8) { animation-delay: 150ms; } @keyframes charBlastOff { 0% { opacity: 1; transform: translateY(0) scale(1) rotate(0deg); } 50% { opacity: 0.6; transform: translateY(-8px) scale(1.2) rotate(var(--blast-rotate, 10deg)); } 100% { opacity: 0; transform: translateY(-20px) scale(0.3) rotate(var(--blast-rotate, 15deg)); } } /* Randomize rotation direction per character */ #preview-title .code-display.stale .code-char:nth-child(odd), .route-code-tab .code-display.stale .code-char:nth-child(odd) { --blast-rotate: 15deg; } #preview-title .code-display.stale .code-char:nth-child(even), .route-code-tab .code-display.stale .code-char:nth-child(even) { --blast-rotate: -15deg; } #preview-title .code-display.stale .code-char:nth-child(3n), .route-code-tab .code-display.stale .code-char:nth-child(3n) { --blast-rotate: 25deg; } #preview-title .code-display.stale .code-char:nth-child(3n+1), .route-code-tab .code-display.stale .code-char:nth-child(3n+1) { --blast-rotate: -20deg; } #preview-title .code-display.generating .code-char, .route-code-tab .code-display.generating .code-char { animation: charScramble 0.1s ease-in-out infinite; } #preview-title .code-display .code-char, .route-code-tab .code-display .code-char { display: inline-block; } /* Color coding for $code: / is gray, $ is light green, code is darker green */ #preview-title .code-display .code-char.slash, .route-code-tab .code-display .code-char.slash { color: #888 !important; font-weight: normal !important; transition: color 0.05s ease, text-shadow 0.05s ease, transform 0.1s ease; } #preview-title .code-display .code-char.dollar, .route-code-tab .code-display .code-char.dollar { color: limegreen; font-weight: bold; transition: color 0.05s ease, text-shadow 0.05s ease, transform 0.1s ease; text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); /* Harsh shadow for light mode visibility */ } #preview-title .code-display .code-char.code, .route-code-tab .code-display .code-char.code { color: lime; transition: color 0.05s ease, text-shadow 0.05s ease, transform 0.1s ease; text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); /* Harsh shadow for light mode visibility */ } /* Bump/shake animation on state change */ @keyframes codeBump { 0% { transform: scale(1) rotate(0deg); } 25% { transform: scale(1.15) rotate(-3deg); } 50% { transform: scale(1.1) rotate(2deg); } 75% { transform: scale(1.05) rotate(-1deg); } 100% { transform: scale(1) rotate(0deg); } } #preview-title.state-changed .code-display .code-char, .route-code-tab.state-changed .code-display .code-char { animation: codeBump 0.2s ease-out; } /* Stagger the bump for each character */ #preview-title.state-changed .code-display .code-char:nth-child(1), .route-code-tab.state-changed .code-display .code-char:nth-child(1) { animation-delay: 0ms; } #preview-title.state-changed .code-display .code-char:nth-child(2), .route-code-tab.state-changed .code-display .code-char:nth-child(2) { animation-delay: 15ms; } #preview-title.state-changed .code-display .code-char:nth-child(3), .route-code-tab.state-changed .code-display .code-char:nth-child(3) { animation-delay: 30ms; } #preview-title.state-changed .code-display .code-char:nth-child(4), .route-code-tab.state-changed .code-display .code-char:nth-child(4) { animation-delay: 45ms; } #preview-title.state-changed .code-display .code-char:nth-child(5), .route-code-tab.state-changed .code-display .code-char:nth-child(5) { animation-delay: 60ms; } #preview-title.state-changed .code-display .code-char:nth-child(6), .route-code-tab.state-changed .code-display .code-char:nth-child(6) { animation-delay: 75ms; } #preview-title.state-changed .code-display .code-char:nth-child(7), .route-code-tab.state-changed .code-display .code-char:nth-child(7) { animation-delay: 90ms; } #preview-title.state-changed .code-display .code-char:nth-child(8), .route-code-tab.state-changed .code-display .code-char:nth-child(8) { animation-delay: 105ms; } /* Dark mode: remove the dark shadow, use glow instead */ @media (prefers-color-scheme: dark) { #preview-title .code-display .code-char.dollar, #preview-title .code-display .code-char.code, .route-code-tab .code-display .code-char.dollar, .route-code-tab .code-display .code-char.code { text-shadow: none; } } [data-theme="dark"] #preview-title .code-display .code-char.dollar, [data-theme="dark"] #preview-title .code-display .code-char.code, [data-theme="dark"] .route-code-tab .code-display .code-char.dollar, [data-theme="dark"] .route-code-tab .code-display .code-char.code { text-shadow: none; } [data-theme="light"] #preview-title .code-display .code-char.dollar, [data-theme="light"] #preview-title .code-display .code-char.code, [data-theme="light"] .route-code-tab .code-display .code-char.dollar, [data-theme="light"] .route-code-tab .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); } /* State-based code colors: running=green (default), paused=yellow, stopped=red */ /* Dark mode: glow effect only */ #preview-title.state-running .code-display .code-char.dollar, #preview-title.state-running .code-display .code-char.code { color: #90EE90; /* Light green */ text-shadow: 0 0 6px rgba(144, 238, 144, 0.5); } #preview-title.state-paused .code-display .code-char.dollar, #preview-title.state-paused .code-display .code-char.code { color: #ffd93d; /* Yellow */ text-shadow: 0 0 6px rgba(255, 217, 61, 0.5); } #preview-title.state-stopped .code-display .code-char.dollar, #preview-title.state-stopped .code-display .code-char.code { color: #9370DB; /* Purple - matches stop button */ text-shadow: 0 0 6px rgba(147, 112, 219, 0.5); } /* Light mode: harsh dark shadow + subtle glow */ @media (prefers-color-scheme: light) { #preview-title.state-running .code-display .code-char.dollar, #preview-title.state-running .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5), 0 0 6px rgba(144, 238, 144, 0.3); } #preview-title.state-paused .code-display .code-char.dollar, #preview-title.state-paused .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5), 0 0 6px rgba(255, 217, 61, 0.3); } #preview-title.state-stopped .code-display .code-char.dollar, #preview-title.state-stopped .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5), 0 0 6px rgba(147, 112, 219, 0.3); } } [data-theme="light"] #preview-title.state-running .code-display .code-char.dollar, [data-theme="light"] #preview-title.state-running .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5), 0 0 6px rgba(144, 238, 144, 0.3); } [data-theme="light"] #preview-title.state-paused .code-display .code-char.dollar, [data-theme="light"] #preview-title.state-paused .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5), 0 0 6px rgba(255, 217, 61, 0.3); } [data-theme="light"] #preview-title.state-stopped .code-display .code-char.dollar, [data-theme="light"] #preview-title.state-stopped .code-display .code-char.code { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5), 0 0 6px rgba(147, 112, 219, 0.3); } /* Stagger animation delay for each character */ #preview-title .code-display .code-char:nth-child(1) { animation-delay: 0ms; } #preview-title .code-display .code-char:nth-child(2) { animation-delay: 30ms; } #preview-title .code-display .code-char:nth-child(3) { animation-delay: 60ms; } #preview-title .code-display .code-char:nth-child(4) { animation-delay: 90ms; } #preview-title .code-display .code-char:nth-child(5) { animation-delay: 120ms; } #preview-title .code-display .code-char:nth-child(6) { animation-delay: 150ms; } #preview-title .code-display .code-char:nth-child(7) { animation-delay: 180ms; } #preview-title .code-display .code-char:nth-child(8) { animation-delay: 210ms; } @keyframes charScramble { 0%, 100% { opacity: 1; transform: translateY(0); } 50% { opacity: 0.5; transform: translateY(-1px); } } /* Code generating animation - scramble effect */ @keyframes scramble { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } #preview-title .code-display.generating { animation: scramble 0.3s ease-in-out 3; } /* Bounce dot during code generation */ #preview-title .bounce-dot.generating { animation: bounce 0.6s ease-in-out infinite; }
.baby-colors span:nth-child(1) { animation-delay: 0s; } .baby-colors span:nth-child(2) { animation-delay: -1s; } .baby-colors span:nth-child(3) { animation-delay: -2s; } .baby-colors span:nth-child(4) { animation-delay: -3s; } .baby-colors span:nth-child(5) { animation-delay: -4s; } .baby-colors span:nth-child(6) { animation-delay: -5s; } .baby-colors span:nth-child(7) { animation-delay: -6s; }
.reference-section, .console-section { /* flex: 1; Removed for Split.js */ display: flex; flex-direction: column; overflow: hidden; }
.reference-section { /* border-right: 2px solid var(--border-color); Removed for Split.js */ }
.console-output { flex: 1; overflow-y: auto; padding: 2px; font-size: 14px; font-family: 'Noto Sans Mono', 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'Courier New', monospace; line-height: 1.4; background: #f5ebe0; /* Light beige background (default for light mode) */ color: #c87137; /* Darker orange text for better contrast */ }
@media (prefers-color-scheme: dark) { .console-output { background: #4a3020; color: #ff9f43; } }
[data-theme="light"] .console-output { background: #f5ebe0; /* Much lighter beige for light mode */ color: #c87137; /* Darker orange text for better contrast */ }
[data-theme="dark"] .console-output { background: #4a3020; color: #ff9f43; }
.console-entry { padding: 4px 8px; border-bottom: 1px solid var(--border-subtle); line-height: 1.5; word-wrap: break-word; overflow-wrap: break-word; } /* Console timestamp - semi-opaque */ .console-timestamp { opacity: 0.4; font-size: 11px; margin-right: 4px; user-select: none; } /* Only use flex for entries with line numbers */ .console-entry:has(.console-line) { display: flex; align-items: flex-start; }
.console-entry:nth-child(even) { background-color: rgba(255, 159, 67, 0.05); /* Subtle orange zebra striping */ }
.console-line { color: var(--text-tertiary); margin-right: 8px; font-size: 11px; min-width: 24px; /* Fixed width for alignment */ text-align: right; user-select: none; }
.console-entry:last-child { border-bottom: none; }
/* Console log colors (bright on brown) */ .console-entry.error { color: #ff6b6b; } .console-entry.warn { color: #ffd93d; } .console-entry.info { color: #6bcfff; } .console-entry.state { color: #9370db; opacity: 0.8; } .console-entry.code { color: #c9b896; font-size: 12px; } /* State change styles */ .console-entry.state-play { color: #689068; } /* Dark muted green */ .console-entry.state-pause { color: #a0983a; } /* Dark muted yellow */ .console-entry.state-stop { color: #a07038; } /* Dark muted orange */ .console-entry.state-load { color: #6090a0; } /* Dark muted blue */ .console-entry.state-run { color: #689068; } /* Dark muted green */ /* Console code display - matches Monaco editor styling */ .console-label { color: #9080a0; font-weight: 500; } .console-code { font-family: 'Berkeley Mono', 'Fira Code', 'SF Mono', monospace; background: #2a2520; /* Dark mode: matches editor.background */ padding: 2px 6px; border-radius: 3px; color: #d4d4d4; /* Dark mode: matches editor.foreground */ } /* Syntax highlighting in console - dark mode (default) */ /* Uses same colors as Monaco decorations: yellow, cyan, lime, pink, orange */ .hl-comment { color: gray; font-style: italic; } .hl-string { color: orange; } .hl-number { color: lime; } .hl-keyword { color: pink; } .hl-api { color: cyan; } .hl-color { color: magenta; } .hl-timing { color: yellow; font-weight: bold; } .hl-hashcode { color: orange; font-weight: bold; } .hl-dollarcode { color: limegreen; font-weight: bold; } .hl-paren { color: silver; } .hl-operator { color: pink; } .hl-symbol { color: skyblue; }
/* Light mode console code styling */ [data-theme="light"] .console-code { background: #fffacd; /* Legal pad yellow - matches light editor */ color: #333333; } [data-theme="light"] .console-label { color: #666688; } /* Light mode uses high-contrast colors matching Monaco decorator lightModeColorMap */ [data-theme="light"] .hl-comment { color: #666666; } [data-theme="light"] .hl-string { color: #cc6600; } [data-theme="light"] .hl-number { color: #00aa00; } [data-theme="light"] .hl-keyword { color: #cc0066; } [data-theme="light"] .hl-api { color: #0099cc; } [data-theme="light"] .hl-color { color: #cc00cc; } [data-theme="light"] .hl-timing { color: #cc9900; } [data-theme="light"] .hl-hashcode { color: #cc6600; } [data-theme="light"] .hl-dollarcode { color: #00aa00; } /* Error decorations in Monaco editor */ .kidlisp-error-word { text-decoration: underline wavy #ff6b6b; text-decoration-skip-ink: none; background-color: rgba(255, 107, 107, 0.15); } .kidlisp-error-glyph { background-color: #ff6b6b; border-radius: 50%; margin-left: 3px; } /* Welcome message styling */ .console-entry.welcome-message { color: #a0a090; /* Muted warm gray */ padding: 8px 8px; border-bottom: 1px solid rgba(160, 160, 140, 0.15); } .welcome-title { font-weight: 600; color: #889078; /* Dark sage */ } .console-entry.hint-message { color: #908898; /* Muted lavender-gray */ font-size: 11px; padding: 6px 8px; opacity: 0.9; } .hint-text { color: #908898; /* Muted lavender-gray */ } .inline-btn-icon { display: inline-block; vertical-align: middle; margin: 0 3px; }
/* Dark mode keeps same colors */ @media (prefers-color-scheme: dark) { .console-entry.error { color: #ff6b6b; } .console-entry.warn { color: #ffd93d; } .console-entry.info { color: #6bcfff; } }
/* Tab-specific header colors */ .editor-header { background: #fffacd; /* Legal pad yellow background for KidLisp.com tab */ border-bottom-color: #f0e68c; } @media (prefers-color-scheme: dark) { .editor-header { background: #3a3530; /* Dark brown for dark mode */ border-bottom-color: #2a2520; } } [data-theme="dark"] .editor-header { background: #3a3530; /* Dark brown for dark mode */ border-bottom-color: #2a2520; } [data-theme="light"] .editor-header { background: #fffacd; /* Legal pad yellow background for light mode */ border-bottom-color: #f0e68c; }
#console-panel .docs-header { background: #3d2817; /* Darker brown background for Console tab bar */ border-bottom: none; /* No border gap between header and console */ } @media (prefers-color-scheme: dark) { #console-panel .docs-header { background: #3d2817; /* Same in dark mode - already dark */ border-bottom-color: #2a1a0f; } } [data-theme="light"] #console-panel .docs-header { background: #e8dcc8; /* Light tan for light mode */ border-bottom-color: #d4c4a8; } [data-theme="dark"] #console-panel .docs-header { background: #3d2817; /* Darker brown in dark mode */ border-bottom-color: #2a1a0f; }
#console-title { color: #ff9f43; /* Orange text for Console title */ } @media (prefers-color-scheme: dark) { #console-title { color: #ff9f43; /* Same in dark mode - already bright */ } } [data-theme="light"] #console-title { color: #b85c2a; /* Darker orange for light mode contrast */ } [data-theme="dark"] #console-title { color: #ff9f43; /* Bright orange in dark mode */ } } [data-theme="dark"] #console-title { color: #ff9f43; /* Same in dark mode - already bright */ }
.clear-console { padding: 2px 8px; background: transparent; color: var(--text-tertiary); border: 1px solid var(--border-color); border-radius: 4px; font-family: monospace; font-size: 10px; cursor: pointer; margin-left: auto; }
@media (hover: hover) { .clear-console:hover { background: var(--bg-tertiary); color: var(--text-primary); } }
.docs-header { padding: 0 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); display: flex; align-items: center; font-size: 13px; font-weight: bold; color: var(--text-primary); height: 44px; box-sizing: border-box; overflow: visible; /* Allow dropdowns to overflow */ flex-shrink: 0; /* Prevent header from shrinking */ position: relative; /* For z-index context */ }
/* Remove padding when header contains tabs */ .docs-header:has(.tab) { padding: 0; }
.tab { padding: 0 18px; cursor: pointer; border-right: 1px solid var(--border-color); border-left: 1px solid transparent; height: 100%; display: flex; align-items: center; justify-content: center; background: var(--bg-secondary); color: var(--text-tertiary); transition: all 0.2s; white-space: nowrap; flex: 0 0 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; } /* Responsive tab labels */ .tab-short { display: none; } .tab-full { display: inline; } /* Reference panel - enable container queries */ #reference-panel { container-type: inline-size; container-name: refpanel; } /* Reference panel tabs container - fill width */ #reference-panel .docs-header { display: flex; width: 100%; } /* Match reference panel header to console header colors */ [data-theme="light"] #reference-panel .docs-header { background: #e8dcc8; border-bottom-color: #d4c4a8; } [data-theme="dark"] #reference-panel .docs-header { background: #3d2817; border-bottom-color: #2a1a0f; } /* Progressive responsive using container queries (based on panel width, not viewport) */ @container refpanel (max-width: 600px) { .tab { padding: 0 8px; font-size: 12px; } } @container refpanel (max-width: 500px) { .tab { padding: 0 6px; font-size: 11px; } .tab-short { display: inline; } .tab-full { display: none; } } @container refpanel (max-width: 400px) { .tab { padding: 0 4px; font-size: 10px; } } @container refpanel (max-width: 300px) { .tab { padding: 0 2px; font-size: 9px; } }
@media (hover: hover) { .tab:hover { filter: brightness(1.05); } #guide-tab:hover { background: rgba(34, 197, 94, 0.35); } #examples-tab:hover { background: rgba(250, 204, 21, 0.45); } #recent-tab:hover { background: rgba(249, 115, 22, 0.35); } #history-tab:hover { background: rgba(180, 130, 90, 0.35); } #keeps-tab:hover { background: rgba(120, 120, 120, 0.3); } #jukebox-tab:hover { background: rgba(59, 130, 246, 0.35); } #keep-tab:hover { background: rgba(245, 158, 11, 0.4); } #reference-tab:hover { background: rgba(34, 197, 94, 0.35); } }
.tab.active { color: var(--text-primary); border-bottom: none; position: relative; z-index: 1; font-weight: 600; }
/* Color-coded tabs */ /* Guides - Green */ #guide-tab { background: rgba(34, 197, 94, 0.2); color: #15803d; } #guide-tab.active { background: rgba(34, 197, 94, 0.5); color: #14532d; } /* Top Hits - Yellow */ #examples-tab { background: rgba(250, 204, 21, 0.25); color: #a16207; } #examples-tab.active { background: rgba(250, 204, 21, 0.55); color: #854d0e; } /* Recent - Orange */ #recent-tab { background: rgba(249, 115, 22, 0.2); color: #c2410c; } #recent-tab.active { background: rgba(249, 115, 22, 0.5); color: #9a3412; } /* History - Brown */ #history-tab { background: rgba(180, 130, 90, 0.2); color: #78350f; } #history-tab.active { background: rgba(180, 130, 90, 0.5); color: #451a03; } /* Keeps - Gray */ #keeps-tab { background: rgba(120, 120, 120, 0.15); color: #525252; } #keeps-tab.active { background: rgba(120, 120, 120, 0.35); color: #262626; } /* Jukebox - Blue */ #jukebox-tab { background: rgba(59, 130, 246, 0.2); color: #1d4ed8; } #jukebox-tab.active { background: rgba(59, 130, 246, 0.5); color: #1e40af; }
/* Cards/Reference - keep existing green */ #reference-tab { background: rgba(34, 197, 94, 0.2); color: #166534; } #reference-tab.active { background: rgba(34, 197, 94, 0.55); color: #14532d; }
/* Keep - Amber/Gold (Tezos) */ #keep-tab { background: rgba(245, 158, 11, 0.22); color: #b45309; } #keep-tab.active { background: rgba(245, 158, 11, 0.55); color: #7c2d12; } :root.dark #keep-tab { background: rgba(245, 158, 11, 0.3); color: #fde68a; } :root.dark #keep-tab.active { background: rgba(245, 158, 11, 0.55); color: #fffbeb; }
/* Dark mode adjustments for tab colors - brighter text for contrast */ :root.dark #guide-tab { background: rgba(34, 197, 94, 0.4); color: #86efac; } :root.dark #guide-tab.active { background: rgba(34, 197, 94, 0.6); color: #bbf7d0; } :root.dark #examples-tab { background: rgba(250, 204, 21, 0.3); color: #fef3c7; } :root.dark #examples-tab.active { background: rgba(250, 204, 21, 0.5); color: #fffbeb; } :root.dark #recent-tab { background: rgba(249, 115, 22, 0.3); color: #fed7aa; } :root.dark #recent-tab.active { background: rgba(249, 115, 22, 0.5); color: #ffedd5; } :root.dark #history-tab { background: rgba(180, 130, 90, 0.3); color: #f5e6d3; } :root.dark #history-tab.active { background: rgba(180, 130, 90, 0.5); color: #fdf4eb; } :root.dark #keeps-tab { background: rgba(150, 150, 150, 0.25); color: #d4d4d4; } :root.dark #keeps-tab.active { background: rgba(150, 150, 150, 0.4); color: #f5f5f5; } :root.dark #jukebox-tab { background: rgba(59, 130, 246, 0.3); color: #93c5fd; } :root.dark #jukebox-tab.active { background: rgba(59, 130, 246, 0.5); color: #dbeafe; } :root.dark #reference-tab { background: rgba(34, 197, 94, 0.35); color: #a7f3d0; } :root.dark #reference-tab.active { background: rgba(34, 197, 94, 0.55); color: #ffffff; } /* Dark mode tab content backgrounds - gradient fade from colored top to neutral */ :root.dark #guide-content:not(.hidden) { background: linear-gradient(to bottom, rgba(34, 197, 94, 0.4) 0%, rgba(34, 197, 94, 0.1) 40%, transparent 100%); } :root.dark #examples-content:not(.hidden) { background: linear-gradient(to bottom, rgba(250, 204, 21, 0.4) 0%, rgba(250, 204, 21, 0.1) 40%, transparent 100%); } :root.dark #recent-content:not(.hidden) { background: linear-gradient(to bottom, rgba(249, 115, 22, 0.4) 0%, rgba(249, 115, 22, 0.1) 40%, transparent 100%); } :root.dark #history-content:not(.hidden) { background: linear-gradient(to bottom, rgba(180, 130, 90, 0.4) 0%, rgba(180, 130, 90, 0.1) 40%, transparent 100%); } :root.dark #keeps-content:not(.hidden) { background: linear-gradient(to bottom, rgba(150, 150, 150, 0.35) 0%, rgba(150, 150, 150, 0.1) 40%, transparent 100%); } :root.dark #jukebox-content:not(.hidden) { background: linear-gradient(to bottom, rgba(236, 72, 153, 0.4) 0%, rgba(236, 72, 153, 0.1) 40%, transparent 100%); } :root.dark #jockey-content:not(.hidden) { background: linear-gradient(to bottom, rgba(205, 92, 155, 0.35) 0%, rgba(205, 92, 155, 0.1) 40%, transparent 100%); } :root.dark #reference-content:not(.hidden) { background: linear-gradient(to bottom, rgba(34, 197, 94, 0.45) 0%, rgba(34, 197, 94, 0.1) 40%, transparent 100%); }
/* List Tab - Yellow (like old Top Hits) */ #list-tab { background: rgba(250, 204, 21, 0.35); color: #a16207; } #list-tab.active { background: rgba(250, 204, 21, 0.65); color: #713f12; } [data-theme="dark"] #list-tab { background: rgba(253, 224, 71, 0.55); color: #fef9c3; } [data-theme="dark"] #list-tab.active { background: rgba(253, 224, 71, 0.8); color: #422006; } @media (hover: hover) { #list-tab:hover { background: rgba(253, 224, 71, 0.6); } }
/* List sub-tabs bar */ .list-subtabs { display: flex; justify-content: flex-start; width: 100%; border-bottom: none; background: rgba(250, 204, 21, 0.65); height: 44px; } [data-theme="dark"] .list-subtabs { background: rgba(253, 224, 71, 0.8); } .list-subtab { width: 94px; min-width: 94px; box-sizing: border-box; padding: 0; font-size: 12px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; border-right: 1px solid var(--border-color); border-left: 1px solid transparent; transition: all 0.2s; } .list-subtab.active { font-weight: 600; } /* Sub-tab colors - Top Hits (Blue) */ .list-subtab[data-subtab="examples"] { background: rgba(250, 204, 21, 0.65); color: #78716c; } .list-subtab[data-subtab="examples"].active { background: linear-gradient(to bottom, #93c5fd 0%, #bfdbfe 100%); color: #1e3a8a; } /* Sub-tab colors - Recent (Orange) */ .list-subtab[data-subtab="recent"] { background: rgba(250, 204, 21, 0.65); color: #78716c; } .list-subtab[data-subtab="recent"].active { background: linear-gradient(to bottom, #fdba74 0%, #fed7aa 100%); color: #7c2d12; } /* Sub-tab colors - History (Brown) */ .list-subtab[data-subtab="history"] { background: rgba(250, 204, 21, 0.65); color: #78716c; } .list-subtab[data-subtab="history"].active { background: linear-gradient(to bottom, #d4a574 0%, #e8d4bc 100%); color: #5c3d1e; } /* Dark mode sub-tabs */ [data-theme="dark"] .list-subtab[data-subtab="examples"] { background: rgba(253, 224, 71, 0.8); color: #57534e; } [data-theme="dark"] .list-subtab[data-subtab="examples"].active { background: linear-gradient(to bottom, #3b82f6 0%, #1e40af 100%); color: #7fffff; } [data-theme="dark"] .list-subtab[data-subtab="recent"] { background: rgba(253, 224, 71, 0.8); color: #57534e; } [data-theme="dark"] .list-subtab[data-subtab="recent"].active { background: linear-gradient(to bottom, #ea580c 0%, #9a3412 100%); color: #ffedd5; } [data-theme="dark"] .list-subtab[data-subtab="history"] { background: rgba(253, 224, 71, 0.8); color: #57534e; } [data-theme="dark"] .list-subtab[data-subtab="history"].active { background: linear-gradient(to bottom, #a3724a 0%, #6b4423 100%); color: #e8d4bc; } /* Slideshow icon button in subtabs bar */ .slideshow-icon-btn { margin-left: auto; margin-right: 8px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-size: 14px; background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: white; border: none; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3); align-self: center; } .slideshow-icon-btn:hover { transform: scale(1.08); box-shadow: 0 3px 6px rgba(79, 70, 229, 0.4); background: linear-gradient(135deg, #5b52f0 0%, #8b49f6 100%); } .slideshow-icon-btn:active { transform: scale(0.95); box-shadow: 0 1px 2px rgba(79, 70, 229, 0.3); }
/* Play on TV card (first item in Top Hits) */ .play-on-tv-card { padding: 12px 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); cursor: pointer; display: flex; align-items: center; gap: 12px; background: linear-gradient(135deg, rgba(24, 24, 28, 0.95) 0%, rgba(40, 40, 46, 0.9) 100%); transition: all 0.2s ease; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 6px 16px rgba(0, 0, 0, 0.25); } .play-on-tv-card:hover { background: linear-gradient(135deg, rgba(36, 36, 42, 0.98) 0%, rgba(52, 52, 60, 0.95) 100%); } .play-on-tv-icon { width: 48px; height: 48px; border-radius: 10px; background: linear-gradient(135deg, #2b2b33 0%, #3a3a45 100%); display: flex; align-items: center; justify-content: center; color: white; flex-shrink: 0; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 6px 12px rgba(0, 0, 0, 0.35); position: relative; overflow: hidden; }
.play-on-tv-stack { position: relative; width: 30px; height: 22px; }
.play-on-tv-slide { position: absolute; inset: 0; border-radius: 5px; border: 1px solid rgba(255, 255, 255, 0.18); background: linear-gradient(90deg, rgba(255, 255, 255, 0.65) 0%, rgba(255, 255, 255, 0.1) 100%); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); }
.play-on-tv-slide.back { transform: translate(-4px, -4px); opacity: 0.4; border-color: rgba(255, 255, 255, 0.12); }
.play-on-tv-slide.mid { transform: translate(-2px, -2px); opacity: 0.65; }
.play-on-tv-slide.front { opacity: 0.9; } .play-on-tv-content { flex: 1; min-width: 0; } .play-on-tv-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 2px; } .play-on-tv-subtitle { font-size: 11px; color: var(--text-secondary); opacity: 0.8; }
.play-on-tv-highlight { color: #3b82f6; font-weight: 600; }
.play-on-tv-arrow { font-size: 16px; color: var(--text-secondary); opacity: 0.6; }
/* Light theme overrides for Play on TV card */ [data-theme="light"] .play-on-tv-card { background: linear-gradient(135deg, #f0f0f4 0%, #e4e4ea 100%); border-bottom-color: rgba(0, 0, 0, 0.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6), 0 4px 12px rgba(0, 0, 0, 0.08); } [data-theme="light"] .play-on-tv-card:hover { background: linear-gradient(135deg, #e8e8ee 0%, #dcdce4 100%); } [data-theme="light"] .play-on-tv-icon { background: linear-gradient(135deg, #d8d8e0 0%, #c8c8d2 100%); color: #333; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 4px 8px rgba(0, 0, 0, 0.1); } [data-theme="light"] .play-on-tv-slide { border-color: rgba(0, 0, 0, 0.15); background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.08) 100%); } [data-theme="light"] .play-on-tv-title { color: #111; } [data-theme="light"] .play-on-tv-subtitle { color: #555; } [data-theme="light"] .play-on-tv-arrow { color: #666; }
/* Jukebox Panel Styles */ #jukebox-content { padding: 0; overflow-y: auto; display: flex; flex-direction: column; } /* Songs subcontent: Spotify-style (lime green #1DB954 on dark) */ #songs-subcontent { --jukebox-accent: #1DB954; --jukebox-accent-hover: #1ed760; --jukebox-accent-dim: rgba(29, 185, 84, 0.15); --jukebox-accent-mid: rgba(29, 185, 84, 0.3); --jukebox-bg: #121212; --jukebox-bg-elevated: #181818; --jukebox-text: #fff; --jukebox-text-dim: #b3b3b3; background: var(--jukebox-bg); color: var(--jukebox-text); } /* Strudel subcontent: dark with gold accent */ #strudel-subcontent { --jukebox-accent: #ffcc00; --jukebox-accent-hover: #ffd633; --jukebox-accent-dim: rgba(255, 204, 0, 0.15); --jukebox-accent-mid: rgba(255, 204, 0, 0.3); --jukebox-bg: #222; --jukebox-bg-elevated: #2a2a2a; --jukebox-text: #fff; --jukebox-text-dim: #8a919966; background: var(--jukebox-bg); color: var(--jukebox-text); } /* Jukebox fixed controls area (used by Strudel) */ .jukebox-controls-area { flex-shrink: 0; padding: 12px; display: flex; flex-direction: column; gap: 10px; border-bottom: 1px solid var(--jukebox-accent-dim, rgba(236, 72, 153, 0.2)); }
/* Jukebox scrollable content area (tracklist) */ .jukebox-scroll-area { flex: 1; overflow-y: auto; padding: 6px 8px; min-height: 0; }
/* Mini Player bar at bottom */ .jukebox-mini-player { flex-shrink: 0; border-top: 1px solid var(--jukebox-accent-dim, rgba(29, 185, 84, 0.2)); background: var(--jukebox-bg-elevated, #181818); padding: 6px 10px; display: flex; flex-direction: column; gap: 4px; } .jukebox-mini-player-top { display: flex; align-items: center; gap: 8px; } .jukebox-mini-art { width: 32px; height: 32px; border-radius: 3px; object-fit: cover; flex-shrink: 0; } .jukebox-track-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 600; } .jukebox-mini-transport { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } .jukebox-mini-player-bottom { display: flex; align-items: center; } .jukebox-progress { display: flex; align-items: center; gap: 6px; flex: 1; } .jukebox-progress input[type="range"] { flex: 1; height: 4px; -webkit-appearance: none; background: var(--jukebox-accent-mid, rgba(29, 185, 84, 0.3)); border-radius: 2px; } .jukebox-progress input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 10px; height: 10px; background: var(--jukebox-accent, #1DB954); border-radius: 50%; cursor: pointer; } .jukebox-time { font-size: 10px; color: var(--jukebox-text-dim, #b3b3b3); min-width: 70px; text-align: right; } .jukebox-volume { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } .jukebox-volume input[type="range"] { width: 50px; height: 3px; -webkit-appearance: none; background: var(--jukebox-accent-mid, rgba(29, 185, 84, 0.3)); border-radius: 2px; } .jukebox-volume input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 10px; height: 10px; background: var(--jukebox-accent, #1DB954); border-radius: 50%; cursor: pointer; } .jukebox-beat-indicator { width: 12px; height: 12px; border-radius: 50%; background: #333; transition: background 0.05s, box-shadow 0.05s; flex-shrink: 0; } .jukebox-beat-indicator.active { background: var(--jukebox-accent, #1DB954); box-shadow: 0 0 8px var(--jukebox-accent, #1DB954); } .jukebox-meters { display: flex; flex-direction: column; gap: 6px; padding: 8px; background: rgba(0, 0, 0, 0.2); border-radius: 6px; } .jukebox-meter-row { display: flex; align-items: center; gap: 8px; } .jukebox-meter-label { width: 32px; font-size: 11px; font-weight: 600; color: var(--jukebox-accent, #1DB954); } .jukebox-meter-bar { flex: 1; height: 8px; background: var(--jukebox-accent-dim, rgba(29, 185, 84, 0.2)); border-radius: 4px; overflow: hidden; } .jukebox-meter-fill { height: 100%; width: 0%; background: var(--jukebox-accent, #1DB954); border-radius: 4px; transition: width 0.05s; } .jukebox-meter-value { width: 32px; font-size: 11px; text-align: right; font-family: monospace; color: var(--jukebox-text-dim, #b3b3b3); } .jukebox-tracklist { display: flex; flex-direction: column; gap: 2px; } .jukebox-track { padding: 6px 10px; background: var(--jukebox-bg-elevated, #181818); border-radius: 3px; cursor: pointer; transition: background 0.15s; font-size: 12px; color: var(--jukebox-text, #fff); display: flex; flex-direction: row; align-items: center; gap: 8px; } .jukebox-track:hover { background: var(--jukebox-accent-dim, rgba(29, 185, 84, 0.15)); } .jukebox-track-title { font-weight: 500; } .jukebox-track-artist { font-size: 11px; color: var(--jukebox-text-dim, #b3b3b3); margin-left: auto; } .jukebox-track.playing { background: var(--jukebox-accent-mid, rgba(29, 185, 84, 0.3)); color: var(--jukebox-accent, #1DB954); font-weight: 600; } .jukebox-track.playing::before { content: ''; } .jukebox-status { font-size: 11px; color: var(--jukebox-text-dim, #b3b3b3); text-align: center; min-height: 16px; padding: 8px; } /* Strudel Editor - matches strudel.cc theme */ .strudel-editor-container { flex: 1; min-height: 100px; display: flex; flex-direction: column; } #strudel-code { width: 100%; flex: 1; min-height: 100px; padding: 12px; border: 1px solid rgba(255, 204, 0, 0.3); border-radius: 8px; background: #22222299; color: #fff; font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; font-size: 13px; line-height: 1.6; resize: none; box-sizing: border-box; caret-color: #ffcc00; } #strudel-code:focus { outline: none; border-color: #ffcc00; box-shadow: 0 0 0 2px rgba(255, 204, 0, 0.25); } #strudel-code::placeholder { color: rgba(255, 204, 0, 0.4); } #strudel-code::selection { background: rgba(128, 203, 196, 0.5); } /* Strudel Presets - gold accents */ .strudel-presets { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } .strudel-preset { padding: 5px 12px; border: 1px solid rgba(255, 204, 0, 0.4); border-radius: 14px; background: transparent; color: #ffcc00; font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; } .strudel-preset:hover { background: rgba(255, 204, 0, 0.15); border-color: #ffcc00; transform: scale(1.05); } @media (hover: hover) { .list-subtab:hover { filter: brightness(1.1); } } /* Jukebox sub-tabs bar (similar to list subtabs) */ .jukebox-subtabs { display: flex; justify-content: flex-start; width: 100%; border-bottom: none; background: rgba(59, 130, 246, 0.65); height: 44px; } [data-theme="dark"] .jukebox-subtabs { background: rgba(59, 130, 246, 0.8); } .jukebox-subtab { width: 94px; min-width: 94px; box-sizing: border-box; padding: 0; font-size: 12px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; border-right: 1px solid var(--border-color); border-left: 1px solid transparent; border: none; transition: all 0.2s; background: transparent; } .jukebox-subtab.active { font-weight: 600; } /* Sub-tab colors - Songs (Spotify Green) */ .jukebox-subtab[data-subtab="songs"] { background: rgba(59, 130, 246, 0.65); color: #1e3a5f; } .jukebox-subtab[data-subtab="songs"].active { background: linear-gradient(to bottom, #1DB954 0%, #169c46 100%); color: #fff; } /* Sub-tab colors - Strudel (Gold) */ .jukebox-subtab[data-subtab="strudel"] { background: rgba(59, 130, 246, 0.65); color: #1e3a5f; } .jukebox-subtab[data-subtab="strudel"].active { background: linear-gradient(to bottom, #ffcc00 0%, #e6b800 100%); color: #1a1a00; } /* Dark mode jukebox sub-tabs */ [data-theme="dark"] .jukebox-subtab[data-subtab="songs"] { background: rgba(59, 130, 246, 0.8); color: #93c5fd; } [data-theme="dark"] .jukebox-subtab[data-subtab="songs"].active { background: linear-gradient(to bottom, #1DB954 0%, #15803d 100%); color: #fff; } [data-theme="dark"] .jukebox-subtab[data-subtab="strudel"] { background: rgba(59, 130, 246, 0.8); color: #93c5fd; } [data-theme="dark"] .jukebox-subtab[data-subtab="strudel"].active { background: linear-gradient(to bottom, #ffcc00 0%, #cc9900 100%); color: #1a1a00; }
/* Sub-tab colors - Aux (Coral/Red for live mic) */ .jukebox-subtab[data-subtab="aux"] { background: rgba(59, 130, 246, 0.65); color: #1e3a5f; } .jukebox-subtab[data-subtab="aux"].active { background: linear-gradient(to bottom, #e85d4a 0%, #c94232 100%); color: #fff; } [data-theme="dark"] .jukebox-subtab[data-subtab="aux"] { background: rgba(59, 130, 246, 0.8); color: #93c5fd; } [data-theme="dark"] .jukebox-subtab[data-subtab="aux"].active { background: linear-gradient(to bottom, #e85d4a 0%, #b33a2a 100%); color: #fff; }
.aux-btn { background: none; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; padding: 4px 8px; font-size: 14px; color: var(--text-primary); transition: all 0.15s; } .aux-btn:hover:not(:disabled) { background: rgba(232, 93, 74, 0.15); } .aux-btn:disabled { opacity: 0.4; cursor: default; }
.aux-meter-fill { background: linear-gradient(90deg, #e85d4a, #ff7b6b) !important; }
.aux-advanced-meters { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 10px; margin-top: 2px; }
.aux-graph-wrap { margin-top: 6px; border: 1px solid var(--border-color); border-radius: 6px; background: linear-gradient(to bottom, rgba(232, 93, 74, 0.1), rgba(0, 0, 0, 0.08)); padding: 6px; }
.aux-live-graph { width: 100%; height: 92px; display: block; border-radius: 4px; background: rgba(0, 0, 0, 0.2); }
.aux-graph-legend { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px 10px; font-size: 10px; color: var(--jukebox-text-dim); }
.aux-legend-item { display: inline-flex; align-items: center; gap: 5px; letter-spacing: 0.02em; }
.aux-legend-dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; flex-shrink: 0; box-shadow: 0 0 4px rgba(255, 255, 255, 0.2); }
@media (max-width: 560px) { .aux-advanced-meters { grid-template-columns: 1fr; } .aux-live-graph { height: 78px; } }
@media (hover: hover) { .jukebox-subtab:hover { filter: brightness(1.1); } } .jukebox-subcontent { flex: 1; overflow: auto; width: 100%; min-width: 0; min-height: 0; display: flex; flex-direction: column; } .jukebox-subcontent.hidden { display: none; } /* Compact transport controls for mini player */ .spotify-btn { border: none; background: transparent; cursor: pointer; transition: transform 0.1s, opacity 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; }
.spotify-btn:hover:not(:disabled) { transform: scale(1.1); }
.spotify-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.spotify-btn svg { fill: currentColor; }
.spotify-btn-prev, .spotify-btn-next { width: 24px; height: 24px; color: var(--jukebox-text, #fff); }
.spotify-btn-play { width: 32px; height: 32px; border-radius: 50%; background: var(--jukebox-accent, #1DB954) !important; color: #000; }
.spotify-btn-play:hover:not(:disabled) { background: var(--jukebox-accent-hover, #1ed760) !important; } /* Strudel play controls - compact inline style */ .strudel-transport { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.strudel-btn { width: 36px; height: 36px; border: 2px solid var(--jukebox-accent, #ffcc00); border-radius: 6px; background: transparent; color: var(--jukebox-accent, #ffcc00); cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; font-size: 14px; } .strudel-btn:hover:not(:disabled) { background: var(--jukebox-accent-dim, rgba(255, 204, 0, 0.2)); transform: scale(1.05); } .strudel-btn:disabled { opacity: 0.4; cursor: not-allowed; } .strudel-btn.playing { background: var(--jukebox-accent, #ffcc00); color: #000; animation: strudel-pulse 0.5s ease-in-out infinite alternate; } @keyframes strudel-pulse { from { box-shadow: 0 0 5px var(--jukebox-accent); } to { box-shadow: 0 0 15px var(--jukebox-accent); } } .list-subcontent { flex: 1; overflow: auto; width: 100%; min-width: 0; min-height: 0; } .list-subcontent.hidden { display: none; } #list-content { display: flex; flex-direction: column; align-items: stretch; background: rgba(250, 204, 21, 0.12); } [data-theme="dark"] #list-content { background: rgba(250, 204, 21, 0.08); } /* Per-subcontent gradients — yellow base tint with colored top gradient */ #examples-subcontent:not(.hidden) { background: linear-gradient(to bottom, rgba(59, 130, 246, 0.3) 0%, rgba(59, 130, 246, 0.1) 40%, rgba(250, 204, 21, 0.08) 100%); } [data-theme="dark"] #examples-subcontent:not(.hidden) { background: linear-gradient(to bottom, rgba(59, 130, 246, 0.35) 0%, rgba(59, 130, 246, 0.1) 40%, rgba(250, 204, 21, 0.05) 100%); }
#recent-subcontent:not(.hidden) { background: linear-gradient(to bottom, rgba(249, 115, 22, 0.3) 0%, rgba(249, 115, 22, 0.1) 40%, rgba(250, 204, 21, 0.08) 100%); } [data-theme="dark"] #recent-subcontent:not(.hidden) { background: linear-gradient(to bottom, rgba(249, 115, 22, 0.35) 0%, rgba(249, 115, 22, 0.1) 40%, rgba(250, 204, 21, 0.05) 100%); }
#history-subcontent:not(.hidden) { background: linear-gradient(to bottom, rgba(180, 130, 90, 0.3) 0%, rgba(180, 130, 90, 0.1) 40%, rgba(250, 204, 21, 0.08) 100%); } [data-theme="dark"] #history-subcontent:not(.hidden) { background: linear-gradient(to bottom, rgba(180, 130, 90, 0.35) 0%, rgba(180, 130, 90, 0.1) 40%, rgba(250, 204, 21, 0.05) 100%); } .language-tab { border-left: 1px solid var(--border-color); border-right: 1px solid var(--border-color); display: flex; align-items: center; gap: 6px; cursor: pointer; position: relative; }
@media (hover: hover) { .language-tab:hover { background: var(--bg-primary); color: var(--text-primary); } }
.lang-flag { font-size: 14px; }
.lang-text { font-size: 12px; min-width: 50px; }
.lang-arrow { font-size: 12px; opacity: 0.6; }
.language-dropdown { position: absolute; top: 100%; right: 0; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); z-index: 99999; min-width: 120px; margin-top: 2px; }
.lang-option { padding: 8px 12px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
@media (hover: hover) { .lang-option:hover { background: rgba(205, 92, 155, 0.1); color: rgb(205, 92, 155); } }
.lang-option:first-child { border-radius: 4px 4px 0 0; }
.lang-option:last-child { border-radius: 0 0 4px 4px; }
.language-tab { overflow: visible; /* Allow dropdown to overflow */ position: relative; }
.language-tab.cycling .lang-flag, .language-tab.cycling .lang-text { animation: langFade 0.3s ease-in-out; }
@keyframes langFade { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
.docs-content { flex: 1; overflow: hidden; padding: 0; font-size: 13px; line-height: 1.5; background: var(--bg-primary); color: var(--text-primary); display: flex; align-items: center; justify-content: center; min-height: 0; /* Allow proper flex shrinking */ position: relative; /* For positioning child elements like card-controls */ }
#examples-content, #history-content, #recent-content, #keeps-content, #guide-content { align-items: flex-start; justify-content: flex-start; overflow-y: auto; padding: 0; /* Remove padding so list items go edge-to-edge */ } /* Tab content backgrounds - gradient fade from colored top to neutral */ #guide-content:not(.hidden) { background: linear-gradient(to bottom, rgba(34, 197, 94, 0.4) 0%, rgba(34, 197, 94, 0.1) 40%, transparent 100%); } #examples-content:not(.hidden) { background: linear-gradient(to bottom, rgba(250, 204, 21, 0.45) 0%, rgba(250, 204, 21, 0.1) 40%, transparent 100%); } #recent-content:not(.hidden) { background: linear-gradient(to bottom, rgba(249, 115, 22, 0.4) 0%, rgba(249, 115, 22, 0.1) 40%, transparent 100%); } #history-content:not(.hidden) { background: linear-gradient(to bottom, rgba(180, 130, 90, 0.4) 0%, rgba(180, 130, 90, 0.1) 40%, transparent 100%); } #keeps-content:not(.hidden) { background: linear-gradient(to bottom, rgba(120, 120, 120, 0.3) 0%, rgba(120, 120, 120, 0.08) 40%, transparent 100%); } #jukebox-content:not(.hidden) { background: linear-gradient(to bottom, rgba(236, 72, 153, 0.4) 0%, rgba(236, 72, 153, 0.1) 40%, transparent 100%); } #jockey-content:not(.hidden) { background: linear-gradient(to bottom, rgba(205, 92, 155, 0.3) 0%, rgba(205, 92, 155, 0.08) 40%, transparent 100%); } #reference-content:not(.hidden) { background: linear-gradient(to bottom, rgba(34, 197, 94, 0.45) 0%, rgba(34, 197, 94, 0.1) 40%, transparent 100%); }
#examples-content > div, #history-content > div, #recent-content > div, #keeps-content > div, #guides-list, #guide-detail { width: 100%; height: 100%; }
.book-deck-controls { position: absolute; bottom: 16px; right: 16px; display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.05); padding: 6px 10px; border-radius: 12px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.65), inset 0 -1px 0 rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.12); z-index: 3; }
.hidden { display: none !important; }
.docs-content.hidden { display: none; }
.example-item { padding: 8px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 10px; content-visibility: auto; contain-intrinsic-size: 76px; }
.example-meta-right { display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 2px; min-width: 44px; flex-shrink: 0; }
.example-rank { font-size: 10px; font-weight: 700; letter-spacing: 0.2px; color: #3b82f6; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35); } .thumb-container { flex-shrink: 0; width: 48px; height: 48px; position: relative; overflow: hidden; border-radius: 10px; background: var(--bg-tertiary); }
.thumb-container > .example-thumb, .thumb-container > .thumb-noise, .thumb-container > .example-thumb-placeholder { position: absolute; inset: 0; } .example-thumb { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; background: var(--bg-tertiary); flex-shrink: 0; opacity: 0; transition: opacity 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.1); } .example-thumb.loaded { opacity: 1; } .example-thumb-placeholder { width: 48px; height: 48px; border-radius: 10px; background: var(--bg-tertiary); flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 18px; opacity: 0.5; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.08); }
.history-thumb-pending { position: relative; overflow: hidden; animation: thumb-pulse 1.2s ease-in-out infinite; } @keyframes thumb-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.8; } } /* Static fallback square shown while thumbnail is missing/loading */ .thumb-noise { width: 48px; height: 48px; border-radius: 10px; flex-shrink: 0; position: relative; overflow: hidden; background: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.2), 0 4px 8px rgba(0,0,0,0.15); }
.thumb-fallback-square { width: 100%; height: 100%; background-color: #3a3a3a; background-image: linear-gradient(0deg, rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px); background-size: 6px 6px; border-radius: 10px; }
/* Oven status indicator bar */ .thumb-oven-status { position: absolute; bottom: 0; left: 0; right: 0; height: 6px; border-radius: 0 0 10px 10px; background: transparent; } .thumb-oven-status.queued { background: linear-gradient(90deg, #4488ff, #6699ff); animation: oven-queue-pulse 1.5s ease-in-out infinite; } .thumb-oven-status.processing { background: linear-gradient(90deg, #ff8844, #ffaa66); animation: oven-process-pulse 0.8s ease-in-out infinite; } .thumb-oven-status.cached { background: linear-gradient(90deg, #44ff88, #66ffaa); } .thumb-oven-status.pending { background: linear-gradient(90deg, #6b7280, #9ca3af); opacity: 0.8; }
.thumb-oven-label { display: none; }
@keyframes oven-queue-pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } } @keyframes oven-process-pulse { 0%, 100% { opacity: 0.7; transform: scaleX(1); } 50% { opacity: 1; transform: scaleX(0.95); } } /* Code list panel container - wraps controls + scrollable list */ .code-list-panel { display: flex; flex-direction: column; height: 100%; width: 100%; } /* Sticky control bar at top of list panels */ .code-list-controls { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; gap: 8px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; position: sticky; top: 0; z-index: 10; } .code-list-status { font-size: 12px; color: var(--text-tertiary); } .code-list-actions { display: flex; gap: 8px; } .code-list-actions button { padding: 6px 10px; font-size: 12px; } /* Scrollable list container */ .code-list-items { flex: 1; overflow-y: auto; min-height: 0; }
/* Loading state for code lists */ .code-list-loading { display: flex; justify-content: center; align-items: center; height: 100%; min-height: 120px; padding: 24px; color: var(--text-tertiary); font-size: 13px; font-style: italic; animation: code-list-loading-pulse 1.5s ease-in-out infinite; } /* Empty state for code lists */ .code-list-empty { display: flex; justify-content: center; align-items: center; height: 100%; min-height: 120px; padding: 24px; color: var(--text-tertiary); font-size: 13px; font-style: italic; } /* "... and X more from @author" indicator */ .owner-more-indicator { display: flex; justify-content: flex-start; align-items: center; padding: 6px 12px 6px 60px; /* Indent to align with code text */ cursor: pointer; transition: background 0.15s ease; border-bottom: 1px solid var(--border-subtle); } .owner-more-indicator:hover { background: rgba(var(--accent-rgb, 147, 51, 234), 0.08); } .owner-more-indicator .more-count { font-size: 11px; color: var(--text-tertiary); font-style: italic; opacity: 0.8; } .owner-more-indicator:hover .more-count { color: var(--accent-color, #9333ea); opacity: 1; } /* "Back to all" header when viewing single author */ .owner-back-indicator { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; cursor: pointer; background: rgba(var(--accent-rgb, 147, 51, 234), 0.06); border-bottom: 1px solid var(--border-subtle); position: sticky; top: 0; z-index: 5; transition: background 0.15s ease; } .owner-back-indicator:hover { background: rgba(var(--accent-rgb, 147, 51, 234), 0.12); } .owner-back-indicator .back-text { font-size: 11px; color: var(--accent-color, #9333ea); font-weight: 500; } .owner-back-indicator .owner-filter { font-size: 10px; color: var(--text-tertiary); font-style: italic; } @keyframes code-list-loading-pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } } /* Footer with action buttons (bottom right) */ .code-list-footer { display: flex; justify-content: flex-end; align-items: center; padding: 8px 12px; gap: 8px; background: var(--bg-secondary); border-top: 1px solid var(--border-subtle); flex-shrink: 0; } .code-list-footer button { padding: 6px 10px; font-size: 12px; } /* New items indicator */ .code-list-new-indicator { display: flex; justify-content: center; align-items: center; padding: 8px 12px; background: rgba(13, 148, 136, 0.15); border-bottom: 1px solid rgba(13, 148, 136, 0.3); color: #0d9488; font-size: 12px; cursor: pointer; flex-shrink: 0; transition: background 0.2s; } .code-list-new-indicator:hover { background: rgba(13, 148, 136, 0.25); } :root.dark .code-list-new-indicator { background: rgba(45, 212, 191, 0.15); border-bottom-color: rgba(45, 212, 191, 0.3); color: #2dd4bf; } :root.dark .code-list-new-indicator:hover { background: rgba(45, 212, 191, 0.25); }
@media (hover: hover) { .example-item:hover { background: var(--bg-secondary); } }
.example-code { font-family: monospace; color: var(--code-text); white-space: nowrap; overflow: hidden; flex: 1; min-width: 0; position: relative; /* Prevent text descenders and shadows from being clipped */ padding-bottom: 4px; margin-bottom: -4px; /* Monaco-style sharp text shadow for depth */ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3); } /* Light mode: subtler shadow that still adds depth */ [data-theme="light"] .example-code { text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15), 0 2px 3px rgba(0, 0, 0, 0.1); } /* Ticker scrolling animation for Top Hits - auto-scrolls only when content overflows */ .example-code .ticker-wrapper { display: inline-block; white-space: nowrap; padding-right: 0; } /* Only animate when has ticker-scroll class (added by JS when content overflows) */ .example-code .ticker-wrapper.ticker-scroll { animation: ticker-scroll var(--ticker-duration, 20s) linear infinite; } .example-item:hover .example-code .ticker-wrapper.ticker-scroll { animation-duration: calc(var(--ticker-duration, 20s) * 0.4); /* Faster on hover */ } @keyframes ticker-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } /* Program separator - blinking stop indicator */ .ticker-separator { display: inline-block; padding: 0 1.5em; animation: separator-blink 1.2s ease-in-out infinite; } .ticker-separator::before { content: '●'; font-size: 0.6em; vertical-align: middle; color: rgba(255, 165, 0, 0.8); /* Orange for visibility on both themes */ } [data-theme="light"] .ticker-separator::before { color: rgba(255, 100, 0, 0.9); /* Darker orange in light mode */ } @keyframes separator-blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } } /* Static display when content fits - no animation needed */ .example-code .ticker-static { display: inline-block; white-space: nowrap; } /* Timing token blink animation for Top Hits */ .example-code .timing-token { animation: timing-blink 1s ease-in-out infinite; } @keyframes timing-blink { 0%, 80%, 100% { opacity: 1; color: yellow; } 40% { opacity: 0.7; color: lime; } } /* Text shadows for dark syntax colors - subtle outline */ .example-code .dark-text-shadow { text-shadow: 0 0 2px rgba(255, 255, 255, 0.6), 1px 1px 0px rgba(255, 255, 255, 0.4); } /* Light mode: minimal shadow */ [data-theme="light"] .example-code .dark-text-shadow { text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.5); } /* Subtle glow effect for bright syntax colors */ .example-code .glow-text { text-shadow: 0 0 2px currentColor; } /* Light mode: no glow needed */ [data-theme="light"] .example-code .glow-text { text-shadow: none; } /* Timing token specific styling - yellow with subtle glow */ .example-code .timing-token { font-weight: bold; color: #ffff00 !important; text-shadow: 0 0 3px rgba(255, 255, 0, 0.5); }
.example-hits { font-size: 11px; color: var(--text-tertiary); }
/* Guides List - matches Top Hits style */ .guide-item { padding: 8px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 10px; } @media (hover: hover) { .guide-item:hover { background: var(--bg-secondary); } } .guide-item-icon { width: 32px; height: 32px; border-radius: 6px; background: var(--bg-tertiary); flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 16px; } .guide-item-content { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } .guide-item-title { font-family: monospace; font-size: 12px; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .guide-item-author { font-size: 10px; color: rgb(205, 92, 155); font-family: monospace; } .guide-item-arrow { font-size: 12px; color: var(--text-tertiary); opacity: 0.5; } /* Guide Detail View */ .guide-detail-header { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid var(--border-subtle); background: var(--bg-secondary); } .guide-back-btn { background: none; border: none; color: rgb(205, 92, 155); font-family: monospace; font-size: 12px; cursor: pointer; padding: 4px 8px; border-radius: 4px; } .guide-back-btn:hover { background: var(--bg-tertiary); } .guide-detail-author { font-size: 10px; color: rgb(205, 92, 155); font-family: monospace; } .guide-detail-title { font-size: 14px; font-weight: bold; color: var(--text-primary); padding: 12px; border-bottom: 1px solid var(--border-subtle); } .guide-detail-body { padding: 12px; font-size: 12px; line-height: 1.6; color: var(--text-secondary); } .guide-detail-body p { margin: 0 0 12px 0; } .guide-detail-body p:last-child { margin-bottom: 0; } .guide-detail-body code { background: #fff9c0; padding: 2px 4px; border-radius: 2px; font-size: 11px; color: #333333; border: 1px solid #f0e68c; } @media (prefers-color-scheme: dark) { .guide-detail-body code { background: #332f28; color: #d4d4d4; border-color: #4a4540; } }
/* Learn Tab - Live Documentation */ .learn-container { padding: 0; height: 100%; display: flex; flex-direction: column; overflow: hidden; } .learn-doc-section { flex: 1; overflow-y: auto; padding: 12px; border-bottom: 1px solid var(--border-subtle); } .learn-doc-empty { font-family: monospace; font-size: 11px; color: var(--text-tertiary); font-style: italic; padding: 8px 0; } .learn-doc-hint { font-family: monospace; font-size: 10px; color: var(--text-tertiary); opacity: 0.7; margin-top: 8px; }
.learn-doc-ticker { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 6px 8px; margin-bottom: 8px; background: var(--bg-tertiary); border-radius: 4px; font-family: 'Berkeley Mono Variable', monospace; font-size: 10px; }
.learn-doc-ticker-nav { color: rgb(205, 92, 155); opacity: 0.6; transition: opacity 0.15s ease; }
.learn-doc-ticker-nav.disabled { opacity: 0.2; color: var(--text-tertiary); }
.learn-doc-ticker-current { color: rgb(205, 92, 155); font-weight: bold; padding: 2px 6px; background: rgba(205, 92, 155, 0.1); border-radius: 3px; }
.learn-doc-content { font-family: monospace; } .learn-doc-name { font-size: 14px; font-weight: bold; color: rgb(205, 92, 155); margin-bottom: 4px; } .learn-doc-signature { font-size: 11px; color: var(--text-secondary); background: var(--bg-tertiary); padding: 4px 8px; border-radius: 4px; margin-bottom: 8px; display: inline-block; } .learn-doc-category { font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-tertiary); margin-bottom: 8px; } .learn-doc-description { font-size: 11px; color: var(--text-secondary); line-height: 1.5; margin-bottom: 12px; } .learn-doc-examples-title { font-size: 10px; color: var(--text-tertiary); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; } .learn-doc-example { font-size: 10px; color: var(--text-secondary); background: var(--bg-tertiary); padding: 4px 8px; border-radius: 3px; margin-bottom: 4px; cursor: pointer; transition: background 0.15s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .learn-doc-example:hover { background: rgba(205, 92, 155, 0.15); } /* Syntax highlighting in Learn docs */ .learn-doc-signature, .learn-doc-example { font-family: 'Berkeley Mono Variable', monospace; } .learn-doc-signature .kl-fn { color: #00b8b8; font-weight: bold; } .learn-doc-signature .kl-paren { color: #ff6b6b; } .learn-doc-signature .kl-param { color: #ffa500; font-style: italic; } .learn-doc-signature .kl-bracket { color: #888; } .learn-doc-example .kl-fn { color: #00b8b8; font-weight: bold; } .learn-doc-example .kl-paren { color: #ff6b6b; } .learn-doc-example .kl-str { color: #cc9900; } .learn-doc-example .kl-num { color: #ff69b4; } .learn-doc-example .kl-var { color: #ffa500; } .learn-doc-example .kl-math { color: #00ff00; font-weight: bold; } .learn-doc-example .kl-color { font-weight: bold; } .learn-doc-example .kl-rainbow { background: linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold; } .learn-doc-example .kl-special { color: #00b8b8; font-weight: bold; } .learn-doc-example .kl-timing { color: #ffff00; font-weight: bold; } @media (prefers-color-scheme: dark) { .learn-doc-signature .kl-fn { color: #00ffff; } .learn-doc-signature .kl-param { color: #ffb347; } .learn-doc-example .kl-fn { color: #00ffff; } .learn-doc-example .kl-str { color: #ffff00; } .learn-doc-example .kl-num { color: #ffb6c1; } .learn-doc-example .kl-var { color: #ffb347; } } [data-theme="dark"] .learn-doc-signature .kl-fn, [data-theme="dark"] .learn-doc-example .kl-fn { color: #00ffff; } [data-theme="dark"] .learn-doc-signature .kl-param, [data-theme="dark"] .learn-doc-example .kl-var { color: #ffb347; } [data-theme="dark"] .learn-doc-example .kl-str { color: #ffff00; } [data-theme="dark"] .learn-doc-example .kl-num { color: #ffb6c1; } /* Guides Coming Soon - with preset buttons */ .guides-coming-soon { padding: 12px; border-top: 1px solid var(--border-subtle); flex-shrink: 0; } .guides-coming-text { font-family: monospace; font-size: 11px; color: var(--text-secondary); margin-bottom: 10px; line-height: 1.5; } .guides-coming-text .author { color: rgb(205, 92, 155); } .guides-fund-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } .guides-amount-btn { font-family: monospace; font-size: 10px; padding: 4px 8px; background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border-subtle); border-radius: 4px; cursor: pointer; transition: all 0.15s ease; } .guides-amount-btn:hover { background: rgba(205, 92, 155, 0.15); border-color: rgb(205, 92, 155); color: rgb(205, 92, 155); } .guides-amount-btn.selected { background: rgb(205, 92, 155); color: #fff; border-color: rgb(205, 92, 155); } .guides-give-btn { font-family: monospace; font-size: 10px; font-weight: bold; padding: 4px 12px; background: rgb(165, 62, 125); color: #fff; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; transition: background 0.15s ease; text-transform: uppercase; letter-spacing: 0.5px; } .guides-give-btn:hover { background: rgb(145, 42, 105); }
[data-theme="dark"] .guide-detail-body code { background: #332f28; color: #d4d4d4; border-color: #4a4540; }
.docs-content h3 { font-size: 14px; margin: 16px 0 8px 0; color: var(--text-secondary); font-weight: bold; }
.docs-content h3:first-child { margin-top: 0; }
.docs-content ul { margin: 0; padding-left: 20px; }
.docs-content li { margin: 4px 0; }
.docs-content a { color: rgb(205, 92, 155); text-decoration: none; }
@media (hover: hover) { .docs-content a:hover { text-decoration: underline; } }
.docs-content code { background: #fff9c0; /* Ultra light yellow to match editor zebra stripes */ padding: 2px 4px; border-radius: 2px; font-size: 12px; color: #333333; border: 1px solid #f0e68c; }
@media (prefers-color-scheme: dark) { .docs-content code { background: #332f28; /* Dark brown to match editor zebra stripes */ color: #d4d4d4; border-color: #4a4540; } }
.function-category { margin-bottom: 20px; }
.function-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.function-item { padding: 4px 10px; background: #fff9c0; /* Ultra light yellow to match editor zebra stripes */ border: 1px solid #f0e68c; border-radius: 12px; cursor: pointer; transition: all 0.15s; font-size: 15px; color: #333333; }
@media (prefers-color-scheme: dark) { .function-item { background: #332f28; /* Dark brown to match editor zebra stripes */ border-color: #4a4540; color: #d4d4d4; } }
@media (hover: hover) { .function-item:hover { background: rgb(205, 92, 155); color: white; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-color: rgb(205, 92, 155); } }
/* Book styles */ #book-container { flex: 1; display: flex; align-items: center; justify-content: center; height: 100%; width: 100%; position: relative; touch-action: manipulation; min-height: 0; /* Allow proper flex shrinking */ }
/* Card stack 3D perspective */ .book-stack { perspective: 1200px; transform-style: preserve-3d; } .book-frame { transform-style: preserve-3d; cursor: pointer; } .book-frame:hover { filter: brightness(1.05); }
/* Discard pile in corner */ .discard-pile { position: absolute; bottom: 20px; right: 20px; width: 70px; height: 98px; perspective: 800px; transform-style: preserve-3d; cursor: pointer; z-index: 50; transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease; } .discard-pile:hover { transform: scale(1.1); } .discard-pile:active { transform: scale(0.95); } .discard-pile:empty { opacity: 0; pointer-events: none; } .discard-pile .discard-card { position: absolute; width: 100%; height: 100%; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); transform-style: preserve-3d; backface-visibility: hidden; transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.4s ease; } .discard-pile .discard-card img { width: 100%; height: 100%; object-fit: contain; border-radius: 6px; }
.book-stack { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; pointer-events: all; padding: 8px; }
.book-frame { position: absolute; top: 50%; left: 50%; /* Dimensions controlled by JS fitBookFrame() */ display: flex; align-items: stretch; justify-content: center; filter: drop-shadow(0 10px 18px rgba(0,0,0,0.16)); padding: 6px; background: linear-gradient(180deg, rgba(255,255,255,0.9), rgba(240,240,240,0.9)); border-radius: 5%; box-shadow: inset 0 1px 0 rgba(255,255,255,0.8), inset 0 -1px 0 rgba(0,0,0,0.08), 0 12px 24px rgba(0,0,0,0.18); transform-origin: center; cursor: pointer; transition: transform 0.18s ease, opacity 0.18s ease; will-change: transform; }
:root.dark .book-frame, [data-theme="dark"] .book-frame { background: linear-gradient(180deg, #2c2620, #1b1712); box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), inset 0 -1px 0 rgba(0,0,0,0.45), 0 18px 28px rgba(0,0,0,0.45); }
/* SVG cards from LaTeX - simpler frame */ .book-frame.svg-card { background: transparent; padding: 0; box-shadow: none; filter: drop-shadow(0 12px 24px rgba(0,0,0,0.25)); } .book-frame.svg-card img { border-radius: 12px; }
/* Card back design - only used as fallback for non-SVG cards */ .book-frame .card-back { position: absolute; inset: 0; border-radius: 12px; background: repeating-linear-gradient( 45deg, #8B4513 0px, #8B4513 2px, #A0522D 2px, #A0522D 8px ), linear-gradient(135deg, #654321, #8B4513); background-blend-mode: overlay; backface-visibility: hidden; transform: rotateY(180deg); display: flex; align-items: center; justify-content: center; box-shadow: inset 0 0 40px rgba(0,0,0,0.3); } /* SVG cards use the SVG image, not CSS background */ .book-frame.svg-card .card-back { background: none; box-shadow: none; } /* Flip animation */ .book-stack { perspective: 1000px; } .book-frame.svg-card { transform-style: preserve-3d; } .book-frame.svg-card .card-front, .book-frame.svg-card .card-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; transition: transform 0.6s ease; } .book-frame.svg-card .card-front { transform: rotateY(0deg); } .book-frame.svg-card .card-back { transform: rotateY(180deg); } .book-frame.svg-card.flipped { /* The frame itself rotates */ }
/* Spread view */ .book-stack.spread-view { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 16px; padding: 20px; } .book-stack.spread-view .book-frame { position: relative !important; top: auto !important; left: auto !important; transform: none !important; width: 120px !important; height: 168px !important; transition: transform 0.3s ease, box-shadow 0.3s ease; } .book-stack.spread-view .book-frame:hover { transform: scale(1.1) translateY(-8px) !important; z-index: 100 !important; filter: drop-shadow(0 16px 32px rgba(0,0,0,0.4)); } .book-stack.spread-view .book-frame.selected { transform: scale(1.15) translateY(-12px) !important; z-index: 101 !important; outline: 3px solid #4ECDC4; outline-offset: 2px; }
.book-page { position: relative; flex: 1; background: linear-gradient(135deg, rgba(255, 249, 192, 0.96), rgba(255, 255, 255, 0.98)); border: 1px solid rgba(0,0,0,0.06); border-radius: 5%; overflow: hidden; display: flex; align-items: flex-start; justify-content: flex-start; box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), inset 0 -1px 0 rgba(0,0,0,0.07); }
:root.dark .book-page, [data-theme="dark"] .book-page { background: linear-gradient(135deg, #231e19, #15120f); border-color: #3a342f; box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), inset 0 -1px 0 rgba(0,0,0,0.5); }
.book-page-inner { width: 750px; height: 1050px; display: flex; flex-direction: column; gap: 0; transform-origin: top left; padding: 24px 24px 32px 24px; }
.book-titleplate { text-align: center; padding: 12px 16px; border-radius: 16px; background: linear-gradient(180deg, rgba(255,255,255,0.65), rgba(255,255,255,0.4)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 0 rgba(0,0,0,0.08); margin-bottom: 16px; flex-shrink: 0; }
:root.dark .book-titleplate, [data-theme="dark"] .book-titleplate { background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.12), inset 0 -1px 0 rgba(0,0,0,0.55); }
.book-title-main { font-size: clamp(36px, 5vw, 48px); font-weight: 800; letter-spacing: 0.6px; }
.book-title-sub { font-size: clamp(28px, 4vw, 38px); color: var(--text-tertiary); letter-spacing: 0.4px; }
.book-body { flex: 1; overflow: hidden; padding: 0; display: flex; flex-direction: column; gap: 20px; min-height: 0; font-size: 36px; line-height: 1.4; }
.book-header { display: none; }
.book-title { font-size: 44px; font-weight: 800; letter-spacing: 0.4px; }
.book-runtime { font-size: 12px; color: var(--text-tertiary); }
.book-status { font-size: 11px; padding: 4px 10px; border-radius: 999px; border: 1px solid currentColor; text-transform: uppercase; letter-spacing: 0.6px; line-height: 1; }
.book-status.live { color: #16a34a; background: rgba(22, 163, 74, 0.12); } .book-status.preview { color: #f97316; background: rgba(249, 115, 22, 0.14); } .book-status.experimental { color: #a855f7; background: rgba(168, 85, 247, 0.14); } .book-status.soon { color: #2563eb; background: rgba(37, 99, 235, 0.14); }
.book-summary { margin: 0 0 6px; color: var(--text-secondary); font-size: 13px; line-height: 1.6; }
.book-section { margin-bottom: 8px; }
.book-section-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-tertiary); margin-bottom: 6px; }
.book-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.book-list li { padding: 8px 10px; border: 1px dashed var(--border-subtle); border-radius: 10px; background: rgba(255, 255, 255, 0.7); color: var(--text-secondary); line-height: 1.4; font-size: 13px; }
:root.dark .book-list li, [data-theme="dark"] .book-list li { background: rgba(51, 47, 40, 0.9); border-color: #4a4540; color: #d4d4d4; }
.book-snippet-block { margin-top: auto; border-radius: 16px; overflow: hidden; border: 2px solid var(--border-subtle); background: #1f1b16; color: #f4e9d7; box-shadow: inset 0 1px 0 rgba(255,255,255,0.08); flex-shrink: 1; min-height: 0; display: flex; flex-direction: column; }
:root.dark .book-snippet-block, [data-theme="dark"] .book-snippet-block { background: #0f0c0a; border-color: #4a4540; color: #f4e9d7; }
.book-snippet-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; border-bottom: 2px solid rgba(255, 255, 255, 0.08); flex-shrink: 0; }
.book-snippet-title { font-weight: 700; font-size: 36px; letter-spacing: 0.3px; }
.book-snippet { margin: 0; padding: 18px 20px; font-family: 'Berkeley Mono', 'Comic Relief', monospace; font-size: 34px; white-space: pre-wrap; background: transparent; color: inherit; word-break: break-word; flex: 1; overflow: hidden; min-height: 0; }
.book-snippet-note { font-size: 12px; color: rgba(244, 233, 215, 0.9); }
.book-card-title { font-size: 52px; font-weight: 800; margin: 0 0 16px; letter-spacing: 0.3px; flex-shrink: 0; }
.book-card-text { margin: 0 0 12px; color: var(--text-secondary); line-height: 1.35; font-size: 38px; flex-shrink: 1; }
.book-deck-button { width: 36px; height: 36px; border-radius: 50%; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 16px; }
.book-page-count { position: absolute; bottom: 10px; right: 12px; font-size: 11px; color: var(--text-tertiary); letter-spacing: 0.5px; background: rgba(0,0,0,0.04); padding: 4px 8px; border-radius: 8px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); }
:root.dark .book-page-count, [data-theme="dark"] .book-page-count { background: rgba(255,255,255,0.05); box-shadow: inset 0 1px 0 rgba(255,255,255,0.08); color: #b9b9b9; }
@media (max-width: 700px) { .book-frame { /* Dimensions controlled by JS fitBookFrame() */ } .book-page { padding: 12px 12px 24px 12px; } .book-body { font-size: clamp(12px, 3vw, 15px); } }
.book-actions { display: flex; align-items: center; gap: 8px; }
.book-cta { background: rgb(205, 92, 155); color: white; border: none; border-radius: 8px; padding: 8px 12px; font-weight: 700; cursor: pointer; transition: transform 0.1s ease, box-shadow 0.1s ease; }
.book-cta:disabled { opacity: 0.5; cursor: not-allowed; }
.book-cta:not(:disabled):active { transform: translateY(1px); box-shadow: none; }
.book-snippet-note { font-size: 12px; color: rgba(244, 233, 215, 0.9); }
#book-container[data-platform="ff1"] .book-title, #book-container[data-platform="ff1"] .book-summary { font-family: 'PP Mori', 'Comic Relief', monospace; }
#book-container[data-platform="playdate"] .book-title, #book-container[data-platform="playdate"] .book-summary { font-family: 'Roobert', 'Comic Relief', monospace; }
#book-container[data-platform="ableton"] .book-title, #book-container[data-platform="ableton"] .book-summary { font-family: 'Futura PT', 'Comic Relief', monospace; letter-spacing: 0.5px; }
/* Keeps iframe */ #keeps-iframe { width: 100%; height: 100%; border: none; flex: 1; }
/* Header auth buttons container */ .header-auth-buttons { display: flex; gap: 8px; margin-left: auto; margin-right: 0; /* No collapse arrow on desktop */ padding-left: 12px; /* Extra space from QR code */ flex-shrink: 0; } /* Header login button - subtle dark blue with soft border */ .header-login-btn { padding: 6px 14px; background: rgba(30, 45, 80, 0.6); /* Softer, more transparent */ color: rgba(255, 255, 255, 0.75); /* Slightly muted text */ border: 1px solid rgba(255, 255, 255, 0.3); /* Subtle border */ border-radius: 0; /* Square like TextButton */ font-size: 12px; font-weight: normal; cursor: pointer; transition: all 0.15s; }
.header-login-btn:hover { background: rgba(40, 55, 100, 0.75); color: white; border-color: rgba(255, 255, 255, 0.5); } /* Header signup button - subtle dark green with soft border */ .header-signup-btn { padding: 6px 14px; background: rgba(30, 70, 45, 0.6); /* Softer, more transparent */ color: rgba(255, 255, 255, 0.75); /* Slightly muted text */ border: 1px solid rgba(255, 255, 255, 0.3); /* Subtle border */ border-radius: 0; /* Square like TextButton */ font-size: 12px; font-weight: normal; cursor: pointer; transition: all 0.15s; } .header-signup-btn:hover { background: rgba(40, 90, 55, 0.75); color: white; border-color: rgba(255, 255, 255, 0.5); } /* Dark mode auth buttons - keep subtle */ [data-theme="dark"] .header-login-btn, :root.dark .header-login-btn { background: rgba(20, 35, 70, 0.6); } [data-theme="dark"] .header-login-btn:hover, :root.dark .header-login-btn:hover { background: rgba(30, 45, 90, 0.75); } [data-theme="dark"] .header-signup-btn, :root.dark .header-signup-btn { background: rgba(20, 55, 35, 0.6); } [data-theme="dark"] .header-signup-btn:hover, :root.dark .header-signup-btn:hover { background: rgba(30, 90, 50, 0.95); }
/* Header user menu - black box with white border like prompt.mjs handle */ .header-user-menu { margin-left: auto; margin-right: 0; /* No collapse arrow on desktop */ position: relative; display: flex; align-items: center; gap: 4px; cursor: pointer; padding: 6px 12px; background: black; border: 2px solid white; border-radius: 0; /* Square like TextButton */ transition: all 0.15s; }
.header-user-menu:hover { background: rgb(30, 30, 30); box-shadow: 0 0 8px rgba(255, 255, 255, 0.3); }
.header-user-handle { font-size: 12px; color: white; font-weight: bold; }
/* header-menu-arrow removed - no longer used */
.header-user-dropdown { display: none; position: fixed; /* Fixed to escape overflow:hidden containers */ background: black; border: 2px solid white; border-radius: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.3); min-width: 100px; z-index: 99999; }
.header-user-menu.open .header-user-dropdown { display: block; }
.header-logout-btn { width: 100%; padding: 8px 12px; background: black; border: none; text-align: left; font-size: 12px; color: white; cursor: pointer; font-weight: bold; }
.header-logout-btn:hover { background: rgb(60, 60, 60); }
/* ===== Progressive Header Collapse (narrow preview pane) ===== */ /* Container query based on preview-panel width */ #preview-panel { container-type: inline-size; container-name: preview; } /* Short title shown on narrow widths */ #preview-title .title-short { display: none; } #preview-title .title-full { display: inline; } /* Level 1 (< 420px): Aesthetic.Computer → prompt.AC */ @container preview (max-width: 420px) { #preview-title .title-full { display: none; } #preview-title .title-short { display: inline; } }
/* When a slug is present, switch to prompt.AC sooner to make room */ @container preview (max-width: 520px) { body.has-route-code #preview-title .title-full { display: none; } body.has-route-code #preview-title .title-short { display: inline; } } /* Level 2 (< 360px): Hide "I'm new" button */ @container preview (max-width: 360px) { #header-signup-btn { display: none !important; } } /* Level 3 (< 300px): Compact login buttons (user handle stays full size) */ @container preview (max-width: 300px) { #header-auth-buttons { padding-left: 4px; gap: 4px; } #header-login-btn { padding: 4px 8px; font-size: 10px; } #header-signup-btn { display: none !important; } } /* Level 4 (< 240px): Hide $code, keep only prompt.AC + QR */ @container preview (max-width: 240px) { #preview-title .code-display { display: none !important; } } /* Level 5 (< 180px): Hide prompt.AC entirely, keep only $code + QR */ @container preview (max-width: 180px) { #preview-title .title-short { display: none !important; } #preview-title .bounce-dot { display: none !important; } #preview-title .code-display { display: inline !important; } .platform-arrow { display: none !important; } }
/* Pop-out button styles */ .popout-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; margin: 0 2px; background: transparent; border: none; border-radius: 4px; cursor: pointer; color: var(--text-tertiary); opacity: 0.4; transition: opacity 0.2s, background 0.15s, color 0.15s, transform 0.1s; flex-shrink: 0; } .popout-btn:hover { background: rgba(205, 92, 155, 0.15); color: rgb(205, 92, 155); } .popout-btn:active { transform: scale(0.9); } /* When popped out, show a "return" indicator */ .popout-btn.active { opacity: 1; color: rgb(205, 92, 155); background: rgba(205, 92, 155, 0.1); } .popout-btn.active:hover { background: rgba(205, 92, 155, 0.25); } @media (prefers-color-scheme: dark) { .popout-btn:hover { background: rgba(205, 92, 155, 0.25); } } [data-theme="dark"] .popout-btn:hover { background: rgba(205, 92, 155, 0.25); }
/* Platform selector dropdown */ .platform-selector { display: flex; align-items: center; gap: 4px; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.2s; position: relative; min-width: 0; /* Allow shrinking */ overflow: visible; /* Allow popout button to show */ }
.platform-selector:hover { background: rgba(0, 0, 0, 0.1); } @media (prefers-color-scheme: dark) { .platform-selector:hover { background: rgba(255, 255, 255, 0.1); } } [data-theme="dark"] .platform-selector:hover { background: rgba(255, 255, 255, 0.1); }
/* Platform name font styling - each platform has its own typography */ .platform-opt-name { font-weight: 500; }
/* Preview title inherits platform font styling */ #preview-title[data-platform="aesthetic-computer"] { font-family: 'YWFTProcessing-Regular', monospace; } #preview-title[data-platform="ff1"] { font-family: 'PP Mori', 'Futura', 'Century Gothic', sans-serif; font-weight: 400; } #preview-title[data-platform="ableton"] { font-family: 'Futura PT', 'Futura', 'Century Gothic', sans-serif; } #preview-title[data-platform="playdate"] { font-family: 'Roobert', 'Varela Round', sans-serif; } #preview-title[data-platform="gameboy"] { font-family: 'Press Start 2P', 'Courier New', monospace; font-size: 12px; } #preview-title[data-platform="n64"] { font-family: 'Impact', 'Haettenschweiler', sans-serif; }
/* Platform dropdown icons */ .platform-icon { display: inline-block; vertical-align: middle; margin-right: 6px; } /* Aesthetic.Computer pink rectangle icon */ .platform-icon.ac-pink-rect { width: 10px; height: 16px; background: rgb(205, 92, 155); /* AC Pink */ } /* Ableton logo SVG */ .platform-icon.ableton-logo { width: 24px; height: 18px; color: #333; }
/* Aesthetic.Computer - uses our YWFTProcessing font */ .platform-option[data-platform="aesthetic-computer"] .platform-opt-name { font-family: 'YWFTProcessing-Regular', monospace; font-weight: bold; }
/* FF1 Art Computer - PP Mori style (geometric sans) */ .platform-option[data-platform="ff1"] .platform-opt-name { font-family: 'PP Mori', 'Futura', 'Century Gothic', sans-serif; font-weight: 400; letter-spacing: 0.02em; }
/* Ableton Live - Futura PT */ .platform-option[data-platform="ableton"] .platform-opt-name { font-family: 'Futura PT', 'Futura', 'Century Gothic', sans-serif; font-weight: 400; }
/* Playdate - Roobert font */ .platform-option[data-platform="playdate"] .platform-opt-name { font-family: 'Roobert', 'Varela Round', sans-serif; font-weight: 700; }
/* Game Boy - pixel/retro style */ .platform-option[data-platform="gameboy"] .platform-opt-name { font-family: 'Press Start 2P', 'Courier New', monospace; font-size: 11px; letter-spacing: -0.02em; }
/* Nintendo 64 - bold condensed */ .platform-option[data-platform="n64"] .platform-opt-name { font-family: 'Impact', 'Haettenschweiler', sans-serif; font-weight: bold; letter-spacing: 0.01em; }
/* Platform dropdown option colors */ .platform-option[data-platform="aesthetic-computer"] { background: rgb(240, 235, 250); } .platform-option[data-platform="aesthetic-computer"]:hover { background: rgb(230, 220, 245); } .platform-option[data-platform="aesthetic-computer"] .platform-opt-name { color: rgb(88, 78, 108); } .platform-option[data-platform="ff1"] { background: rgb(26, 26, 26); } .platform-option[data-platform="ff1"]:hover { background: rgb(40, 40, 40); } .platform-option[data-platform="ff1"] .platform-opt-name { color: white; } .platform-option[data-platform="ableton"] { background: rgb(255, 255, 255); } .platform-option[data-platform="ableton"]:hover { background: rgb(240, 240, 240); } .platform-option[data-platform="ableton"].active { background: rgb(230, 230, 230); } .platform-option[data-platform="ableton"].active:hover { background: rgb(220, 220, 220); } .platform-option[data-platform="ableton"] .platform-opt-name { color: black; } .platform-option[data-platform="playdate"] { background: rgb(255, 204, 0); } .platform-option[data-platform="playdate"]:hover { background: rgb(255, 215, 50); } .platform-option[data-platform="playdate"] .platform-opt-name { color: black; } .platform-option[data-platform="gameboy"] { background: rgb(155, 188, 15); } .platform-option[data-platform="gameboy"]:hover { background: rgb(170, 200, 40); } .platform-option[data-platform="gameboy"] .platform-opt-name { color: rgb(15, 56, 15); } .platform-option[data-platform="n64"] { background: rgb(200, 50, 50); } .platform-option[data-platform="n64"]:hover { background: rgb(220, 70, 70); } .platform-option[data-platform="n64"] .platform-opt-name { color: white; } /* Dark mode dropdown colors - same as light for brand consistency */ [data-theme="dark"] .platform-option[data-platform="aesthetic-computer"] { background: rgb(48, 43, 58); } [data-theme="dark"] .platform-option[data-platform="aesthetic-computer"]:hover { background: rgb(60, 54, 72); } [data-theme="dark"] .platform-option[data-platform="aesthetic-computer"] .platform-opt-name { color: rgb(188, 168, 218); }
.platform-arrow { font-size: 8px; color: var(--text-tertiary); margin-left: -2px; }
.platform-dropdown { position: fixed; /* Fixed to escape overflow:hidden containers */ background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 0; box-shadow: 0 8px 24px rgba(0,0,0,0.4); min-width: 220px; z-index: 99999; padding: 0; /* Use visibility + opacity for smooth open without flash */ visibility: hidden; opacity: 0; pointer-events: none; transition: opacity 0.15s ease, visibility 0.15s ease; }
.platform-selector.open .platform-dropdown { visibility: visible; opacity: 1; pointer-events: auto; }
.platform-option { display: flex; align-items: center; gap: 8px; padding: 10px 14px; cursor: pointer; transition: background 0.15s; }
.platform-option:hover { background: var(--bg-secondary); }
.platform-option.active { background: rgba(205, 92, 155, 0.15); }
.platform-option.active:hover { background: rgba(205, 92, 155, 0.25); }
.platform-opt-icon { font-size: 16px; width: 24px; text-align: center; }
.platform-opt-name { color: var(--text-primary); flex: 1; font-size: 16px; }
.platform-status { font-size: 9px; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; font-weight: bold; }
.platform-option.coming-soon { opacity: 0.5; }
.platform-option.coming-soon .platform-status { background: rgba(59, 130, 246, 0.2); color: rgb(59, 130, 246); }
.platform-option.experimental { opacity: 0.4; }
.platform-option.experimental .platform-status { background: rgba(234, 179, 8, 0.2); color: rgb(180, 140, 0); } @media (prefers-color-scheme: dark) { .platform-option.coming-soon .platform-status { color: rgb(147, 197, 253); } .platform-option.experimental .platform-status { color: rgb(253, 224, 71); } } [data-theme="dark"] .platform-option.coming-soon .platform-status { color: rgb(147, 197, 253); } [data-theme="dark"] .platform-option.experimental .platform-status { color: rgb(253, 224, 71); }
.platform-divider { display: none; }
/* Keeps logged out state */ a { color: inherit; text-decoration: none; }
@media (hover: hover) { a:hover { color: rgb(205, 92, 155); } }
/* Mobile optimization */ @media (max-width: 768px) { /* Normalize text sizing for iOS */ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } body { font-size: 16px; /* Base size for mobile */ /* iOS safe area support */ padding-top: env(safe-area-inset-top); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); /* Prevent pull-to-refresh and overscroll */ overscroll-behavior: none; overflow: hidden; position: fixed; width: 100%; height: 100%; } /* MOBILE LAYOUT: main becomes a flex column with 3 panels */ main { display: flex !important; flex-direction: column !important; height: 100% !important; height: calc(100% - env(safe-area-inset-top) - env(safe-area-inset-bottom)) !important; overflow: hidden !important; } /* Hide Split.js grid containers */ #main-split > .top-row, #main-split > .bottom-row, #main-split > .gutter, #main-split > #left-col, #main-split > #right-col { display: none !important; } /* Show panels directly in main-split */ #main-split { display: flex !important; flex-direction: column !important; height: 100% !important; } /* Mobile-only elements - hidden on desktop, shown on mobile */ .mobile-only { display: flex !important; } /* Console tab styling in mobile */ #console-tab.mobile-only { display: flex !important; align-items: center; background: rgba(255, 159, 67, 0.25); /* Orange tint like console */ } #console-tab.mobile-only.active { background: rgba(255, 159, 67, 0.5); } /* Hide console panel in mobile - it's merged into reference panel as a tab */ #console-panel { display: none !important; } /* 3 visible panels in mobile mode - flexbox children */ #editor-panel, #preview-panel, #reference-panel { display: flex !important; flex-direction: column !important; width: 100% !important; min-height: 40px !important; /* Header height minimum */ flex-shrink: 0 !important; overflow: hidden !important; } /* Ensure iframe container fills preview panel properly in mobile */ #preview-panel { display: flex !important; flex-direction: column !important; overflow: hidden !important; } #preview-panel .preview-iframe-container { flex: 1 1 auto !important; min-height: 0 !important; height: 0 !important; /* Let flex-grow handle the height */ position: relative !important; overflow: hidden !important; } /* Ensure preview panel header is exactly 40px and doesn't shrink/grow */ #preview-panel > .docs-header { flex: 0 0 40px !important; height: 40px !important; min-height: 40px !important; max-height: 40px !important; } /* Default flex distribution for mobile (totals ~100) */ /* NO !important - JS needs to override during drag */ #editor-panel { flex: 25 1 0; } #preview-panel { flex: 45 1 0; } #reference-panel { flex: 30 1 0; } /* Mobile console content styling */ #mobile-console-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; width: 100%; } #mobile-console-output { flex: 1; overflow-y: auto; padding: 0; font-family: 'Berkeley Mono Variable', monospace; font-size: 12px; line-height: 1.4; background: #f5ebe0; /* Light beige - same as .console-output */ color: #c87137; /* Darker orange text */ width: 100%; box-sizing: border-box; } @media (prefers-color-scheme: dark) { #mobile-console-output { background: #4a3020; color: #ff9f43; } } [data-theme="light"] #mobile-console-output { background: #f5ebe0; color: #c87137; } [data-theme="dark"] #mobile-console-output { background: #4a3020; color: #ff9f43; }
/* Collapsed panel state in mobile */ .panel-collapsed { flex: 0 0 40px !important; min-height: 40px !important; max-height: 40px !important; } .panel-collapsed > *:not(.editor-header):not(.docs-header) { display: none !important; } /* Compact headers on mobile */ .editor-header, .docs-header { height: 40px; padding: 0 8px; font-size: 12px; flex-shrink: 0; cursor: pointer; } /* Make KidLisp logo smaller on mobile */ .baby-colors { font-size: 16px !important; line-height: 28px !important; } .baby-colors + span { font-size: 8px !important; margin-top: -1px !important; margin-bottom: 6px !important; } /* Compact language selector on mobile */ .language-tab { padding: 0 4px !important; } .lang-flag { font-size: 9px !important; } .lang-text { font-size: 9px !important; min-width: 40px !important; } /* Compact theme toggle */ #theme-toggle { width: 28px; height: 28px; font-size: 14px; } /* Make tabs touch-friendly */ .tab { padding: 0 12px; font-size: 12px; min-height: 40px; } /* Preview title smaller on mobile */ #preview-title { font-size: 13px !important; } #preview-title .code-display { font-size: 11px !important; } /* Compact platform selector on mobile */ .platform-selector { flex-shrink: 1; min-width: 0; /* Allow dropdown to overflow - only hide text overflow in children */ } #preview-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Auth buttons compact on mobile */ .header-auth-buttons { gap: 6px; margin-left: auto; margin-right: 28px; /* More space for the collapse arrow */ } .header-login-btn, .header-signup-btn { padding: 4px 8px; font-size: 10px; border-width: 1px; } /* User menu on mobile - keep full size */ .header-user-menu { flex-shrink: 0; margin-left: auto; margin-right: 28px; /* More space for the collapse arrow */ } .header-user-handle { white-space: nowrap; font-size: 12px; } /* header-menu-arrow removed */ /* QR code hidden on very small screens */ @media (max-width: 380px) { #preview-qr { display: none !important; } } /* Console title smaller */ #console-title { font-size: 12px; } /* Remove borders that might double up with gutters */ .editor-panel { border-right: none; border-bottom: none; } .reference-section { border-right: none; } .docs-panel.collapsed { height: 33.333% !important; } /* Play/Stop/Clear buttons - compact for single column mode */ /* Button size 36px with ~0.44 icon ratio (same as desktop 44px buttons) */ #send-button, #stop-button, #clear-button { width: 36px !important; height: 36px !important; min-width: 36px !important; min-height: 36px !important; flex-shrink: 0 !important; position: relative !important; left: auto !important; right: auto !important; top: auto !important; bottom: auto !important; } /* Scale SVG icons to maintain desktop ratio (18/44 ≈ 0.41 → 15px for 36px button) */ #send-button svg { width: 15px !important; height: 15px !important; } #stop-button svg { width: 13px !important; height: 13px !important; } #clear-button svg { width: 16px !important; height: 16px !important; } /* AI Assist Bar mobile */ #ai-assist-bar { bottom: 8px; left: 8px; right: 80px; } #ai-assist-input { height: 28px; font-size: 11px; padding: 0 10px; } #ai-assist-send { width: 28px; height: 28px; } #playback-controls { bottom: 8px; right: 8px; gap: 8px; display: flex !important; flex-direction: row !important; align-items: center !important; } #playback-controls > button { margin-right: 0 !important; margin-left: 0 !important; } /* Monaco editor adjustments for mobile */ #kidlisp-editor { padding-left: 8px; } /* Docs content compact */ .docs-content { padding: 0; font-size: 12px; flex: 1; min-height: 0; overflow: hidden; } /* Function grid compact */ .function-item { padding: 3px 8px; font-size: 13px; } /* Console compact */ .console-output { font-size: 12px; padding: 6px; } /* Mobile panels - smooth transitions */ .editor-panel, .preview-panel, .reference-section { transition: flex 0.2s ease-out; } /* Content areas should scroll */ #examples-content, #keeps-content, #reference-content, #mobile-console-content { overflow-y: auto !important; flex: 1 !important; min-height: 0 !important; } /* Clickable/draggable header on mobile */ .docs-header { position: relative; user-select: none; -webkit-user-select: none; transition: background-color 0.15s ease; box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.15); z-index: 1; } /* Editor header */ .editor-header { position: relative; user-select: none; -webkit-user-select: none; box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.15); z-index: 1; }
[data-theme="dark"] .docs-header, [data-theme="dark"] .editor-header { box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.4); } .docs-header:hover { background: rgba(128, 128, 128, 0.15); } .docs-header:active, .docs-header.dragging { background: rgba(128, 128, 128, 0.25); } /* Collapse/expand indicator - positioned triangle, not clickable itself */ .mobile-collapse-indicator { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 0; height: 0; pointer-events: none; /* Down-pointing triangle (expanded state) */ border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 8px solid currentColor; opacity: 0.85; } /* Right-pointing triangle (collapsed state) */ .panel-collapsed .mobile-collapse-indicator { border-left: 8px solid currentColor; border-right: none; border-top: 5px solid transparent; border-bottom: 5px solid transparent; transform: translateY(-50%); } /* Per-panel arrow colors */ #preview-panel .mobile-collapse-indicator { color: rgb(88, 78, 108); } #reference-panel .mobile-collapse-indicator { color: rgb(100, 149, 237); } #editor-panel .mobile-collapse-indicator { color: var(--text-primary); } [data-theme="dark"] #preview-panel .mobile-collapse-indicator { color: rgb(200, 190, 220); } /* Headers are draggable - cursor indicates this */ .docs-header, .editor-header { cursor: ns-resize; touch-action: none; } /* Arrow colors inherited from panel headers - no extra rules needed */ /* Backdrop that fills remaining space - shows when panels collapsed */ .mobile-checkerboard { display: none; flex: 1; min-height: 0; background: rgba(100, 149, 237, 0.25); /* More visible blue */ border-top: 2px dashed rgba(100, 149, 237, 0.5); } /* When ALL panels collapsed - distinct checkerboard pattern */ .mobile-checkerboard.all-collapsed { background: repeating-linear-gradient( 45deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.1) 10px, rgba(128, 128, 128, 0.2) 10px, rgba(128, 128, 128, 0.2) 20px ); border-top: 2px dashed rgba(128, 128, 128, 0.5); } [data-theme="dark"] .mobile-checkerboard { background: rgba(100, 149, 237, 0.15); border-top: 2px dashed rgba(100, 149, 237, 0.3); } [data-theme="dark"] .mobile-checkerboard.all-collapsed { background: repeating-linear-gradient( 45deg, rgba(200, 200, 200, 0.05), rgba(200, 200, 200, 0.05) 10px, rgba(200, 200, 200, 0.1) 10px, rgba(200, 200, 200, 0.1) 20px ); border-top: 2px dashed rgba(200, 200, 200, 0.3); } /* Hide Split.js gutters on mobile - we use headers instead */ .gutter { display: none !important; } }
/* Desktop gutters - still use Split.js */ @media (min-width: 769px) { .gutter { display: block !important; } /* Hide mobile-only elements on desktop */ .mobile-only { display: none !important; } /* Hide mobile console content on desktop */ #mobile-console-content { display: none !important; } }
/* Split.js Gutter Styles - smaller since headers handle reordering */ .gutter { transition: all 0.2s; background-color: var(--border-color) !important; } .gutter.gutter-vertical { height: 4px !important; min-height: 4px !important; } .gutter.gutter-horizontal { width: 4px !important; min-width: 4px !important; }
/* Main vertical gutter (between top and bottom rows) */ #top-row + .gutter.gutter-vertical { background: linear-gradient(to right, rgba(255, 140, 0, 0.3) 0%, rgba(255, 140, 0, 0.3) 50%, rgba(100, 149, 237, 0.3) 50%, rgba(100, 149, 237, 0.3) 100%) !important; /* Orange left, Blue right */ }
/* Top row horizontal gutter (between editor and preview) */ #editor-panel + .gutter.gutter-horizontal { background-color: rgb(240, 235, 250) !important; /* Light muted purple to match aesthetic.computer tab */ } @media (prefers-color-scheme: dark) { #editor-panel + .gutter.gutter-horizontal { background-color: rgb(48, 43, 58) !important; /* Dark muted purple for dark mode */ } } [data-theme="dark"] #editor-panel + .gutter.gutter-horizontal { background-color: rgb(48, 43, 58) !important; /* Dark muted purple for dark mode */ } [data-theme="light"] #editor-panel + .gutter.gutter-horizontal { background-color: rgb(240, 235, 250) !important; /* Light muted purple for light mode */ }
/* Bottom row horizontal gutter (between reference and console) */ #reference-panel + .gutter.gutter-horizontal { background-color: rgba(100, 149, 237, 0.3) !important; /* Blue */ }
@media (hover: hover) { .gutter:hover { opacity: 0.6; } }
/* Default cursors for gutters */ .gutter.gutter-horizontal { cursor: col-resize; }
.gutter.gutter-vertical { cursor: row-resize; } /* Ensure rows contain their gutters properly */ .top-row, .bottom-row { position: relative; }
/* Center square at the intersection of all 4 gutters */ #center-square { position: fixed; width: 16px; height: 16px; display: none; /* Hidden by default — shown by JS when Split.js gutters exist */ background: radial-gradient(circle at 35% 35%, rgba(255, 180, 220, 0.95), rgba(205, 120, 170, 0.9)); border: 2px solid rgba(180, 100, 150, 0.6); border-radius: 50%; cursor: move; z-index: 1001; pointer-events: auto; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.4); transition: transform 0.1s ease, box-shadow 0.1s ease; }
#center-square:hover { transform: translate(-50%, -50%) scale(1.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 2px rgba(255, 255, 255, 0.5); border-color: rgba(200, 100, 160, 0.8); }
#center-square.dragging { transform: translate(-50%, -50%) scale(1.2); background: radial-gradient(circle at 35% 35%, rgba(255, 200, 230, 1), rgba(220, 140, 190, 1)); box-shadow: 0 3px 10px rgba(180, 100, 150, 0.4), inset 0 1px 2px rgba(255, 255, 255, 0.6); border-color: rgba(180, 80, 140, 0.9); }
/* Syntax highlighting blink effect */ .blink-highlight { animation: highlight-blink 0.2s ease-in-out; }
@keyframes highlight-blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
/* Monaco Editor Selection Highlighting */ /* Let Monaco's theme handle selection via editor.selectionBackground */ /* Only use CSS as fallback for ::selection pseudo-element */ /* Fallback ::selection for copy/paste operations */ .monaco-editor *::selection { background-color: rgba(255, 140, 0, 0.6) !important; color: inherit !important; } .monaco-editor .view-line::selection, .monaco-editor .view-line *::selection { background-color: rgba(255, 140, 0, 0.6) !important; } /* Dark mode fallback */ @media (prefers-color-scheme: dark) { .monaco-editor *::selection, .monaco-editor .view-line::selection, .monaco-editor .view-line *::selection { background-color: rgba(255, 140, 0, 0.8) !important; } } [data-theme="dark"] .monaco-editor *::selection, [data-theme="dark"] .monaco-editor .view-line::selection, [data-theme="dark"] .monaco-editor .view-line *::selection { background-color: rgba(255, 140, 0, 0.8) !important; } /* Light mode override - must come after dark media query */ [data-theme="light"] .monaco-editor *::selection, [data-theme="light"] .monaco-editor .view-line::selection, [data-theme="light"] .monaco-editor .view-line *::selection { background-color: rgba(255, 140, 0, 0.6) !important; }
/* Text shadow for editor - matches prompt HUD preview */ @media (prefers-color-scheme: light) { .monaco-editor .view-line span { text-shadow: 0.5px 0.5px 0px rgba(0, 0, 0, 0.25); } .function-item { text-shadow: 0.5px 0.5px 0px rgba(0, 0, 0, 0.25); } } @media (prefers-color-scheme: dark) { .monaco-editor .view-line span { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); } .function-item { background: #2d2d30; text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); } } /* Manual theme overrides for text shadow - must come after media queries */ [data-theme="light"] .monaco-editor .view-line span { text-shadow: 0.5px 0.5px 0px rgba(0, 0, 0, 0.25); } [data-theme="light"] .function-item { text-shadow: 0.5px 0.5px 0px rgba(0, 0, 0, 0.25); background: var(--bg-tertiary); } [data-theme="dark"] .monaco-editor .view-line span { text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); } [data-theme="dark"] .function-item { background: #2d2d30; text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); }
/* QR Code in header - border is drawn into the image */ #preview-qr { cursor: pointer; transition: filter 0.15s; margin-left: 0px; margin-right: 0; display: block; height: 34px !important; /* Compact but scannable */ } @media (hover: hover) { #preview-qr:hover { filter: brightness(1.1); } [data-theme="dark"] #preview-qr:hover { filter: brightness(1.15); } } #preview-qr:active { filter: brightness(0.9); } [data-theme="dark"] #preview-qr:active { filter: brightness(0.85); } @media (prefers-color-scheme: dark) { #preview-qr:active { filter: brightness(0.85); } } /* QR Modal - Aesthetic Computer style */ #qr-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.95); z-index: 999999; /* Above everything */ justify-content: center; align-items: center; cursor: pointer; } #qr-modal.show { display: flex; } #qr-modal-content { display: flex; flex-direction: column; align-items: center; gap: 16px; background: black; border: 3px solid white; padding: 24px; } #qr-modal canvas { image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; border: 3px solid white; } #qr-modal-url { color: white; font-family: 'YWFTProcessing-Regular', monospace; font-size: 14px; background: black; padding: 8px 16px; border: 2px solid white; user-select: all; word-break: break-all; max-width: 350px; text-align: center; } #qr-modal-close { color: rgba(255, 255, 255, 0.6); font-family: 'YWFTProcessing-Regular', monospace; font-size: 11px; margin-top: 4px; } </style></head>
<body> <!-- Loading splash - logo starts here, flies to header and stays --> <div id="loading-splash" style=" position: fixed; inset: 0; z-index: 99999; background: var(--bg-primary); display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; "> </div> <!-- The actual logo - starts centered, flies to header --> <div id="splash-logo" class="baby-colors" style=" position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-weight: bold; font-size: var(--splash-logo-size); display: inline-flex; width: fit-content; height: fit-content; z-index: 100000; will-change: transform, font-size, top, left; "> <span class="splash-letter" style="color: #FF6B6B;">K</span> <span class="splash-letter" style="color: #4ECDC4;">i</span> <span class="splash-letter" style="color: #FFE66D;">d</span> <span class="splash-letter" style="color: #95E1D3;">L</span> <span class="splash-letter" style="color: #F38181;">i</span> <span class="splash-letter" style="color: #AA96DA;">s</span> <span class="splash-letter" style="color: #70D6FF;">p</span> <span class="splash-letter splash-dot connectivity-dot disconnected" style="color: var(--text-tertiary);">.</span> <span class="splash-letter com-part" style="color: #FF6B6B;">c</span> <span class="splash-letter com-part" style="color: #9370DB;">o</span> <span class="splash-letter com-part" style="color: #90EE90;">m</span> <span class="splash-route" aria-hidden="true"></span> </div> <style> /* Splash animation styles */ :root { /* Slightly smaller on desktop for more breathing room */ --splash-logo-size: clamp(48px, min(12vw, 16vh), 120px); } @media (max-width: 768px) { /* Slightly larger on single-column/mobile */ :root { --splash-logo-size: clamp(64px, min(22vw, 24vh), 140px); } } @keyframes letterWiggle { 0%, 100% { transform: translateY(0) rotate(0deg); } 25% { transform: translateY(-3px) rotate(-3deg); } 75% { transform: translateY(2px) rotate(2deg); } } @keyframes dotBounce { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-8px) scale(1.2); } } .splash-letter { display: inline-block; animation: letterWiggle 0.4s ease-in-out infinite; text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2); will-change: transform; } .splash-letter:nth-child(1) { animation-delay: 0ms; } .splash-letter:nth-child(2) { animation-delay: 50ms; } .splash-letter:nth-child(3) { animation-delay: 100ms; } .splash-letter:nth-child(4) { animation-delay: 150ms; } .splash-letter:nth-child(5) { animation-delay: 200ms; } .splash-letter:nth-child(6) { animation-delay: 250ms; } .splash-letter:nth-child(7) { animation-delay: 300ms; } .splash-letter.splash-dot { animation: dotBounce 0.6s ease-in-out infinite; animation-delay: 350ms; } .splash-letter:nth-child(9) { animation-delay: 400ms; } .splash-letter:nth-child(10) { animation-delay: 450ms; } .splash-letter:nth-child(11) { animation-delay: 500ms; } #splash-logo.flying { transition: top 0.5s ease-out, left 0.5s ease-out, font-size 0.5s ease-out, transform 0.5s ease-out; } #splash-logo.flying .splash-letter { animation: none !important; } #splash-logo.landed { position: relative !important; top: auto !important; left: auto !important; transform: none !important; font-size: 20px; z-index: 1 !important; line-height: 32px; } #splash-logo.landed .splash-letter { animation: none !important; }
#splash-logo .splash-route { margin-left: 0; font-family: inherit; font-size: inherit; font-weight: inherit; letter-spacing: inherit; color: var(--text-tertiary); text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2); display: inline-flex; align-items: center; }
#splash-logo .splash-route:empty { display: none; }
@media (max-width: 768px) { #splash-logo.landed { font-size: 16px !important; line-height: 28px !important; } } #loading-splash.fading { transition: opacity 0.3s ease; opacity: 0; } </style> <script> // Check if coming from prompt for back navigation (function() { try { const labelBackSource = sessionStorage.getItem('aesthetic-labelBack-source'); if (labelBackSource === 'prompt') { // Clear the source since we've captured it sessionStorage.removeItem('aesthetic-labelBack-source'); // Determine back URL based on environment const isLocal = window.location.hostname === 'localhost' || window.location.hostname.includes('127.0.0.1'); window.kidlispBackToPrompt = isLocal ? '/' : 'https://aesthetic.computer'; } } catch (e) { // sessionStorage not available } })(); </script> <script> // Splash animation - must be inline to run early (function() { let animationTriggered = false;
window.getInitialRouteCode = window.getInitialRouteCode || function() { const urlPath = window.location.pathname; const match = urlPath.match(/\/\$?([a-zA-Z0-9]+)$/); return match?.[1] || ''; };
window.setRouteCodeInUI = window.setRouteCodeInUI || function() {}; window.triggerSplashAnimation = function() { if (animationTriggered) return; animationTriggered = true; const splash = document.getElementById('loading-splash'); const splashLogo = document.getElementById('splash-logo'); const mainEl = document.getElementById('main-split'); const landingZone = document.getElementById('logo-landing-zone'); const initialRouteCode = window.getInitialRouteCode ? window.getInitialRouteCode() : ''; const splashRoute = splashLogo?.querySelector('.splash-route'); if (splash && splashLogo && mainEl && landingZone) { if (splashRoute) { if (initialRouteCode) { const slug = `/$${initialRouteCode}`; const routeColors = ['#FF6B6B','#4ECDC4','#FFE66D','#95E1D3','#F38181','#AA96DA','#70D6FF']; splashRoute.innerHTML = slug .split('') .map((char, index) => { const delay = 520 + index * 35; const safeChar = char === ' ' ? ' ' : char; const color = routeColors[index % routeColors.length]; return `<span class="splash-letter" style="color: ${color}; animation-delay: ${delay}ms;">${safeChar}</span>`; }) .join(''); } else { splashRoute.innerHTML = ''; } }
// Get actual computed font size (from clamp CSS) const computedStyle = window.getComputedStyle(splashLogo); const startFontSize = parseFloat(computedStyle.fontSize); const isMobile = window.innerWidth <= 768; const endFontSize = isMobile ? 16 : 20; // Calculate start position - logo starts centered with transform const logoWidth = splashLogo.offsetWidth; const logoHeight = splashLogo.offsetHeight; const startTop = (window.innerHeight / 2) - (logoHeight / 2); const startLeft = (window.innerWidth / 2) - (logoWidth / 2); // Stop the wiggle animation immediately const letters = splashLogo.querySelectorAll('.splash-letter'); letters.forEach(letter => { letter.style.animation = 'none'; }); // Convert from percentage + transform to absolute pixel position splashLogo.style.top = startTop + 'px'; splashLogo.style.left = startLeft + 'px'; splashLogo.style.transform = 'none'; // Show main content (logo still visible on top) mainEl.style.opacity = '1';
// Dwell: if loading a $code, pause so the user can read it on the centered logo const dwell = initialRouteCode ? 800 : 0; let flyClones = null;
function startFlyAnimation() { // Force reflow to apply position lock and get landing zone position void landingZone.offsetHeight; const landingRect = landingZone.getBoundingClientRect();
// Calculate target - center logo vertically in landing zone const targetTop = landingRect.top + (landingRect.height - endFontSize) / 2; const targetLeft = landingRect.left + 12;
// === Fly-in: measure /$code while logo is STILL BIG, break off simultaneously === if (initialRouteCode && splashRoute) { const splashChars = splashRoute.querySelectorAll('.splash-letter'); if (splashChars.length > 0) { // Measure source positions at BIG size const sourceRects = Array.from(splashChars).map(ch => { const r = ch.getBoundingClientRect(); const cs = getComputedStyle(ch); return { top: r.top, left: r.left, text: ch.textContent, fontSize: parseFloat(cs.fontSize), color: cs.color }; });
// Populate hidden target to measure destination const routeCodeTab = document.getElementById('route-code-tab'); if (routeCodeTab) { routeCodeTab.classList.add('fly-target'); window.setRouteCodeInUI?.(initialRouteCode, { immediate: true, animate: false }); void routeCodeTab.offsetHeight; const targetChars = routeCodeTab.querySelectorAll('.code-char'); const targetRects = Array.from(targetChars).map(ch => ch.getBoundingClientRect());
// Hide splash route text (clones will replace it) splashRoute.style.visibility = 'hidden';
// Create fixed-position clones at BIG source positions const clones = []; sourceRects.forEach((src, i) => { const clone = document.createElement('span'); clone.className = 'route-code-fly-char'; clone.textContent = src.text; clone.style.top = src.top + 'px'; clone.style.left = src.left + 'px'; clone.style.fontSize = src.fontSize + 'px'; clone.style.color = src.color || 'rgb(147, 112, 219)'; document.body.appendChild(clone); clones.push(clone); }); flyClones = { clones, targetRects, targetChars, routeCodeTab }; } } }
// Enable transitions splashLogo.classList.add('flying');
// Animate logo to header AND clones to dropdown simultaneously requestAnimationFrame(() => { requestAnimationFrame(() => { splashLogo.style.top = targetTop + 'px'; splashLogo.style.left = targetLeft + 'px'; splashLogo.style.fontSize = endFontSize + 'px'; // Fly clones to dropdown at the same time if (flyClones) { flyClones.clones.forEach((clone, i) => { if (flyClones.targetRects[i]) { clone.style.top = flyClones.targetRects[i].top + 'px'; clone.style.left = flyClones.targetRects[i].left + 'px'; clone.style.fontSize = getComputedStyle(flyClones.targetChars[i]).fontSize; } }); } }); });
// Clean up clones after animation if (flyClones) { setTimeout(() => { flyClones.routeCodeTab.classList.remove('fly-target'); flyClones.clones.forEach(c => c.remove()); if (splashRoute) splashRoute.innerHTML = ''; }, 500); } }
if (dwell > 0) { setTimeout(startFlyAnimation, dwell); } else { startFlyAnimation(); } // After animation completes, snap into place setTimeout(() => { splashLogo.classList.remove('flying'); splashLogo.classList.add('landed'); // Move into DOM and clear inline styles landingZone.appendChild(splashLogo); splashLogo.style.cssText = ''; // Re-apply stochastic coloring (but preserve .com-part colors) const colors = ['#FF7096', '#FFB870', '#FFEA70', '#8AFF8A', '#70D6FF', '#C596FF', '#FF96C5']; // Fixed colors for com-part letters (c=red, o=purple, m=green) const comColors = { 'c': '#FF6B6B', 'o': '#9370DB', 'm': '#90EE90' }; let lastColorIndex = -1; letters.forEach(letter => { let color; if (letter.classList.contains('com-part')) { color = comColors[letter.textContent] || '#70D6FF'; } else if (letter.classList.contains('splash-dot')) { return; } else { let randomIndex; do { randomIndex = Math.floor(Math.random() * colors.length); } while (randomIndex === lastColorIndex && colors.length > 1); lastColorIndex = randomIndex; color = colors[randomIndex]; } letter.style.color = color; const rgb = color.match(/\w\w/g); if (rgb) { const r = Math.max(0, parseInt(rgb[0], 16) - 80); const g = Math.max(0, parseInt(rgb[1], 16) - 80); const b = Math.max(0, parseInt(rgb[2], 16) - 80); letter.style.textShadow = `1px 1px 0px rgb(${r}, ${g}, ${b})`; } });
// Clean up splash route (clones already handled above) if (splashRoute) splashRoute.innerHTML = ''; if (initialRouteCode && !flyClones) { // Fallback: no fly animation happened, just show immediately window.setRouteCodeInUI?.(initialRouteCode, { immediate: true, animate: false }); }
// Add click handler - back navigation OR preferences modal splashLogo.style.cursor = 'pointer'; if (window.kidlispBackToPrompt) { splashLogo.title = 'Back to prompt'; splashLogo.addEventListener('click', () => { window.location.href = window.kidlispBackToPrompt; }); } else { splashLogo.title = 'Preferences'; splashLogo.addEventListener('click', () => { if (typeof window.showPreferencesModal === 'function') { window.showPreferencesModal(); } }); } }, 550 + dwell);
// Fade out splash background setTimeout(() => { splash.classList.add('fading'); setTimeout(() => splash.remove(), 300); }, 200 + dwell); } else { // Fallback: just show everything if (mainEl) mainEl.style.opacity = '1'; if (splash) { splash.classList.add('fading'); setTimeout(() => splash.remove(), 300); } if (splashLogo && landingZone) { splashLogo.classList.add('landed'); splashLogo.style.cssText = ''; landingZone.appendChild(splashLogo); if (initialRouteCode) { window.setRouteCodeInUI?.(initialRouteCode, { immediate: true, animate: false }); } // Add click handler - back navigation OR preferences modal splashLogo.style.cursor = 'pointer'; if (window.kidlispBackToPrompt) { splashLogo.title = 'Back to prompt'; splashLogo.addEventListener('click', () => { window.location.href = window.kidlispBackToPrompt; }); } else { splashLogo.title = 'Preferences'; splashLogo.addEventListener('click', () => { if (typeof window.showPreferencesModal === 'function') { window.showPreferencesModal(); } }); } } } }; // Safety timeout - never leave splash stuck for more than 5 seconds setTimeout(() => { if (!animationTriggered) { window.triggerSplashAnimation(); } }, 5000); // Also try on window load as backup window.addEventListener('load', () => { setTimeout(() => { if (!animationTriggered && window.kidlispReady) { window.triggerSplashAnimation(); } }, 100); }); })(); </script>
<script> // 🔓 KidLisp Cryptographic Decryption Animation // Modes: 'quick' (~300ms), 'initial' (waits for signal), 'instant' (no animation) (function() { // Scramble character set (matching the $code animation) const SCRAMBLE_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789(),-=+*/<>:;\"\'.!?@#$%^&_~`[]{}|'; let isDecrypting = false; let currentAnimationId = 0; // Track which animation is active let pendingTargetText = null; // Store target text for safety let waitingForSignal = false; // For initial mode - keep scrambling until signaled let signalResolver = null; // Resolves when signal received // Preset animation modes const ANIMATION_MODES = { // Quick flash - ultra-fast ~100ms coded flash quick: { maxDuration: 100, // Target total animation time in ms scrambleIterations: 1, // Single scramble before settling minCharDelay: 1, // Minimum delay between chars waitForSignal: false, // Complete on its own flash: true, // Enable flash effect }, // Initial load - keeps scrambling until piece starts running initial: { revealDuration: 200, // How long the reveal takes once signaled (was 400) scrambleIterations: 1, // Single scramble during reveal (was 2) - faster! minCharDelay: 1, // Reveal speed (was 3) - faster! waitForSignal: true, // Keep scrambling until signaled maxWait: 5000, // Safety: auto-complete after 5s (was 10s - too long on mobile) }, // Instant - no animation at all instant: { skipAnimation: true } }; // 🚀 Flying code animation - blast old code away, bring new code in! // codeHtml = highlighted HTML for animation colors // fullSourceCode = actual source to set in editor (may be longer than animated portion) // Track current flying animation state let flyingAnimationState = { active: false, sourceCode: null, // Track which code is being animated container: null, // Reference to cleanup container timeouts: [], // Track all setTimeout IDs for cleanup resolve: null // Store resolve function for early completion }; // Cancel any in-progress flying animation function cancelFlyingAnimation(setContent = false) { if (!flyingAnimationState.active) return; // Clear all pending timeouts flyingAnimationState.timeouts.forEach(id => clearTimeout(id)); flyingAnimationState.timeouts = []; // Remove the animation container if (flyingAnimationState.container) { flyingAnimationState.container.remove(); flyingAnimationState.container = null; } // Resolve the previous promise if (flyingAnimationState.resolve) { flyingAnimationState.resolve(); flyingAnimationState.resolve = null; } flyingAnimationState.active = false; flyingAnimationState.sourceCode = null; } window.flyCodeToEditor = function(sourceEl, codeHtml, fullSourceCode) { return new Promise((resolve) => { const editorEl = document.getElementById('kidlisp-editor'); const monacoEditor = window.editor; // Global Monaco editor reference if (!sourceEl || !editorEl || !monacoEditor) { resolve(); return; } // Check if clicking the same code that's already animating - ignore if (flyingAnimationState.active && flyingAnimationState.sourceCode === fullSourceCode) { resolve(); return; } // If different animation is running, cancel it and set content immediately if (flyingAnimationState.active) { cancelFlyingAnimation(true); } // Start new animation state flyingAnimationState.active = true; flyingAnimationState.sourceCode = fullSourceCode; flyingAnimationState.resolve = resolve; flyingAnimationState.timeouts = []; // Get positions const sourceRect = sourceEl.getBoundingClientRect(); const targetRect = editorEl.getBoundingClientRect(); // Find Monaco's actual content area and get real metrics const viewLines = editorEl.querySelector('.view-lines'); const firstViewLine = editorEl.querySelector('.view-line'); const viewLinesRect = viewLines ? viewLines.getBoundingClientRect() : null; const firstLineRect = firstViewLine ? firstViewLine.getBoundingClientRect() : null; // Get actual Monaco line height from rendered lines const allViewLines = editorEl.querySelectorAll('.view-line'); let actualLineHeight = 22; // Default for 16px font if (allViewLines.length >= 2) { const line1 = allViewLines[0].getBoundingClientRect(); const line2 = allViewLines[1].getBoundingClientRect(); actualLineHeight = line2.top - line1.top; } // Monaco editor font metrics - match actual editor (fontSize: 16) const fontSize = 16; const charWidth = 9.6; // ~0.6em for monospace at 16px const lineHeight = actualLineHeight; // Get Monaco's actual font from the rendered editor const monacoFontFamily = firstViewLine ? window.getComputedStyle(firstViewLine).fontFamily : "Menlo, Monaco, 'Courier New', monospace"; // Detect theme for proper text shadow const isDark = document.documentElement.getAttribute('data-theme') === 'dark' || (document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches); const textShadow = isDark ? '1px 1px 0px rgba(0, 0, 0, 0.5)' : '0.5px 0.5px 0px rgba(0, 0, 0, 0.25)'; // Use actual Monaco content position - align with first character const editorContentLeft = firstLineRect ? firstLineRect.left : (viewLinesRect ? viewLinesRect.left : (targetRect.left + 50)); const editorContentTop = firstLineRect ? firstLineRect.top : (viewLinesRect ? viewLinesRect.top : (targetRect.top + 5)); // ═══════════════════════════════════════════════════════════ // PHASE 0: SWING TICKER BACK (visual windup before chars fly) // ═══════════════════════════════════════════════════════════ const tickerSwingDuration = 120; sourceEl.style.transition = `transform ${tickerSwingDuration}ms ease-out`; sourceEl.style.transform = 'translateX(-8px) scale(0.98)'; // Create container for all flying characters const container = document.createElement('div'); container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 100001; overflow: hidden;'; document.body.appendChild(container); // Store container in state for cleanup flyingAnimationState.container = container; // Helper to track timeouts const trackTimeout = (fn, delay) => { const id = setTimeout(fn, delay); flyingAnimationState.timeouts.push(id); return id; }; // ═══════════════════════════════════════════════════════════ // PHASE 1: BLAST OLD CODE AWAY (from editor) // Extract colors and font weights from Monaco's actual rendered spans // ═══════════════════════════════════════════════════════════ const oldCode = monacoEditor.getValue() || ''; const oldChars = oldCode.slice(0, 80); // Try to get colors and weights from Monaco's rendered view BEFORE clearing const monacoSpans = viewLines ? viewLines.querySelectorAll('.view-line span span') : []; const spanStyles = []; monacoSpans.forEach(span => { const computed = window.getComputedStyle(span); const color = computed.color || 'rgb(220, 220, 220)'; const weight = computed.fontWeight || '400'; const text = span.textContent || ''; for (let i = 0; i < text.length; i++) { spanStyles.push({ color, weight }); } }); // CLEAR the editor immediately so flying chars appear to leave! isSettingValueProgrammatically = true; monacoEditor.setValue(''); isSettingValueProgrammatically = false; let oldLine = 0; let oldCol = 0; let oldCharIndex = 0; let colorIndex = 0; for (let i = 0; i < oldChars.length; i++) { const char = oldChars[i]; if (char === '\n') { oldLine++; oldCol = 0; continue; } if (char === ' ') { oldCol++; colorIndex++; continue; } const charEl = document.createElement('span'); charEl.textContent = char; // EXACT position in editor where this character appears const startX = editorContentLeft + (oldCol * charWidth); const startY = editorContentTop + (oldLine * lineHeight); // Blast direction - upward and outward from editor const spreadAngle = ((oldCol / 40) - 0.5) * Math.PI * 0.8; const distance = 200 + Math.random() * 150; const targetX = startX + Math.sin(spreadAngle) * distance * 0.5; const targetY = startY - distance; // Use actual Monaco color and weight if available const charStyle = spanStyles[colorIndex] || { color: 'rgb(255, 255, 255)', weight: '400' }; charEl.style.cssText = ` position: absolute; font-family: ${monacoFontFamily}; font-size: ${fontSize}px; line-height: ${lineHeight}px; font-weight: ${charStyle.weight}; letter-spacing: 0px; color: ${charStyle.color}; text-shadow: ${textShadow}; pointer-events: none; left: ${startX}px; top: ${startY}px; opacity: 1; transform: rotate(0deg); will-change: transform, opacity, left, top; white-space: pre; `; container.appendChild(charEl); // Staggered blast - slower const delay = oldCharIndex * 8; ((el, tx, ty, d) => { trackTimeout(() => { el.style.transition = ` left 400ms cubic-bezier(0.2, 0.8, 0.3, 1), top 400ms cubic-bezier(0.1, 0.6, 0.2, 1), transform 400ms ease-out, opacity 150ms ease-out 300ms `; el.style.left = tx + 'px'; el.style.top = ty + 'px'; el.style.transform = `rotate(${(Math.random() - 0.5) * 60}deg)`; el.style.opacity = '0'; }, d); })(charEl, targetX, targetY, delay); oldCol++; oldCharIndex++; colorIndex++; } // ═══════════════════════════════════════════════════════════ // PHASE 2: BRING NEW CODE IN (from ticker) // Parse the highlighted HTML to get colors // ═══════════════════════════════════════════════════════════ // Parse highlighted HTML into chars with colors and font weights const tempDiv = document.createElement('div'); tempDiv.innerHTML = codeHtml; const coloredChars = []; function extractColoredChars(node, inheritedWeight = 'normal') { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent || ''; // Try to get color and weight from parent span let color = 'rgb(255, 255, 255)'; let weight = inheritedWeight; if (node.parentElement) { if (node.parentElement.style.color) { color = node.parentElement.style.color; } if (node.parentElement.style.fontWeight) { weight = node.parentElement.style.fontWeight; } } for (const char of text) { coloredChars.push({ char, color, weight }); } } else if (node.nodeType === Node.ELEMENT_NODE) { // Get color and weight from this element's style const el = node; const style = el.getAttribute('style') || ''; const colorMatch = style.match(/color:\s*([^;]+)/); const weightMatch = style.match(/font-weight:\s*([^;]+)/); const elColor = colorMatch ? colorMatch[1].trim() : null; const elWeight = weightMatch ? weightMatch[1].trim() : inheritedWeight; for (const child of node.childNodes) { if (child.nodeType === Node.TEXT_NODE) { const text = child.textContent || ''; const useColor = elColor || 'rgb(255, 255, 255)'; for (const char of text) { coloredChars.push({ char, color: useColor, weight: elWeight }); } } else { extractColoredChars(child, elWeight); } } } } extractColoredChars(tempDiv); // Limit chars for performance const maxChars = 100; const charsToAnimate = coloredChars.slice(0, maxChars); // Delay new code entrance - account for ticker swing const blastHeadStart = tickerSwingDuration + 100; // Reset ticker transform after swing trackTimeout(() => { sourceEl.style.transition = 'transform 200ms ease-out'; sourceEl.style.transform = ''; }, tickerSwingDuration + 50); let newLine = 0; let newCol = 0; let newCharIndex = 0; for (let i = 0; i < charsToAnimate.length; i++) { const { char, color, weight } = charsToAnimate[i]; if (char === '\n') { newLine++; newCol = 0; continue; } if (char === ' ') { newCol++; continue; } const charEl = document.createElement('span'); charEl.textContent = char; // Start position - all chars emerge from left side of ticker (queue up) const startX = sourceRect.left + 4; const startY = sourceRect.top + sourceRect.height / 2 - fontSize / 2; // EXACT target position in editor const targetX = editorContentLeft + (newCol * charWidth); const targetY = editorContentTop + (newLine * lineHeight); charEl.style.cssText = ` position: absolute; font-family: ${monacoFontFamily}; font-size: ${fontSize}px; line-height: ${lineHeight}px; font-weight: ${weight || 'normal'}; letter-spacing: 0px; color: ${color}; text-shadow: ${textShadow}; pointer-events: none; left: ${startX}px; top: ${startY}px; opacity: 1; will-change: left, top; white-space: pre; `; container.appendChild(charEl); // Staggered entrance - slower const delay = blastHeadStart + newCharIndex * 10; ((el, tx, ty, d) => { trackTimeout(() => { el.style.transition = ` left 350ms cubic-bezier(0.2, 0.7, 0.3, 1), top 350ms cubic-bezier(0.1, 0.6, 0.2, 1) `; el.style.left = tx + 'px'; el.style.top = ty + 'px'; // Stay opaque - no fade, no scale! }, d); })(charEl, targetX, targetY, delay); newCol++; newCharIndex++; } // Clean up after both animations complete and set editor content const totalDuration = blastHeadStart + (newCharIndex * 10) + 400; trackTimeout(() => { container.remove(); // Set editor content directly - use full source if provided, else animated text const textToSet = fullSourceCode || charsToAnimate.map(c => c.char).join(''); if (monacoEditor && textToSet) { isSettingValueProgrammatically = true; monacoEditor.setValue(textToSet); isSettingValueProgrammatically = false; } editorEl.classList.add('code-flash'); trackTimeout(() => editorEl.classList.remove('code-flash'), 100); // Mark animation complete and clear state flyingAnimationState.active = false; flyingAnimationState.sourceCode = null; flyingAnimationState.container = null; flyingAnimationState.timeouts = []; flyingAnimationState.resolve = null; resolve(textToSet); }, totalDuration); }); }; // Signal that piece is running - trigger the reveal phase window.signalDecryptionReady = function() { if (waitingForSignal && signalResolver) { waitingForSignal = false; signalResolver(); signalResolver = null; } }; // Decrypt text in Monaco editor with scramble animation // mode: 'quick' (default), 'initial', or 'instant' window.decryptIntoEditor = function(targetText, editor, options = {}) { if (!editor || !targetText) return Promise.resolve(); // Cancel any existing animation immediately if (isDecrypting) { isDecrypting = false; waitingForSignal = false; if (signalResolver) { signalResolver(); signalResolver = null; } } // Determine mode and settings const mode = options.mode || 'quick'; const modeSettings = ANIMATION_MODES[mode] || ANIMATION_MODES.quick; // Allow custom overrides const settings = { ...modeSettings, ...options }; // Store target for safety pendingTargetText = targetText; // Skip animation entirely if requested if (settings.skipAnimation) { editor.setValue(targetText); pendingTargetText = null; return Promise.resolve(); } // Assign unique ID to this animation const animationId = ++currentAnimationId; isDecrypting = true; return new Promise(async (resolve) => { const chars = targetText.split(''); // Initialize with scrambled version const getScrambledChar = (original) => { if (original === '\n' || original === ' ') return original; return SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)]; }; const buildScrambledText = () => { return chars.map(char => { if (char === '\n' || char === ' ') return char; return getScrambledChar(char); }).join(''); }; // Safety: Always ensure we end with correct text const finalize = () => { if (pendingTargetText === targetText) { editor.setValue(targetText); pendingTargetText = null; } if (currentAnimationId === animationId) { isDecrypting = false; waitingForSignal = false; } resolve(); }; // Set initial scrambled text editor.setValue(buildScrambledText()); // Phase 1: Wait for signal (if waitForSignal mode) if (settings.waitForSignal) { waitingForSignal = true; // Create promise that resolves when signaled or timeout const waitPromise = new Promise((resolveWait) => { signalResolver = resolveWait; // Safety timeout setTimeout(() => { if (waitingForSignal && currentAnimationId === animationId) { waitingForSignal = false; resolveWait(); } }, settings.maxWait || 10000); }); // Scramble animation while waiting // THROTTLED: Only update editor every ~120ms to avoid crashing // iOS Safari / mobile WebKit. editor.setValue() is expensive - triggers // full re-tokenization, layout, and DOM update each call. let scrambleFrame; let lastScrambleUpdate = 0; const SCRAMBLE_INTERVAL = 120; // ms between editor updates const scrambleWhileWaiting = () => { if (currentAnimationId !== animationId || !isDecrypting) { finalize(); return; }
if (!waitingForSignal) { // Signal received - proceed to reveal phase return; }
// Only update editor at throttled rate const now = performance.now(); if (now - lastScrambleUpdate >= SCRAMBLE_INTERVAL) { editor.setValue(buildScrambledText()); lastScrambleUpdate = now; } scrambleFrame = requestAnimationFrame(scrambleWhileWaiting); };
scrambleWhileWaiting(); await waitPromise; cancelAnimationFrame(scrambleFrame); // Check if cancelled during wait if (currentAnimationId !== animationId || !isDecrypting) { finalize(); return; } } // Phase 2: Reveal animation (throttled setValue for mobile perf) const settled = new Array(chars.length).fill(false); const scrambleCounts = new Array(chars.length).fill(0); let currentRevealIndex = 0; let lastRevealTime = 0; let lastRevealUpdate = 0; const REVEAL_INTERVAL = 50; // ms between editor updates (faster than scramble since bounded) const revealStartTime = performance.now();
const { revealDuration, maxDuration, scrambleIterations, minCharDelay } = settings; const duration = revealDuration || maxDuration || 300;
// Calculate charDelay to fit animation in duration let charDelay = Math.max(minCharDelay || 2, Math.floor(duration / (chars.length * scrambleIterations))); charDelay = Math.min(charDelay, 15);
const animateReveal = () => { // Check if this animation was cancelled if (currentAnimationId !== animationId) { resolve(); return; }
// Check if stopped externally if (!isDecrypting) { finalize(); return; }
const now = performance.now(); const elapsed = now - revealStartTime;
// Safety: Force complete if we've exceeded duration if (elapsed > duration * 1.5) { finalize(); return; }
// Advance reveal index (logic runs every frame for timing accuracy) while (now - lastRevealTime >= charDelay && currentRevealIndex < chars.length) { scrambleCounts[currentRevealIndex]++;
if (scrambleCounts[currentRevealIndex] >= scrambleIterations) { settled[currentRevealIndex] = true; currentRevealIndex++; lastRevealTime = now; } else { break; } }
// Check if done if (settled.every(s => s)) { finalize(); return; }
// Only update the editor at throttled rate if (now - lastRevealUpdate >= REVEAL_INTERVAL) { const currentText = chars.map((char, i) => { if (settled[i]) return char; if (char === '\n' || char === ' ') return char; return getScrambledChar(char); }).join('');
if (currentAnimationId === animationId) { editor.setValue(currentText); } lastRevealUpdate = now; }
requestAnimationFrame(animateReveal); };
// Start reveal animation requestAnimationFrame(animateReveal); }); }; // Stop decryption immediately and set final text window.stopDecryption = function(setFinalText = true) { if (isDecrypting && setFinalText && pendingTargetText && window.editor) { window.editor.setValue(pendingTargetText); } isDecrypting = false; waitingForSignal = false; pendingTargetText = null; if (signalResolver) { signalResolver(); signalResolver = null; } }; // Check if currently decrypting window.isDecrypting = function() { return isDecrypting; }; // Check if waiting for signal window.isWaitingForDecryptSignal = function() { return waitingForSignal; }; // Get pending target text (useful for debugging) window.getPendingDecryptText = function() { return pendingTargetText; }; // Legacy stubs for compatibility (chunking canvas removed) window.chunkingAddLog = function(message) {}; window.chunkingStop = function() {}; })(); </script>
<!-- <header> <h1>kidlisp</h1> <div class="tagline">The next-level pattern syntax for gen beta</div> </header> -->
<main id="main-split" style="opacity: 0; transition: opacity 0.4s ease 0.2s;"> <!-- Center square for simultaneous 4-panel resize --> <div id="center-square" title="Drag to resize all panels"></div> <div class="top-row" id="top-row"> <div class="editor-panel" id="editor-panel"> <div class="editor-header"> <!-- Settings gear - always visible, positioned right of header before collapse arrow --> <svg id="settings-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Preferences"> <circle cx="12" cy="12" r="3"/> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/> </svg> <div style="display: flex; flex-direction: column; align-items: flex-start;"> <!-- Logo landing zone - splash logo will fly here and stay (clickable for preferences) --> <div id="logo-landing-zone" style="margin-top: 4px; height: 24px; display: flex; align-items: center; cursor: pointer;" title="Preferences"> </div> </div> <!-- Mini playback controls for mobile when main controls are obscured --> <div id="mini-playback-controls"> <button id="mini-stop-button" aria-label="Stop"> <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"> <rect x="3" y="3" width="18" height="18" rx="3" ry="3"/> </svg> </button> <button id="mini-send-button" aria-label="Play"> <svg class="play-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"> <path d="M6 4.5c0-1.2 1.3-1.9 2.3-1.3l12 8c.9.6.9 2 0 2.6l-12 8c-1 .6-2.3-.1-2.3-1.3v-16z"/> </svg> <svg class="pause-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"> <rect x="5" y="3" width="5" height="18" rx="2" ry="2"/> <rect x="14" y="3" width="5" height="18" rx="2" ry="2"/> </svg> </button> </div> </div> <!-- Visualization Mode Tabs (hidden for now) --> <div class="viz-tabs" id="viz-tabs" style="display: none !important;"> </div> <!-- Text View (Monaco Editor) --> <div class="viz-container active" id="viz-text"> <div id="kidlisp-editor" class="decrypting"> </div> <!-- AI Assist Input - describe what you want --> <div id="ai-assist-bar"> <input type="text" id="ai-assist-input" placeholder="I wish..." autocomplete="off" /> <button id="ai-assist-send" aria-label="Generate" title="Generate KidLisp from description"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M22 2L11 13"/> <path d="M22 2L15 22L11 13L2 9L22 2Z"/> </svg> </button> </div> <!-- Playback Controls - inside viz-text so they can't overlap editor-header --> <!-- Order: slide (left), clear, stop (middle), send (right) --> <!-- Slide mode actions (legacy - hidden) --> <div id="slide-actions"></div> <div id="playback-controls"> <button id="slide-button" aria-label="Toggle slide mode" title="Slide mode - drag numbers to adjust"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="4" y1="12" x2="20" y2="12"/><circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/></svg></button> <button id="slide-set-btn" aria-label="Set slide values" title="Set current values">Set</button> <button id="clear-button" aria-label="Clear code"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> <line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18"/> </svg> </button> <button id="stop-button" aria-label="Stop"> <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"> <rect x="3" y="3" width="18" height="18" rx="3" ry="3"/> </svg> </button> <button id="send-button" aria-label="Play"> <svg class="play-icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> <path d="M6 4.5c0-1.2 1.3-1.9 2.3-1.3l12 8c.9.6.9 2 0 2.6l-12 8c-1 .6-2.3-.1-2.3-1.3v-16z"/> </svg> <svg class="pause-icon" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> <rect x="5" y="3" width="5" height="18" rx="2" ry="2"/> <rect x="14" y="3" width="5" height="18" rx="2" ry="2"/> </svg> </button> </div> </div> <!-- Pills View (3D Matter) - hidden for now --> <div class="viz-container" id="viz-pills" style="display: none !important;"> <canvas id="pills-canvas"></canvas> </div> </div> <div class="preview-panel" id="preview-panel" style="display: flex; flex-direction: column;"> <div class="docs-header" style="display: flex; align-items: center; gap: 8px;"> <div id="platform-selector" class="platform-selector"> <span id="preview-title" class="booting"><span class="title-full"><span class="title-letter">A</span><span class="title-letter">e</span><span class="title-letter">s</span><span class="title-letter">t</span><span class="title-letter">h</span><span class="title-letter">e</span><span class="title-letter">t</span><span class="title-letter">i</span><span class="title-letter">c</span></span><span class="title-short">prompt</span><span class="bounce-dot">.</span><span class="title-full"><span class="title-letter">C</span><span class="title-letter">o</span><span class="title-letter">m</span><span class="title-letter">p</span><span class="title-letter">u</span><span class="title-letter">t</span><span class="title-letter">e</span><span class="title-letter">r</span></span><span class="title-short">AC</span><span id="route-code-tab" class="route-code-tab"></span></span> <span class="platform-arrow">▼</span> <div class="platform-dropdown" id="platform-dropdown"> <div class="platform-option active" data-platform="aesthetic-computer"> <span class="platform-icon ac-pink-rect"></span> <span class="platform-opt-name">Aesthetic.Computer</span> </div> <div class="platform-option" data-platform="ff1"> <img src="https://assets.aesthetic.computer/kidlisp.com/feralfile-logo.png" alt="" class="platform-logo" style="height: 18px; margin-right: 6px; vertical-align: middle; filter: invert(1);"> <span class="platform-opt-name">FF1 Art Computer</span> </div> <div class="platform-option" data-platform="ableton"> <svg class="platform-icon ableton-logo" viewBox="0 0 51 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M0 0h3v24H0V0zm6 0h3v24H6V0zm6 0h3v24h-3V0zm6 0h3v24h-3V0zm9 0h24v3H27V0zm0 7h24v3H27V7zm0 7h24v3H27v-3zm0 7h24v3H27v-3z"/></svg> <span class="platform-opt-name">Ableton Live 12 Suite</span> </div> <!-- Hidden platforms (coming soon) <div class="platform-divider"></div> <div class="platform-option coming-soon" data-platform="playdate"> <span class="platform-opt-name">playdate</span> <span class="platform-status">soon</span> </div> <div class="platform-option coming-soon" data-platform="gameboy"> <span class="platform-opt-name">Game Boy</span> <span class="platform-status">soon</span> </div> <div class="platform-option experimental" data-platform="n64"> <span class="platform-opt-name">Nintendo 64</span> <span class="platform-status">exp</span> </div> --> </div> </div> <div style="position: relative;"> <button id="popout-btn" class="popout-btn" title="Start PJ broadcast" aria-label="Toggle PJ broadcast"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M4.9 16.1C1 12.2 1 5.8 4.9 1.9"/> <path d="M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"/> <circle cx="12" cy="9" r="2"/> <path d="M16.2 4.7c2 2.3 2.4 5.5.8 7.5"/> <path d="M19.1 1.9a12.98 12.98 0 0 1 0 14.2"/> <path d="M12 11v11"/> <path d="M8 22h8"/> </svg> </button> <div class="pj-dropdown hidden" id="pj-dropdown"> <div class="pj-dropdown-header"> <span class="pj-dropdown-channel" id="pj-dropdown-channel" title="Click to copy URL">—</span> <span class="pj-dropdown-viewers" id="pj-dropdown-viewers">0 viewers</span> </div> <canvas id="pj-dropdown-qr" width="120" height="120" style="image-rendering: pixelated; cursor: pointer;" title="Click to copy URL"></canvas> <button class="pj-dropdown-popout" id="pj-dropdown-popout">Popout Window</button> <button class="pj-dropdown-stop" id="pj-dropdown-stop">Stop Broadcasting</button> </div> </div> <img id="preview-qr" src="" alt="QR Code" title="Click to enlarge" style="display: none; width: auto; image-rendering: pixelated;" /> <div id="header-auth-buttons" class="header-auth-buttons" style="display: none;"> <button id="header-login-btn" class="header-login-btn">Log in</button> <button id="header-signup-btn" class="header-signup-btn">I'm new</button> </div> <div id="header-user-menu" class="header-user-menu" style="display: none;"> <span id="header-user-handle" class="header-user-handle"></span> <div class="header-user-dropdown" id="header-user-dropdown"> <button id="header-logout-btn" class="header-logout-btn">Logout</button> </div> </div> </div> <div class="preview-iframe-container" id="preview-iframe-container"> <!-- Platform-specific boot screens --> <div class="boot-screen" id="boot-screen" data-platform="aesthetic-computer"> <div class="color-bar bar-1" style="background: #ffffff;"></div> <div class="color-bar bar-2" style="background: #ffff00;"></div> <div class="color-bar bar-3" style="background: #00ffff;"></div> <div class="color-bar bar-4" style="background: #00ff00;"></div> <div class="color-bar bar-5" style="background: #ff00ff;"></div> <div class="color-bar bar-6" style="background: #ff0000;"></div> <div class="color-bar bar-7" style="background: #0000ff;"></div> <div class="color-bar bar-8" style="background: #000000;"></div> </div> <div class="boot-screen hidden" id="boot-screen-ff1" data-platform="ff1"> <img src="https://assets.aesthetic.computer/kidlisp.com/feralfile-logo.png" alt="Feral File" class="ff1-logo"> <div class="boot-text boot-text-title">FF1 Art Computer</div> <div class="boot-text boot-text-action" data-i18n="pressToRun">Press <span class="blink">▶</span> to run</div> <div class="ff1-settings" id="ff1-settings"> <!-- Desktop App Connected State --> <div class="ff1-bridge-status" id="ff1-bridge-status" style="display:none;"> <span class="ff1-status success">✓ Desktop App Connected</span> <div class="ff1-devices-list" id="ff1-devices-list"></div> <button id="ff1-refresh-btn" class="ff1-btn">Refresh Devices</button> </div> <!-- No Desktop App State --> <div class="ff1-no-bridge" id="ff1-no-bridge"> <span class="ff1-status" id="ff1-status">Checking...</span> <a href="https://github.com/whistlegraph/aesthetic-computer/releases/latest" target="_blank" rel="noopener" class="ff1-btn ff1-download-btn" id="ff1-download-btn" style="display:none;">Download Desktop App</a> <button id="ff1-pair-btn" class="ff1-btn" style="display:none;">Manual Pair (Advanced)</button> </div> <!-- Test button (shown when connected) --> <button id="ff1-test-btn" class="ff1-btn" style="display:none;">Test Connection</button> <!-- QR Pairing (fallback) --> <div class="ff1-pairing" id="ff1-pairing"> <button class="ff1-pairing-close" id="ff1-pairing-close" title="Close">✕</button> <div class="ff1-pairing-content"> <canvas id="ff1-qr-canvas" class="ff1-pairing-qr"></canvas> <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> </div> <div class="ff1-pairing-status" id="ff1-pairing-status">Waiting…</div> <div class="ff1-pairing-link" id="ff1-pairing-link" title="Click to copy"></div> </div> </div> </div> <div class="boot-screen hidden" id="boot-screen-ableton" data-platform="ableton"> <svg class="ableton-boot-logo" viewBox="0 0 51 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M0 0h3v24H0V0zm6 0h3v24H6V0zm6 0h3v24h-3V0zm6 0h3v24h-3V0zm9 0h24v3H27V0zm0 7h24v3H27V7zm0 7h24v3H27v-3zm0 7h24v3H27v-3z"/></svg> <div class="boot-text boot-text-title">Ableton Live 12 Suite</div> <div class="boot-text boot-text-action" data-i18n="pressToSend">Press <span class="blink">▶</span> to send to AC KidLisp.com device</div> <a href="https://assets.aesthetic.computer/m4l/AC%20KidLisp.com.amxd" target="_blank" rel="noopener" class="m4l-download-btn">Download AC KidLisp.com.amxd <span style="opacity: 0.6;">(13 KB)</span></a> </div> <div class="boot-screen hidden" id="boot-screen-playdate" data-platform="playdate" style="background: #ffcc00; flex-direction: column; align-items: center; justify-content: center;"> <div style="font-size: 64px; margin-bottom: 16px;">🎮</div> <div class="boot-text" style="font-size: 18px; font-weight: bold; color: #000;">playdate</div> <div class="boot-text boot-text-status" style="font-size: 12px; opacity: 0.6; margin-top: 8px; color: #000;" data-i18n="comingSoon">Coming Soon</div> </div> <div class="boot-screen hidden" id="boot-screen-gameboy" data-platform="gameboy" style="background: #9bbc0f; flex-direction: column; align-items: center; justify-content: center;"> <div style="font-size: 64px; margin-bottom: 16px; filter: grayscale(1);">👾</div> <div class="boot-text" style="font-size: 18px; font-weight: bold; color: #0f380f;">Game Boy</div> <div class="boot-text boot-text-status" style="font-size: 12px; opacity: 0.6; margin-top: 8px; color: #0f380f;" data-i18n="comingSoon">Coming Soon</div> </div> <div class="boot-screen hidden" id="boot-screen-n64" data-platform="n64" style="background: #1a1a1a; flex-direction: column; align-items: center; justify-content: center;"> <div style="font-size: 64px; margin-bottom: 16px;">🕹️</div> <div class="boot-text" style="font-size: 18px; font-weight: bold; color: #fff;">Nintendo 64</div> <div class="boot-text boot-text-status" style="font-size: 11px; opacity: 0.5; margin-top: 8px; color: #ff3333;" data-i18n="experimental">⚠️ Experimental</div> </div>
<iframe id="preview-iframe" frameborder="0"></iframe> <!-- (PJ overlay removed — preview stays local, dropdown handles QR/info) --> <!-- Pixel rulers (shown when paused) - all 4 sides --> <div class="preview-rulers" id="preview-rulers"> <!-- Corners --> <div class="ruler-corner ruler-corner-tl">px</div> <div class="ruler-corner ruler-corner-tr"></div> <div class="ruler-corner ruler-corner-bl"></div> <div class="ruler-corner ruler-corner-br"></div> <!-- Top ruler --> <div class="ruler-horizontal" id="ruler-h"> <div class="ruler-ticks" id="ruler-h-ticks"></div> <div class="ruler-crosshair-h" id="crosshair-h"></div> </div> <!-- Bottom ruler --> <div class="ruler-horizontal-bottom" id="ruler-h-bottom"> <div class="ruler-ticks" id="ruler-h-bottom-ticks"></div> <div class="ruler-crosshair-h" id="crosshair-h-bottom"></div> </div> <!-- Left ruler --> <div class="ruler-vertical" id="ruler-v"> <div class="ruler-ticks" id="ruler-v-ticks"></div> <div class="ruler-crosshair-v" id="crosshair-v"></div> </div> <!-- Right ruler --> <div class="ruler-vertical-right" id="ruler-v-right"> <div class="ruler-ticks" id="ruler-v-right-ticks"></div> <div class="ruler-crosshair-v" id="crosshair-v-right"></div> </div> <!-- Full preview crosshairs (military reticle) --> <div class="preview-crosshair-h" id="preview-crosshair-h"></div> <div class="preview-crosshair-v" id="preview-crosshair-v"></div> <div class="coord-display" id="coord-display">x: 0, y: 0</div> </div> <div class="preview-mouse-overlay" id="preview-mouse-overlay"></div> </div> </div> </div> <div class="bottom-row" id="bottom-row"> <div class="reference-section" id="reference-panel"> <div class="docs-header"> <div class="tab mobile-only" data-tab="console" id="console-tab" style="width: 94px; min-width: 94px; box-sizing: border-box; padding: 0;">Console</div> <div class="tab" data-tab="reference" id="reference-tab" style="display: none;">Cards</div> <div class="tab active" data-tab="guide" id="guide-tab" style="width: 94px; min-width: 94px; box-sizing: border-box; padding: 0;">Learn</div> <div class="tab" data-tab="list" id="list-tab" style="width: 94px; min-width: 94px; box-sizing: border-box; padding: 0;">List</div> <div class="tab" data-tab="jukebox" id="jukebox-tab" style="width: 94px; min-width: 94px; box-sizing: border-box; padding: 0;">Music</div> <div class="tab" data-tab="keep" id="keep-tab" style="width: 94px; min-width: 94px; box-sizing: border-box; padding: 0;">Keep</div> <div class="tab" data-tab="jockey" id="jockey-tab" style="width: 94px; min-width: 94px; box-sizing: border-box; padding: 0; display: none;">Jockey</div> </div> <!-- Console content (shown in mobile single-column mode) --> <div class="docs-content hidden" id="mobile-console-content"> <div class="console-output" id="mobile-console-output"></div> </div> <div class="docs-content" id="guide-content"> <iframe id="learn-iframe" data-src="https://kidlisp.com/learn" style="width: 100%; height: 100%; border: none; flex: 1; background: transparent;"></iframe> </div> <div class="docs-content hidden" id="list-content"> <!-- List sub-tabs --> <div class="list-subtabs"> <div class="list-subtab active" data-subtab="examples">Top Hits</div> <div class="list-subtab" data-subtab="recent">Recent</div> <div class="list-subtab" data-subtab="history">History</div> </div> <!-- List sub-contents --> <div class="list-subcontent" id="examples-subcontent"> <div class="code-list-panel"> <div id="examples-list" class="code-list-items"><div class="code-list-loading">Loading...</div></div> </div> </div> <div class="list-subcontent hidden" id="history-subcontent"> <div class="code-list-panel"> <div id="history-list" class="code-list-items"><div class="code-list-empty">No history yet.</div></div> </div> </div> <div class="list-subcontent hidden" id="recent-subcontent"> <div class="code-list-panel"> <div id="recent-new-indicator" class="code-list-new-indicator hidden"></div> <div id="recent-list" class="code-list-items"><div class="code-list-loading">Loading...</div></div> </div> </div> </div><!-- /list-content --> <div class="docs-content hidden" id="keep-content"> <!-- keeps.html directly (same-origin) — NOT /keeps, which 301s cross-origin to keep.kidlisp.com and breaks the embedded postMessage/localStorage handshake. --> <iframe id="keeps-iframe" data-src="https://kidlisp.com/keeps.html" style="width: 100%; height: 100%; border: none; flex: 1;"></iframe> </div> <div class="docs-content hidden" id="jukebox-content"> <!-- Jukebox sub-tabs --> <div class="jukebox-subtabs"> <div class="jukebox-subtab active" data-subtab="songs">Songs</div> <div class="jukebox-subtab" data-subtab="strudel">Strudel</div> <div class="jukebox-subtab" data-subtab="aux">Aux</div> </div> <!-- Songs subcontent --> <div class="jukebox-subcontent" id="songs-subcontent"> <!-- Track List (scrollable, at top) --> <div class="jukebox-scroll-area"> <div class="jukebox-tracklist" id="jukebox-tracklist"> <div class="jukebox-track" data-track="zzzZWAP.wav"><span class="jukebox-track-title">zzzZWAP</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="ARrrGh.wav"><span class="jukebox-track-title">ARrrGh</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="BLuRP.wav"><span class="jukebox-track-title">BLuRP</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="GrRr.wav"><span class="jukebox-track-title">GrRr</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="HoNK.wav"><span class="jukebox-track-title">HoNK</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="KLOnK.wav"><span class="jukebox-track-title">KLOnK</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="PaFf_master.wav"><span class="jukebox-track-title">PaFf</span><span class="jukebox-track-artist">oskie</span></div> <div class="jukebox-track" data-track="WHoOSh.wav"><span class="jukebox-track-title">WHoOSh</span><span class="jukebox-track-artist">oskie</span></div> </div> </div>
<!-- Mini Player (compact, at bottom) --> <div class="jukebox-mini-player"> <div class="jukebox-mini-player-top"> <img id="jukebox-cover-art" src="https://aesthetic.computer/assets/oskie/wipppps/wipppps_cover.jpeg" alt="Album cover" class="jukebox-mini-art"> <span class="jukebox-track-name" id="jukebox-track-name">Select a track</span> <div class="jukebox-mini-transport"> <button class="spotify-btn spotify-btn-prev" id="jukebox-prev" title="Previous"> <svg width="20" height="20" viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg> </button> <button class="spotify-btn spotify-btn-play" id="jukebox-play-pause" title="Play"> <svg width="20" height="20" viewBox="0 0 24 24" id="jukebox-play-icon"><polygon points="8,5 19,12 8,19"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" id="jukebox-pause-icon" style="display:none;"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> </button> <button class="spotify-btn spotify-btn-next" id="jukebox-next" title="Next"> <svg width="20" height="20" viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6z"/></svg> </button> </div> <div class="jukebox-volume"> <input type="range" id="jukebox-volume" min="0" max="100" value="70"> </div> <div class="jukebox-beat-indicator" id="jukebox-kick-indicator"></div> </div> <div class="jukebox-mini-player-bottom"> <div class="jukebox-progress"> <input type="range" id="jukebox-seek" min="0" max="100" value="0" disabled> <span class="jukebox-time" id="jukebox-time">0:00 / 0:00</span> </div> </div> <!-- Hidden meter for JS compatibility --> <div style="display:none;"> <div class="jukebox-meter-fill" id="jukebox-amp-meter"></div> <span id="jukebox-amp-value">0.0</span> </div> </div> </div> <!-- Strudel subcontent --> <div class="jukebox-subcontent hidden" id="strudel-subcontent"> <div class="jukebox-controls-area" style="padding: 8px 12px; gap: 8px;"> <!-- Compact control row: play/stop, status, volume --> <div style="display: flex; align-items: center; gap: 10px;"> <div class="strudel-transport"> <button class="strudel-btn" id="strudel-play" title="Play (Ctrl+Enter)">▶</button> <button class="strudel-btn" id="strudel-stop" title="Stop" disabled>⏹</button> </div> <span id="strudel-status" style="font-size: 12px; color: var(--jukebox-text-dim); flex-shrink: 0;">Ready</span> <div class="jukebox-volume" style="margin-left: auto;"> <span style="font-size: 12px;">🔊</span> <input type="range" id="strudel-volume" min="0" max="100" value="70"> </div> </div> <!-- Preset Patterns --> <div class="strudel-presets"> <button class="strudel-preset" data-code="s('bd sd hh sd')">Basic</button> <button class="strudel-preset" data-code="s('bd*2, hh*4, ~ sd')">Drums</button> <button class="strudel-preset" data-code="note('c3 e3 g3 b3').s('sawtooth').cutoff(800)">Synth</button> <button class="strudel-preset" data-code="s('bd sd hh sd').jux(rev)">Jux</button> <button class="strudel-preset" data-code="s('ape_breaks_3').loopAt(2).chop(8)">c_robo</button> </div> <!-- Audio Meters (for Strudel) - compact --> <div class="jukebox-meters" style="gap: 4px;"> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">amp</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill" id="strudel-amp-meter"></div></div> <span class="jukebox-meter-value" id="strudel-amp-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">kick</span> <div class="jukebox-beat-indicator" id="strudel-kick-indicator"></div> </div> </div> </div> <!-- Strudel Code Editor (scrollable area) --> <div class="jukebox-scroll-area"> <div class="strudel-editor-container"> <textarea id="strudel-code" placeholder="// Strudel live coding - press Ctrl+Enter to play // Try: s('bd sd hh sd') // Or: note('c3 e3 g3').s('sawtooth')">// c_robo's sample library: cc.oooooooooooooooo.ccs("ape_breaks_3").loopAt(2)</textarea> </div> </div> </div>
<!-- Aux subcontent (microphone input) --> <div class="jukebox-subcontent hidden" id="aux-subcontent"> <div class="jukebox-controls-area" style="padding: 8px 12px; gap: 8px;"> <!-- Compact control row: start/stop, status, gain --> <div style="display: flex; align-items: center; gap: 10px;"> <div class="strudel-transport"> <button class="aux-btn" id="aux-start" title="Start Mic">🎤</button> <button class="aux-btn" id="aux-stop" title="Stop" disabled>⏹</button> </div> <span id="aux-status" style="font-size: 12px; color: var(--jukebox-text-dim); flex-shrink: 0;">Ready</span> <div class="jukebox-volume" style="margin-left: auto;"> <span style="font-size: 12px;">Gain</span> <input type="range" id="aux-gain" min="0" max="200" value="100"> </div> </div>
<!-- Device selector --> <div style="display: flex; align-items: center; gap: 8px;"> <label for="aux-device" style="font-size: 11px; color: var(--jukebox-text-dim); flex-shrink: 0;">Input:</label> <select id="aux-device" style="flex: 1; font-size: 11px; padding: 3px 6px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px;"> <option value="">Default</option> </select> </div>
<!-- Audio Meters --> <div class="jukebox-meters" style="gap: 4px;"> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">amp</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-amp-meter"></div></div> <span class="jukebox-meter-value" id="aux-amp-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">kick</span> <div class="jukebox-beat-indicator" id="aux-kick-indicator"></div> </div>
<div class="aux-advanced-meters"> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">leftAmp</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-left-amp-meter"></div></div> <span class="jukebox-meter-value" id="aux-left-amp-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">rightAmp</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-right-amp-meter"></div></div> <span class="jukebox-meter-value" id="aux-right-amp-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">beat</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-beat-meter"></div></div> <span class="jukebox-meter-value" id="aux-beat-value">0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">bass</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-bass-meter"></div></div> <span class="jukebox-meter-value" id="aux-bass-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">mid</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-mid-meter"></div></div> <span class="jukebox-meter-value" id="aux-mid-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">treble</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-treble-meter"></div></div> <span class="jukebox-meter-value" id="aux-treble-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">highMid</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-high-mid-meter"></div></div> <span class="jukebox-meter-value" id="aux-high-mid-value">0.0</span> </div> <div class="jukebox-meter-row"> <span class="jukebox-meter-label">presence</span> <div class="jukebox-meter-bar"><div class="jukebox-meter-fill aux-meter-fill" id="aux-presence-meter"></div></div> <span class="jukebox-meter-value" id="aux-presence-value">0.0</span> </div> </div>
<div class="aux-graph-wrap"> <canvas id="aux-live-graph" class="aux-live-graph" width="380" height="92" aria-label="Aux live graph of amplitude and frequency bands"></canvas> <div class="aux-graph-legend"> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#ff7b6b;"></span>amp</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#ffd166;"></span>left</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#f8961e;"></span>right</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#4cc9f0;"></span>bass</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#06d6a0;"></span>mid</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#9b5de5;"></span>treble</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#90be6d;"></span>highMid</span> <span class="aux-legend-item"><span class="aux-legend-dot" style="background:#f72585;"></span>presence</span> </div> </div> </div> </div> </div>
<!-- Status --> <div class="jukebox-status" id="jukebox-status"></div> </div><!-- /jukebox-content --> <div class="docs-content hidden" id="jockey-content"> <div class="jockey-panel" id="jockey-panel"> <div class="jockey-off" id="jockey-off-msg">PJ is off. Click the antenna to start broadcasting.</div> <div id="jockey-live" style="display: none; flex-direction: column; align-items: center; gap: 10px;"> <div class="jockey-channel" id="jockey-channel" title="Click to copy URL">—</div> <canvas id="jockey-qr" width="160" height="160" style="image-rendering: pixelated; cursor: pointer; border: 2px solid rgb(205,92,155);" title="Click to copy URL"></canvas> <div class="jockey-url" id="jockey-url" title="Click to copy URL"></div> <div class="jockey-viewers" id="jockey-viewers">0 viewers</div> <button class="jockey-stop" id="jockey-stop">Stop Broadcasting</button> </div> </div> </div> <div class="docs-content hidden" id="reference-content"> <div id="book-container"></div> </div> </div> <div class="console-section" id="console-panel"> <div class="docs-header"> <span id="console-title">Console</span> </div> <div class="console-output" id="console-output"></div> </div> </div> <!-- Checkerboard backdrop for when panels are collapsed on mobile --> <div class="mobile-checkerboard" id="mobile-checkerboard"></div> </main> <!-- QR Code Modal --> <div id="qr-modal"> <div id="qr-modal-content"> <canvas id="qr-modal-canvas"></canvas> <div id="qr-modal-url"></div> <div id="qr-modal-close">Tap anywhere to close</div> </div> </div>
<!-- Split.js (async, self-hosted, UMD patched to always set window.Split) --> <script src="/aesthetic.computer/dep/split.min.js" async></script>
<!-- Monaco Editor ESM - using jsDelivr for modern ESM support --> <script type="module"> // Configure Monaco Environment before loading window.MonacoEnvironment = { getWorkerUrl: function(moduleId, label) { // Use data URLs for workers to avoid CORS issues const workerCode = `self.MonacoEnvironment = { baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/' }; importScripts('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/base/worker/workerMain.js');`; return 'data:text/javascript;charset=utf-8,' + encodeURIComponent(workerCode); } }; </script> <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.min.js"></script> <script type="module"> // Lazy-load QR code library — not needed at startup import('https://aesthetic.computer/aesthetic.computer/dep/@akamfoad/qr/qr.mjs').then(m => { window.qr = m.qrcode; window.ErrorCorrectLevel = m.ErrorCorrectLevel; }); </script> <!-- Development reload logic. --> <script type="module"> // Non-blocking logger import — no-op proxies until loaded so init isn't delayed const _noop = () => {}; const _noopLogger = new Proxy({}, { get: () => new Proxy(_noop, { get: () => _noop }) }); let log = _noopLogger; let showHeader = _noop, logPlatformSwitch = _noop, logCodeOperation = _noop; let logEditorInit = _noop, logAuth = _noop, logBoot = _noop, debug = _noop;
const loggerBase = (window.location.hostname === 'localhost' || window.location.hostname.includes('local.')) ? 'https://localhost:8888' : 'https://aesthetic.computer'; import(`${loggerBase}/kidlisp.com/js/logger.js`).then(m => { log = m.log; showHeader = m.showHeader; logPlatformSwitch = m.logPlatformSwitch; logCodeOperation = m.logCodeOperation; logEditorInit = m.logEditorInit; logAuth = m.logAuth; logBoot = m.logBoot; debug = m.debug; showHeader(); logBoot('start', window.location.pathname); }).catch(err => console.warn('Logger load failed:', err)); // Internationalization const i18n = { en: { editorTitle: 'KidLisp', clearButton: 'Clear', playButton: 'Play', previewTitle: 'Aesthetic.Computer', consoleTitle: 'Console', referenceTab: 'Cards', guideTab: 'Learn', examplesTab: 'Top Hits', keepsTab: 'Keeps', keepsConnectWallet: 'Connect Wallet', keepsConnecting: 'Connecting...', keepsDisconnect: 'Disconnect', keepsMintBtn: 'Mint as Keep', keepsMinting: 'Minting...', keepsYourCode: 'Your code:', keepsNetwork: 'mainnet', examplesLoading: 'Loading examples...', examplesNone: 'No examples found.', examplesError: 'Error loading examples.', running: 'Running...', hits: 'hits', categoryStructure: 'Structure', categoryDrawing: 'Drawing', categoryEffects: 'Effects', categoryColors: 'Colors', categoryNumbers: 'Numbers', wizardPrompt: 'Tap to build your KidLisp program:', wizardEmpty: 'Start by selecting a form below', pressToRun: 'Press ▶ to run', pressToSend: 'Press ▶ to send to device', comingSoon: 'Coming Soon', experimental: '⚠️ Experimental', platformComingSoon: 'support coming soon!', platformExperimental: 'is experimental - features may be limited', mintPreparing: 'Preparing mint...', mintSigning: 'Please sign in your wallet...', mintConfirming: 'Confirming transaction...', }, es: { editorTitle: 'KidLisp', clearButton: 'Limpiar', playButton: 'Ejecutar', previewTitle: 'Aesthetic.Computer', consoleTitle: 'Consola', referenceTab: 'Tarjetas', guideTab: 'Guías', examplesTab: 'Más Populares', keepsTab: 'Keeps', keepsConnectWallet: 'Conectar Cartera', keepsDisconnect: 'Desconectar', keepsMintBtn: 'Acuñar como Keep', keepsMinting: 'Acuñando...', keepsYourCode: 'Tu código:', keepsNetwork: 'mainnet', examplesLoading: 'Cargando ejemplos...', examplesNone: 'No se encontraron ejemplos.', examplesError: 'Error al cargar ejemplos.', running: 'Ejecutando...', hits: 'visitas', categoryStructure: 'Estructura', categoryDrawing: 'Dibujo', categoryEffects: 'Efectos', categoryColors: 'Colores', categoryNumbers: 'Números', wizardPrompt: 'Toca para construir tu programa KidLisp:', wizardEmpty: 'Comienza seleccionando una forma a continuación', pressToRun: 'Presiona ▶ para ejecutar', pressToSend: 'Presiona ▶ para enviar al dispositivo', comingSoon: 'Próximamente', experimental: '⚠️ Experimental', platformComingSoon: '¡soporte próximamente!', platformExperimental: 'es experimental - funciones limitadas', mintPreparing: 'Preparando mint...', mintSigning: 'Por favor firma en tu cartera...', mintConfirming: 'Confirmando transacción...', }, zh: { editorTitle: 'KidLisp', clearButton: '清空', playButton: '运行', previewTitle: 'Aesthetic.Computer', consoleTitle: '控制台', referenceTab: '卡片', guideTab: '指南', examplesTab: '热门', keepsTab: 'Keeps', keepsConnectWallet: '连接钱包', keepsDisconnect: '断开连接', keepsMintBtn: '铸造为 Keep', keepsMinting: '铸造中...', keepsYourCode: '你的代码:', keepsNetwork: 'mainnet', examplesLoading: '加载示例中...', examplesNone: '未找到示例。', examplesError: '加载示例时出错。', running: '运行中...', hits: '次点击', categoryStructure: '结构', categoryDrawing: '绘图', categoryEffects: '效果', categoryColors: '颜色', categoryNumbers: '数字', wizardPrompt: '点击构建你的 KidLisp 程序:', wizardEmpty: '从下面选择一个形式开始', pressToRun: '按 ▶ 运行', pressToSend: '按 ▶ 发送到设备', comingSoon: '即将推出', experimental: '⚠️ 实验性', platformComingSoon: '支持即将推出!', platformExperimental: '是实验性的 - 功能可能有限', mintPreparing: '准备铸造中...', mintSigning: '请在钱包中签名...', mintConfirming: '确认交易中...', }, da: { editorTitle: 'KidLisp', clearButton: 'Ryd', playButton: 'Kør', previewTitle: 'Aesthetic.Computer', consoleTitle: 'Konsol', referenceTab: 'Kort', guideTab: 'Lær', examplesTab: 'Top Hits', keepsTab: 'Keeps', keepsConnectWallet: 'Forbind Wallet', keepsDisconnect: 'Afbryd', keepsMintBtn: 'Mint som Keep', keepsMinting: 'Minter...', keepsYourCode: 'Din kode:', keepsNetwork: 'mainnet', examplesLoading: 'Indlæser eksempler...', examplesNone: 'Ingen eksempler fundet.', examplesError: 'Fejl ved indlæsning af eksempler.', running: 'Kører...', hits: 'visninger', categoryStructure: 'Struktur', categoryDrawing: 'Tegning', categoryEffects: 'Effekter', categoryColors: 'Farver', categoryNumbers: 'Tal', wizardPrompt: 'Tryk for at bygge dit KidLisp program:', wizardEmpty: 'Start med at vælge en form nedenfor', pressToRun: 'Tryk ▶ for at køre', pressToSend: 'Tryk ▶ for at sende til enhed', comingSoon: 'Kommer snart', experimental: '⚠️ Eksperimentel', platformComingSoon: 'understøttelse kommer snart!', platformExperimental: 'er eksperimentel - funktioner kan være begrænsede', mintPreparing: 'Forbereder mint...', mintSigning: 'Venligst underskriv i din wallet...', mintConfirming: 'Bekræfter transaktion...', } };
// Get or set language preference let currentLang = localStorage.getItem('kidlisp-lang') || 'en'; function t(key) { return i18n[currentLang]?.[key] || i18n.en[key] || key; } // Helper to open external URLs - handles VS Code sandboxed iframe case function openExternal(url) { if (window.acVSCODE && window.parent !== window) { // In VS Code extension webview, send message to parent to open externally window.parent.postMessage({ type: 'openExternal', url }, '*'); return true; } // Normal browser - open in new tab window.open(url, '_blank'); return true; } // Detect if running in VS Code webview (sandboxed iframe) function isInVSCode() { return window.acVSCODE && window.parent !== window; } function setLanguage(lang) { currentLang = lang; localStorage.setItem('kidlisp-lang', lang); updateUILanguage(); } function updateUILanguage() { // Update all translatable elements // Clear button uses CSS × icon, no text needed const consoleHeader = document.querySelector('#console-panel .docs-header span'); if (consoleHeader) consoleHeader.textContent = t('consoleTitle'); const refTab = document.querySelector('.tab[data-tab="reference"]'); if (refTab) refTab.textContent = t('referenceTab'); const guideTab = document.querySelector('.tab[data-tab="guide"]'); if (guideTab) guideTab.textContent = t('guideTab'); // Update List dropdown items with translations const listDropdownItems = document.querySelectorAll('.tab-dropdown-item'); listDropdownItems.forEach(item => { const subtab = item.dataset.subtab; if (subtab === 'examples') item.textContent = t('examplesTab'); }); // Update category headers const categoryHeaders = document.querySelectorAll('.docs-content h3'); const categories = ['categoryStructure', 'categoryDrawing', 'categoryEffects', 'categoryColors', 'categoryNumbers']; categoryHeaders.forEach((header, index) => { if (categories[index]) { header.textContent = t(categories[index]); } }); // Update boot screen text (data-i18n attributes) document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); const translated = t(key); // Preserve the blink span for action text if (key === 'pressToRun' || key === 'pressToSend') { el.innerHTML = translated.replace('▶', '<span class="blink">▶</span>'); } else { el.textContent = translated; } }); } // Simple toast notification system function showToast(message, type = 'info') { // Remove existing toast if any const existing = document.querySelector('.kidlisp-toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = `kidlisp-toast toast-${type}`; toast.textContent = message; // Style the toast Object.assign(toast.style, { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', padding: '12px 24px', borderRadius: '8px', backgroundColor: type === 'warning' ? '#f59e0b' : type === 'error' ? '#ef4444' : '#8b5cf6', color: 'white', fontFamily: 'var(--font-mono)', fontSize: '14px', zIndex: '10000', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', opacity: '0', transition: 'opacity 0.3s ease' }); document.body.appendChild(toast); // Animate in requestAnimationFrame(() => { toast.style.opacity = '1'; }); // Remove after 3 seconds setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3000); } // Make showToast globally available window.showToast = showToast; // ============================================ // FF1 ART COMPUTER INTEGRATION // ============================================ const FF1_CONFIG_KEY = 'kidlisp-ff1-config'; // FF1 configuration let ff1Config = { deviceId: '', relayTopicId: '', relayApiKey: '', }; // Load FF1 config from localStorage function loadFF1Config() { try { const stored = localStorage.getItem(FF1_CONFIG_KEY); if (stored) { ff1Config = { ...ff1Config, ...JSON.parse(stored) }; } } catch (e) { console.warn('⚠️ Failed to load FF1 config:', e); } // Update UI const input = document.getElementById('ff1-topic-id'); if (input && ff1Config.relayTopicId) { input.value = ff1Config.relayTopicId; } } // Save FF1 config to localStorage function saveFF1Config() { localStorage.setItem(FF1_CONFIG_KEY, JSON.stringify(ff1Config)); } // Build a DP-1 playlist for a URL function buildFF1Playlist(url) { return { dpVersion: '1.0.0', items: [{ source: url, duration: 0, license: 'open', provenance: { type: 'offChainURI', uri: url } }] }; }
function getFF1ProxyUrl() { const isDev = window.location.hostname === 'localhost'; return isDev ? 'http://localhost:8889/ff1/cast' : 'https://aesthetic.computer/api/ff1/proxy'; }
async function persistFF1DeviceIfAuthenticated(topicID, apiKey) { if (!window.acToken || !topicID) return; try { await fetch('/api/ff1/devices', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${window.acToken}` }, body: JSON.stringify({ topicID, apiKey: apiKey || null, deviceId: ff1Config.deviceId || null }) }); } catch (err) { console.warn('⚠️ Failed to save FF1 device:', err); } } // Send code to FF1 device async function sendToFF1Device() { const code = window.editor?.getValue()?.trim(); if (!code) { showToast('No code to send', 'info'); return; } if (!ff1Config.relayTopicId) { showToast('Enter your FF1 Topic ID first', 'warning'); const input = document.getElementById('ff1-topic-id'); if (input) input.focus(); return; } // First, we need to get a code URL from aesthetic.computer // Send to iframe to create the code, then get the URL back showToast('Creating code...', 'info'); try { // Send code to iframe to upload and get code ID const iframe = document.getElementById('preview-iframe'); if (!iframe?.contentWindow) { showToast('Preview not ready', 'error'); return; } // Send with createCode=true to get a code ID const aestheticUrl = (window.location.hostname === 'localhost' || window.location.hostname.includes('local.')) ? 'https://localhost:8888' : 'https://aesthetic.computer'; iframe.contentWindow.postMessage({ type: 'kidlisp-reload', code, createCode: true, authToken: window.acToken || null }, aestheticUrl); // Wait for code ID (will be received via message event) const codeId = await waitForFF1CodeId(5000); if (!codeId) { showToast('Failed to create code URL', 'error'); return; } // Build the AC URL for this code const codeUrl = `https://aesthetic.computer/code~${codeId}`; // Build FF1 playlist const playlist = buildFF1Playlist(codeUrl); // Send to FF1 device via Netlify proxy (to avoid CORS issues) const proxyUrl = getFF1ProxyUrl(); const payload = { topicID: ff1Config.relayTopicId, apiKey: ff1Config.relayApiKey || undefined, command: 'displayPlaylist', request: { dp1_call: playlist, intent: { action: 'now_display' } } }; console.log('📡 Sending to FF1 via proxy:', proxyUrl, payload); const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`${response.status} - ${errorText}`); } showToast('Sent to FF1! 🖼️', 'success'); // Visual feedback const sendButton = document.getElementById('send-button'); if (sendButton) { sendButton.classList.add('playing'); setTimeout(() => sendButton.classList.remove('playing'), 1000); } } catch (e) { console.error('❌ FF1 error:', e); if (e.message.includes('Failed to fetch') || e.message.includes('NetworkError')) { showToast(`Can't reach FF1 proxy.`, 'error'); } else { showToast(`FF1 error: ${e.message}`, 'error'); } } } // Wait for code ID from iframe function waitForFF1CodeId(timeout = 5000) { return new Promise((resolve) => { const startCode = localStorage.getItem('kidlisp-current-code'); // If we already have a code, use it if (startCode) { resolve(startCode); return; } let elapsed = 0; const interval = setInterval(() => { const codeId = localStorage.getItem('kidlisp-current-code'); if (codeId && codeId !== startCode) { clearInterval(interval); resolve(codeId); } elapsed += 100; if (elapsed >= timeout) { clearInterval(interval); resolve(null); } }, 100); }); } // Test FF1 device connection via proxy async function testFF1Connection() { const statusEl = document.getElementById('ff1-status'); const testBtn = document.getElementById('ff1-test-btn'); const proxyHint = document.getElementById('ff1-proxy-hint'); if (!ff1Config.relayTopicId) { if (statusEl) { statusEl.textContent = 'Enter Topic ID'; statusEl.className = 'ff1-status error'; } return; } if (statusEl) { statusEl.textContent = 'Testing...'; statusEl.className = 'ff1-status testing'; } if (testBtn) testBtn.disabled = true; if (proxyHint) proxyHint.style.display = 'none'; try { // Try to send a blank playlist via proxy const response = await fetch(getFF1ProxyUrl(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topicID: ff1Config.relayTopicId, apiKey: ff1Config.relayApiKey || undefined, command: 'displayPlaylist', request: { dp1_call: buildFF1Playlist('https://aesthetic.computer/blank'), intent: { action: 'now_display' } } }), signal: AbortSignal.timeout(8000) }); if (response.ok) { if (statusEl) { statusEl.textContent = '✓ Connected'; statusEl.className = 'ff1-status success'; } showToast('FF1 connected!', 'success'); persistFF1DeviceIfAuthenticated(ff1Config.relayTopicId, ff1Config.relayApiKey); } else { const errData = await response.json().catch(() => ({})); throw new Error(errData.error || `HTTP ${response.status}`); } } catch (e) { if (statusEl) { statusEl.textContent = '✗ Not reachable'; statusEl.className = 'ff1-status error'; } if (e.message.includes('Failed to fetch') || e.message.includes('NetworkError')) { showToast(`Can't reach FF1 proxy.`, 'error'); } else { showToast(`Can't reach FF1: ${e.message}`, 'error'); } } finally { if (testBtn) testBtn.disabled = false; } }
let ff1PairPollInterval = null;
function toggleFF1Pairing() { const pairing = document.getElementById('ff1-pairing'); const pairBtn = document.getElementById('ff1-pair-btn'); if (!pairing) return; if (pairing.classList.contains('open')) { pairing.classList.remove('open'); if (pairBtn) pairBtn.textContent = 'Pair'; stopFF1PairPolling(); } else { startFF1Pairing(); } }
function startFF1Pairing() { const pairing = document.getElementById('ff1-pairing'); const canvas = document.getElementById('ff1-qr-canvas'); const linkEl = document.getElementById('ff1-pairing-link'); const statusEl = document.getElementById('ff1-pairing-status'); const pairBtn = document.getElementById('ff1-pair-btn'); if (!pairing || !canvas) return;
const token = Array.from(crypto.getRandomValues(new Uint8Array(6))) .map((b) => b.toString(16).padStart(2, '0')).join('');
const isLocal = window.location.hostname === 'localhost' || window.location.hostname.includes('local.'); const pairingUrl = isLocal ? `${window.location.origin}/kidlisp.com/qr.html?token=${token}` : `https://device.kidlisp.com/qr/${token}`;
if (linkEl) linkEl.textContent = pairingUrl; if (statusEl) statusEl.textContent = 'Waiting…'; if (pairBtn) pairBtn.textContent = 'Cancel';
if (window.qr && window.ErrorCorrectLevel) { const cells = window.qr(pairingUrl, { errorCorrectLevel: window.ErrorCorrectLevel.L }).modules; const cellSize = Math.max(3, Math.floor(160 / cells.length)); const actualSize = cells.length * cellSize; canvas.width = actualSize; canvas.height = actualSize; const ctx = canvas.getContext('2d'); for (let y = 0; y < cells.length; y++) { for (let x = 0; x < cells.length; x++) { ctx.fillStyle = cells[y][x] ? '#000' : '#fff'; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } } }
pairing.classList.add('open'); startFF1PairPolling(token, statusEl); }
function stopFF1PairPolling() { if (ff1PairPollInterval) { clearInterval(ff1PairPollInterval); ff1PairPollInterval = null; } }
function startFF1PairPolling(token, statusEl) { stopFF1PairPolling(); let attempts = 0; ff1PairPollInterval = setInterval(async () => { attempts++; try { const response = await fetch(`/api/ff1/pair?token=${token}`); if (response.status === 404) { if (attempts > 120) { if (statusEl) statusEl.textContent = 'Timed out'; stopFF1PairPolling(); } return; } if (!response.ok) return; const data = await response.json(); if (data && data.topicID) { ff1Config.relayTopicId = data.topicID; ff1Config.relayApiKey = data.apiKey || ''; saveFF1Config(); if (statusEl) statusEl.textContent = '✓ Paired!'; const mainStatus = document.getElementById('ff1-status'); if (mainStatus) { mainStatus.textContent = '✓ Paired'; mainStatus.className = 'ff1-status success'; } const testBtn = document.getElementById('ff1-test-btn'); if (testBtn) testBtn.style.display = 'inline-block'; const pairBtn = document.getElementById('ff1-pair-btn'); if (pairBtn) pairBtn.textContent = 'Re-pair'; persistFF1DeviceIfAuthenticated(ff1Config.relayTopicId, ff1Config.relayApiKey); stopFF1PairPolling(); setTimeout(() => { const pairing = document.getElementById('ff1-pairing'); if (pairing) pairing.classList.remove('open'); }, 1500); } } catch (err) { if (statusEl) statusEl.textContent = 'Error'; stopFF1PairPolling(); } }, 1500); } // Check for Electron FF1 bridge let ff1BridgeDetected = false; let ff1BridgePollingInterval = null; async function checkFF1Bridge() { // Safari (and other browsers) block HTTP requests from HTTPS pages (mixed content). // The Electron bridge runs on localhost HTTP, so skip on production HTTPS to avoid // repeated console errors that can degrade Safari performance. if (location.protocol === 'https:' && location.hostname !== 'localhost') { return { available: false }; } try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 2000); // Use mode: 'no-cors' won't work, so just catch silently const response = await fetch('http://127.0.0.1:19999/status', { signal: controller.signal, // Prevent preflight which causes extra console noise headers: { 'Accept': 'application/json' } }); clearTimeout(timeout); if (response.ok) { const data = await response.json(); return { available: data.ok === true, ...data }; } } catch (e) { // Bridge not running - this is expected, suppress silently } return { available: false }; } // Auto-detect FF1 bridge when desktop app starts function startFF1BridgePolling() { if (ff1BridgePollingInterval) return; // Already polling ff1BridgePollingInterval = setInterval(async () => { const bridge = await checkFF1Bridge(); if (bridge.available && !ff1BridgeDetected) { // Desktop app just connected! ff1BridgeDetected = true; onFF1BridgeConnected(); } else if (!bridge.available && ff1BridgeDetected) { // Desktop app disconnected ff1BridgeDetected = false; onFF1BridgeDisconnected(); } }, 3000); // Check every 3 seconds } function stopFF1BridgePolling() { if (ff1BridgePollingInterval) { clearInterval(ff1BridgePollingInterval); ff1BridgePollingInterval = null; } } async function onFF1BridgeConnected() { const bridgeStatusEl = document.getElementById('ff1-bridge-status'); const noBridgeEl = document.getElementById('ff1-no-bridge'); const testBtn = document.getElementById('ff1-test-btn'); const statusEl = document.getElementById('ff1-status'); if (bridgeStatusEl) bridgeStatusEl.style.display = 'flex'; if (noBridgeEl) noBridgeEl.style.display = 'none'; if (testBtn) testBtn.style.display = 'inline-block'; if (statusEl) { statusEl.textContent = '✓ Desktop app connected'; statusEl.className = 'ff1-status success'; } // Discover devices const devices = await discoverFF1Devices(); updateFF1DeviceList(devices); showToast('FF1 Desktop app detected!', 'success'); } function onFF1BridgeDisconnected() { const bridgeStatusEl = document.getElementById('ff1-bridge-status'); const noBridgeEl = document.getElementById('ff1-no-bridge'); const statusEl = document.getElementById('ff1-status'); const downloadBtn = document.getElementById('ff1-download-btn'); const pairBtn = document.getElementById('ff1-pair-btn'); if (bridgeStatusEl) bridgeStatusEl.style.display = 'none'; if (noBridgeEl) noBridgeEl.style.display = 'flex'; if (statusEl) statusEl.textContent = 'Desktop app not detected'; if (downloadBtn) downloadBtn.style.display = 'inline-block'; if (pairBtn) pairBtn.style.display = 'inline-block'; // If already paired via QR, restore that status if (ff1Config.relayTopicId && statusEl) { statusEl.textContent = '✓ Paired (cloud relay)'; statusEl.className = 'ff1-status success'; } showToast('FF1 Desktop app disconnected', 'info'); }
// Discover FF1 devices via Electron bridge async function discoverFF1Devices() { try { const response = await fetch('http://127.0.0.1:19999/discover', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const data = await response.json(); return data.ok ? (data.devices || []) : []; } catch (e) { return []; } }
// Update FF1 device list UI function updateFF1DeviceList(devices) { const listEl = document.getElementById('ff1-devices-list'); if (!listEl) return; if (devices.length === 0) { listEl.innerHTML = '<div style="font-size:11px;opacity:0.6;">Scanning for FF1 devices...</div>'; } else { listEl.innerHTML = devices.map((d, i) => { const name = d.info?.deviceId || d.deviceId || `Device ${i + 1}`; const ip = d.ip || ''; const selected = i === 0 ? 'selected' : ''; return `<div class="ff1-device-item ${selected}" data-ip="${ip}" data-id="${name}"> <span class="ff1-device-name">${name}</span> <span class="ff1-device-ip" style="font-size:10px;opacity:0.5;">${ip}</span> </div>`; }).join(''); // Store first device as default if (devices[0]) { ff1Config.deviceId = devices[0].info?.deviceId || devices[0].deviceId || ''; ff1Config.deviceIp = devices[0].ip || ''; saveFF1Config(); } } }
// Initialize FF1 settings UI async function initFF1Settings() { loadFF1Config(); const testBtn = document.getElementById('ff1-test-btn'); const pairBtn = document.getElementById('ff1-pair-btn'); const closeBtn = document.getElementById('ff1-pairing-close'); const linkEl = document.getElementById('ff1-pairing-link'); const statusEl = document.getElementById('ff1-status'); const bridgeStatusEl = document.getElementById('ff1-bridge-status'); const noBridgeEl = document.getElementById('ff1-no-bridge'); const downloadBtn = document.getElementById('ff1-download-btn'); const refreshBtn = document.getElementById('ff1-refresh-btn'); // Check for Electron bridge const bridge = await checkFF1Bridge(); if (bridge.available) { // Desktop app is running! ff1BridgeDetected = true; if (bridgeStatusEl) bridgeStatusEl.style.display = 'flex'; if (noBridgeEl) noBridgeEl.style.display = 'none'; if (testBtn) testBtn.style.display = 'inline-block'; // Discover devices const devices = await discoverFF1Devices(); updateFF1DeviceList(devices); // Refresh button if (refreshBtn) refreshBtn.addEventListener('click', async () => { refreshBtn.textContent = 'Scanning...'; const devices = await discoverFF1Devices(); updateFF1DeviceList(devices); refreshBtn.textContent = 'Refresh Devices'; }); } else { // No desktop app - show download link ff1BridgeDetected = false; if (bridgeStatusEl) bridgeStatusEl.style.display = 'none'; if (noBridgeEl) noBridgeEl.style.display = 'flex'; if (statusEl) statusEl.textContent = 'Desktop app not detected'; if (downloadBtn) downloadBtn.style.display = 'inline-block'; if (pairBtn) pairBtn.style.display = 'inline-block'; // If already paired via QR, show that if (ff1Config.relayTopicId && statusEl) { statusEl.textContent = '✓ Paired (cloud relay)'; statusEl.className = 'ff1-status success'; if (testBtn) testBtn.style.display = 'inline-block'; if (pairBtn) pairBtn.textContent = 'Re-pair'; } } // Start polling for bridge connection/disconnection startFF1BridgePolling(); if (testBtn) testBtn.addEventListener('click', testFF1Connection); if (pairBtn) pairBtn.addEventListener('click', toggleFF1Pairing); if (closeBtn) closeBtn.addEventListener('click', toggleFF1Pairing); if (linkEl) linkEl.addEventListener('click', async () => { const text = linkEl.textContent || ''; if (!text) return; try { await navigator.clipboard.writeText(text); showToast('Link copied', 'success'); } catch { showToast('Copy failed', 'error'); } }); } // Initialize on load setTimeout(initFF1Settings, 100);
// Platform-specific book content let bookContainer = null; const platformBooks = { 'aesthetic-computer': { name: 'Aesthetic.Computer', pageCount: 1, }, 'ff1': { name: 'FF1 Art Computer', pageCount: 1, }, 'ableton': { name: 'Ableton Live 12 Suite', pageCount: 1, }, 'playdate': { name: 'Playdate', pageCount: 1, }, 'gameboy': { name: 'Game Boy', pageCount: 1, }, 'n64': { name: 'Nintendo 64', pageCount: 1, }, };
// SVG cards generated from LaTeX (in /cards/svg/) — disabled for now const svgCards = [];
const cardDecks = { 'aesthetic-computer': [ { title: 'KidLisp by Example', lines: [ 'One line can paint a whole mood.', 'Repeat small gestures to build texture.' ], code: `(do (wipe "navy") (repeat 24 (ink "cyan") (line (random width) 0 (random width) height)) (ink "magenta") (circle (/ width 2) (/ height 2) 56))` }, { title: 'Loops and timing', lines: ['Layer delays to stagger moves.'], code: `(do (wipe "black") (ink "white") (repeat 12 (later (* 0.1 (index)) (line (/ width 2) 0 (random width) height))))` }, { title: 'Simple palette swap', lines: ['Color flips on every frame.'], code: `(do (ink (if (even? frame) "yellow" "blue")) (wipe) (circle (/ width 2) (/ height 2) 80))` }, { title: 'Dummy practice card', lines: [ 'A placeholder card for the deck.', 'Swap in any KidLisp snippet you like.' ], code: `(do (wipe "white") (ink "black") (circle (/ width 2) (/ height 2) 24))` } ], 'ff1': [ { title: 'KidLisp by Example', lines: ['Layer stripes, then frame in gold.'], code: `(do (wipe "black") (repeat 18 (ink "orchid") (line (random width) 0 (random width) height)) (ink "gold") (rect 0 (- height 8) width 8))` }, { title: 'Slow pan', lines: ['Ambient drift for gallery walls.'], code: `(do (ink "orchid") (rect (mod (floor (/ frame 4)) width) 0 40 height))` }, { title: 'Dummy practice card', lines: ['Fill this with FF1-specific notes.', 'Use keeps + DP-1 hooks later.'], code: `(do (wipe "black") (ink "orchid") (rect 20 20 60 60))` } ], 'ableton': [ { title: 'KidLisp by Example', lines: ['Sketch a 3-note pattern for later export.'], code: `; Bridge-coming-soon idea(do (let ((root 60)) (note root 100 0.5) (note (+ root 7) 90 0.5) (later 0.5 (note (+ root 12) 110 0.5))))` }, { title: 'Idea: hi-hat roll', lines: ['Short bursts, ready for MIDI.'], code: `(do (let ((h 42)) (repeat 8 (later (* 0.125 (index)) (note h 70 0.05)))) )` }, { title: 'Dummy practice card', lines: ['Sketch ideas for a future M4L bridge.'], code: `(do (note 60 80 0.25) (later 0.25 (note 64 80 0.25)))` } ], 'playdate': [ { title: 'KidLisp by Example', lines: ['Keep it monochrome for the cranky handheld.'], code: `(do (wipe "white") (ink "black") (repeat 16 (line 200 120 (random 400) (random 240))) (circle 200 120 48))` }, { title: 'Crank rays', lines: ['Imagine crank angle driving rotation.'], code: `(do (wipe "white") (ink "black") (repeat 12 (line 200 120 (random 400) (random 240))))` }, { title: 'Dummy practice card', lines: ['Placeholder for Playdate crank sketches.'], code: `(do (wipe "white") (ink "black") (circle 200 120 24))` } ], 'gameboy': [ { title: 'KidLisp by Example', lines: ['Think in 4 shades and tiny tiles.'], code: `(do (wipe "darkolivegreen") (repeat 24 (ink "palegreen") (rect (random 160) (random 144) 4 4)) (ink "black") (circle 80 72 18))` }, { title: 'Sprite grid', lines: ['Pretend tiles for future ROM builds.'], code: `(do (wipe "darkolivegreen") (repeat 16 (ink "black") (rect (* 10 (index)) 64 8 8)))` }, { title: 'Dummy practice card', lines: ['Placeholder for more 4-shade ideas.'], code: `(do (wipe "darkolivegreen") (ink "black") (line 0 0 160 144))` } ], 'n64': [ { title: 'KidLisp by Example', lines: ['Chunky split-screen sketch for future libdragon builds.'], code: `(do (wipe "darkgreen") (rect 20 20 280 120) (rect 40 140 240 80))` }, { title: 'Quad blocks', lines: ['Think split-screen fighters.'], code: `(do (wipe "black") (ink "lime") (rect 20 20 120 90) (ink "purple") (rect 180 20 120 90) (ink "cyan") (rect 20 130 120 90) (ink "yellow") (rect 180 130 120 90))` }, { title: 'Dummy practice card', lines: ['Placeholder for more libdragon notes.'], code: `(do (wipe "darkgreen") (ink "white") (line 0 0 width height))` } ] };
// Pad decks so the stack always feels like a full pack const MIN_DECK_SIZE = 8; Object.keys(cardDecks).forEach(key => { const deck = cardDecks[key]; while (deck.length < MIN_DECK_SIZE) { deck.push({ title: `Practice card ${deck.length + 1}`, lines: [ 'Swap this with your own KidLisp idea.', 'Tap cards to shuffle through the stack.' ], code: `(do (wipe "ivory") (ink "black") (write "card ${deck.length + 1}" 18 28) (circle (/ width 2) (/ height 2) 22))` }); } });
function createListSection(title, items) { if (!items || !items.length) return null; const section = document.createElement('div'); section.className = 'book-section'; const heading = document.createElement('div'); heading.className = 'book-section-title'; heading.textContent = title; section.appendChild(heading);
const list = document.createElement('ul'); list.className = 'book-list'; items.forEach(item => { const li = document.createElement('li'); li.textContent = item; list.appendChild(li); }); section.appendChild(list); return section; }
function renderBook(platformKey) { if (!bookContainer) return; const book = platformBooks[platformKey] || platformBooks['aesthetic-computer']; const deck = cardDecks[platformKey] || cardDecks['aesthetic-computer']; let currentIndex = 0; let animatingCard = false; let discardPile = []; // Track discarded card indices bookContainer.dataset.platform = platformKey; bookContainer.innerHTML = '';
const stack = document.createElement('div'); stack.className = 'book-stack'; bookContainer.appendChild(stack);
// Create discard pile in corner const discard = document.createElement('div'); discard.className = 'discard-pile'; bookContainer.appendChild(discard);
// Build an SVG card frame (for LaTeX-generated cards) const buildSvgCard = (svgName) => { const frame = document.createElement('div'); frame.className = 'book-frame svg-card'; frame.style.background = 'transparent'; frame.style.boxShadow = 'none'; // Card front (the actual card content) const front = document.createElement('div'); front.className = 'card-front'; front.style.width = '100%'; front.style.height = '100%'; front.style.position = 'absolute'; front.style.backfaceVisibility = 'hidden'; const img = document.createElement('img'); const isKidlispDomain = window.location.hostname === 'kidlisp.com' || window.location.hostname === 'www.kidlisp.com'; img.src = isKidlispDomain ? `/cards/svg/${svgName}.svg` : `/kidlisp.com/cards/svg/${svgName}.svg`; img.alt = svgName; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; img.style.borderRadius = '12px'; img.style.boxShadow = '0 4px 20px rgba(0,0,0,0.3)'; img.draggable = false; front.appendChild(img); // Card back const back = document.createElement('div'); back.className = 'card-back'; back.style.width = '100%'; back.style.height = '100%'; back.style.position = 'absolute'; back.style.backfaceVisibility = 'hidden'; back.style.transform = 'rotateY(180deg)'; const backImg = document.createElement('img'); backImg.src = isKidlispDomain ? '/cards/svg/card-back.svg' : '/kidlisp.com/cards/svg/card-back.svg'; backImg.alt = 'Card Back'; backImg.style.width = '100%'; backImg.style.height = '100%'; backImg.style.objectFit = 'contain'; backImg.style.borderRadius = '12px'; backImg.draggable = false; back.appendChild(backImg); frame.appendChild(front); frame.appendChild(back); frame.dataset.cardName = svgName; return frame; };
// Check if we should use SVG cards for this platform const useSvgCards = platformKey === 'aesthetic-computer' && svgCards.length > 0; const effectiveDeck = useSvgCards ? svgCards : deck;
const buildCard = (cardData) => { const frame = document.createElement('div'); frame.className = 'book-frame';
const page = document.createElement('div'); page.className = 'book-page';
const inner = document.createElement('div'); inner.className = 'book-page-inner';
const titleplate = document.createElement('div'); titleplate.className = 'book-titleplate'; titleplate.innerHTML = ` <div class="book-title-main">${book.name}</div> `; inner.appendChild(titleplate);
const body = document.createElement('div'); body.className = 'book-body';
const header = document.createElement('div'); header.className = 'book-header'; const title = document.createElement('div'); title.className = 'book-title'; title.textContent = `KidLisp for ${book.name}`; header.appendChild(title); body.appendChild(header);
const cardTitle = document.createElement('div'); cardTitle.className = 'book-card-title'; cardTitle.textContent = cardData.title; body.appendChild(cardTitle);
if (cardData.lines) { cardData.lines.forEach(line => { const p = document.createElement('p'); p.className = 'book-card-text'; p.textContent = line; body.appendChild(p); }); }
if (cardData.code) { const snippetBlock = document.createElement('div'); snippetBlock.className = 'book-snippet-block'; const snippetHeader = document.createElement('div'); snippetHeader.className = 'book-snippet-header'; const snippetTitle = document.createElement('div'); snippetTitle.className = 'book-snippet-title'; snippetTitle.textContent = 'Example'; snippetHeader.appendChild(snippetTitle); snippetBlock.appendChild(snippetHeader); const snippetCode = document.createElement('pre'); snippetCode.className = 'book-snippet'; snippetCode.textContent = cardData.code.trim(); snippetBlock.appendChild(snippetCode); body.appendChild(snippetBlock); }
inner.appendChild(body); page.appendChild(inner); frame.appendChild(page); return frame; };
const offsets = [ { x: 0, y: 0, scale: 1, rotate: 0, z: 8 }, { x: 10, y: 12, scale: 0.992, rotate: -1.1, z: 7 }, { x: 20, y: 22, scale: 0.985, rotate: 1.2, z: 6 }, { x: 30, y: 32, scale: 0.978, rotate: -1.8, z: 5 }, { x: 38, y: 42, scale: 0.97, rotate: 1.6, z: 4 }, { x: 46, y: 50, scale: 0.962, rotate: -0.6, z: 3 }, ];
let isSpreadView = false;
// Flip card animation - rotates the card front/back const flipCard = (frame) => { if (!frame.classList.contains('svg-card')) return; // Only SVG cards have backs const front = frame.querySelector('.card-front'); const back = frame.querySelector('.card-back'); if (!front || !back) return; if (frame.classList.contains('flipped')) { frame.classList.remove('flipped'); front.style.transform = 'rotateY(0deg)'; back.style.transform = 'rotateY(180deg)'; } else { frame.classList.add('flipped'); front.style.transform = 'rotateY(-180deg)'; back.style.transform = 'rotateY(0deg)'; } };
const renderDeck = () => { stack.innerHTML = ''; // Clear discard pile when resetting deck discardPile = []; if (discard) discard.innerHTML = ''; const total = effectiveDeck?.length || 1; const visible = Math.min(total, offsets.length); for (let i = 0; i < visible; i++) { const deckIndex = (currentIndex + i) % total; const cardData = effectiveDeck[deckIndex]; // Use SVG builder for SVG cards, DOM builder for regular cards const frame = useSvgCards ? buildSvgCard(cardData) : buildCard(cardData); frame.dataset.deckIndex = deckIndex; const { x, y, scale, rotate, z } = offsets[i]; const transform = `translate(-50%, -50%) translate(${x}px, ${y}px) scale(${scale}) rotate(${rotate}deg)`; frame.dataset.baseTransform = transform; frame.style.transform = transform; frame.style.zIndex = z; stack.appendChild(frame); } };
const renderSpread = () => { stack.innerHTML = ''; stack.classList.add('spread-view'); const total = effectiveDeck?.length || 1; for (let i = 0; i < total; i++) { const cardData = effectiveDeck[i]; const frame = useSvgCards ? buildSvgCard(cardData) : buildCard(cardData); frame.dataset.deckIndex = i; frame.dataset.baseTransform = ''; // No base transform in spread view frame.style.animationDelay = `${i * 0.05}s`; // Click to select this card and restack frame.addEventListener('click', (e) => { e.stopPropagation(); selectCardAndRestack(i); }); stack.appendChild(frame); } // Animate cards spreading out const frames = stack.querySelectorAll('.book-frame'); frames.forEach((frame, i) => { frame.style.opacity = '0'; frame.style.transform = 'scale(0.8)'; setTimeout(() => { frame.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; frame.style.opacity = '1'; frame.style.transform = 'scale(1)'; }, i * 50); }); };
// Internal function to collapse spread view - does NOT check animatingCard const _collapseToStack = (selectedIndex) => { const frames = stack.querySelectorAll('.book-frame'); const selectedFrame = frames[selectedIndex]; if (selectedFrame) { selectedFrame.classList.add('selected'); } // Animate all cards collapsing back setTimeout(() => { frames.forEach((frame, i) => { const delay = Math.abs(i - selectedIndex) * 30; setTimeout(() => { frame.style.transition = 'opacity 0.25s ease, transform 0.25s ease'; frame.style.opacity = '0'; frame.style.transform = 'scale(0.6) translateY(20px)'; }, delay); }); // After collapse animation, switch to stack view setTimeout(() => { isSpreadView = false; stack.classList.remove('spread-view'); // Update spread button state const spreadBtn = document.getElementById('card-spread-btn'); if (spreadBtn) spreadBtn.classList.remove('active'); currentIndex = selectedIndex; renderDeck(); fitBookFrame(); fitBookTypography(); animatingCard = false; }, 400); }, 150); };
const selectCardAndRestack = (selectedIndex) => { if (animatingCard) return; animatingCard = true; _collapseToStack(selectedIndex); };
const toggleSpreadView = () => { if (animatingCard) return; animatingCard = true; if (isSpreadView) { // Collapse back to stack (select current top card) _collapseToStack(currentIndex); } else { // Expand to spread view isSpreadView = true; // Animate current stack flying apart const frames = stack.querySelectorAll('.book-frame'); frames.forEach((frame, i) => { const angle = (Math.random() - 0.5) * 30; const tx = (Math.random() - 0.5) * 100; const ty = (Math.random() - 0.5) * 50; frame.style.transition = 'transform 0.3s ease, opacity 0.3s ease'; frame.style.transform = `translate(-50%, -50%) translate(${tx}px, ${ty}px) rotate(${angle}deg) scale(0.5)`; frame.style.opacity = '0.5'; }); setTimeout(() => { renderSpread(); animatingCard = false; }, 350); } };
// Add card to discard pile (face down in corner) const addToDiscard = (cardIndex) => { const cardData = effectiveDeck[cardIndex]; discardPile.push(cardIndex); // Create a small face-down card for the discard pile const discardCard = document.createElement('div'); discardCard.className = 'discard-card'; discardCard.dataset.cardIndex = cardIndex; const backImg = document.createElement('img'); const isKidlispDomain = window.location.hostname === 'kidlisp.com' || window.location.hostname === 'www.kidlisp.com'; backImg.src = isKidlispDomain ? '/cards/svg/card-back.svg' : '/kidlisp.com/cards/svg/card-back.svg'; backImg.alt = 'Card Back'; backImg.draggable = false; discardCard.appendChild(backImg); // Stack with slight offset const stackIndex = discard.children.length; const offsetX = (Math.random() - 0.5) * 4; const offsetY = (Math.random() - 0.5) * 4; const rotation = (Math.random() - 0.5) * 8; discardCard.style.transform = `translate(${offsetX}px, ${offsetY - stackIndex * 2}px) rotate(${rotation}deg)`; discardCard.style.zIndex = stackIndex; discard.appendChild(discardCard); };
// Reset deck from discard pile with flip animation const resetFromDiscard = () => { if (animatingCard || discardPile.length === 0) return; animatingCard = true; // Animate discard pile flying to center discard.style.transition = 'transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)'; discard.style.transform = 'translate(-50%, -50%) scale(2.5)'; discard.style.left = '50%'; discard.style.top = '50%'; discard.style.bottom = 'auto'; discard.style.right = 'auto'; setTimeout(() => { // Flip animation discard.style.transition = 'transform 0.4s ease-in-out'; discard.style.transform = 'translate(-50%, -50%) scale(2.5) rotateY(180deg)'; setTimeout(() => { // Reset position discard.style.transition = 'none'; discard.style.transform = ''; discard.style.left = ''; discard.style.top = ''; discard.style.bottom = ''; discard.style.right = ''; // Clear discard and rebuild main deck discard.innerHTML = ''; currentIndex = 0; discardPile = []; renderDeck(); fitBookFrame(); fitBookTypography(); animatingCard = false; }, 450); }, 550); };
const advance = () => { if (isSpreadView) return; if (animatingCard) return; const frames = Array.from(stack.querySelectorAll('.book-frame')); if (!frames.length) { // No cards in main stack - reset from discard if we have cards there if (discardPile.length > 0) { resetFromDiscard(); } return; } const topFrame = frames[0]; const topCardIndex = parseInt(topFrame.dataset.deckIndex); const baseTransform = topFrame.dataset.baseTransform || topFrame.style.transform; animatingCard = true;
// Get discard pile position for animation target const discardRect = discard.getBoundingClientRect(); const stackRect = stack.getBoundingClientRect(); const targetX = discardRect.left - stackRect.left - stackRect.width / 2 + discardRect.width / 2; const targetY = discardRect.top - stackRect.top - stackRect.height / 2 + discardRect.height / 2;
// Animate top card flying to discard pile with 3D arc topFrame.animate([ { transform: baseTransform, opacity: 1, offset: 0 }, { transform: `${baseTransform} translateX(60px) translateZ(120px) rotateY(-30deg)`, opacity: 0.95, offset: 0.3 }, { transform: `translate(-50%, -50%) translateX(${targetX * 0.6}px) translateY(${targetY * 0.4}px) translateZ(80px) rotateY(-90deg) scale(0.5)`, opacity: 0.7, offset: 0.6 }, { transform: `translate(-50%, -50%) translateX(${targetX}px) translateY(${targetY}px) translateZ(0) rotateY(-180deg) scale(0.35)`, opacity: 0, offset: 1 } ], { duration: 480, easing: 'cubic-bezier(0.33, 1, 0.68, 1)', fill: 'forwards' });
// Simultaneously animate back cards sliding forward for (let i = 1; i < frames.length; i++) { const frame = frames[i]; const currentOffset = offsets[i]; const targetOffset = offsets[i - 1]; if (currentOffset && targetOffset) { const currentTransform = `translate(-50%, -50%) translate(${currentOffset.x}px, ${currentOffset.y}px) scale(${currentOffset.scale}) rotate(${currentOffset.rotate}deg)`; const targetTransform = `translate(-50%, -50%) translate(${targetOffset.x}px, ${targetOffset.y}px) scale(${targetOffset.scale}) rotate(${targetOffset.rotate}deg)`; frame.animate([ { transform: currentTransform, zIndex: currentOffset.z }, { transform: targetTransform, zIndex: targetOffset.z } ], { duration: 380, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', fill: 'forwards' }); } }
// After animation, update state setTimeout(() => { // Add to discard pile addToDiscard(topCardIndex); // Remove from main stack topFrame.remove(); // Update remaining cards const remainingFrames = Array.from(stack.querySelectorAll('.book-frame')); remainingFrames.forEach((frame, i) => { const offset = offsets[i]; if (offset) { const transform = `translate(-50%, -50%) translate(${offset.x}px, ${offset.y}px) scale(${offset.scale}) rotate(${offset.rotate}deg)`; frame.dataset.baseTransform = transform; frame.style.transform = transform; frame.style.zIndex = offset.z; } }); // DON'T add new cards from back - we're depleting the deck currentIndex = (currentIndex + 1) % effectiveDeck.length; animatingCard = false; }, 440); };
// Click discard pile to undo/reverse (bring back one card) discard.addEventListener('click', (e) => { e.stopPropagation(); if (animatingCard || discardPile.length === 0) return; // If main stack is empty, do the full reset animation const frames = stack.querySelectorAll('.book-frame'); if (frames.length === 0) { resetFromDiscard(); return; } // Otherwise bring back one card from discard animatingCard = true; const lastDiscardIndex = discardPile.pop(); const lastDiscardCard = discard.lastElementChild; if (lastDiscardCard) { // Animate card flying back from discard to top of stack lastDiscardCard.style.transition = 'transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.4s ease'; lastDiscardCard.style.transform = 'translateX(-200px) translateY(-100px) scale(3) rotateY(180deg)'; lastDiscardCard.style.opacity = '0'; setTimeout(() => { lastDiscardCard.remove(); // Add card back to front of stack currentIndex = (currentIndex - 1 + effectiveDeck.length) % effectiveDeck.length; // Shift existing cards back const existingFrames = Array.from(stack.querySelectorAll('.book-frame')); existingFrames.forEach((frame, i) => { const newOffset = offsets[i + 1]; if (newOffset) { frame.animate([ { transform: frame.style.transform }, { transform: `translate(-50%, -50%) translate(${newOffset.x}px, ${newOffset.y}px) scale(${newOffset.scale}) rotate(${newOffset.rotate}deg)` } ], { duration: 300, easing: 'ease-out', fill: 'forwards' }); frame.style.zIndex = newOffset.z; frame.dataset.baseTransform = `translate(-50%, -50%) translate(${newOffset.x}px, ${newOffset.y}px) scale(${newOffset.scale}) rotate(${newOffset.rotate}deg)`; } }); // Create and add new top card const cardData = effectiveDeck[lastDiscardIndex]; const newFrame = useSvgCards ? buildSvgCard(cardData) : buildCard(cardData); newFrame.dataset.deckIndex = lastDiscardIndex; const topOffset = offsets[0]; const transform = `translate(-50%, -50%) translate(${topOffset.x}px, ${topOffset.y}px) scale(${topOffset.scale}) rotate(${topOffset.rotate}deg)`; newFrame.dataset.baseTransform = transform; newFrame.style.transform = `translate(-50%, -50%) translateX(200px) translateY(-100px) scale(0.5) rotateY(-180deg)`; newFrame.style.zIndex = topOffset.z; newFrame.style.opacity = '0'; stack.insertBefore(newFrame, stack.firstChild); // Animate new card flipping into place requestAnimationFrame(() => { newFrame.style.transition = 'transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease'; newFrame.style.transform = transform; newFrame.style.opacity = '1'; setTimeout(() => { newFrame.style.transition = ''; animatingCard = false; }, 450); }); }, 350); } });
// Go to previous card const goBack = () => { if (isSpreadView || !effectiveDeck?.length || effectiveDeck.length === 1 || animatingCard) return; animatingCard = true; currentIndex = (currentIndex - 1 + effectiveDeck.length) % effectiveDeck.length; renderDeck(); fitBookFrame(); fitBookTypography(); animatingCard = false; };
// Simple click to advance card stack.addEventListener('click', (e) => { if (isSpreadView) return; // Handled by individual card clicks in spread view advance(); });
renderDeck(); fitBookFrame(); fitBookTypography(); // Re-fit after layout settles (handles case where container has no initial size) requestAnimationFrame(() => { fitBookFrame(); fitBookTypography(); }); }
function fitBookFrame() { if (!bookContainer) return; const frames = Array.from(bookContainer.querySelectorAll('.book-frame')); if (!frames.length) return; const inners = Array.from(bookContainer.querySelectorAll('.book-page-inner'));
const stackGuard = 64; // Leave room for visible stack fan-out const availableWidth = Math.max(0, bookContainer.clientWidth - stackGuard); const availableHeight = Math.max(0, bookContainer.clientHeight - stackGuard); if (availableWidth <= 0 || availableHeight <= 0) { // Fallback to reasonable default size const fallbackWidth = 250; const fallbackHeight = 350; frames.forEach(frame => { frame.style.width = `${fallbackWidth}px`; frame.style.height = `${fallbackHeight}px`; }); return; } // Playing card aspect ratio: 2.5 wide x 3.5 tall (taller than wide) const aspectRatio = 2.5 / 3.5; // ~0.714 // Start with width-constrained size let targetWidth = availableWidth; let targetHeight = targetWidth / aspectRatio; // Divide to get taller height // If too tall, constrain by height instead if (targetHeight > availableHeight) { targetHeight = availableHeight; targetWidth = targetHeight * aspectRatio; // Multiply to get narrower width } // Enforce minimum size to prevent tiny cards const minWidth = 100; const minHeight = 140; if (targetWidth < minWidth || targetHeight < minHeight) { targetWidth = Math.max(targetWidth, minWidth); targetHeight = Math.max(targetHeight, minHeight); } frames.forEach(frame => { frame.style.width = `${targetWidth}px`; frame.style.height = `${targetHeight}px`; });
// Scale inner content to fit while preserving aspect const baseW = 750; const baseH = 1050; const scale = Math.min(targetWidth / baseW, targetHeight / baseH); inners.forEach(inner => { inner.style.transform = `scale(${scale})`; inner.style.transformOrigin = 'top left'; }); }
function fitBookTypography() { if (!bookContainer) return; const bodies = Array.from(bookContainer.querySelectorAll('.book-body')); if (!bodies.length) return;
const parent = bookContainer.getBoundingClientRect(); const frame = bookContainer.querySelector('.book-frame')?.getBoundingClientRect(); if (!frame || parent.height === 0 || parent.width === 0) return;
// Target size uses available frame size vs. a baseline (560x746 approx. for 3:4 with padding) const baselineW = 750; const baselineH = 1050; const scaleW = frame.width / baselineW; const scaleH = frame.height / baselineH; const scale = Math.min(scaleW, scaleH, 1.3); // Cap upscale to avoid giant text
// Clamp font sizes to keep readability; base is 14px const base = 14; const min = 11; const max = 18; bodies.forEach(body => { let size = Math.max(min, Math.min(max, base * scale)); let snippetSize = Math.max(11, Math.min(16, 13 * scale));
const applySizes = () => { body.style.fontSize = `${size}px`; body.querySelectorAll('.book-snippet').forEach(snippet => { snippet.style.fontSize = `${snippetSize}px`; }); };
applySizes();
// If content overflows, shrink until it fits let guard = 0; while (body.scrollHeight > body.clientHeight && guard < 5) { size *= 0.9; snippetSize *= 0.9; applySizes(); guard += 1; } }); } // Initialize Split.js let splits = []; let gutterStates = { vertical: false, // Start uncentered for standard 2x2 grid horizontalTop: false, horizontalBottom: false }; // Track current layout mode to avoid unnecessary DOM restructuring let currentLayoutMode = null; // 'mobile', 'desktop-centered', 'desktop-standard'
// Global UI mode (persisted): studio | stage const UI_MODE_STORAGE_KEY = 'kidlisp-ui-mode'; let currentUiMode = localStorage.getItem(UI_MODE_STORAGE_KEY) === 'stage' ? 'stage' : 'studio';
function isStageMode() { return currentUiMode === 'stage'; }
function syncUiModeClass() { if (!document.body) return; document.body.classList.toggle('stage-mode', isStageMode()); }
function ensureStageLayoutStructure() { const mainSplit = document.getElementById('main-split'); if (!mainSplit) return;
const centerSquare = document.getElementById('center-square'); const editorPanel = document.getElementById('editor-panel'); const previewPanel = document.getElementById('preview-panel'); const referencePanel = document.getElementById('reference-panel'); const consolePanel = document.getElementById('console-panel'); let topRow = document.getElementById('top-row'); let bottomRow = document.getElementById('bottom-row');
if (!editorPanel || !previewPanel) return;
if (!topRow) { topRow = document.createElement('div'); topRow.id = 'top-row'; topRow.className = 'top-row'; }
if (!bottomRow) { bottomRow = document.createElement('div'); bottomRow.id = 'bottom-row'; bottomRow.className = 'bottom-row'; }
if (centerSquare && mainSplit.firstChild !== centerSquare) { mainSplit.insertBefore(centerSquare, mainSplit.firstChild); }
if (topRow.parentElement !== mainSplit) { mainSplit.appendChild(topRow); } if (bottomRow.parentElement !== mainSplit) { mainSplit.appendChild(bottomRow); }
if (editorPanel.parentElement !== topRow) { topRow.appendChild(editorPanel); } if (previewPanel.parentElement !== topRow) { topRow.appendChild(previewPanel); } if (referencePanel && referencePanel.parentElement !== bottomRow) { bottomRow.appendChild(referencePanel); } if (consolePanel && consolePanel.parentElement !== bottomRow) { bottomRow.appendChild(consolePanel); } }
function setUiMode(mode) { const normalized = mode === 'stage' ? 'stage' : 'studio'; const changed = currentUiMode !== normalized; currentUiMode = normalized; localStorage.setItem(UI_MODE_STORAGE_KEY, normalized); syncUiModeClass();
if (changed && typeof initSplits === 'function') { initSplits(); }
// Larger font in stage mode for readability over animation if (window.editor) { window.editor.updateOptions({ fontSize: normalized === 'stage' ? 24 : 16 }); }
if (typeof updatePreferencesModalContent === 'function') { updatePreferencesModalContent(); } if (typeof window.updatePlayButtonState === 'function') { window.updatePlayButtonState(); } }
window.getUiMode = () => currentUiMode; window.isStageMode = isStageMode; window.setUiMode = setUiMode; syncUiModeClass(); function initSplits() { // Guard: Split.js might not be loaded yet (async script) if (typeof Split === 'undefined' && window.innerWidth > 768) { console.log('[layout] initSplits: Split.js not loaded yet, skipping'); return; } console.log('[layout] initSplits: running, isMobile:', window.innerWidth <= 768, 'Split defined:', typeof Split !== 'undefined');
// Destroy existing splits splits.forEach(s => s.destroy()); splits = [];
const isMobile = window.innerWidth <= 768; const mainSplit = document.getElementById('main-split'); let topRow = document.getElementById('top-row'); let bottomRow = document.getElementById('bottom-row'); const editorPanel = document.getElementById('editor-panel'); const previewPanel = document.getElementById('preview-panel'); const consolePanel = document.getElementById('console-panel'); const referencePanel = document.getElementById('reference-panel');
// Stage mode bypasses split layout and uses single-pane overlay composition. if (isStageMode() && !document.body.classList.contains('device-mode')) { currentLayoutMode = 'stage'; mobileHeadersInitialized = false; ensureStageLayoutStructure();
[editorPanel, previewPanel, consolePanel, referencePanel].forEach(panel => { if (!panel) return; panel.style.height = ''; panel.style.minHeight = ''; panel.style.flex = ''; });
if (mainSplit) { mainSplit.style.display = ''; mainSplit.style.flexDirection = ''; }
requestAnimationFrame(() => { if (typeof editor !== 'undefined' && editor && editor.layout) { editor.layout(); } if (typeof updateCenterSquarePosition === 'function') { updateCenterSquarePosition(); } }); return; } // Recreate row elements if they were destroyed (e.g., switching from mobile to desktop) if (!topRow && !isMobile) { topRow = document.createElement('div'); topRow.id = 'top-row'; topRow.className = 'top-row'; } if (!bottomRow && !isMobile) { bottomRow = document.createElement('div'); bottomRow.id = 'bottom-row'; bottomRow.className = 'bottom-row'; }
if (isMobile) { // Mobile: Single vertical column with 3 panels // CSS handles the layout - panels become flex children of main-split // Console is merged into the reference panel as a tab // Only do DOM setup if not already in mobile layout const needsDomSetup = currentLayoutMode !== 'mobile'; currentLayoutMode = 'mobile'; if (needsDomSetup) { // Ensure panels are in correct order for mobile // CSS hides top-row, bottom-row, and gutters via media query // We just need to append the 3 visible panels directly to main-split if (mainSplit && editorPanel && previewPanel && referencePanel) { // Append panels directly (CSS will handle visibility of rows/gutters) mainSplit.appendChild(editorPanel); mainSplit.appendChild(previewPanel); mainSplit.appendChild(referencePanel); // Add checkerboard for visual feedback let checkerboard = document.getElementById('mobile-checkerboard'); if (!checkerboard) { checkerboard = document.createElement('div'); checkerboard.className = 'mobile-checkerboard'; checkerboard.id = 'mobile-checkerboard'; } mainSplit.appendChild(checkerboard); // Reset any inline styles that might interfere with CSS flexbox [editorPanel, previewPanel, referencePanel].forEach((panel, i) => { if (panel) { panel.style.height = ''; panel.style.minHeight = ''; // Let CSS set initial flex values, but restore saved state if present if (!panel.dataset.savedFlex) { panel.style.flex = ''; } } }); } } // Always initialize mobile header interactions (has its own guard) setupMobileHeaders(); // Don't use Split.js on mobile } else { // Desktop: Dynamic layout based on centering // When all centered (perfect +): 4 independent panels with 2 vertical gutters // When off-center: Standard 2x2 grid with 1 vertical gutter // Reset mobile headers flag so they can be re-initialized when returning to mobile mobileHeadersInitialized = false;
// Clear mobile-mode inline styles from all panels so Split.js can control them [editorPanel, previewPanel, consolePanel, referencePanel].forEach(panel => { if (panel) { panel.style.height = ''; panel.style.minHeight = ''; panel.style.flex = ''; panel.classList.remove('panel-collapsed'); delete panel.dataset.collapsed; delete panel.dataset.savedHeight; delete panel.dataset.savedFlex;
// Clean up mobile header artifacts (inline cursor, collapse indicators) const headers = panel.querySelectorAll('.editor-header, .docs-header'); headers.forEach(h => { h.style.cursor = ''; h.style.userSelect = ''; h.style.webkitUserSelect = ''; h.style.touchAction = ''; h.classList.remove('dragging'); }); const indicators = panel.querySelectorAll('.mobile-collapse-indicator'); indicators.forEach(ind => ind.remove()); } });
// Move gear closer to edge when no collapse arrows exist const gear = document.getElementById('settings-gear'); if (gear) gear.style.right = '12px';
// Also reset main-split flex styles from mobile if (mainSplit) { mainSplit.style.display = ''; mainSplit.style.flexDirection = ''; } const allCentered = gutterStates.vertical && gutterStates.horizontalTop && gutterStates.horizontalBottom; const targetMode = allCentered ? 'desktop-centered' : 'desktop-standard'; if (allCentered) { // PERFECT + MODE: 4 truly independent quadrants // Use nested splits to create independent vertical controls // Skip DOM restructuring if already in this mode if (currentLayoutMode === 'desktop-centered') { // Still need to re-create Split.js instances } else { currentLayoutMode = 'desktop-centered'; // Create wrapper rows for independent vertical control const leftCol = document.createElement('div'); leftCol.id = 'left-col'; leftCol.style.display = 'flex'; leftCol.style.flexDirection = 'column'; leftCol.style.height = '100%'; const rightCol = document.createElement('div'); rightCol.id = 'right-col'; rightCol.style.display = 'flex'; rightCol.style.flexDirection = 'column'; rightCol.style.height = '100%'; if (mainSplit) { // Preserve center-square before removing children const centerSquare = document.getElementById('center-square'); // Remove children without using innerHTML to preserve iframe while (mainSplit.firstChild) { mainSplit.removeChild(mainSplit.firstChild); } // Re-add center-square first (so it's in the DOM) if (centerSquare) mainSplit.appendChild(centerSquare); mainSplit.appendChild(leftCol); mainSplit.appendChild(rightCol); leftCol.appendChild(editorPanel); leftCol.appendChild(referencePanel); rightCol.appendChild(previewPanel); rightCol.appendChild(consolePanel); } } const snapThreshold = 5; // Left column vertical split (Editor / Reference) const leftVerticalSplit = Split(['#editor-panel', '#reference-panel'], { sizes: [50, 50], direction: 'vertical', gutterSize: 4, cursor: 'row-resize', onDrag: (sizes) => { if (Math.abs(sizes[0] - 50) > snapThreshold) { // Breaking out of perfect + mode gutterStates.vertical = false; initSplits(); } } }); splits.push(leftVerticalSplit); // Right column vertical split (Preview / Console) const rightVerticalSplit = Split(['#preview-panel', '#console-panel'], { sizes: [50, 50], direction: 'vertical', gutterSize: 4, cursor: 'row-resize', onDrag: (sizes) => { if (Math.abs(sizes[0] - 50) > snapThreshold) { // Breaking out of perfect + mode gutterStates.vertical = false; initSplits(); } } }); splits.push(rightVerticalSplit); // Main horizontal split (Left Col / Right Col) const mainHorizontalSplit = Split(['#left-col', '#right-col'], { sizes: [50, 50], gutterSize: 4, cursor: 'col-resize', onDrag: (sizes) => { if (Math.abs(sizes[0] - 50) > snapThreshold) { // Breaking out of perfect + mode gutterStates.horizontalTop = false; gutterStates.horizontalBottom = false; initSplits(); } } }); splits.push(mainHorizontalSplit); } else { // STANDARD 2x2 MODE: Traditional layout with rows // Skip DOM restructuring if already in this mode if (currentLayoutMode === 'desktop-standard') { // Still need to re-create Split.js instances } else { currentLayoutMode = 'desktop-standard'; // Restore row structure - remove children without using innerHTML if (mainSplit) { // Preserve center-square before removing children const centerSquare = document.getElementById('center-square'); while (mainSplit.firstChild) { mainSplit.removeChild(mainSplit.firstChild); } // Re-add center-square first (so it's in the DOM) if (centerSquare) mainSplit.appendChild(centerSquare); mainSplit.appendChild(topRow); mainSplit.appendChild(bottomRow); } // Reset row inline styles so Split.js can control them if (topRow) { topRow.style.height = ''; topRow.style.flex = ''; while (topRow.firstChild) { topRow.removeChild(topRow.firstChild); } topRow.appendChild(editorPanel); topRow.appendChild(previewPanel); } if (bottomRow) { bottomRow.style.height = ''; bottomRow.style.flex = ''; while (bottomRow.firstChild) { bottomRow.removeChild(bottomRow.firstChild); } bottomRow.appendChild(referencePanel); bottomRow.appendChild(consolePanel); } } const snapThreshold = 5; // 1. Main vertical split (Top Row vs Bottom Row) const verticalSplit = Split(['#top-row', '#bottom-row'], { sizes: [50, 50], direction: 'vertical', gutterSize: 4, cursor: 'row-resize', onDragEnd: (sizes) => { if (Math.abs(sizes[0] - 50) < snapThreshold) { verticalSplit.setSizes([50, 50]); gutterStates.vertical = true; // Check if we should switch to perfect + mode if (gutterStates.horizontalTop && gutterStates.horizontalBottom) { setTimeout(() => initSplits(), 100); } } else { gutterStates.vertical = false; } }, onDrag: (sizes) => { if (gutterStates.vertical && Math.abs(sizes[0] - 50) > snapThreshold) { gutterStates.vertical = false; } } }); splits.push(verticalSplit);
// 2. Top Row horizontal split (Editor vs Preview) const horizontalTopSplit = Split(['#editor-panel', '#preview-panel'], { sizes: [50, 50], gutterSize: 4, cursor: 'col-resize', onDragEnd: (sizes) => { if (Math.abs(sizes[0] - 50) < snapThreshold) { horizontalTopSplit.setSizes([50, 50]); gutterStates.horizontalTop = true; // Check if we should switch to perfect + mode if (gutterStates.vertical && gutterStates.horizontalBottom) { setTimeout(() => initSplits(), 100); } } else { gutterStates.horizontalTop = false; } }, onDrag: (sizes) => { if (gutterStates.horizontalTop && Math.abs(sizes[0] - 50) > snapThreshold) { gutterStates.horizontalTop = false; } } }); splits.push(horizontalTopSplit);
// 3. Bottom Row horizontal split (Reference vs Console) const horizontalBottomSplit = Split(['#reference-panel', '#console-panel'], { sizes: [50, 50], gutterSize: 4, cursor: 'col-resize', onDragEnd: (sizes) => { if (Math.abs(sizes[0] - 50) < snapThreshold) { horizontalBottomSplit.setSizes([50, 50]); gutterStates.horizontalBottom = true; // Check if we should switch to perfect + mode if (gutterStates.vertical && gutterStates.horizontalTop) { setTimeout(() => initSplits(), 100); } } else { gutterStates.horizontalBottom = false; } }, onDrag: (sizes) => { if (gutterStates.horizontalBottom && Math.abs(sizes[0] - 50) > snapThreshold) { gutterStates.horizontalBottom = false; } } }); splits.push(horizontalBottomSplit); } // After desktop layout is set up, trigger Monaco editor to recalculate its size // Use requestAnimationFrame to ensure DOM has settled requestAnimationFrame(() => { if (typeof editor !== 'undefined' && editor && editor.layout) { editor.layout(); } // Update center square position after Split.js has inserted gutters // Use a small delay to ensure gutters are fully rendered setTimeout(() => { if (typeof updateCenterSquarePosition === 'function') { updateCenterSquarePosition(); } }, 50); }); } } // Track if mobile headers have been set up to avoid duplicate listeners let mobileHeadersInitialized = false; // Mobile header interactions - tap to toggle, drag to resize function setupMobileHeaders() { // Prevent duplicate initialization if (mobileHeadersInitialized) { // Just update flex distribution if already initialized if (window.redistributeFlex) window.redistributeFlex(); if (window.updateCheckerboard) window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); return; } mobileHeadersInitialized = true; const HEADER_HEIGHT = 40; const DRAG_THRESHOLD = 5; const MIN_EXPANDED_HEIGHT = 80; // Minimum height when expanded (header + some content) // Panel order (top to bottom) - only 3 panels in mobile, console merged into reference const panelOrder = ['editor-panel', 'preview-panel', 'reference-panel']; const panels = panelOrder.map(id => document.getElementById(id)).filter(Boolean); // Store state per panel using data attributes const panelConfigs = [ { id: 'editor-panel', headerSelector: '.editor-header', defaultFlex: 25, showIndicator: true, indicatorPosition: 'after-tagline' }, { id: 'preview-panel', headerSelector: '.docs-header', defaultFlex: 45, showIndicator: true, indicatorPosition: 'right' }, { id: 'reference-panel', headerSelector: '.docs-header', defaultFlex: 30, showIndicator: true, indicatorPosition: 'right' } ]; // All panels start expanded in single column view const initiallyCollapsed = []; // Get iframe to disable during drag const previewIframe = document.getElementById('preview-iframe'); // Get checkerboard element const checkerboard = document.getElementById('mobile-checkerboard'); // Recalculate and redistribute flex values so all panels fill 100% function redistributeFlex() { const isMobile = window.innerWidth <= 768; if (!isMobile) return; // Get current flex values for expanded panels let totalExpandedFlex = 0; let collapsedCount = 0; panels.forEach((p, i) => { if (p && p.dataset.collapsed !== 'true') { const flex = parseFloat(p.dataset.savedFlex || panelConfigs[i].defaultFlex); totalExpandedFlex += flex; } else if (p) { collapsedCount++; } }); // If no expanded panels, nothing to distribute if (totalExpandedFlex === 0) return; // Apply flex values - collapsed panels get fixed height, expanded panels share remaining space panels.forEach((p, i) => { if (!p) return; if (p.dataset.collapsed === 'true') { p.style.flex = '0 0 auto'; p.style.height = `${HEADER_HEIGHT}px`; p.style.minHeight = `${HEADER_HEIGHT}px`; } else { const flex = parseFloat(p.dataset.savedFlex || panelConfigs[i].defaultFlex); // Normalize so total expanded flex equals 100 const normalizedFlex = (flex / totalExpandedFlex) * 100; p.style.flex = `${normalizedFlex} 1 0`; p.style.height = ''; p.style.minHeight = `${HEADER_HEIGHT}px`; } }); } // Make redistributeFlex globally accessible for tab click handlers window.redistributeFlex = redistributeFlex; // Update checkerboard visibility based on collapsed state // Make globally accessible for tab click handlers window.updateCheckerboard = function() { if (!checkerboard) return; const isMobile = window.innerWidth <= 768; if (!isMobile) { checkerboard.style.display = 'none'; return; } // Check collapsed state of all panels const allCollapsed = panels.every(p => p && p.dataset.collapsed === 'true'); const anyCollapsed = panels.some(p => p && p.dataset.collapsed === 'true'); if (anyCollapsed) { checkerboard.style.display = 'block'; // Use different style when ALL panels collapsed vs just some if (allCollapsed) { checkerboard.classList.add('all-collapsed'); } else { checkerboard.classList.remove('all-collapsed'); } } else { checkerboard.style.display = 'none'; } } // Update mini playback controls visibility based on whether main controls are obscured window.updateMiniPlaybackControls = function() { const isMobile = window.innerWidth <= 768; const miniControls = document.getElementById('mini-playback-controls'); const mainControls = document.getElementById('playback-controls'); const editorPanel = document.getElementById('editor-panel'); if (!miniControls || !mainControls || !editorPanel) return; if (!isMobile) { miniControls.style.display = 'none'; mainControls.style.visibility = 'visible'; return; } // Get the editor panel's actual content height (below header) const editorRect = editorPanel.getBoundingClientRect(); const editorHeader = editorPanel.querySelector('.editor-header'); const headerHeight = editorHeader ? editorHeader.offsetHeight : 44; const contentHeight = editorRect.height - headerHeight; // Check if editor is collapsed const isEditorCollapsed = editorPanel.dataset.collapsed === 'true' || editorPanel.classList.contains('panel-collapsed'); // Main controls are 44px tall + 14px bottom margin = need at least ~70px to show // For 1-line mode (~43px content), use mini controls const minHeightForControls = 70; // Show mini controls if editor collapsed OR content area too small (1 line mode) if (isEditorCollapsed || contentHeight < minHeightForControls) { miniControls.style.display = 'flex'; mainControls.style.visibility = 'hidden'; // Hide main controls } else { miniControls.style.display = 'none'; mainControls.style.visibility = 'visible'; // Show main controls } } // Add CSS for drag cursor override (must use class + !important to override button cursors) const dragCursorStyle = document.createElement('style'); dragCursorStyle.textContent = ` html.dragging-panel, html.dragging-panel * { cursor: ns-resize !important; } `; document.head.appendChild(dragCursorStyle); // Disable transitions and iframe pointer events during drag function setDragMode(dragging) { panels.forEach(p => { if (p) p.style.transition = dragging ? 'none' : ''; }); // Disable iframe interaction during drag so it doesn't steal events if (previewIframe) { previewIframe.style.pointerEvents = dragging ? 'none' : ''; } // Set cursor on entire document during drag (class + !important overrides button cursors) document.documentElement.classList.toggle('dragging-panel', dragging); } // Initialize all panels with flex values panelConfigs.forEach((config, i) => { const panel = document.getElementById(config.id); if (!panel) return; panel.dataset.savedFlex = config.defaultFlex; panel.dataset.collapsed = initiallyCollapsed.includes(config.id) ? 'true' : 'false'; if (panel.dataset.collapsed === 'true') { panel.classList.add('panel-collapsed'); } }); // Initial flex distribution redistributeFlex(); // Initial checkerboard state window.updateCheckerboard(); // Initial mini playback controls state window.updateMiniPlaybackControls(); // Now set up header interactions panelConfigs.forEach((config, index) => { const panel = document.getElementById(config.id); if (!panel) return; const header = panel.querySelector(config.headerSelector); if (!header) return; // Style header for dragging header.style.cursor = 'ns-resize'; header.style.userSelect = 'none'; header.style.webkitUserSelect = 'none'; header.style.touchAction = 'none'; // Add collapse indicator arrow - just visual, not clickable let indicator = header.querySelector('.mobile-collapse-indicator'); if (!indicator) { indicator = document.createElement('span'); indicator.className = 'mobile-collapse-indicator'; header.appendChild(indicator); }
// Push gear left of the collapse arrow const gear = document.getElementById('settings-gear'); if (gear) gear.style.right = '32px'; // Elements that should NOT start a drag (interactive elements) const skipDragSelectors = [ 'button', '.tab', '.platform-selector', '.platform-dropdown', '.header-auth-buttons', '.header-user-menu', 'select', 'input', '#logo-landing-zone', '#splash-logo' ]; function shouldSkipDrag(target) { return skipDragSelectors.some(sel => target.closest(sel)); } // Toggle collapse function function toggleCollapse() { if (panel.dataset.collapsed === 'true') { panel.dataset.collapsed = 'false'; panel.classList.remove('panel-collapsed'); } else { panel.dataset.collapsed = 'true'; panel.classList.add('panel-collapsed'); } redistributeFlex(); window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); if (window.updatePlayButtonState) window.updatePlayButtonState(); } // Drag state let dragState = { pending: false, // Touch/mouse down, waiting to see if it's a drag dragging: false, // Actually dragging (moved past threshold) startY: 0, startHeights: {}, // Heights of all panels when drag started startedOnInteractive: false // Whether drag started on a button/tab/etc }; function captureAllHeights() { const heights = {}; panels.forEach(p => { if (p) heights[p.id] = p.offsetHeight; }); return heights; } function startPending(clientY, startedOnInteractive = false) { dragState.pending = true; dragState.dragging = false; dragState.startY = clientY; dragState.startHeights = captureAllHeights(); dragState.startedOnInteractive = startedOnInteractive; } function moveDrag(clientY) { if (!dragState.pending && !dragState.dragging) return false; const deltaY = clientY - dragState.startY; // Check if we should start dragging if (dragState.pending && !dragState.dragging) { if (Math.abs(deltaY) > DRAG_THRESHOLD) { dragState.dragging = true; dragState.pending = false; setDragMode(true); header.classList.add('dragging'); } else { return false; // Not dragging yet } } if (!dragState.dragging) return false; // Stacked card resize logic: // - Dragging DOWN: panel above grows, panels below get pushed/compressed // - Dragging UP: panels above get compressed (from bottom to top), current panel can grow // - Collapsed panels move together as a stack const container = panels[0].parentElement; const totalHeight = container.offsetHeight; if (index > 0) { // We're dragging a header that's not the top panel // This header is the TOP EDGE of panel[index] // Dragging it moves the boundary between panel[index-1] and panel[index] if (deltaY < 0) { // DRAGGING UP - compress panels above, give space to this panel and below const takeFromAbove = -deltaY; // Compress panels above from bottom to top (closest first) let totalTaken = 0; for (let i = index - 1; i >= 0; i--) { const p = panels[i]; if (!p) continue; const currentH = dragState.startHeights[p.id]; const minH = HEADER_HEIGHT; const canTake = currentH - minH; const needToTake = takeFromAbove - totalTaken; const actualTake = Math.min(canTake, needToTake); const newHeight = currentH - actualTake; p.style.flex = `0 0 ${newHeight}px`; p.style.height = `${newHeight}px`; // Update collapsed state if (newHeight <= HEADER_HEIGHT) { if (p.dataset.collapsed !== 'true') { p.dataset.collapsed = 'true'; p.classList.add('panel-collapsed'); } } else { if (p.dataset.collapsed === 'true') { p.dataset.collapsed = 'false'; p.classList.remove('panel-collapsed'); } p.dataset.savedHeight = newHeight; } totalTaken += actualTake; if (totalTaken >= takeFromAbove) break; } // Give the taken space to the current panel (the one whose header we're dragging) const currentPanelNewHeight = dragState.startHeights[panel.id] + totalTaken; panel.style.flex = `0 0 ${currentPanelNewHeight}px`; panel.style.height = `${currentPanelNewHeight}px`; if (currentPanelNewHeight > HEADER_HEIGHT) { panel.dataset.collapsed = 'false'; panel.classList.remove('panel-collapsed'); panel.dataset.savedHeight = currentPanelNewHeight; } } else { // DRAGGING DOWN - panel above grows, compress panels at and below current const giveToAbove = deltaY; // Calculate how much space we can take from current panel and below let availableBelow = 0; for (let i = index; i < panels.length; i++) { const p = panels[i]; if (!p) continue; availableBelow += dragState.startHeights[p.id] - HEADER_HEIGHT; } const actualGive = Math.min(giveToAbove, availableBelow); // Give space to the panel above const abovePanel = panels[index - 1]; const newAboveHeight = dragState.startHeights[abovePanel.id] + actualGive; abovePanel.style.flex = `0 0 ${newAboveHeight}px`; abovePanel.style.height = `${newAboveHeight}px`; if (newAboveHeight > HEADER_HEIGHT) { abovePanel.dataset.collapsed = 'false'; abovePanel.classList.remove('panel-collapsed'); abovePanel.dataset.savedHeight = newAboveHeight; } // Take space from current panel and below (from top to bottom) let totalToTake = actualGive; for (let i = index; i < panels.length && totalToTake > 0; i++) { const p = panels[i]; if (!p) continue; const currentH = dragState.startHeights[p.id]; const minH = HEADER_HEIGHT; const canTake = currentH - minH; const actualTake = Math.min(canTake, totalToTake); const newHeight = currentH - actualTake; p.style.flex = `0 0 ${newHeight}px`; p.style.height = `${newHeight}px`; if (newHeight <= HEADER_HEIGHT) { if (p.dataset.collapsed !== 'true') { p.dataset.collapsed = 'true'; p.classList.add('panel-collapsed'); } } else { if (p.dataset.collapsed === 'true') { p.dataset.collapsed = 'false'; p.classList.remove('panel-collapsed'); } p.dataset.savedHeight = newHeight; } totalToTake -= actualTake; } } window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); if (window.updatePlayButtonState) window.updatePlayButtonState(); } else { // This is the top panel (editor) - can only resize itself // Dragging down grows editor, compresses below // Dragging up shrinks editor, gives space below if (deltaY > 0) { // Grow editor, compress below let availableBelow = 0; for (let i = 1; i < panels.length; i++) { availableBelow += dragState.startHeights[panels[i].id] - HEADER_HEIGHT; } const actualGrow = Math.min(deltaY, availableBelow); panel.style.flex = `0 0 ${dragState.startHeights[panel.id] + actualGrow}px`; panel.style.height = `${dragState.startHeights[panel.id] + actualGrow}px`; if (panel.offsetHeight > HEADER_HEIGHT) { panel.dataset.savedHeight = panel.offsetHeight; } // Take from panels below let toTake = actualGrow; for (let i = 1; i < panels.length && toTake > 0; i++) { const p = panels[i]; const currentH = dragState.startHeights[p.id]; const canTake = currentH - HEADER_HEIGHT; const take = Math.min(canTake, toTake); const newH = currentH - take; p.style.flex = `0 0 ${newH}px`; p.style.height = `${newH}px`; if (newH <= HEADER_HEIGHT) { p.dataset.collapsed = 'true'; p.classList.add('panel-collapsed'); } else { p.dataset.savedHeight = newH; } toTake -= take; } } else { // Shrink editor, give space to first non-collapsed panel below const shrinkBy = Math.min(-deltaY, dragState.startHeights[panel.id] - HEADER_HEIGHT); const newHeight = dragState.startHeights[panel.id] - shrinkBy; panel.style.flex = `0 0 ${newHeight}px`; panel.style.height = `${newHeight}px`; if (newHeight <= HEADER_HEIGHT) { panel.dataset.collapsed = 'true'; panel.classList.add('panel-collapsed'); } else { panel.dataset.savedHeight = newHeight; } // Give space to panel directly below if (panels[1]) { const belowH = dragState.startHeights[panels[1].id] + shrinkBy; panels[1].style.flex = `0 0 ${belowH}px`; panels[1].style.height = `${belowH}px`; if (belowH > HEADER_HEIGHT) { panels[1].dataset.collapsed = 'false'; panels[1].classList.remove('panel-collapsed'); panels[1].dataset.savedHeight = belowH; } } } window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); if (window.updatePlayButtonState) window.updatePlayButtonState(); } return true; } function endDrag() { const wasDragging = dragState.dragging; const wasPending = dragState.pending; // Disable auto-height if user manually dragged the editor panel if (wasDragging && config.id === 'editor-panel') { window.editorAutoHeight = false; } dragState.pending = false; dragState.dragging = false; setDragMode(false); header.classList.remove('dragging'); // After dragging, convert pixel heights back to flex values if (wasDragging) { // Calculate total height and each panel's percentage const container = panels[0].parentElement; const totalHeight = container.offsetHeight; panels.forEach((p, i) => { if (!p) return; const height = p.offsetHeight; // If in dead zone (between header and minimum expanded), snap to collapsed if (height > HEADER_HEIGHT && height < MIN_EXPANDED_HEIGHT) { p.dataset.collapsed = 'true'; p.classList.add('panel-collapsed'); } else if (height >= MIN_EXPANDED_HEIGHT) { // Convert current height to flex proportion p.dataset.collapsed = 'false'; p.classList.remove('panel-collapsed'); // Save as percentage of total (will be normalized in redistributeFlex) p.dataset.savedFlex = (height / totalHeight) * 100; } }); // Redistribute to clean up and ensure 100% fill redistributeFlex(); window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); if (window.updatePlayButtonState) window.updatePlayButtonState(); } // If wasn't dragging, it's a tap - toggle collapse // But only if we didn't start on an interactive element if (wasPending && !wasDragging && !dragState.startedOnInteractive) { toggleCollapse(); } dragState.startedOnInteractive = false; } // Touch events on header (entire header is draggable) // Even if touching interactive elements, allow drag - just suppress their click header.addEventListener('touchstart', (e) => { // Only allow drag in mobile/single-column mode if (window.innerWidth > 768) return;
const startedOnInteractive = shouldSkipDrag(e.target); const targetElement = e.target;
// Only stop propagation if NOT on interactive element // (let interactive elements handle their own events normally) if (!startedOnInteractive) { e.stopPropagation(); } startPending(e.touches[0].clientY, startedOnInteractive); const onTouchMove = (e) => { if (moveDrag(e.touches[0].clientY)) { e.preventDefault(); } }; // Block click if dragging occurred const blockClick = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }; const onTouchEnd = () => { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); document.removeEventListener('touchcancel', onTouchEnd); // If dragging occurred and started on interactive element, block the click if (dragState.dragging && startedOnInteractive) { // Add capture-phase click blocker that removes itself after one use targetElement.addEventListener('click', blockClick, { capture: true, once: true }); // Also block on header in case click bubbles there header.addEventListener('click', blockClick, { capture: true, once: true }); // Clean up blockers after a short delay if click didn't fire setTimeout(() => { targetElement.removeEventListener('click', blockClick, { capture: true }); header.removeEventListener('click', blockClick, { capture: true }); }, 100); } endDrag(); }; document.addEventListener('touchmove', onTouchMove, { passive: false }); document.addEventListener('touchend', onTouchEnd); document.addEventListener('touchcancel', onTouchEnd); }, { passive: true }); // Mouse events on header (entire header is draggable) // Even if clicking interactive elements, allow drag - just suppress their click header.addEventListener('mousedown', (e) => { // Only allow drag in mobile/single-column mode if (window.innerWidth > 768) return;
const startedOnInteractive = shouldSkipDrag(e.target); const targetElement = e.target;
// Only stop propagation if NOT on interactive element if (!startedOnInteractive) { e.stopPropagation(); e.preventDefault(); } startPending(e.clientY, startedOnInteractive); const onMouseMove = (e) => { // If dragging started and we began on interactive element, prevent default if (dragState.dragging && startedOnInteractive) { e.preventDefault(); } moveDrag(e.clientY); }; // Block click if dragging occurred on interactive element const blockClick = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }; const cleanup = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', cleanup); document.removeEventListener('mouseleave', cleanup); // If dragging occurred and started on interactive element, block the click if (dragState.dragging && startedOnInteractive) { targetElement.addEventListener('click', blockClick, { capture: true, once: true }); header.addEventListener('click', blockClick, { capture: true, once: true }); setTimeout(() => { targetElement.removeEventListener('click', blockClick, { capture: true }); header.removeEventListener('click', blockClick, { capture: true }); }, 100); } endDrag(); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', cleanup); // End drag when mouse leaves window document.addEventListener('mouseleave', cleanup); }); }); } // ── Global Init Pipeline ── // Wait for async deps, then run init stages in order. function whenReady(checkFn) { return new Promise(resolve => { (function poll() { if (checkFn()) return resolve(); setTimeout(poll, 50); })(); }); }
// Stage 1: Split.js loaded (or mobile — no Split needed) console.log('[layout] pipeline started, width:', window.innerWidth, 'Split defined:', typeof Split !== 'undefined'); whenReady(() => window.innerWidth <= 768 || typeof Split !== 'undefined') .then(() => { console.log('[layout] stage 1: Split.js ready, calling initSplits()'); initSplits(); console.log('[layout] initSplits done, splits.length:', splits.length, 'gutters:', document.querySelectorAll('.gutter').length); if (window.innerWidth <= 768) return; // No center square on mobile // Stage 2: Wait for gutters to have real dimensions return whenReady(() => { const g = document.querySelector('.gutter'); if (!g) return false; const r = g.getBoundingClientRect(); return r.width > 0 || r.height > 0; }); }) .then(() => { if (window.innerWidth <= 768) return; console.log('[layout] stage 2: gutters laid out'); // Stage 3: One frame for layout to settle, then place the nob return new Promise(resolve => requestAnimationFrame(resolve)); }) .then(() => { if (window.innerWidth <= 768) return; console.log('[layout] stage 3: placing center square'); updateCenterSquarePosition(); setupCenterSquareDrag();
const observer = new MutationObserver(() => updateCenterSquarePosition()); const mainSplit = document.getElementById('main-split'); if (mainSplit) { observer.observe(mainSplit, { attributes: true, attributeFilter: ['style'], subtree: true }); } }); // Editor auto-height for single column mode // Starts enabled, disabled once user manually drags the editor panel window.editorAutoHeight = true; // Update editor panel height based on line count (single column mode only) // Also distributes remaining height to other panels window.updateEditorAutoHeight = function() { if (!window.editorAutoHeight) return; const isSingleColumn = window.innerWidth <= 768; if (!isSingleColumn) return; const editorPanel = document.getElementById('editor-panel'); if (!editorPanel || editorPanel.dataset.collapsed === 'true') return; // Get Monaco editor and line count if (typeof editor === 'undefined' || !editor) return; const model = editor.getModel(); if (!model) return; const lineCount = model.getLineCount(); const HEADER_HEIGHT = 40; const LINE_HEIGHT = 19; // Monaco default line height at fontSize 16 const PADDING = 24; // Top padding (12) + bottom padding (12) const MIN_LINES = 1; const MAX_LINES = 15; // Cap auto-height to prevent taking over the screen const effectiveLines = Math.max(MIN_LINES, Math.min(lineCount, MAX_LINES)); const editorContentHeight = (effectiveLines * LINE_HEIGHT) + PADDING; const newEditorHeight = HEADER_HEIGHT + editorContentHeight; // Set editor height editorPanel.style.height = `${newEditorHeight}px`; editorPanel.dataset.savedHeight = newEditorHeight; // Distribute remaining height to other expanded panels const viewportHeight = window.innerHeight; const otherPanels = ['preview-panel', 'console-panel', 'reference-panel'] .map(id => document.getElementById(id)) .filter(p => p && p.dataset.collapsed !== 'true'); if (otherPanels.length > 0) { const remainingHeight = viewportHeight - newEditorHeight; const heightPerPanel = Math.floor(remainingHeight / otherPanels.length); const minPanelHeight = 100; // Minimum height for other panels otherPanels.forEach(p => { const panelHeight = Math.max(minPanelHeight, heightPerPanel); p.style.height = `${panelHeight}px`; p.dataset.savedHeight = panelHeight; }); } };
// Handle resize with debounce let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { initSplits(); updateCenterSquarePosition(); if (window.updateCheckerboard) window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); }, 200); });
// Position and handle center square function updateCenterSquarePosition() { const centerSquare = document.getElementById('center-square'); if (!centerSquare) return;
// Hide in single-column / mobile mode (no Split.js gutters) const gutterCount = document.querySelectorAll('.gutter').length; if (window.innerWidth <= 768 || gutterCount === 0) { centerSquare.style.display = 'none'; return; } // Check if we're in 4-panel symmetric mode (desktop-centered) const leftCol = document.getElementById('left-col'); const rightCol = document.getElementById('right-col'); const isSymmetricMode = leftCol && rightCol; if (isSymmetricMode) { // In symmetric 4-panel mode, show center square at the intersection // Find the horizontal gutter between left and right columns const mainHorizontalGutter = document.querySelector('#left-col + .gutter.gutter-horizontal'); // Find a vertical gutter in either column (they should align) const leftVerticalGutter = document.querySelector('#editor-panel + .gutter.gutter-vertical'); if (mainHorizontalGutter && leftVerticalGutter) { centerSquare.style.display = 'block'; const hRect = mainHorizontalGutter.getBoundingClientRect(); const vRect = leftVerticalGutter.getBoundingClientRect(); // Position at intersection of the + const left = hRect.left + (hRect.width / 2); const top = vRect.top + (vRect.height / 2); centerSquare.style.left = `${left}px`; centerSquare.style.top = `${top}px`; centerSquare.style.transform = 'translate(-50%, -50%)'; } else { centerSquare.style.display = 'none'; } } else { // Standard 2x2 mode - original logic const verticalGutter = document.querySelector('#top-row + .gutter.gutter-vertical'); const horizontalGutter = document.querySelector('#editor-panel + .gutter.gutter-horizontal'); if (verticalGutter && horizontalGutter) { centerSquare.style.display = 'block'; const vRect = verticalGutter.getBoundingClientRect(); const hRect = horizontalGutter.getBoundingClientRect(); // Position center square at intersection const left = hRect.left + (hRect.width / 2); const top = vRect.top + (vRect.height / 2); centerSquare.style.left = `${left}px`; centerSquare.style.top = `${top}px`; centerSquare.style.transform = 'translate(-50%, -50%)'; } else { centerSquare.style.display = 'none'; } } }
// Center square drag handler function setupCenterSquareDrag() { const centerSquare = document.getElementById('center-square'); if (!centerSquare) return; let isDragging = false; let startX, startY; let startSizes = {}; let isSymmetricMode = false; const startDrag = (e) => { e.preventDefault(); e.stopPropagation(); isDragging = true; startX = e.clientX; startY = e.clientY; centerSquare.classList.add('dragging'); // Capture pointer for better drag locking centerSquare.setPointerCapture(e.pointerId); // Prevent text selection during drag document.body.style.userSelect = 'none'; document.body.style.cursor = 'move'; // Check if we're in symmetric 4-panel mode const leftCol = document.getElementById('left-col'); const rightCol = document.getElementById('right-col'); isSymmetricMode = leftCol && rightCol; // Store initial sizes const editorPanel = document.getElementById('editor-panel'); const previewPanel = document.getElementById('preview-panel'); const referencePanel = document.getElementById('reference-panel'); const consolePanel = document.getElementById('console-panel'); if (isSymmetricMode) { // Symmetric mode: left-col / right-col structure startSizes = { leftColWidth: leftCol.offsetWidth, rightColWidth: rightCol.offsetWidth, editorHeight: editorPanel.offsetHeight, referenceHeight: referencePanel.offsetHeight, previewHeight: previewPanel.offsetHeight, consoleHeight: consolePanel.offsetHeight, totalWidth: leftCol.offsetWidth + rightCol.offsetWidth, leftTotalHeight: editorPanel.offsetHeight + referencePanel.offsetHeight, rightTotalHeight: previewPanel.offsetHeight + consolePanel.offsetHeight }; } else { // Standard mode: top-row / bottom-row structure const topRow = document.getElementById('top-row'); const bottomRow = document.getElementById('bottom-row'); startSizes = { topHeight: topRow?.offsetHeight || 0, bottomHeight: bottomRow?.offsetHeight || 0, editorWidth: editorPanel.offsetWidth, previewWidth: previewPanel.offsetWidth, referenceWidth: referencePanel.offsetWidth, consoleWidth: consolePanel.offsetWidth, totalHeight: (topRow?.offsetHeight || 0) + (bottomRow?.offsetHeight || 0), totalWidth: editorPanel.offsetWidth + previewPanel.offsetWidth }; } }; const onDrag = (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; if (isSymmetricMode) { // Symmetric 4-panel mode: // - Horizontal drag adjusts left-col vs right-col widths // - Vertical drag adjusts editor/reference AND preview/console heights const newLeftColWidth = startSizes.leftColWidth + deltaX; const newRightColWidth = startSizes.rightColWidth - deltaX; const newEditorHeight = startSizes.editorHeight + deltaY; const newReferenceHeight = startSizes.referenceHeight - deltaY; const newPreviewHeight = startSizes.previewHeight + deltaY; const newConsoleHeight = startSizes.consoleHeight - deltaY; // Convert to percentages const leftColPercent = (newLeftColWidth / startSizes.totalWidth) * 100; const rightColPercent = (newRightColWidth / startSizes.totalWidth) * 100; const editorPercent = (newEditorHeight / startSizes.leftTotalHeight) * 100; const referencePercent = (newReferenceHeight / startSizes.leftTotalHeight) * 100; const previewPercent = (newPreviewHeight / startSizes.rightTotalHeight) * 100; const consolePercent = (newConsoleHeight / startSizes.rightTotalHeight) * 100; // Apply new sizes if they're reasonable (min 10%) if (leftColPercent > 10 && rightColPercent > 10 && editorPercent > 10 && referencePercent > 10 && previewPercent > 10 && consolePercent > 10) { if (splits.length >= 3) { // In symmetric mode: // splits[0] = leftVerticalSplit (Editor / Reference) // splits[1] = rightVerticalSplit (Preview / Console) // splits[2] = mainHorizontalSplit (Left Col / Right Col) splits[0].setSizes([editorPercent, referencePercent]); splits[1].setSizes([previewPercent, consolePercent]); splits[2].setSizes([leftColPercent, rightColPercent]); } } } else { // Standard 2x2 mode (original logic) const newTopHeight = startSizes.topHeight + deltaY; const newBottomHeight = startSizes.bottomHeight - deltaY; const newEditorWidth = startSizes.editorWidth + deltaX; const newPreviewWidth = startSizes.previewWidth - deltaX; const newReferenceWidth = startSizes.referenceWidth + deltaX; const newConsoleWidth = startSizes.consoleWidth - deltaX; // Convert to percentages const topPercent = (newTopHeight / startSizes.totalHeight) * 100; const bottomPercent = (newBottomHeight / startSizes.totalHeight) * 100; const editorPercent = (newEditorWidth / startSizes.totalWidth) * 100; const previewPercent = (newPreviewWidth / startSizes.totalWidth) * 100; const referencePercent = (newReferenceWidth / startSizes.totalWidth) * 100; const consolePercent = (newConsoleWidth / startSizes.totalWidth) * 100; // Apply new sizes if they're reasonable if (topPercent > 10 && bottomPercent > 10 && editorPercent > 10 && previewPercent > 10) { if (splits.length >= 3) { splits[0].setSizes([topPercent, bottomPercent]); splits[1].setSizes([editorPercent, previewPercent]); splits[2].setSizes([referencePercent, consolePercent]); } } } }; const endDrag = (e) => { if (isDragging) { isDragging = false; centerSquare.classList.remove('dragging'); // Release pointer capture if (e && e.pointerId) { centerSquare.releasePointerCapture(e.pointerId); } // Restore normal cursor and text selection document.body.style.userSelect = ''; document.body.style.cursor = ''; updateCenterSquarePosition(); } }; // Use pointer events for better cross-device support and capture centerSquare.addEventListener('pointerdown', startDrag); centerSquare.addEventListener('pointermove', onDrag); centerSquare.addEventListener('pointerup', endDrag); centerSquare.addEventListener('pointercancel', endDrag); // Also listen on document for moves/ups that escape the element document.addEventListener('pointermove', onDrag); document.addEventListener('pointerup', endDrag); }
// Center square init is handled by the Global Init Pipeline above.
import { tokenize } from 'https://aesthetic.computer/aesthetic.computer/lib/kidlisp.mjs'; import { KidLisp } from 'https://aesthetic.computer/aesthetic.computer/lib/kidlisp.mjs'; import { MonacoKidLispHighlighter } from 'https://aesthetic.computer/kidlisp.com/js/monaco-kidlisp-highlighting.mjs?v=3';
const editorContainer = document.getElementById('kidlisp-editor'); const sendButton = document.getElementById('send-button'); const stopButton = document.getElementById('stop-button'); const previewIframe = document.getElementById('preview-iframe'); const bootScreen = document.getElementById('boot-screen'); // Rainbow title animation during boot (like KidLisp baby-colors) const titleEl = document.getElementById('preview-title'); const rainbowColors = ['#FF7096', '#FFB870', '#FFEA70', '#8AFF8A', '#70D6FF', '#C596FF', '#FF96C5']; let rainbowAnimationId = null; function animateRainbowTitle() { if (!titleEl) return; const letters = titleEl.querySelectorAll('.title-letter'); if (letters.length === 0) return; letters.forEach((letter, i) => { const colorIndex = Math.floor(Math.random() * rainbowColors.length); const color = rainbowColors[colorIndex]; letter.style.color = color; // Add subtle glow matching the color const rgb = color.match(/\w\w/g); if (rgb) { const r = parseInt(rgb[0], 16); const g = parseInt(rgb[1], 16); const b = parseInt(rgb[2], 16); letter.style.textShadow = `0 0 8px rgba(${r}, ${g}, ${b}, 0.6)`; } }); // Continue animating every 80ms rainbowAnimationId = setTimeout(animateRainbowTitle, 80); } function stopRainbowTitle() { if (rainbowAnimationId) { clearTimeout(rainbowAnimationId); rainbowAnimationId = null; } if (titleEl) { titleEl.classList.remove('booting'); // Reset to default color const letters = titleEl.querySelectorAll('.title-letter'); letters.forEach(letter => { letter.style.color = ''; letter.style.textShadow = ''; }); } } // Start rainbow animation immediately animateRainbowTitle(); // Track if iframe is ready (set when iframe sends ready signal) let iframeReady = false; // Update play button state based on editor content and iframe readiness function updatePlayButtonState() { const editorValue = window.editor?.getValue() || ''; const hasContent = editorValue.trim().length > 0; const editorPanel = document.getElementById('editor-panel'); const isEditorCollapsed = editorPanel && (editorPanel.dataset.collapsed === 'true' || editorPanel.classList.contains('panel-collapsed')); // Main send button: enable only when iframe ready AND has content const shouldEnableMain = iframeReady && hasContent; sendButton.disabled = !shouldEnableMain; sendButton.style.opacity = shouldEnableMain ? '1' : '0.4'; sendButton.style.cursor = shouldEnableMain ? 'pointer' : 'not-allowed'; // Mini send button: enable when iframe ready AND (has content OR editor is collapsed) // This allows "open editor" action when collapsed and empty const miniSendButton = document.getElementById('mini-send-button'); if (miniSendButton) { const shouldEnableMini = iframeReady && (hasContent || isEditorCollapsed); miniSendButton.disabled = !shouldEnableMini; miniSendButton.style.opacity = shouldEnableMini ? '1' : '0.4'; miniSendButton.style.cursor = shouldEnableMini ? 'pointer' : 'not-allowed'; // Add/remove empty class for outline-only visual when collapsed and empty if (isEditorCollapsed && !hasContent) { miniSendButton.classList.add('empty'); } else { miniSendButton.classList.remove('empty'); } } // Also sync mini button state if (window.syncMiniPlaybackButtons) window.syncMiniPlaybackButtons(); } // Make it globally accessible for content change handlers window.updatePlayButtonState = updatePlayButtonState; // Console functionality (must be defined early for boot messages) const consoleOutput = document.getElementById('console-output'); const mobileConsoleOutput = document.getElementById('mobile-console-output'); // Independent zoom controls for bottom panel tabs (Cmd/Ctrl + Plus/Minus/0) let bottomPanelFontSize = 14; // Default font size (matches CSS) const BOTTOM_FONT_MIN = 8; const BOTTOM_FONT_MAX = 28; const BOTTOM_FONT_DEFAULT = 14; // All bottom panel content areas that should zoom together const bottomPanelElements = [ '#console-output', '#mobile-console-output', '#list-content', '#guide-content', '#reference-content', '#console-panel' ]; // Helper to apply font size to all bottom panel content function applyBottomPanelFontSize(size) { bottomPanelElements.forEach(selector => { const el = document.querySelector(selector); if (el) el.style.fontSize = size + 'px'; }); } // Track which panel was last clicked for zoom targeting let lastClickedPanel = null; // 'bottom' or null (editor handles itself) // Detect clicks in bottom panel area const bottomRow = document.getElementById('bottom-row'); if (bottomRow) { bottomRow.addEventListener('mousedown', () => { lastClickedPanel = 'bottom'; }); } // Clear bottom panel selection when clicking elsewhere (but not editor - it has its own handler) document.addEventListener('mousedown', (e) => { if (bottomRow && !bottomRow.contains(e.target)) { lastClickedPanel = null; } }); // Global keyboard handler for bottom panel zoom document.addEventListener('keydown', (e) => { // Only handle if bottom panel was last clicked if (lastClickedPanel !== 'bottom') return; const isMeta = e.metaKey || e.ctrlKey; if (!isMeta) return; // Check for Plus (= or +), Minus (-), or 0 const isPlus = e.key === '=' || e.key === '+'; const isMinus = e.key === '-' || e.key === '_'; const isZero = e.key === '0'; if (isPlus || isMinus || isZero) { e.preventDefault(); e.stopPropagation(); if (isPlus && bottomPanelFontSize < BOTTOM_FONT_MAX) { bottomPanelFontSize += 2; } else if (isMinus && bottomPanelFontSize > BOTTOM_FONT_MIN) { bottomPanelFontSize -= 2; } else if (isZero) { bottomPanelFontSize = BOTTOM_FONT_DEFAULT; } applyBottomPanelFontSize(bottomPanelFontSize); console.log(`Bottom panel zoom: ${bottomPanelFontSize}px`); } }, true); // Use capture phase to intercept before browser // Track if piece is fully loaded and ready let pieceReady = false; let shouldAutoPlay = false; // Track pending reload for timeout detection (must be defined early for boot-log handler) let pendingReloadTimeout = null; let reloadAttempts = 0; const MAX_RELOAD_ATTEMPTS = 2; // Enhanced KidLisp syntax highlighting for console - matches Top Hits and Monaco // Uses KidLisp tokenizer for accurate coloring including fade: expressions function highlightKidlispCode(code) { // Check if KidLisp tokenizer is available (loaded after Monaco init) if (typeof tokenize !== 'function' || typeof KidLisp !== 'function') { // Fallback to simple highlighting if KidLisp not loaded yet return highlightKidlispCodeSimple(code); } try { // Create a dedicated KidLisp instance for console highlighting (like Top Hits) const consoleKidlisp = new KidLisp(); consoleKidlisp.isEditMode = true; // Edit mode for static display consoleKidlisp.initializeSyntaxHighlighting(code); const coloredString = consoleKidlisp.buildColoredKidlispString(); // Convert KidLisp color codes to HTML spans (same as Top Hits) let highlightedHtml = coloredString.replace(/\\([^\\]+)\\([^\\]*)/g, (match, color, text) => { if (!text) return ''; // Escape HTML entities in text const escapedText = text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); let cssColor = color; // Skip transparent colors (alpha = 0) if (color.includes(',0') && color.split(',').length === 4) { cssColor = 'rgb(128,128,128)'; } // Handle RGB colors if (color.match(/^\d+,\s*\d+,\s*\d+(,\s*\d+)?$/)) { const parts = color.split(',').map(n => parseInt(n.trim())); if (parts.length === 4 && parts[3] === 0) { cssColor = 'rgb(180,180,180)'; } else { cssColor = `rgb(${parts.slice(0,3).join(',')})`; } }
return `<span style="color: ${cssColor}; font-weight: bold;">${escapedText}</span>`; }); return highlightedHtml; } catch (e) { // Fallback to simple highlighting on error return highlightKidlispCodeSimple(code); } } // Simple fallback highlighting (when KidLisp tokenizer not available) function highlightKidlispCodeSimple(code) { // Escape HTML first let html = code .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); // Simple token-based highlighting // Comments (lines starting with ;) html = html.replace(/(^|\n)(;[^\n]*)/g, '$1<span class="hl-comment">$2</span>'); // Strings html = html.replace(/"([^"\\]|\\.)*"/g, '<span class="hl-string">$&</span>'); // Numbers html = html.replace(/\b(\d+\.?\d*)\b/g, '<span class="hl-number">$1</span>'); // Keywords (known commands) const keywords = ['def', 'fn', 'if', 'cond', 'let', 'loop', 'repeat', 'later', 'tap', 'drag', 'lift', 'key', 'draw']; keywords.forEach(kw => { html = html.replace(new RegExp(`\\b(${kw})\\b`, 'g'), '<span class="hl-keyword">$1</span>'); }); // API calls (common ones) const apiCalls = ['wipe', 'ink', 'line', 'box', 'circle', 'text', 'pan', 'zoom', 'blur', 'noise', 'frame', 'width', 'height', 'rainbow', 'wiggle', 'stamp', 'scroll', 'flip', 'bake', 'spin', 'fps', 'write']; apiCalls.forEach(api => { html = html.replace(new RegExp(`\\b(${api})\\b`, 'g'), '<span class="hl-api">$1</span>'); }); // Colors const colors = ['black', 'white', 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta', 'orange', 'purple', 'pink', 'brown', 'gray', 'grey']; colors.forEach(c => { html = html.replace(new RegExp(`\\b(${c})\\b`, 'g'), '<span class="hl-color">$1</span>'); }); // #codes (painting refs) - like #48q html = html.replace(/#[0-9a-zA-Z]{1,8}\b/g, '<span class="hl-hashcode">$&</span>'); // Timing patterns - like 0.5s, 1s... html = html.replace(/\b\d*\.?\d+[sf](\.{1,3})?\b/g, '<span class="hl-timing">$&</span>'); return html; } // Render [$code] references as styled, clickable elements with tooltips function renderEmbeddedSourceRef(message, embeddedSource, embeddedSourceCode) { // Look for [$code] pattern in message const pattern = /\[\$([^\]]+)\]/g; let lastIndex = 0; const parts = []; let match; while ((match = pattern.exec(message)) !== null) { // Add text before match if (match.index > lastIndex) { const textSpan = document.createElement('span'); textSpan.textContent = message.slice(lastIndex, match.index); parts.push(textSpan); } const codeId = match[1]; const codeSpan = document.createElement('span'); codeSpan.className = 'embedded-source-ref'; codeSpan.textContent = `[$${codeId}]`; codeSpan.style.cssText = 'color: #9370DB; font-weight: bold; cursor: pointer; text-decoration: underline; text-decoration-style: dotted;'; // Add tooltip with source code preview if (embeddedSourceCode || embeddedSource === codeId) { const sourceCode = embeddedSourceCode || ''; const preview = sourceCode.length > 300 ? sourceCode.substring(0, 300).trim() + '...' : sourceCode.trim(); codeSpan.title = preview || `Source: $${codeId}`; // Click to copy source or show in modal codeSpan.addEventListener('click', (e) => { e.stopPropagation(); if (sourceCode) { // Show source in a small popup/modal showEmbeddedSourcePopup(codeId, sourceCode); } }); } parts.push(codeSpan); lastIndex = match.index + match[0].length; } // Add remaining text if (lastIndex < message.length) { const textSpan = document.createElement('span'); textSpan.textContent = message.slice(lastIndex); parts.push(textSpan); } return parts.length > 0 ? parts : null; } // Show embedded source code in a popup function showEmbeddedSourcePopup(codeId, sourceCode) { // Remove existing popup if any const existing = document.getElementById('embedded-source-popup'); if (existing) existing.remove(); const popup = document.createElement('div'); popup.id = 'embedded-source-popup'; popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e2e; border: 2px solid #9370DB; border-radius: 8px; padding: 16px; max-width: 500px; max-height: 400px; overflow: auto; z-index: 10000; box-shadow: 0 4px 20px rgba(0,0,0,0.5); font-family: monospace; `; const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; border-bottom: 1px solid #444; padding-bottom: 8px;'; header.innerHTML = `<span style="color: #9370DB; font-weight: bold;">$${codeId}</span>`; const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.cssText = 'background: none; border: none; color: #888; cursor: pointer; font-size: 18px;'; closeBtn.addEventListener('click', () => popup.remove()); header.appendChild(closeBtn); const codeBlock = document.createElement('pre'); codeBlock.style.cssText = 'margin: 0; white-space: pre-wrap; word-break: break-word; color: #cdd6f4; font-size: 12px; line-height: 1.4;'; codeBlock.textContent = sourceCode; popup.appendChild(header); popup.appendChild(codeBlock); document.body.appendChild(popup); // Close on escape or click outside const closeHandler = (e) => { if (e.key === 'Escape' || (e.type === 'click' && !popup.contains(e.target))) { popup.remove(); document.removeEventListener('keydown', closeHandler); document.removeEventListener('click', closeHandler); } }; setTimeout(() => { document.addEventListener('keydown', closeHandler); document.addEventListener('click', closeHandler); }, 100); } // Batch console scroll-to-bottom to avoid forced reflow on every log line let _consoleScrollRafPending = false; function scheduleConsoleScroll() { if (_consoleScrollRafPending) return; _consoleScrollRafPending = true; requestAnimationFrame(() => { _consoleScrollRafPending = false; const co = document.getElementById('console-output'); if (co) co.scrollTop = co.scrollHeight; const mc = document.getElementById('mobile-console-output'); if (mc) mc.scrollTop = mc.scrollHeight; }); }
// 📸 Add console image entry from snap function function addConsoleImageEntry(data) { const { imageDataUrl, frameCount, timestamp, dimensions, pieceCode, pieceLabel, filename, userHandle, embeddedSource } = data; // Determine border color based on theme const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || (!document.documentElement.getAttribute('data-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); const borderColor = isDarkMode ? 'rgba(78, 205, 196, 0.3)' : 'rgba(78, 130, 120, 0.25)'; const entry = document.createElement('div'); entry.className = 'console-entry console-image-entry'; entry.style.cssText = ` display: flex; align-items: flex-start; gap: 12px; padding: 12px 12px 16px 12px; margin: 0; border-top: 1px solid ${borderColor}; border-bottom: 1px solid ${borderColor}; `; // Thumbnail image - tap to view in modal const img = document.createElement('img'); img.src = imageDataUrl; img.className = 'console-image-thumbnail'; img.style.cssText = ` width: 120px; height: auto; image-rendering: pixelated; border-radius: 4px; cursor: pointer; `; img.title = 'Tap to view'; // Use pieceLabel (e.g., "$nece") for display, or fall back to embeddedSource/pieceCode const displayLabel = pieceLabel || (pieceCode ? `$${pieceCode}` : (embeddedSource?.substring(0, 20) + '...' || 'piece')); // Use provided filename or generate one const effectiveFilename = filename || generateConsoleImageFilename(pieceCode); // Click thumbnail to open modal (pass the $code for download filename) img.addEventListener('click', () => showConsoleImagePopup(imageDataUrl, dimensions, pieceCode, frameCount, effectiveFilename)); // Metadata column const meta = document.createElement('div'); meta.className = 'console-image-meta'; meta.style.cssText = 'font-size: 11px; font-family: monospace; color: #888; flex: 1;'; // Build display with $code and optionally @user const userDisplay = userHandle ? ` <span style="color: #9370DB;">${userHandle}</span>` : ''; meta.innerHTML = ` <span style="color: #4ecdc4; font-weight: bold;">📸 ${displayLabel}</span>${userDisplay}<br> <span style="color: #f8b500;">@ ${timestamp || 'now'}</span><br> <span style="color: #666;">Frame ${frameCount || 0}</span><br> <span style="color: #888;">[${dimensions?.width || '?'}×${dimensions?.height || '?'}]</span> `; // ☕ Mug button - turn screenshot into buyable mug (only show when $code exists) let mugBtn = null; if (pieceCode) { mugBtn = document.createElement('button'); mugBtn.innerHTML = '☕ mug <span style="color: #90EE90;">$18</span>'; mugBtn.title = 'Turn into a mug! Opens preview page.'; mugBtn.style.cssText = ` background: rgba(139, 90, 43, 0.8); border: none; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-size: 14px; opacity: 0.85; transition: opacity 0.2s, transform 0.1s, background 0.2s; align-self: flex-start; font-family: monospace; color: white; `; mugBtn.addEventListener('mouseenter', () => { mugBtn.style.opacity = '1'; mugBtn.style.transform = 'scale(1.05)'; mugBtn.style.background = 'rgba(160, 100, 50, 0.95)'; }); mugBtn.addEventListener('mouseleave', () => { mugBtn.style.opacity = '0.85'; mugBtn.style.transform = 'scale(1)'; mugBtn.style.background = 'rgba(139, 90, 43, 0.8)'; }); mugBtn.addEventListener('click', (e) => { e.stopPropagation(); openMugFromScreenshot(imageDataUrl, mugBtn, pieceCode); }); } entry.appendChild(img); entry.appendChild(meta); if (mugBtn) entry.appendChild(mugBtn); consoleOutput.appendChild(entry);
// Also sync to mobile console (clone the entry and re-attach mug button handler) const mobileConsole = document.getElementById('mobile-console-output'); if (mobileConsole) { const clone = entry.cloneNode(true); // Re-attach mug button click handler to cloned button const clonedMugBtn = clone.querySelector('button'); if (clonedMugBtn && mugBtn) { clonedMugBtn.addEventListener('click', (e) => { e.stopPropagation(); openMugFromScreenshot(imageDataUrl, clonedMugBtn, pieceCode); }); clonedMugBtn.addEventListener('mouseenter', () => { clonedMugBtn.style.opacity = '1'; clonedMugBtn.style.transform = 'scale(1.05)'; clonedMugBtn.style.background = 'rgba(160, 100, 50, 0.95)'; }); clonedMugBtn.addEventListener('mouseleave', () => { clonedMugBtn.style.opacity = '0.85'; clonedMugBtn.style.transform = 'scale(1)'; clonedMugBtn.style.background = 'rgba(139, 90, 43, 0.8)'; }); } mobileConsole.appendChild(clone); } scheduleConsoleScroll(); }
// Generate timestamped filename like bios.mjs does function generateConsoleImageFilename(pieceCode) { const d = new Date(); const year = d.getFullYear(); const month = d.getMonth() + 1; const day = d.getDate(); const hour = d.getHours(); const minute = d.getMinutes(); const second = d.getSeconds(); const ms = d.getMilliseconds().toString().padStart(3, '0'); const timestamp = `${year}.${month}.${day}.${hour}.${minute}.${second}.${ms}`; // Add $ prefix for code identifiers (short alphanumeric strings) const isCodeId = pieceCode && /^[a-zA-Z0-9]{2,6}$/.test(pieceCode); const piece = isCodeId ? `$${pieceCode}` : (pieceCode || 'kidlisp'); return `${piece}-${timestamp}.png`; } // Download console image as PNG function downloadConsoleImage(dataUrl, pieceCode) { const filename = generateConsoleImageFilename(pieceCode); const link = document.createElement('a'); link.href = dataUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // 🖼️ Unified modal state (handles both screenshot preview and mug) let unifiedModal = null; let unifiedModalState = { mode: 'screenshot', // 'screenshot' | 'mug-loading' | 'mug-ready' isProcessing: false, dataUrl: null, filename: null, pieceCode: null, mugColor: null, previewData: null, paintingCode: null, kidlispCode: null, }; // ☕ Available mug colors const MUG_COLORS = ['white', 'black', 'blue', 'pink', 'orange', 'green', 'red', 'yellow', 'darkblue', 'darkgreen']; // Pick a random mug color function randomMugColor() { return MUG_COLORS[Math.floor(Math.random() * MUG_COLORS.length)]; } // Add CSS for unified modal if not exists function ensureUnifiedModalStyles() { if (document.getElementById('unified-modal-styles')) return; const style = document.createElement('style'); style.id = 'unified-modal-styles'; style.textContent = ` @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .unified-modal-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 100000; backdrop-filter: blur(4px); } .unified-modal-backdrop.processing { cursor: wait; } .unified-modal-content { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; padding: 20px; max-width: 420px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); } .unified-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .unified-modal-title { margin: 0; color: #f0e6d3; font-family: monospace; font-size: 18px; } .unified-modal-close { background: none; border: none; color: #888; font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s; } .unified-modal-close:hover { color: #fff; background: rgba(255,255,255,0.1); } .unified-modal-close:disabled { opacity: 0.3; cursor: not-allowed; } .unified-modal-preview { background: #0a0a14; border-radius: 12px; padding: 12px; margin-bottom: 12px; display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 180px; } .unified-modal-preview img { max-width: 100%; max-height: 280px; border-radius: 8px; image-rendering: pixelated; } .unified-modal-buttons { display: flex; gap: 10px; flex-wrap: wrap; } .unified-modal-btn { flex: 1; min-width: 120px; padding: 12px 16px; border: none; border-radius: 8px; font-size: 14px; font-family: monospace; cursor: pointer; transition: all 0.2s; } .unified-modal-btn:hover:not(:disabled) { transform: scale(1.02); } .unified-modal-btn:disabled { opacity: 0.6; cursor: not-allowed; } .unified-modal-btn.download { background: #4ecdc4; color: #1e1e2e; } .unified-modal-btn.mug { background: linear-gradient(135deg, #8b5a2b 0%, #a0642a 100%); color: white; box-shadow: 0 4px 12px rgba(139, 90, 43, 0.4); } .unified-modal-btn.mug:hover:not(:disabled) { box-shadow: 0 6px 16px rgba(139, 90, 43, 0.6); } .unified-modal-btn.buy { width: 100%; flex: none; background: linear-gradient(135deg, #8b5a2b 0%, #a0642a 100%); color: white; font-weight: bold; font-size: 16px; padding: 14px 20px; box-shadow: 0 4px 12px rgba(139, 90, 43, 0.4); } .unified-modal-loading { color: #8b5a2b; font-family: monospace; font-size: 16px; animation: pulse 1.5s ease-in-out infinite; } .unified-modal-url-section { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding: 10px; background: rgba(0, 0, 0, 0.4); border-radius: 8px; } .unified-modal-url-section canvas { display: block; image-rendering: pixelated; border-radius: 4px; flex-shrink: 0; } .unified-modal-url-text { font-family: monospace; font-size: 13px; color: #aaa; word-break: break-all; line-height: 1.3; flex: 1; } `; document.head.appendChild(style); } // ⚙️ Preferences modal state let preferencesModal = null; // Add CSS for preferences modal function ensurePreferencesModalStyles() { if (document.getElementById('preferences-modal-styles')) return; const style = document.createElement('style'); style.id = 'preferences-modal-styles'; style.textContent = ` .preferences-modal-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 100000; backdrop-filter: blur(4px); } [data-theme="light"] .preferences-modal-backdrop { background: rgba(255, 255, 255, 0.75); }
.preferences-modal-content { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; padding: 16px; max-width: 340px; width: 90%; max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); } [data-theme="light"] .preferences-modal-content { background: linear-gradient(135deg, #f5f0e8 0%, #ebe4d8 100%); border-color: rgba(0, 0, 0, 0.1); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); }
.preferences-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.preferences-modal-title { margin: 0; color: #f0e6d3; font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-size: 18px; font-weight: bold; display: flex; align-items: center; gap: 8px; } [data-theme="light"] .preferences-modal-title { color: #2a2a2a; }
.preferences-modal-title .title-gear { width: 20px; height: 20px; color: #f0e6d3; animation: gear-spin-slow 4s linear infinite; } [data-theme="light"] .preferences-modal-title .title-gear { color: #2a2a2a; }
@keyframes gear-spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.preferences-modal-close { background: none; border: none; color: #888; font-size: 18px; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s; }
.preferences-modal-close:hover { color: #fff; background: rgba(255,255,255,0.1); } [data-theme="light"] .preferences-modal-close:hover { color: #000; background: rgba(0,0,0,0.08); }
.preferences-section { margin-bottom: 14px; }
.preferences-section:last-child { margin-bottom: 0; }
.preferences-label { color: #888; font-family: monospace; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; display: block; } [data-theme="light"] .preferences-label { color: #999; }
.preferences-options { display: flex; gap: 6px; flex-wrap: wrap; }
.preferences-option { flex: 1; min-width: 70px; padding: 8px 10px; border: 2px solid rgba(255, 255, 255, 0.1); border-radius: 8px; background: rgba(0, 0, 0, 0.3); color: #aaa; font-family: monospace; font-size: 12px; cursor: pointer; transition: all 0.2s; text-align: center; } [data-theme="light"] .preferences-option { border-color: rgba(0, 0, 0, 0.1); background: rgba(255, 255, 255, 0.6); color: #666; }
.preferences-option:hover { border-color: rgba(255, 255, 255, 0.3); color: #fff; } [data-theme="light"] .preferences-option:hover { border-color: rgba(0, 0, 0, 0.25); color: #222; }
.preferences-option.active { border-color: #4ecdc4; background: rgba(78, 205, 196, 0.15); color: #4ecdc4; }
.preferences-option .option-icon { font-size: 16px; display: block; margin-bottom: 2px; }
.preferences-option .option-text { font-size: 10px; opacity: 0.8; }
.lang-options { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.lang-option-btn { padding: 7px 10px; border: 2px solid rgba(255, 255, 255, 0.1); border-radius: 8px; background: rgba(0, 0, 0, 0.3); color: #aaa; font-family: monospace; font-size: 12px; cursor: pointer; transition: all 0.2s; text-align: left; display: flex; align-items: center; gap: 6px; } [data-theme="light"] .lang-option-btn { border-color: rgba(0, 0, 0, 0.1); background: rgba(255, 255, 255, 0.6); color: #666; }
.lang-option-btn:hover { border-color: rgba(255, 255, 255, 0.3); color: #fff; } [data-theme="light"] .lang-option-btn:hover { border-color: rgba(0, 0, 0, 0.25); color: #222; }
.lang-option-btn.active { border-color: #4ecdc4; background: rgba(78, 205, 196, 0.15); color: #4ecdc4; }
.lang-option-btn .lang-flag { font-size: 16px; }
/* GPU Rendering toggle section */ .pref-toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 5px 0; } .pref-toggle-row.master { padding: 8px 10px; background: rgba(0, 0, 0, 0.3); border-radius: 8px; margin-bottom: 6px; } [data-theme="light"] .pref-toggle-row.master { background: rgba(0, 0, 0, 0.05); } .pref-toggle-label { color: #ccc; font-family: monospace; font-size: 12px; } [data-theme="light"] .pref-toggle-label { color: #555; } .pref-toggle-row.master .pref-toggle-label { color: #f0e6d3; font-size: 13px; font-weight: bold; } [data-theme="light"] .pref-toggle-row.master .pref-toggle-label { color: #2a2a2a; } .pref-toggle-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; } .pref-toggle-switch input { opacity: 0; width: 0; height: 0; } .pref-toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.15); border-radius: 10px; cursor: pointer; transition: background 0.2s; } [data-theme="light"] .pref-toggle-slider { background: rgba(0, 0, 0, 0.15); } .pref-toggle-slider::before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; top: 3px; background: #888; border-radius: 50%; transition: transform 0.2s, background 0.2s; } .pref-toggle-switch input:checked + .pref-toggle-slider { background: rgba(78, 205, 196, 0.3); } .pref-toggle-switch input:checked + .pref-toggle-slider::before { transform: translateX(16px); background: #4ecdc4; } .pref-gpu-effects { padding-left: 10px; border-left: 2px solid rgba(255, 255, 255, 0.05); transition: opacity 0.2s; } [data-theme="light"] .pref-gpu-effects { border-left-color: rgba(0, 0, 0, 0.06); } .pref-gpu-effects.disabled { opacity: 0.35; pointer-events: none; }
/* Dashboard / Profile section */ .pref-dashboard { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; } .pref-gauge { background: rgba(0, 0, 0, 0.35); border-radius: 8px; padding: 6px 8px 5px; text-align: center; position: relative; overflow: hidden; } [data-theme="light"] .pref-gauge { background: rgba(0, 0, 0, 0.04); } .pref-gauge.wide { grid-column: 1 / -1; } .pref-gauge-label { font-family: monospace; font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; } .pref-gauge-value { font-family: monospace; font-size: 16px; font-weight: bold; color: #f0e6d3; line-height: 1; } [data-theme="light"] .pref-gauge-value { color: #2a2a2a; } .pref-gauge-value.small { font-size: 11px; color: #aaa; font-weight: normal; } .pref-gauge-unit { font-size: 10px; color: #777; margin-left: 2px; } /* Needle gauge (semicircle) */ .pref-needle-wrap { width: 56px; height: 30px; margin: 0 auto 2px; position: relative; overflow: hidden; } .pref-needle-bg { width: 56px; height: 56px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.08); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(-45deg); box-sizing: border-box; position: absolute; top: 0; left: 0; } [data-theme="light"] .pref-needle-bg { border-color: rgba(0,0,0,0.08); border-bottom-color: transparent; border-right-color: transparent; } .pref-needle { position: absolute; bottom: 0; left: 50%; width: 2px; height: 24px; background: #f0e6d3; transform-origin: bottom center; transform: rotate(-90deg); transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); border-radius: 1px; } .pref-needle::after { content: ''; position: absolute; bottom: -2px; left: -2px; width: 6px; height: 6px; background: #f0e6d3; border-radius: 50%; } .pref-needle.red { background: #ff6b6b; } .pref-needle.red::after { background: #ff6b6b; } .pref-needle.yellow { background: #ffd93d; } .pref-needle.yellow::after { background: #ffd93d; } .pref-needle.green { background: #4ecdc4; } .pref-needle.green::after { background: #4ecdc4; } /* Status dot */ .pref-status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; vertical-align: middle; } .pref-status-dot.running { background: #4ecdc4; box-shadow: 0 0 5px #4ecdc4; } .pref-status-dot.paused { background: #ffd93d; box-shadow: 0 0 5px #ffd93d; } .pref-status-dot.stopped { background: #666; } .pref-info-grid { display: grid; grid-template-columns: auto 1fr; gap: 1px 8px; text-align: left; font-family: monospace; font-size: 10px; } .pref-info-key { color: #666; } [data-theme="light"] .pref-info-key { color: #999; } .pref-info-val { color: #aaa; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } [data-theme="light"] .pref-info-val { color: #555; }
/* Wide/short screens: go horizontal so content doesn't overflow vertically */ @media (min-width: 600px) and (max-height: 700px) { .preferences-modal-content { max-width: 640px; max-height: 90vh; padding: 14px 20px; } .preferences-modal-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; } .preferences-section { margin-bottom: 6px; } }
/* General desktop: allow wider modal */ @media (min-width: 600px) and (min-height: 701px) { .preferences-modal-content { max-width: 440px; } } `; document.head.appendChild(style); } // ⚙️ Show preferences modal (exposed globally for splash logo click) window.showPreferencesModal = function() { ensurePreferencesModalStyles(); // If modal exists, just show it if (preferencesModal) { preferencesModal.style.display = 'flex'; updatePreferencesModalContent(); // Live-update dashboard gauges while modal is open clearInterval(dashboardInterval); dashboardInterval = setInterval(updateDashboardGauges, 250); return; } // Create modal backdrop preferencesModal = document.createElement('div'); preferencesModal.className = 'preferences-modal-backdrop'; // Modal content const content = document.createElement('div'); content.className = 'preferences-modal-content'; // Header with close button const header = document.createElement('div'); header.className = 'preferences-modal-header'; const title = document.createElement('h2'); title.className = 'preferences-modal-title'; title.innerHTML = '<svg class="title-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>Preferences'; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.className = 'preferences-modal-close'; closeBtn.textContent = '✕'; closeBtn.addEventListener('click', closePreferencesModal); header.appendChild(closeBtn); content.appendChild(header);
// Body wrapper for responsive grid layout const body = document.createElement('div'); body.className = 'preferences-modal-body';
// Theme section const themeSection = document.createElement('div'); themeSection.className = 'preferences-section'; themeSection.innerHTML = ` <span class="preferences-label">Theme</span> <div class="preferences-options" id="pref-theme-options"> <button class="preferences-option" data-theme="light"> <span class="option-icon">☀️</span> <span class="option-text">Light</span> </button> <button class="preferences-option" data-theme="dark"> <span class="option-icon">🌙</span> <span class="option-text">Dark</span> </button> <button class="preferences-option" data-theme="system"> <span class="option-icon">💻</span> <span class="option-text">System</span> </button> </div> `; body.appendChild(themeSection);
// Workspace section const workspaceSection = document.createElement('div'); workspaceSection.className = 'preferences-section'; workspaceSection.innerHTML = ` <span class="preferences-label">Workspace</span> <div class="preferences-options" id="pref-workspace-options"> <button class="preferences-option" data-ui-mode="studio"> <span class="option-icon">🧪</span> <span class="option-text">Studio</span> </button> <button class="preferences-option" data-ui-mode="stage"> <span class="option-icon">🎬</span> <span class="option-text">Stage</span> </button> </div> `; body.appendChild(workspaceSection); // Language section const langSection = document.createElement('div'); langSection.className = 'preferences-section'; langSection.innerHTML = ` <span class="preferences-label">Language</span> <div class="lang-options" id="pref-lang-options"> <button class="lang-option-btn" data-lang="en"> <span class="lang-flag fi fi-us"></span> <span>English</span> </button> <button class="lang-option-btn" data-lang="es"> <span class="lang-flag fi fi-es"></span> <span>Español</span> </button> <button class="lang-option-btn" data-lang="zh"> <span class="lang-flag fi fi-cn"></span> <span>中文</span> </button> <button class="lang-option-btn" data-lang="da"> <span class="lang-flag fi fi-dk"></span> <span>Dansk</span> </button> </div> `; body.appendChild(langSection);
// Rendering section (GPU toggle + individual effects) const renderSection = document.createElement('div'); renderSection.className = 'preferences-section'; renderSection.innerHTML = ` <span class="preferences-label">Rendering</span> <div class="pref-toggle-row master"> <span class="pref-toggle-label">GPU Acceleration</span> <label class="pref-toggle-switch"> <input type="checkbox" id="pref-gpu-master" checked> <span class="pref-toggle-slider"></span> </label> </div> <div class="pref-gpu-effects" id="pref-gpu-effects"> <div class="pref-toggle-row"> <span class="pref-toggle-label">Spin / Zoom / Scroll</span> <label class="pref-toggle-switch"> <input type="checkbox" id="pref-gpu-spin" checked> <span class="pref-toggle-slider"></span> </label> </div> <div class="pref-toggle-row"> <span class="pref-toggle-label">Flood Fill</span> <label class="pref-toggle-switch"> <input type="checkbox" id="pref-gpu-flood" checked> <span class="pref-toggle-slider"></span> </label> </div> <div class="pref-toggle-row"> <span class="pref-toggle-label">Contrast / Brightness</span> <label class="pref-toggle-switch"> <input type="checkbox" id="pref-gpu-contrast" checked> <span class="pref-toggle-slider"></span> </label> </div> <div class="pref-toggle-row"> <span class="pref-toggle-label">Layer Compositing</span> <label class="pref-toggle-switch"> <input type="checkbox" id="pref-gpu-composite" checked> <span class="pref-toggle-slider"></span> </label> </div> </div> `; body.appendChild(renderSection);
// Dashboard / Profile section const dashSection = document.createElement('div'); dashSection.className = 'preferences-section'; dashSection.innerHTML = ` <span class="preferences-label">Dashboard</span> <div class="pref-dashboard"> <div class="pref-gauge" id="pref-dash-fps"> <div class="pref-gauge-label">FPS</div> <div class="pref-needle-wrap"> <div class="pref-needle-bg"></div> <div class="pref-needle" id="pref-fps-needle"></div> </div> <div class="pref-gauge-value" id="pref-fps-value">--</div> </div> <div class="pref-gauge" id="pref-dash-status"> <div class="pref-gauge-label">State</div> <div class="pref-gauge-value" id="pref-status-value" style="margin-top:4px"> <span class="pref-status-dot stopped"></span>idle </div> </div> <div class="pref-gauge" id="pref-dash-memory"> <div class="pref-gauge-label">Memory</div> <div class="pref-gauge-value" id="pref-memory-value" style="margin-top:4px">--</div> </div> <div class="pref-gauge wide" id="pref-dash-device"> <div class="pref-gauge-label">Device</div> <div class="pref-info-grid" id="pref-device-info"></div> </div> </div> `; body.appendChild(dashSection);
content.appendChild(body); preferencesModal.appendChild(content); // Close on backdrop click preferencesModal.addEventListener('click', (e) => { if (e.target === preferencesModal) closePreferencesModal(); }); // Close on escape const escHandler = (e) => { if (e.key === 'Escape') { closePreferencesModal(); } }; document.addEventListener('keydown', escHandler); preferencesModal.escHandler = escHandler; document.body.appendChild(preferencesModal); // Setup event handlers setupPreferencesHandlers(); updatePreferencesModalContent(); // Live-update dashboard gauges while modal is open clearInterval(dashboardInterval); dashboardInterval = setInterval(updateDashboardGauges, 250); } // Setup preferences modal event handlers function setupPreferencesHandlers() { if (!preferencesModal) return; // Theme options const themeOptions = preferencesModal.querySelectorAll('#pref-theme-options .preferences-option'); themeOptions.forEach(btn => { btn.addEventListener('click', () => { const theme = btn.getAttribute('data-theme'); if (theme) { window.manualThemeSet = theme !== 'system'; window.applyTheme(theme); updatePreferencesModalContent(); } }); });
// Workspace options const workspaceOptions = preferencesModal.querySelectorAll('#pref-workspace-options .preferences-option'); workspaceOptions.forEach(btn => { btn.addEventListener('click', () => { const mode = btn.getAttribute('data-ui-mode'); if (mode && typeof window.setUiMode === 'function') { window.setUiMode(mode); updatePreferencesModalContent(); } }); }); // Language options const langOptions = preferencesModal.querySelectorAll('#pref-lang-options .lang-option-btn'); langOptions.forEach(btn => { btn.addEventListener('click', () => { const lang = btn.getAttribute('data-lang'); if (lang && typeof setLanguage === 'function') { setLanguage(lang); updatePreferencesModalContent(); } }); });
// GPU rendering toggles const gpuMaster = preferencesModal.querySelector('#pref-gpu-master'); const gpuEffectsPanel = preferencesModal.querySelector('#pref-gpu-effects'); const gpuEffectIds = ['pref-gpu-spin', 'pref-gpu-flood', 'pref-gpu-contrast', 'pref-gpu-composite'];
function onGpuChanged() { const masterOn = gpuMaster.checked; gpuEffectsPanel.classList.toggle('disabled', !masterOn); const config = { gpu: masterOn, effects: { spin: preferencesModal.querySelector('#pref-gpu-spin').checked, flood: preferencesModal.querySelector('#pref-gpu-flood').checked, contrast: preferencesModal.querySelector('#pref-gpu-contrast').checked, composite: preferencesModal.querySelector('#pref-gpu-composite').checked, }, }; try { localStorage.setItem('kidlisp-gpu-config', JSON.stringify(config)); } catch {} sendToIframe({ type: 'kidlisp-gpu-config', config }); }
gpuMaster.addEventListener('change', onGpuChanged); gpuEffectIds.forEach(id => { const el = preferencesModal.querySelector('#' + id); if (el) el.addEventListener('change', onGpuChanged); }); } // Update preferences modal to reflect current state function updatePreferencesModalContent() { if (!preferencesModal) return; // Update theme selection const themeOptions = preferencesModal.querySelectorAll('#pref-theme-options .preferences-option'); const currentVisualTheme = document.documentElement.getAttribute('data-theme') || 'light'; const isSystemTheme = !window.manualThemeSet; themeOptions.forEach(btn => { const theme = btn.getAttribute('data-theme'); btn.classList.remove('active'); if (isSystemTheme && theme === 'system') { btn.classList.add('active'); } else if (!isSystemTheme && theme === currentVisualTheme) { btn.classList.add('active'); } });
// Update workspace selection const workspaceOptions = preferencesModal.querySelectorAll('#pref-workspace-options .preferences-option'); const activeUiMode = typeof window.getUiMode === 'function' ? window.getUiMode() : 'studio'; workspaceOptions.forEach(btn => { const mode = btn.getAttribute('data-ui-mode'); btn.classList.toggle('active', mode === activeUiMode); }); // Update language selection const langOptions = preferencesModal.querySelectorAll('#pref-lang-options .lang-option-btn'); langOptions.forEach(btn => { const lang = btn.getAttribute('data-lang'); btn.classList.toggle('active', lang === currentLang); });
// Update GPU rendering toggles from localStorage try { const stored = localStorage.getItem('kidlisp-gpu-config'); if (stored) { const config = JSON.parse(stored); const gpuMaster = preferencesModal.querySelector('#pref-gpu-master'); const gpuEffectsPanel = preferencesModal.querySelector('#pref-gpu-effects'); if (gpuMaster) { gpuMaster.checked = !!config.gpu; gpuEffectsPanel?.classList.toggle('disabled', !config.gpu); } if (config.effects) { const map = { spin: '#pref-gpu-spin', flood: '#pref-gpu-flood', contrast: '#pref-gpu-contrast', composite: '#pref-gpu-composite' }; for (const [key, sel] of Object.entries(map)) { const el = preferencesModal.querySelector(sel); if (el) el.checked = config.effects[key] !== false; } } } } catch {}
// Dashboard gauges updateDashboardGauges(); }
let dashboardInterval = null;
function updateDashboardGauges() { if (!preferencesModal) return;
// FPS needle: 0 → -90deg, 60 → 0deg, 120 → +90deg (beyond 60 is bonus) const fps = fpsTracker.fps; const fpsNeedle = preferencesModal.querySelector('#pref-fps-needle'); const fpsValue = preferencesModal.querySelector('#pref-fps-value'); if (fpsNeedle && fpsValue) { const clamped = Math.min(120, Math.max(0, fps)); const deg = -90 + (clamped / 120) * 180; fpsNeedle.style.transform = `rotate(${deg}deg)`; fpsNeedle.className = 'pref-needle ' + (fps < 20 ? 'red' : fps < 45 ? 'yellow' : 'green'); const fc = fpsTracker.frameCount; fpsValue.innerHTML = fps ? `${fps} <span class="pref-gauge-unit">${fc.toLocaleString()}f</span>` : '--'; }
// Status const statusValue = preferencesModal.querySelector('#pref-status-value'); if (statusValue) { const state = isPlaying ? (isPaused ? 'paused' : 'running') : 'stopped'; const label = isPlaying ? (isPaused ? 'paused' : 'running') : 'idle'; statusValue.innerHTML = `<span class="pref-status-dot ${state}"></span>${label}`; }
// Memory (Chrome only) const memValue = preferencesModal.querySelector('#pref-memory-value'); if (memValue) { if (performance.memory) { const mb = Math.round(performance.memory.usedJSHeapSize / 1048576); const limit = Math.round(performance.memory.jsHeapSizeLimit / 1048576); memValue.innerHTML = `${mb}<span class="pref-gauge-unit">MB</span>`; } else { memValue.textContent = 'n/a'; memValue.classList.add('small'); } }
// Device info (populate once) const deviceInfo = preferencesModal.querySelector('#pref-device-info'); if (deviceInfo && !deviceInfo.dataset.populated) { deviceInfo.dataset.populated = '1'; const ua = navigator.userAgent; let browser = 'Unknown'; if (ua.includes('Firefox/')) browser = 'Firefox ' + ua.split('Firefox/')[1].split(' ')[0]; else if (ua.includes('Edg/')) browser = 'Edge ' + ua.split('Edg/')[1].split(' ')[0]; else if (ua.includes('Chrome/')) browser = 'Chrome ' + ua.split('Chrome/')[1].split(' ')[0]; else if (ua.includes('Safari/')) browser = 'Safari ' + (ua.split('Version/')[1]||'').split(' ')[0];
let os = navigator.platform || 'Unknown'; if (ua.includes('Mac OS X')) os = 'macOS'; else if (ua.includes('Windows')) os = 'Windows'; else if (ua.includes('Linux')) os = 'Linux'; else if (ua.includes('Android')) os = 'Android'; else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
let gpu = 'Unknown'; try { const c = document.createElement('canvas'); const g = c.getContext('webgl2') || c.getContext('webgl'); if (g) { const ext = g.getExtension('WEBGL_debug_renderer_info'); if (ext) gpu = g.getParameter(ext.UNMASKED_RENDERER_WEBGL); else gpu = g.getParameter(g.RENDERER); } } catch {} // Truncate long GPU strings if (gpu.length > 42) gpu = gpu.slice(0, 40) + '…';
const dpr = window.devicePixelRatio || 1; const res = `${screen.width}×${screen.height} @${dpr}x`;
const rows = [ ['Browser', browser], ['OS', os], ['Screen', res], ['GPU', gpu], ['Cores', navigator.hardwareConcurrency || '?'], ]; deviceInfo.innerHTML = rows.map(([k, v]) => `<span class="pref-info-key">${k}</span><span class="pref-info-val" title="${v}">${v}</span>` ).join(''); } }
// Close preferences modal function closePreferencesModal() { clearInterval(dashboardInterval); dashboardInterval = null; if (preferencesModal) { preferencesModal.style.display = 'none'; if (preferencesModal.escHandler) { document.removeEventListener('keydown', preferencesModal.escHandler); } } } // 🖼️ Show unified modal (screenshot or mug) function showUnifiedModal(options) { const { mode, dataUrl, filename, pieceCode, dimensions, frameCount, mugColor, previewData, paintingCode, kidlispCode, isProcessing } = options; ensureUnifiedModalStyles(); // Update state unifiedModalState = { mode: mode || 'screenshot', isProcessing: isProcessing || false, dataUrl, filename, pieceCode, mugColor: mugColor || randomMugColor(), previewData, paintingCode, kidlispCode, }; // If modal exists, update in place if (unifiedModal) { updateUnifiedModalContent(); return; } // Create modal backdrop unifiedModal = document.createElement('div'); unifiedModal.className = 'unified-modal-backdrop' + (isProcessing ? ' processing' : ''); // Modal content const content = document.createElement('div'); content.className = 'unified-modal-content'; // Header with close button const header = document.createElement('div'); header.className = 'unified-modal-header'; const title = document.createElement('h2'); title.className = 'unified-modal-title'; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.className = 'unified-modal-close'; closeBtn.textContent = '✕'; closeBtn.addEventListener('click', () => { if (!unifiedModalState.isProcessing) closeUnifiedModal(); }); header.appendChild(closeBtn); // Preview container const previewContainer = document.createElement('div'); previewContainer.className = 'unified-modal-preview'; // URL section (for mug mode) const urlSection = document.createElement('div'); urlSection.className = 'unified-modal-url-section'; urlSection.style.display = 'none'; // Button container const buttons = document.createElement('div'); buttons.className = 'unified-modal-buttons'; content.appendChild(header); content.appendChild(previewContainer); content.appendChild(urlSection); content.appendChild(buttons); unifiedModal.appendChild(content); // Close on backdrop click (if not processing) unifiedModal.addEventListener('click', (e) => { if (e.target === unifiedModal && !unifiedModalState.isProcessing) closeUnifiedModal(); }); // Close on escape (if not processing) const escHandler = (e) => { if (e.key === 'Escape' && !unifiedModalState.isProcessing) { closeUnifiedModal(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); unifiedModal.escHandler = escHandler; document.body.appendChild(unifiedModal); updateUnifiedModalContent(); } // Update unified modal content based on current state function updateUnifiedModalContent() { if (!unifiedModal) return; const { mode, isProcessing, dataUrl, filename, pieceCode, mugColor, previewData, paintingCode, kidlispCode } = unifiedModalState; const title = unifiedModal.querySelector('.unified-modal-title'); const closeBtn = unifiedModal.querySelector('.unified-modal-close'); const previewContainer = unifiedModal.querySelector('.unified-modal-preview'); const urlSection = unifiedModal.querySelector('.unified-modal-url-section'); const buttons = unifiedModal.querySelector('.unified-modal-buttons'); // Update processing state unifiedModal.classList.toggle('processing', isProcessing); closeBtn.disabled = isProcessing; // Clear containers previewContainer.innerHTML = ''; urlSection.innerHTML = ''; buttons.innerHTML = ''; if (mode === 'screenshot') { // Screenshot preview mode title.textContent = '📸 screenshot'; urlSection.style.display = 'none'; const img = document.createElement('img'); img.src = dataUrl; previewContainer.appendChild(img); // Download button const downloadBtn = document.createElement('button'); downloadBtn.className = 'unified-modal-btn download'; downloadBtn.textContent = filename || 'download.png'; downloadBtn.addEventListener('click', () => { const link = document.createElement('a'); link.href = dataUrl; link.download = filename || 'kidlisp-screenshot.png'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); buttons.appendChild(downloadBtn); // Mug button (only if pieceCode exists) if (pieceCode) { const mugBtn = document.createElement('button'); mugBtn.className = 'unified-modal-btn mug'; mugBtn.innerHTML = '☕ mug <span style="color: #90EE90;">$18</span>'; mugBtn.addEventListener('click', () => { openMugFromScreenshot(dataUrl, null, pieceCode); }); buttons.appendChild(mugBtn); } } else if (mode === 'mug-loading') { // Mug loading mode title.textContent = `☕ ${mugColor} mug...`; // Show original image while loading (dimmed) if (dataUrl) { const img = document.createElement('img'); img.src = dataUrl; img.style.opacity = '0.5'; previewContainer.appendChild(img); } const loadingText = document.createElement('div'); loadingText.className = 'unified-modal-loading'; loadingText.textContent = '☕ creating your mug...'; loadingText.style.position = 'absolute'; previewContainer.style.position = 'relative'; previewContainer.appendChild(loadingText); // Show URL section with placeholder urlSection.style.display = 'flex'; const urlPlaceholder = document.createElement('div'); urlPlaceholder.style.cssText = 'color: #555; font-family: monospace; font-size: 13px;'; urlPlaceholder.textContent = 'generating url...'; urlSection.appendChild(urlPlaceholder); // Disabled buy button const buyBtn = document.createElement('button'); buyBtn.className = 'unified-modal-btn buy'; buyBtn.innerHTML = '☕ please wait...'; buyBtn.disabled = true; buttons.appendChild(buyBtn); } else if (mode === 'mug-ready') { // Mug ready mode title.textContent = `☕ ${mugColor} mug`; urlSection.style.display = 'flex'; // Show mug preview if (previewData?.animatedWebpUrl) { const img = document.createElement('img'); img.src = previewData.animatedWebpUrl; previewContainer.appendChild(img); } else if (previewData?.mockupUrl) { const img = document.createElement('img'); img.src = previewData.mockupUrl; previewContainer.appendChild(img); } else { const placeholder = document.createElement('div'); placeholder.textContent = '☕ Preview unavailable'; placeholder.style.cssText = 'color: #666; font-family: monospace;'; previewContainer.appendChild(placeholder); } // URL section with QR const rawProductCode = previewData?.rawProductCode || previewData?.productCode?.replace(/^\+/, ''); const isLocal = window.location.hostname === 'localhost' || window.location.hostname.includes('local'); const baseUrl = isLocal ? `${window.location.protocol}//${window.location.host}` : 'https://aesthetic.computer'; const mugUrl = rawProductCode ? `${baseUrl}/mug~+${rawProductCode}` : `${baseUrl}/mugs`; const qrCanvas = document.createElement('canvas'); qrCanvas.width = 56; qrCanvas.height = 56; urlSection.appendChild(qrCanvas); const urlText = document.createElement('div'); urlText.className = 'unified-modal-url-text'; urlText.innerHTML = `<span style="color: #666;">aesthetic.computer/</span><span style="color: #87ceeb;">mug</span><span style="color: #555;">~+</span><span style="color: #00e5ff;">${rawProductCode || '...'}</span>`; urlSection.appendChild(urlText); if (typeof QRious !== 'undefined') { new QRious({ element: qrCanvas, value: mugUrl, size: 56, level: 'L', background: '#1a1a2e', foreground: '#ffffff' }); } // Buy button const buyBtn = document.createElement('button'); buyBtn.className = 'unified-modal-btn buy'; buyBtn.innerHTML = '☕ buy for <span style="color: #90ee90; font-weight: bold;">$18</span>'; buyBtn.addEventListener('click', async () => { buyBtn.disabled = true; buyBtn.innerHTML = '☕ opening checkout...'; buyBtn.style.opacity = '0.7'; try { let checkoutApiUrl = `${baseUrl}/api/mug?new=true&pixels=${paintingCode}.png&color=${mugColor}`; if (kidlispCode) checkoutApiUrl += `&via=${kidlispCode}`; const checkoutRes = await fetch(checkoutApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ quantity: 1, slug: rawProductCode ? `mug~+${rawProductCode}` : undefined }), }); const checkoutData = await checkoutRes.json(); if (checkoutData.location) { openExternal(checkoutData.location); closeUnifiedModal(); } else { throw new Error('No checkout URL returned'); } } catch (e) { console.error('☕ Checkout failed:', e); buyBtn.innerHTML = '❌ failed - retry'; buyBtn.disabled = false; buyBtn.style.opacity = '1'; } }); buttons.appendChild(buyBtn); } } function closeUnifiedModal() { if (unifiedModal) { if (unifiedModal.escHandler) { document.removeEventListener('keydown', unifiedModal.escHandler); } unifiedModal.remove(); unifiedModal = null; } unifiedModalState = { mode: 'screenshot', isProcessing: false, dataUrl: null, filename: null, pieceCode: null, mugColor: null, previewData: null, paintingCode: null, kidlispCode: null, }; } // Legacy compatibility wrappers let mugModal = null; // Keep for any old references function showMugModal(previewData, paintingCode, kidlispCode, initialColor) { const isLoading = previewData?.loading === true; if (isLoading) { // Show loading state showUnifiedModal({ mode: 'mug-loading', isProcessing: true, mugColor: initialColor || randomMugColor(), dataUrl: unifiedModalState.dataUrl, // Keep existing screenshot if any kidlispCode, }); } else { // Show ready state showUnifiedModal({ mode: 'mug-ready', isProcessing: false, previewData, paintingCode, kidlispCode, mugColor: initialColor || unifiedModalState.mugColor, }); } } function closeMugModal() { closeUnifiedModal(); } function updateMugModalContent(previewData, paintingCode, kidlispCode, selectedColor) { unifiedModalState.previewData = previewData; unifiedModalState.paintingCode = paintingCode; unifiedModalState.kidlispCode = kidlispCode; unifiedModalState.mugColor = selectedColor || unifiedModalState.mugColor; unifiedModalState.mode = 'mug-ready'; unifiedModalState.isProcessing = false; updateUnifiedModalContent(); } // ☕ Open mug preview from screenshot async function openMugFromScreenshot(dataUrl, mugBtn, kidlispCode) { if (!dataUrl) return; // 🎯 Show modal IMMEDIATELY with loading state (before any async work) // Preserve the dataUrl so we can show the image dimmed while processing const selectedColor = randomMugColor(); showUnifiedModal({ mode: 'mug-loading', isProcessing: true, dataUrl: dataUrl, // Keep showing the screenshot while processing mugColor: selectedColor, kidlispCode, }); const originalHTML = mugBtn?.innerHTML; if (mugBtn) { mugBtn.disabled = true; mugBtn.innerHTML = '☕ processing...'; mugBtn.style.opacity = '0.6'; } try { // Convert data URL to blob const response = await fetch(dataUrl); const arrayBuffer = await response.arrayBuffer(); // Upload to guest art bucket via presigned URL // Path format: /presigned-upload-url/{extension} // Use current origin if on localhost, otherwise aesthetic.computer const isLocal = window.location.hostname === 'localhost' || window.location.hostname.includes('local'); const baseUrl = isLocal ? `${window.location.protocol}//${window.location.host}` : 'https://aesthetic.computer'; // Get a presigned URL for guest bucket (no bucket suffix = guest "art" bucket) const presignRes = await fetch(`${baseUrl}/presigned-upload-url/png`); if (!presignRes.ok) { const errText = await presignRes.text(); console.error('☕ Presigned URL error:', presignRes.status, errText); throw new Error(`Failed to get upload URL: ${presignRes.status}`); } const presignData = await presignRes.json(); console.log('☕ Got presigned URL:', presignData); // Note: API returns uploadURL (capital URL) const uploadUrl = presignData.uploadURL; const slug = presignData.slug; // Upload using XHR (like bios.mjs does) - more reliable for S3 presigned URLs await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl, true); // Set headers that match the presigned URL signature xhr.setRequestHeader('Content-Type', 'image/png'); xhr.setRequestHeader('Content-Disposition', 'inline'); xhr.setRequestHeader('x-amz-acl', 'public-read'); xhr.onerror = () => reject(new Error('XHR error')); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { resolve(); } else { reject(new Error(`Upload failed: ${xhr.status}`)); } } }; // Create blob without MIME type (like bios.mjs) const blob = new Blob([arrayBuffer]); xhr.send(blob); }); // Extract the code from slug (filename without extension) let paintingCode = slug.replace(/\.png$/, ''); console.log('☕ Uploaded as painting slug:', paintingCode); // Track the painting in the database to get a short code // This ensures the painting has a #code for display on mugs try { const trackRes = await fetch(`${baseUrl}/api/track-media`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: paintingCode, ext: 'png' }), }); if (trackRes.ok) { const trackData = await trackRes.json(); console.log('☕ Painting tracked with short code:', trackData.code); // Use the short code for the mug if available if (trackData.code) { paintingCode = trackData.code; } } else { console.warn('☕ Failed to track painting:', trackRes.status); } } catch (trackErr) { console.warn('☕ Track painting error:', trackErr); } // Call the mug API to create the product and get a +productCode // This ensures the mug is registered in the database with all metadata // Note: mugBtn might be null when coming from unified modal if (mugBtn) mugBtn.innerHTML = '☕ creating mug...'; // Use the color already selected for this mug session const mugColor = unifiedModalState.mugColor || selectedColor; let productApiUrl = `${baseUrl}/api/mug?new=true&pixels=${paintingCode}.png&color=${mugColor}&preview=true`; if (kidlispCode) { productApiUrl += `&via=${kidlispCode}`; } const productRes = await fetch(productApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ quantity: 1 }), }); if (!productRes.ok) { throw new Error(`Mug API failed: ${productRes.status}`); } const productData = await productRes.json(); console.log('☕ Product created:', productData); // Get the product code (with + prefix from API) const productCode = productData.productCode || productData.rawProductCode; if (!productCode) { throw new Error('No product code returned'); } // Strip + prefix if present (we'll add it in the URL) const rawProductCode = productCode.replace(/^\+/, ''); // Transition to mug-ready state in unified modal showUnifiedModal({ mode: 'mug-ready', isProcessing: false, previewData: { ...productData, rawProductCode }, paintingCode, kidlispCode, mugColor, }); if (mugBtn) { mugBtn.innerHTML = '☕ mug <span style="color: #90EE90;">$18</span>'; mugBtn.style.opacity = '0.85'; mugBtn.disabled = false; } } catch (e) { console.error('☕ Mug upload failed:', e); // Close the loading modal on error closeUnifiedModal(); if (mugBtn) { mugBtn.innerHTML = '❌ failed'; mugBtn.style.opacity = '0.85'; setTimeout(() => { mugBtn.innerHTML = originalHTML || '☕ mug <span style="color: #90EE90;">$18</span>'; mugBtn.disabled = false; }, 2000); } } } // Show console image in fullscreen popup (now uses unified modal) function showConsoleImagePopup(dataUrl, dimensions, pieceCode, frameCount, providedFilename) { // Remove any existing old-style popup const existing = document.getElementById('console-image-popup'); if (existing) existing.remove(); // Use provided filename or generate one const filename = providedFilename || generateConsoleImageFilename(pieceCode); // Use unified modal in screenshot mode showUnifiedModal({ mode: 'screenshot', dataUrl, filename, pieceCode, dimensions, frameCount, isProcessing: false, }); } function addConsoleEntry(message, type = 'log', locOrLine = null, options = {}) { const entry = document.createElement('div'); entry.className = `console-entry ${type}`; // Add timestamp const now = new Date(); const timeStr = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const timeSpan = document.createElement('span'); timeSpan.className = 'console-timestamp'; timeSpan.textContent = timeStr + ' '; entry.appendChild(timeSpan);
let loc = null; if (typeof locOrLine === 'number') { loc = { line: locOrLine }; } else if (locOrLine && typeof locOrLine === 'object') { loc = locOrLine; }
if (loc && loc.line) { const lineSpan = document.createElement('span'); lineSpan.className = 'console-line'; lineSpan.textContent = loc.col ? `:${loc.line}:${loc.col}` : `:${loc.line}`; lineSpan.style.cursor = 'pointer'; lineSpan.title = 'Jump to line'; lineSpan.addEventListener('click', () => { if (editor) { editor.revealLineInCenter(loc.line); editor.setPosition({ lineNumber: loc.line, column: loc.col || 1 }); editor.focus(); } }); entry.appendChild(lineSpan); // Check if message contains [$code] pattern and render specially const embeddedParts = renderEmbeddedSourceRef(' ' + message, options.embeddedSource, options.embeddedSourceCode); if (embeddedParts) { embeddedParts.forEach(part => entry.appendChild(part)); } else { const msgSpan = document.createElement('span'); msgSpan.textContent = ' ' + message; entry.appendChild(msgSpan); } } else if (options.html) { // Allow HTML content (for syntax-highlighted code) entry.innerHTML = message; } else { // Check if message contains [$code] pattern and render specially const embeddedParts = renderEmbeddedSourceRef(message, options.embeddedSource, options.embeddedSourceCode); if (embeddedParts) { embeddedParts.forEach(part => entry.appendChild(part)); } else { entry.textContent = message; } } // Support custom CSS classes if (options.className) { entry.classList.add(options.className); } consoleOutput.appendChild(entry);
// Also sync to mobile console (clone the entry) const mobileConsole = document.getElementById('mobile-console-output'); if (mobileConsole) { const clone = entry.cloneNode(true); mobileConsole.appendChild(clone); } scheduleConsoleScroll();
return entry; } // Add state change entry to console (play/pause/stop) function addStateEntry(state, code = null) { const icons = { play: '▶️', pause: '⏸️', stop: '⏹️', load: '📥', run: '🔄' }; const labels = { play: 'Playing', pause: 'Paused', stop: 'Stopped', load: 'Loaded', run: 'Running' }; const icon = icons[state] || '●'; const label = labels[state] || state; const codeStr = code ? ` $${code}` : ''; addConsoleEntry(`${icon} ${label}${codeStr}`, 'state', null, { className: `state-${state}` }); } // Add code display entry with syntax highlighting function addCodeEntry(code, label = 'Code') { // Truncate if too long const maxLen = 200; let displayCode = code; if (code.length > maxLen) { displayCode = code.substring(0, maxLen) + '...'; } // Single-line display (collapse newlines) displayCode = displayCode.replace(/\n/g, ' ↵ ').replace(/\s+/g, ' ').trim(); const highlighted = highlightKidlispCode(displayCode); addConsoleEntry(`<span class="console-label">${label}:</span> <span class="console-code">${highlighted}</span>`, 'code', null, { html: true }); } // Stub for clearErrorDecorations - will be overwritten inside Monaco require block let clearErrorDecorations = function() {}; let addErrorDecoration = function() {}; // Inline SVG for play/pause/stop button icons with colored circular backgrounds const playButtonSVG = `<svg class="inline-btn-icon" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="11" fill="#90EE90"/><polygon points="10,7 17,12 10,17" fill="#1a2f1a"/></svg>`; const pauseButtonSVG = `<svg class="inline-btn-icon" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="11" fill="#90EE90"/><rect x="8" y="7" width="3" height="10" rx="1" fill="#1a2f1a"/><rect x="13" y="7" width="3" height="10" rx="1" fill="#1a2f1a"/></svg>`; const stopButtonSVG = `<svg class="inline-btn-icon" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="11" fill="#9370DB"/><rect x="8" y="8" width="8" height="8" rx="1" fill="#2f1a2f"/></svg>`; // Show welcome message - shows code preview when editor is ready let hasShownWelcome = false; function showWelcomeMessage() { if (hasShownWelcome) return; hasShownWelcome = true; // Get current code to display const currentCode = editor?.getValue()?.trim() || ''; const codePreview = currentCode ? highlightKidlispCode( currentCode.length > 120 ? currentCode.substring(0, 120).replace(/\n/g, ' ↵ ').replace(/\s+/g, ' ').trim() + '...' : currentCode.replace(/\n/g, ' ↵ ').replace(/\s+/g, ' ').trim() ) : ''; // Add welcome message showing ready state with code preview addConsoleEntry( `🎨 <span class="welcome-title">Ready</span>` + (codePreview ? ` <span class="console-code">${codePreview}</span>` : ''), 'welcome', null, { html: true, className: 'welcome-message' } ); // Add helpful hints about play/pause/stop addConsoleEntry( `<span class="hint-text">💡 ${playButtonSVG} play ${pauseButtonSVG} pause ${stopButtonSVG} stop · errors appear here</span>`, 'hint', null, { html: true, className: 'hint-message' } ); }
// Console tab should be KidLisp output only; avoid status/boot chatter here.
// Listen for boot progress messages (do not write these into the Console tab) let bootComplete = false; let hasReceivedLogs = false; let fallbackTimeout = null; let bootLogEntry = null;
window.addEventListener('message', (event) => { if (event.data?.type === 'boot-log') { // If we receive a log, cancel the fallback timeout immediately if (fallbackTimeout) { clearTimeout(fallbackTimeout); fallbackTimeout = null; } hasReceivedLogs = true; // Feed boot log to chunking canvas animation if (window.chunkingAddLog && event.data.message) { window.chunkingAddLog(event.data.message); } // Check if kidlisp module compiled successfully - clear pending reload timeout if (event.data.message && event.data.message.includes('kidlisp compiled')) { if (pendingReloadTimeout) { clearTimeout(pendingReloadTimeout); pendingReloadTimeout = null; reloadAttempts = 0; // Reset attempts on success log.iframe.debug('Piece reload successful'); } } // Keep internal readiness state, but don't spam the Console output. // Check if this is the final "ready: aesthetic.computer/disks/kidlisp" message if (event.data.message.includes('ready: aesthetic.computer/disks/kidlisp')) { pieceReady = true; // If we were waiting to auto-play, do it now (but only once) // Only auto-play on aesthetic-computer platform // Don't auto-play if user explicitly stopped if (shouldAutoPlay && !isPlaying && !userStopped && hasInitialPreviewRun && currentPlatform === 'aesthetic-computer') { log.playback.log('Auto-playing (piece ready)...'); updatePreview(); isPlaying = true; isPaused = false; sendButton.classList.add('playing'); stopButton.style.display = 'flex'; setLiveState('running'); shouldAutoPlay = false; // Clear the flag so we don't auto-play again } } } else if (event.data?.type === 'ready') { bootComplete = true; if (fallbackTimeout) { clearTimeout(fallbackTimeout); fallbackTimeout = null; } // Stop rainbow title animation stopRainbowTitle(); // Give a moment to see the final message before hiding setTimeout(() => { bootScreen.classList.add('hidden'); }, 800); } });
// Fallback: Hide boot screen when iframe loads (if no messages received) previewIframe.addEventListener('load', () => { if (!bootComplete && !hasReceivedLogs) { fallbackTimeout = setTimeout(() => { bootScreen.classList.add('hidden'); stopRainbowTitle(); // Also stop on fallback }, 1500); } });
// Create KidLisp instance for syntax highlighting const kidlisp = new KidLisp(); let editor; let currentQRCode = null; // Track current code for QR regeneration on theme change
// Function to generate and display QR code function generateAndDisplayQR(code) { currentQRCode = code; // Store for regeneration // Wait for QR library to be loaded if (!window.qr || !window.ErrorCorrectLevel) { log.qr.verbose('QR library not loaded yet, retrying...'); setTimeout(() => generateAndDisplayQR(code), 100); return; } const cleanCode = code.startsWith('$') ? code.substring(1) : code; const url = `https://prompt.ac/$${cleanCode}`; try { // Detect dark mode const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark' || (!document.documentElement.getAttribute('data-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); // Generate QR code with lowest error correction for smallest size const cells = window.qr(url, { errorCorrectLevel: window.ErrorCorrectLevel.L }).modules; log.qr.verbose(`Generated ${cells.length}×${cells.length} QR`); // Scale QR to fit 34px height (compact but scannable) const targetSize = 34; const border = 1; // 1px border drawn into image const padding = 1; // 1px padding inside border const innerSize = targetSize - (border * 2) - (padding * 2); const cellSize = Math.max(1, Math.floor(innerSize / cells.length)); const qrSize = cells.length * cellSize; const totalSize = qrSize + (border * 2) + (padding * 2); // Create canvas for QR in header const canvas = document.createElement('canvas'); canvas.width = totalSize; canvas.height = totalSize; const ctx = canvas.getContext('2d'); // Colors for QR code const bgColor = isDarkMode ? 'rgb(188, 168, 218)' : 'rgb(240, 235, 250)'; const fgColor = isDarkMode ? 'rgb(48, 43, 58)' : 'rgb(88, 78, 108)'; const borderColor = isDarkMode ? 'rgb(188, 168, 218)' : 'rgb(88, 78, 108)'; // Draw border (full canvas) ctx.fillStyle = borderColor; ctx.fillRect(0, 0, totalSize, totalSize); // Draw background inside border ctx.fillStyle = bgColor; ctx.fillRect(border, border, totalSize - border * 2, totalSize - border * 2); // Draw QR code cells (offset by border + padding) const offset = border + padding; for (let y = 0; y < cells.length; y++) { for (let x = 0; x < cells.length; x++) { ctx.fillStyle = cells[y][x] ? fgColor : bgColor; ctx.fillRect(offset + x * cellSize, offset + y * cellSize, cellSize, cellSize); } } // Display in header (only for aesthetic-computer platform when playing) const previewQR = document.getElementById('preview-qr'); previewQR.src = canvas.toDataURL(); // Don't auto-show - QR visibility is controlled by playback state // Store for modal previewQR.dataset.url = url; previewQR.dataset.qrSize = qrSize; } catch (error) { log.qr.error('Error generating QR:', error); } } // Function to show QR modal function showQRModal() { const previewQR = document.getElementById('preview-qr'); const url = previewQR.dataset.url; const qrSize = parseInt(previewQR.dataset.qrSize); if (!url || !qrSize) return; // Generate larger QR for modal (use lowest error correction) const cells = window.qr(url, { errorCorrectLevel: window.ErrorCorrectLevel.L }).modules; const largeSize = Math.min(400, window.innerWidth - 100, window.innerHeight - 200); const largeCellSize = Math.floor(largeSize / cells.length); const actualSize = cells.length * largeCellSize; const canvas = document.getElementById('qr-modal-canvas'); canvas.width = actualSize; canvas.height = actualSize; const ctx = canvas.getContext('2d'); // Draw larger QR for (let y = 0; y < cells.length; y++) { for (let x = 0; x < cells.length; x++) { ctx.fillStyle = cells[y][x] ? '#000000' : '#ffffff'; ctx.fillRect(x * largeCellSize, y * largeCellSize, largeCellSize, largeCellSize); } } // Show URL document.getElementById('qr-modal-url').textContent = url; // Show modal document.getElementById('qr-modal').classList.add('show'); } // Function to hide QR modal function hideQRModal() { document.getElementById('qr-modal').classList.remove('show'); } // Add click handlers immediately (module scripts run after DOM is ready) const previewQR = document.getElementById('preview-qr'); if (previewQR) { previewQR.addEventListener('click', showQRModal); // Touch handler for mobile Safari previewQR.addEventListener('touchend', (e) => { e.preventDefault(); showQRModal(); }); }
const qrModal = document.getElementById('qr-modal'); if (qrModal) { const closeQR = (e) => { // Don't close if clicking a link inside the modal if (e.target.tagName === 'A' || e.target.closest('a')) { return; } e.preventDefault(); hideQRModal(); }; qrModal.addEventListener('click', closeQR); // Touch handler for mobile Safari - prevent tap bar interference qrModal.addEventListener('touchend', closeQR); }
// ⚙️ Settings gear click handler — always visible in header const settingsGear = document.getElementById('settings-gear'); if (settingsGear) { const openPrefs = (e) => { e.preventDefault(); e.stopPropagation(); if (typeof window.showPreferencesModal === 'function') { window.showPreferencesModal(); } }; settingsGear.addEventListener('click', openPrefs); settingsGear.addEventListener('touchend', openPrefs); }
// Scramble characters for code transition animation const scrambleChars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let scrambleInterval = null; // Track the last code that was sent for change detection let lastSentCodeSource = ''; // Scramble transition animation for $code display (like give page) function scrambleToCode(targetCode, codeDisplayEl) { if (!codeDisplayEl) return; // Clear any existing scramble if (scrambleInterval) { clearInterval(scrambleInterval); scrambleInterval = null; } const targetText = `/$${targetCode}`; const maxLen = Math.max(codeDisplayEl.textContent.length, targetText.length); let iterations = 0; const maxIterations = 8; codeDisplayEl.classList.add('generating'); scrambleInterval = setInterval(() => { iterations++; let result = ''; for (let i = 0; i < maxLen; i++) { const targetChar = targetText[i] || ''; const charSettled = i < targetText.length && (iterations > maxIterations - 3 + i * 0.5); if (charSettled || iterations >= maxIterations) { // Show final character with color class ($ is dollar class, rest are code class) if (i < targetText.length) { const colorClass = (i === 1) ? 'dollar' : (i > 1) ? 'code' : ''; result += `<span class="code-char${colorClass ? ' ' + colorClass : ''}">${targetChar}</span>`; } } else { // Show random scramble character (keep / and $ stable) if (i === 0) { result += `<span class="code-char">/</span>`; } else if (i === 1) { result += `<span class="code-char dollar">$</span>`; } else { const randChar = scrambleChars[Math.floor(Math.random() * scrambleChars.length)]; result += `<span class="code-char code">${randChar}</span>`; } } } codeDisplayEl.innerHTML = result; if (iterations >= maxIterations) { clearInterval(scrambleInterval); scrambleInterval = null; codeDisplayEl.classList.remove('generating'); } }, 40); } // Mark code as stale when source changes - with POOF animation! function markCodeAsStale() { const previewTitle = document.getElementById('preview-title'); const codeDisplay = previewTitle?.querySelector('.code-display'); if (codeDisplay && !codeDisplay.classList.contains('generating') && !codeDisplay.classList.contains('stale')) { // Ensure we have code-char spans for the animation // If not, wrap each character in a span if (!codeDisplay.querySelector('.code-char') && codeDisplay.textContent.trim()) { const text = codeDisplay.textContent; codeDisplay.innerHTML = text.split('').map(c => `<span class="code-char">${c}</span>`).join(''); } // Add stale class to trigger blast-off animation codeDisplay.classList.add('stale'); // After animation completes, clear the content setTimeout(() => { if (codeDisplay.classList.contains('stale')) { codeDisplay.innerHTML = ''; codeDisplay.classList.remove('stale'); } }, 600); // Animation is 0.4s + stagger delays } } // Clear stale state function clearStaleCode() { const previewTitle = document.getElementById('preview-title'); const codeDisplay = previewTitle?.querySelector('.code-display'); if (codeDisplay) { codeDisplay.classList.remove('stale'); } }
// Function to update browser URL and title when code changes function updateURLForCode(code) { const cleanCode = code.startsWith('$') ? code.substring(1) : code; const isKidlispDomain = window.location.hostname === 'kidlisp.com' || window.location.hostname === 'www.kidlisp.com'; const newPath = isKidlispDomain ? `/$${cleanCode}` : `/kidlisp.com/$${cleanCode}`; // Update URL and title to match if (window.location.pathname !== newPath) { document.title = `KidLisp.com/$${cleanCode}`; window.history.pushState({ code: cleanCode }, document.title, newPath); }
setRouteCodeInUI(cleanCode, { animate: false }); // Generate QR code generateAndDisplayQR(code); }
// Function to load kidlisp code from database by code (e.g., $46k) async function loadKidlispFromDatabase(code) { try { // Remove $ prefix if present const cleanCode = code.startsWith('$') ? code.substring(1) : code; // Fetch from the same API that aesthetic.computer uses const url = `${aestheticUrl}/api/store-kidlisp?code=${cleanCode}`; const response = await fetch(url); if (response.ok) { const data = await response.json(); if (data.source) { return data.source; } else { log.code.error(`No source for code: $${cleanCode}`); return null; } } else { log.code.error(`Failed to load $${cleanCode}: HTTP ${response.status}`); return null; } } catch (error) { log.code.error(`Error loading code:`, error); return null; } }
// ========================================= // SAFARI PERFORMANCE DIAGNOSTICS // ========================================= const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const perfCounters = { messages: 0, decorations: 0, audioTicks: 0, strudelTicks: 0 }; window.perfCounters = perfCounters; // Expose for modules (e.g. highlighting) if (isSafari) { setInterval(() => { if (perfCounters.messages || perfCounters.decorations || perfCounters.audioTicks || perfCounters.strudelTicks) { console.log(`🔍 [PERF 5s] msgs:${perfCounters.messages} decorations:${perfCounters.decorations} audioTicks:${perfCounters.audioTicks} strudelTicks:${perfCounters.strudelTicks}`); perfCounters.messages = 0; perfCounters.decorations = 0; perfCounters.audioTicks = 0; perfCounters.strudelTicks = 0; } }, 5000); console.log('🔍 [PERF] Safari detected — performance counters enabled (logged every 5s)'); }
// Define aestheticUrl early - used for iframe communication and API calls throughout // Note: only use localhost for true localhost, not for Codespaces (app.github.dev) const isLocalDev = window.location.hostname === 'localhost' || window.location.hostname.includes('local.'); const aestheticUrl = isLocalDev ? 'https://localhost:8888' : 'https://aesthetic.computer';
// Set iframe data-src for dev mode (actual loading deferred until tab is clicked) if (isLocalDev) { const keepsIframe = document.getElementById('keeps-iframe'); if (keepsIframe) keepsIframe.dataset.src = 'https://localhost:8888/kidlisp.com/keeps.html'; const learnIframe = document.getElementById('learn-iframe'); if (learnIframe) learnIframe.dataset.src = 'https://localhost:8888/kidlisp.com/learn.html'; }
// Hand the embedded Keeps tab the piece currently open in the editor so it // can keep exactly that (same-origin iframe → postMessage with our origin). function postCurrentPieceToKeeps() { try { const keepsIframe = document.getElementById('keeps-iframe'); if (!keepsIframe?.contentWindow) return; const code = (localStorage.getItem('kidlisp-current-code') || '').trim(); const source = (window.editor?.getValue?.() || '').trim(); if (!code && !source) return; keepsIframe.contentWindow.postMessage( { type: 'keeps:piece', code, source }, window.location.origin, ); } catch (e) { /* iframe not ready / cross-origin — ignore */ } }
// The embedded keeps page announces readiness; (re)send the current piece. window.addEventListener('message', (e) => { if (e.origin !== window.location.origin) return; if (e.data?.type === 'keeps:ready') postCurrentPieceToKeeps(); });
// ── Global Tezos wallet (parent-hosted) ────────────────────────────────── // One Beacon DAppClient lives here in the top-level kidlisp.com window. The // embedded keeps iframe delegates connect / getActiveAccount / // requestOperation / disconnect to it over postMessage, so the wallet's // connect/sign modal renders at full desktop size (not the cramped mobile // modal forced inside the tiny iframe) and the connection is shared app-wide. let _kidlispBeacon = null; let _kidlispBeaconNet = null; async function _loadBeaconSDKForParent() { if (window.beacon) return; // Beacon v4 calls Object.defineProperty(window,'ethereum',…) repeatedly; // a non-configurable extension-defined 'ethereum' makes that throw. Patch // defineProperty to swallow exactly that case (kept active permanently). const orig = Object.defineProperty; Object.defineProperty = function (obj, prop, desc) { if (obj === window && prop === 'ethereum') { try { return orig.call(this, obj, prop, desc); } catch { return obj; } } return orig.call(this, obj, prop, desc); }; // The editor has Monaco's AMD loader (window.define), so the Beacon UMD // bundle would register via define() and never set window.beacon. Hide AMD // during the load so it takes the global-assignment path, then restore. const savedDefine = window.define; try { window.define = undefined; } catch (e) {} try { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://unpkg.com/@airgap/beacon-sdk@4.8.1/dist/walletbeacon.min.js'; s.onload = () => resolve(); s.onerror = (e) => { Object.defineProperty = orig; reject(e); }; document.head.appendChild(s); }); } finally { try { window.define = savedDefine; } catch (e) {} } console.log('🔑 [PARENT wallet] Beacon SDK load done — window.beacon =', !!window.beacon); } async function _ensureKidlispBeacon(network) { const net = network === 'ghostnet' ? 'ghostnet' : 'mainnet'; if (_kidlispBeacon && _kidlispBeaconNet === net) return _kidlispBeacon; if (_kidlispBeacon && _kidlispBeaconNet !== net) { try { await _kidlispBeacon.destroy(); } catch (e) {} _kidlispBeacon = null; } await _loadBeaconSDKForParent(); _kidlispBeacon = new window.beacon.DAppClient({ name: 'KidLisp', network: { type: net === 'ghostnet' ? window.beacon.NetworkType.GHOSTNET : window.beacon.NetworkType.MAINNET }, walletConnectOptions: { projectId: '647c41004744244efab53b7c6ef80443' }, }); _kidlispBeaconNet = net; return _kidlispBeacon; } window.addEventListener('message', async (e) => { if (e.origin !== window.location.origin) return; const d = e.data; if (d?.type !== 'wallet:rpc') return; console.log('🔑 [PARENT wallet:rpc] received:', d.method, 'net=', d.params?.network); const reply = (ok, payload) => { console.log('🔑 [PARENT wallet:rpc] reply:', d.method, ok ? 'OK' : 'ERR', ok ? payload : payload); const kf = document.getElementById('keeps-iframe'); kf?.contentWindow?.postMessage( ok ? { type: 'wallet:rpc:reply', id: d.id, ok: true, result: payload } : { type: 'wallet:rpc:reply', id: d.id, ok: false, error: String(payload?.message || payload || 'wallet error') }, window.location.origin, ); }; try { const client = await _ensureKidlispBeacon(d.params?.network); if (d.method === 'connect') { const active = await client.getActiveAccount(); if (active?.address) { reply(true, { address: active.address }); return; } const perms = await client.requestPermissions(); reply(true, { address: perms.address }); } else if (d.method === 'getActiveAccount') { const active = await client.getActiveAccount(); reply(true, { address: active?.address || null }); } else if (d.method === 'requestOperation') { const res = await client.requestOperation(d.params.operation); reply(true, { transactionHash: res?.transactionHash || null }); } else if (d.method === 'disconnect') { try { await client.clearActiveAccount(); } catch (e) {} reply(true, { ok: true }); } else { reply(false, new Error('unknown wallet method: ' + d.method)); } } catch (err) { console.warn('🔑 [PARENT wallet:rpc] threw for', d.method, '—', err?.message || err); reply(false, err); } });
// Configure Monaco with jsDelivr CDN (modern, well-supported) require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs' } }); require(['vs/editor/editor.main'], function () { // Register KidLisp language monaco.languages.register({ id: 'kidlisp' });
// Simple Monaco language definition - decorations will handle all coloring monaco.languages.setMonarchTokensProvider('kidlisp', { tokenizer: { root: [ [/./, 'text'] ] } });
// Simple neutral theme - KidLisp decorations will handle all syntax coloring monaco.editor.defineTheme('kidlisp-light', { base: 'vs', inherit: true, rules: [ { token: 'text', foreground: '333333' } ], colors: { 'editor.background': '#fffacd', /* Legal pad yellow */ 'editor.foreground': '#333333', 'editorCursor.foreground': '#FF1493', 'editor.lineHighlightBackground': '#fff5b8', /* Slightly darker yellow for highlight */ 'editor.selectionBackground': '#ff8c00dd', /* Bright orange with high opacity for visibility */ 'editor.inactiveSelectionBackground': '#ffa500aa', /* Lighter orange for inactive */ 'editorLineNumber.foreground': '#999999', /* Line number color */ 'editorLineNumber.activeForeground': '#333333', /* Active line number */ } });
monaco.editor.defineTheme('kidlisp-dark', { base: 'vs-dark', inherit: true, rules: [ { token: 'text', foreground: 'd4d4d4' } ], colors: { 'editor.background': '#2a2520', /* Warm dark brown */ 'editor.foreground': '#d4d4d4', 'editorCursor.foreground': '#FF69B4', 'editor.lineHighlightBackground': '#3a3530', /* Slightly lighter brown for highlight */ 'editorLineNumber.foreground': '#858585', 'editorLineNumber.activeForeground': '#d4d4d4', /* Active line number */ 'editor.selectionBackground': '#ff8c00cc', /* Bright orange with high opacity for dark mode */ 'editor.inactiveSelectionBackground': '#ff8c0066', /* Lighter orange for inactive */ } });
// Detect color scheme and set appropriate theme const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; const initialTheme = isDarkMode ? 'kidlisp-dark' : 'kidlisp-light';
// Set max lines for editor const maxLines = 64; // Track if user has manually edited the editor (vs programmatic setValue) let editorUserModified = false; let isSettingValueProgrammatically = false; // Expose setter for external code (handleCodeItemClick needs this) window.setEditorUserModified = (val) => { editorUserModified = val; }; window.getEditorUserModified = () => editorUserModified; window.setSettingValueProgrammatically = (val) => { isSettingValueProgrammatically = val; }; // LOCAL indicator removed - no longer needed // Create the editor editor = monaco.editor.create(editorContainer, { value: localStorage.getItem('kidlisp-code') || '', language: 'kidlisp', theme: initialTheme, minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: window.isStageMode?.() ? 24 : 16, lineNumbers: 'on', glyphMargin: false, folding: false, lineDecorationsWidth: 10, lineNumbersMinChars: 3, roundedSelection: false, renderLineHighlight: 'all', selectionHighlight: true, occurrencesHighlight: 'off', overviewRulerLanes: 0, scrollbar: { vertical: 'auto', horizontal: 'auto' }, automaticLayout: true, wordWrap: 'on', cursorStyle: 'line', cursorBlinking: 'blink', padding: { top: 12 }, // Disable features that load additional modules (prevents marked.js loading errors) hover: { enabled: false }, parameterHints: { enabled: false }, quickSuggestions: false, suggestOnTriggerCharacters: false, codeLens: false, lightbulb: { enabled: 'off' }, inlayHints: { enabled: 'off' }, links: false, contextmenu: true, // Enable for KidLisp documentation lookup fixedOverflowWidgets: true, // Render context menu at body level (above playback controls) // Disable accessibility features (removes keyboard icon in top-right) accessibilitySupport: 'off', ariaLabel: 'KidLisp Code Editor', }); // Make editor globally accessible for other parts of the page window.editor = editor;
// Boost editor panel z-index while context menu is open so it renders // above play buttons, header, center-square, etc. const editorPanel = document.getElementById('editor-panel'); editor.onContextMenu(() => { editorPanel?.classList.add('context-menu-open'); }); // Remove when clicking anywhere (context menu closes) document.addEventListener('mousedown', () => { editorPanel?.classList.remove('context-menu-open'); });
// --- YIELD TO BROWSER --- // Editor DOM is now in the page. Yield so the browser can paint it and // keep splash / CSS animations smooth on lower-end CPUs. // Use requestAnimationFrame to let the browser render the editor DOM // before continuing with heavyweight init (KIDLISP_DOCS, event handlers). requestAnimationFrame(() => { setTimeout(() => {
// ==================== KIDLISP DOCUMENTATION CONTEXT MENU ==================== // Custom right-click context menu for KidLisp documentation lookup // KidLisp Documentation - mirrors KIDLISP_DOCS from kidlisp.mjs // Keep this in sync with system/public/aesthetic.computer/lib/kidlisp.mjs const KIDLISP_DOCS = { // === SCREEN MANAGEMENT === wipe: { signature: "(wipe color)", description: "Clear the entire screen with the specified color.", category: "screen", examples: ['(wipe "black")', '(wipe "navy")', "(wipe 30 30 40)"] }, resolution: { signature: "(resolution width height)", description: "Set the canvas resolution. Changes the drawing area dimensions.", category: "screen", examples: ["(resolution 512 512)", "(resolution 128 128)"] }, // === DRAWING PRIMITIVES === ink: { signature: "(ink color [alpha])", description: "Set the drawing color for subsequent operations. All shapes and lines will use this color until changed.", category: "drawing", examples: ['(ink "red")', "(ink 255 0 128)", '(ink "blue" 128)', "(ink rainbow)"] }, line: { signature: "(line x1 y1 x2 y2)", description: "Draw a line from point (x1, y1) to point (x2, y2) using the current ink color.", category: "drawing", examples: ["(line 0 0 100 100)", "(line 50 0 50 height)"] }, box: { signature: "(box x y width height [mode])", description: "Draw a rectangle. Respects the global fill/outline mode, or can override with explicit mode parameter.", category: "drawing", examples: ["(box 10 10 50 50)", '(box 100 100 40 40 "outline")', '(box 0 0 width height "fill")'] }, circle: { signature: "(circle x y radius [mode])", description: "Draw a circle centered at (x, y). Respects the global fill/outline mode.", category: "drawing", examples: ["(circle 100 100 50)", '(circle 50 50 25 "outline")', '(circle (/ width 2) (/ height 2) 30 "fill")'] }, tri: { signature: "(tri x1 y1 x2 y2 x3 y3 [mode])", description: "Draw a triangle with three vertices. Respects the global fill/outline mode.", category: "drawing", examples: ["(tri 50 0 100 100 0 100)", "(tri 0 0 width 0 (/ width 2) height)"] }, plot: { signature: "(plot x y)", description: "Set a single pixel at the specified coordinates using the current ink color.", category: "drawing", examples: ["(plot 50 50)", "(plot (wiggle width) (wiggle height))"] }, point: { signature: "(point x y)", description: "Alias for plot. Set a single pixel at (x, y).", category: "drawing", examples: ["(point 100 100)"] }, shape: { signature: "(shape points... [mode])", description: "Draw a polygon from a list of coordinate pairs.", category: "drawing", examples: ['(shape 0 0 50 0 50 50 0 50 "fill")'] }, flood: { signature: "(flood x y [color])", description: "Flood fill starting at (x, y). Uses current ink if no color specified.", category: "drawing", examples: ["(flood 100 100)", '(flood 50 50 "red")'] }, // === FILL/OUTLINE MODE === fill: { signature: "(fill)", description: "Set global fill mode. All subsequent shapes (circle, box, tri) will be filled. This is the default mode.", category: "mode", examples: ["(fill)", "(fill) (circle 50 50 30)"] }, outline: { signature: "(outline)", description: "Set global outline mode. All subsequent shapes will only draw their outline/stroke.", category: "mode", examples: ["(outline)", "(outline) (box 0 0 100 100)"] }, stroke: { signature: "(stroke)", description: "Alias for outline. Set global outline mode (Processing compatibility).", category: "mode", examples: ["(stroke)"] }, nofill: { signature: "(nofill)", description: "Alias for outline. Set shapes to be outlined only (Processing-style).", category: "mode", examples: ["(nofill)"] }, nostroke: { signature: "(nostroke)", description: "Alias for fill. Set shapes to be filled (Processing-style).", category: "mode", examples: ["(nostroke)"] }, // === IMAGE FUNCTIONS === paste: { signature: "(paste url x y [scale])", description: "Paste an image from a URL at the specified coordinates. URLs can be unquoted.", category: "image", examples: ["(paste https://example.com/image.png 0 0)", '(paste "https://example.com/logo.png" 50 50 0.5)'] }, stamp: { signature: "(stamp url x y [scale])", description: "Paste an image centered at the specified coordinates.", category: "image", examples: ["(stamp https://example.com/sprite.png (/ width 2) (/ height 2))"] }, // === TEXT === write: { signature: "(write text position [options])", description: "Write text at a position. Position can be {x, y} or {center: 'xy'}.", category: "text", examples: ['(write "Hello!" { x: 10 y: 10 })', '(write "Centered" { center: "xy" })'] }, // === ANIMATION & EFFECTS === wiggle: { signature: "(wiggle amount)", description: "Returns a random value in the range ±amount/2. Changes each frame for animation.", category: "animation", examples: ["(wiggle 10)", "(circle (+ 100 (wiggle 20)) 100 10)", "(ink (wiggle 255) 0 0)"] }, scroll: { signature: "(scroll [dx] [dy])", description: "Scroll the canvas by dx horizontally and dy vertically. With no args, picks a random direction once.", category: "animation", examples: ["(scroll)", "(scroll 1 0)", "(scroll 0 -1)"] }, flip: { signature: "(flip)", description: "Flip the current image vertically (top-to-bottom).", category: "animation", examples: ["(flip)", "(1s... (flip))"] }, spin: { signature: "(spin [speed])", description: "Rotate the canvas. Creates spinning/rotating effects.", category: "animation", examples: ["(spin)", "(spin 0.5)"] }, resetSpin: { signature: "(resetSpin)", description: "Reset the spin rotation back to zero.", category: "animation", examples: ["(resetSpin)"] }, smoothspin: { signature: "(smoothspin [speed])", description: "Smooth rotation of the canvas with interpolation.", category: "animation", examples: ["(smoothspin 0.01)"] }, zoom: { signature: "(zoom factor)", description: "Zoom the canvas by a factor.", category: "animation", examples: ["(zoom 1.01)", "(zoom 0.99)"] }, fade: { signature: "(fade amount)", description: "Fade the screen by reducing alpha. Creates trail effects.", category: "animation", examples: ["(fade 10)", "(fade 50)"] }, bake: { signature: "(bake)", description: "Render current drawing to a persistent background layer. Drawing accumulates on top.", category: "animation", examples: ["(bake)", "(once (bake))"] }, // === VARIABLES & CONTROL === def: { signature: "(def name value)", description: "Define a variable with a name and value. The variable persists across frames.", category: "variable", examples: ["(def x 100)", '(def color "red")', "(def speed 2.5)"] }, set: { signature: "(set name value)", description: "Update an existing variable's value.", category: "variable", examples: ["(set x (+ x 1))"] }, later: { signature: "(later name params... body)", description: "Define a reusable function with parameters. Call it later by name.", category: "control", examples: ["(later star x y (circle x y 10))", "(later cross x y (line (- x 5) y (+ x 5) y) (line x (- y 5) x (+ y 5)))"] }, repeat: { signature: "(repeat count [iterator] body...)", description: "Execute body multiple times. Optional iterator variable gets the current index (0-based).", category: "control", examples: ["(repeat 10 (plot (wiggle width) (wiggle height)))", "(repeat height i (line 0 i width i))", "(repeat 100 n (ink rainbow) (circle n n 5))"] }, bunch: { signature: "(bunch count [iterator] body...)", description: "Alias for repeat. Shorter, more playful name.", category: "control", examples: ["(bunch 50 (plot (wiggle width) (wiggle height)))"] }, loop: { signature: "(loop count body...)", description: "Alias for repeat.", category: "control", examples: ["(loop 10 (circle (wiggle width) (wiggle height) 5))"] }, once: { signature: "(once expression)", description: "Execute expression only once (first frame), not every frame. Useful for initialization.", category: "control", examples: ['(once (wipe "black"))', "(once (def x 0))", "(once (bake))"] }, now: { signature: "(now expression)", description: "Execute expression immediately (opposite of scheduled). Executes every frame.", category: "control", examples: ["(now (wipe))"] }, jump: { signature: "(jump destination)", description: "Navigate to another piece or URL. Resets KidLisp state.", category: "control", examples: ['(jump "prompt")', '(jump "https://example.com")', "(jump $abc123)"] }, // === LOGIC === if: { signature: "(if condition then-expr [else-expr])", description: "Conditional expression. Returns then-expr if condition is truthy, else-expr otherwise.", category: "logic", examples: ["(if (> x 100) (set x 0))", '(if (< x 50) (ink "red") (ink "blue"))'] }, choose: { signature: "(choose option1 option2 ...)", description: "Randomly choose one of the provided options each frame.", category: "logic", examples: ['(ink (choose "red" "blue" "green"))', "(circle 100 100 (choose 10 20 30))"] }, and: { signature: "(and a b ...)", description: "Logical AND. Returns true if all arguments are truthy.", category: "logic", examples: ["(and (> x 0) (< x 100))"] }, or: { signature: "(or a b ...)", description: "Logical OR. Returns true if any argument is truthy.", category: "logic", examples: ['(or (= color "red") (= color "blue"))'] }, not: { signature: "(not value)", description: "Logical NOT. Returns true if value is falsy.", category: "logic", examples: ["(not (= x 0))"] }, // === MATH === "+": { signature: "(+ a b ...)", description: "Add multiple numbers together.", category: "math", examples: ["(+ 1 2)", "(+ x 10)", "(+ 1 2 3 4 5)"] }, "-": { signature: "(- a b)", description: "Subtract b from a.", category: "math", examples: ["(- 10 5)", "(- x 1)"] }, "*": { signature: "(* a b ...)", description: "Multiply multiple numbers together.", category: "math", examples: ["(* 2 3)", "(* x 1.5)"] }, "/": { signature: "(/ a b)", description: "Divide a by b.", category: "math", examples: ["(/ 10 2)", "(/ width 2)"] }, "%": { signature: "(% a b)", description: "Modulo (remainder) of a divided by b.", category: "math", examples: ["(% frame 60)", "(% x width)"] }, mod: { signature: "(mod a b)", description: "Alias for %. Modulo operation.", category: "math", examples: ["(mod frame 100)"] }, sin: { signature: "(sin angle)", description: "Sine of angle (in radians).", category: "math", examples: ["(sin time)", "(* 50 (sin (* time 2)))"] }, cos: { signature: "(cos angle)", description: "Cosine of angle (in radians).", category: "math", examples: ["(cos time)", "(+ 100 (* 50 (cos time)))"] }, tan: { signature: "(tan angle)", description: "Tangent of angle (in radians).", category: "math", examples: ["(tan time)"] }, abs: { signature: "(abs value)", description: "Absolute value.", category: "math", examples: ["(abs -10)", "(abs (- x 100))"] }, floor: { signature: "(floor value)", description: "Round down to nearest integer.", category: "math", examples: ["(floor 3.7)", "(floor (/ width 3))"] }, ceil: { signature: "(ceil value)", description: "Round up to nearest integer.", category: "math", examples: ["(ceil 3.2)"] }, round: { signature: "(round value)", description: "Round to nearest integer.", category: "math", examples: ["(round 3.5)"] }, sqrt: { signature: "(sqrt value)", description: "Square root.", category: "math", examples: ["(sqrt 16)", "(sqrt (+ (* dx dx) (* dy dy)))"] }, pow: { signature: "(pow base exponent)", description: "Raise base to the power of exponent.", category: "math", examples: ["(pow 2 8)", "(pow x 2)"] }, min: { signature: "(min a b ...)", description: "Return the smallest value.", category: "math", examples: ["(min 10 20)", "(min x width)"] }, max: { signature: "(max a b ...)", description: "Return the largest value.", category: "math", examples: ["(max 0 x)", "(max width height)"] }, lerp: { signature: "(lerp a b t)", description: "Linear interpolation between a and b. t=0 returns a, t=1 returns b.", category: "math", examples: ["(lerp 0 100 0.5)", "(lerp startX endX progress)"] }, random: { signature: "(random [max] [min max])", description: "Generate a random number. With no args: 0-1. With one arg: 0-max. With two args: min-max.", category: "math", examples: ["(random)", "(random 100)", "(random 50 100)"] }, noise: { signature: "(noise x [y] [z])", description: "Perlin noise function. Returns smooth random values based on coordinates.", category: "math", examples: ["(noise time)", "(noise x y)"] }, dist: { signature: "(dist x1 y1 x2 y2)", description: "Calculate distance between two points.", category: "math", examples: ["(dist 0 0 100 100)", "(dist x y mouseX mouseY)"] }, map: { signature: "(map value inMin inMax outMin outMax)", description: "Re-map a value from one range to another.", category: "math", examples: ["(map x 0 width 0 255)"] }, constrain: { signature: "(constrain value min max)", description: "Constrain a value to be within a range.", category: "math", examples: ["(constrain x 0 width)"] }, // === SPECIAL VALUES === rainbow: { signature: "rainbow", description: "Special color value that cycles through rainbow colors. Changes each time it's used.", category: "color", examples: ["(ink rainbow)", "(repeat 100 i (ink rainbow) (line 0 i width i))"] }, zebra: { signature: "zebra", description: "Special color value that alternates between black and white.", category: "color", examples: ["(ink zebra)"] }, width: { signature: "width", description: "The current canvas width in pixels. Read-only global variable.", category: "global", examples: ["(line 0 0 width height)", "(/ width 2)"] }, height: { signature: "height", description: "The current canvas height in pixels. Read-only global variable.", category: "global", examples: ["(circle (/ width 2) (/ height 2) 50)"] }, time: { signature: "time", description: "Elapsed time in seconds since the piece started. Useful for smooth animations.", category: "global", examples: ["(sin time)", "(* 100 (sin (* time 2)))"] }, elapsed: { signature: "elapsed", description: "Alias for time. Elapsed time in seconds.", category: "global", examples: ["elapsed"] }, frame: { signature: "frame", description: "The current frame number (increments each animation frame).", category: "global", examples: ["(% frame 60)", "(sin (/ frame 30))"] }, delta: { signature: "delta", description: "Time since last frame in seconds. Useful for frame-rate independent animation.", category: "global", examples: ["(* speed delta)"] }, fps: { signature: "fps", description: "Current frames per second.", category: "global", examples: ["fps"] }, mic: { signature: "mic", description: "Microphone amplitude (0-1). Responds to audio input when enabled.", category: "audio", examples: ["(circle 100 100 (* mic 100))", "(if (> mic 0.5) (wipe))"] }, // === AUDIO === tone: { signature: "(tone frequency [duration] [type])", description: "Play a tone at the specified frequency.", category: "audio", examples: ["(tone 440)", '(tone 880 0.5 "square")'] }, beep: { signature: "(beep [frequency])", description: "Play a short beep sound.", category: "audio", examples: ["(beep)", "(beep 880)"] }, melody: { signature: "(melody notes)", description: "Play a sequence of notes. Uses ABC notation or custom format.", category: "audio", examples: ['(melody "C D E F G")'] }, amplitude: { signature: "amplitude", description: "Current audio amplitude (0-1). Alias for mic.", category: "audio", examples: ["amplitude"] }, // === EMBEDDED CONTENT === embed: { signature: "(embed $code x y width height [alpha])", description: "Embed another KidLisp piece at a position with custom dimensions.", category: "embed", examples: ["(embed $abc123 0 0 128 128)", "(embed $xyz789 50 50 64 64 200)"] }, tape: { signature: "(tape !code [x y] [w h] [speed])", description: "Embed a tape video. Use ! prefix for tape codes.", category: "embed", examples: ["(tape !abc)", "(tape !xyz 0 0 256 256)", "(tape !video 0 0 width height 2)"] }, // === INTERACTIVITY === tap: { signature: "(tap body...)", description: "Define a tap/click handler. Body executes when user taps the screen.", category: "interactive", examples: ['(tap (wipe (choose "red" "blue")))', "(tap (set x mouseX) (set y mouseY))"] }, draw: { signature: "(draw body...)", description: "Define a drag/draw handler. Body executes during finger/mouse drag.", category: "interactive", examples: ["(draw (line lastX lastY mouseX mouseY))"] }, // === OUTPUT === print: { signature: "(print value...)", description: "Print values to the KidLisp console (visible in kidlisp.com).", category: "output", examples: ['(print "Hello")', "(print x y)", '(print "x =" x)'] }, debug: { signature: "(debug value...)", description: "Print debug information to the console.", category: "output", examples: ["(debug frame)", "(debug x y)"] }, log: { signature: "(log value...)", description: "Alias for print. Output to console.", category: "output", examples: ["(log x)"] }, // === DATA STRUCTURES === list: { signature: "(list item1 item2 ...)", description: "Create a list of items.", category: "data", examples: ['(list 1 2 3)', '(list "red" "green" "blue")'] }, get: { signature: "(get list index)", description: "Get item at index from a list (0-based).", category: "data", examples: ["(get myList 0)", "(get colors 2)"] }, first: { signature: "(first list)", description: "Get the first item of a list.", category: "data", examples: ["(first myList)"] }, rest: { signature: "(rest list)", description: "Get all items except the first.", category: "data", examples: ["(rest myList)"] }, len: { signature: "(len list)", description: "Get the length of a list.", category: "data", examples: ["(len myList)"] }, push: { signature: "(push list item)", description: "Add an item to the end of a list.", category: "data", examples: ["(push myList 42)"] }, pop: { signature: "(pop list)", description: "Remove and return the last item from a list.", category: "data", examples: ["(pop myList)"] }, // === DISPLAY EFFECTS === blur: { signature: "(blur amount)", description: "Apply blur effect to the canvas.", category: "effects", examples: ["(blur 2)", "(blur 5)"] }, contrast: { signature: "(contrast amount)", description: "Adjust canvas contrast.", category: "effects", examples: ["(contrast 1.5)"] }, mask: { signature: "(mask)", description: "Start masking mode. Drawing only affects visible areas.", category: "effects", examples: ["(mask)"] }, unmask: { signature: "(unmask)", description: "End masking mode.", category: "effects", examples: ["(unmask)"] }, // === LABELS === label: { signature: "(label text)", description: "Set a label/title for the piece. Shown in UI and embeds.", category: "meta", examples: ['(label "My Cool Piece")', '(label "Untitled")'] }, // === COMPARISON OPERATORS === "=": { signature: "(= a b)", description: "Check if a equals b.", category: "logic", examples: ['(= x 10)', '(= color "red")'] }, ">": { signature: "(> a b)", description: "Check if a is greater than b.", category: "logic", examples: ["(> x 100)", "(> frame 60)"] }, "<": { signature: "(< a b)", description: "Check if a is less than b.", category: "logic", examples: ["(< x 0)", "(< y height)"] }, ">=": { signature: "(>= a b)", description: "Check if a is greater than or equal to b.", category: "logic", examples: ["(>= x 0)"] }, "<=": { signature: "(<= a b)", description: "Check if a is less than or equal to b.", category: "logic", examples: ["(<= y height)"] }, // === SPECIAL SYNTAX === "?": { signature: "(? option1 option2 ...)", description: "Alias for choose. Randomly pick one of the options each frame. Short and sweet!", category: "logic", examples: ['(ink (? "red" "blue" "green"))', "(? 10 20 30 40)"] }, "...": { signature: "(timing... expr1 expr2 ...)", description: "Cycle through expressions at timing intervals. The ... makes expressions repeat in sequence.", category: "timing", examples: ['(1s... (ink "red") (ink "blue"))', '(500ms... (wipe "black") (wipe "white"))'] }, "..": { signature: "(timing.. expr1 expr2 ...)", description: "Like ... but plays expressions once in sequence, then stops. Good for animations.", category: "timing", examples: ['(1s.. (circle 10 10 5) (circle 20 20 10) (circle 30 30 15))'] }, // === TIMING PATTERNS === "1s": { signature: "1s, 0.5s, 2.5s, etc.", description: "Delay timer in seconds. Waits the specified time before executing the following expressions.", category: "timing", examples: ["(1s (wipe))", "(0.5s (beep))", "(2s (jump))"] }, "1f": { signature: "1f, 10f, 60f, etc.", description: "Delay timer in frames. Waits the specified number of frames (at 60fps) before executing.", category: "timing", examples: ["(30f (wipe))", "(60f (beep))"] }, "1s...": { signature: "1s..., 0.5s..., etc.", description: "Cycle timer in seconds. Cycles through following expressions at this interval forever.", category: "timing", examples: ['(1s... (ink "red") (ink "blue"))', "(0.25s... (wiggle 10))"] }, // === COLOR NAMES (Common ones) === red: { signature: "red", description: "The color red (255, 0, 0). Use with ink to set the drawing color.", category: "color", examples: ['(ink red)', '(ink "red")', "(wipe red)"] }, blue: { signature: "blue", description: "The color blue (0, 0, 255). A primary color.", category: "color", examples: ['(ink blue)', "(wipe blue)"] }, green: { signature: "green", description: "The color green (0, 128, 0). Note: this is darker than lime.", category: "color", examples: ['(ink green)', "(wipe green)"] }, yellow: { signature: "yellow", description: "The color yellow (255, 255, 0). Bright and sunny!", category: "color", examples: ['(ink yellow)', "(wipe yellow)"] }, purple: { signature: "purple", description: "The color purple (128, 0, 128). A mix of red and blue.", category: "color", examples: ['(ink purple)', "(wipe purple)"] }, orange: { signature: "orange", description: "The color orange (255, 165, 0). Warm and vibrant!", category: "color", examples: ['(ink orange)', "(wipe orange)"] }, pink: { signature: "pink", description: "The color pink (255, 192, 203). Soft and rosy.", category: "color", examples: ['(ink pink)', "(wipe pink)"] }, cyan: { signature: "cyan", description: "The color cyan (0, 255, 255). A bright blue-green.", category: "color", examples: ['(ink cyan)', "(wipe cyan)"] }, magenta: { signature: "magenta", description: "The color magenta (255, 0, 255). Bright pink-purple.", category: "color", examples: ['(ink magenta)', "(wipe magenta)"] }, lime: { signature: "lime", description: "The color lime (0, 255, 0). Bright neon green!", category: "color", examples: ['(ink lime)', "(wipe lime)"] }, white: { signature: "white", description: "The color white (255, 255, 255). Maximum brightness.", category: "color", examples: ['(ink white)', "(wipe white)"] }, black: { signature: "black", description: "The color black (0, 0, 0). Complete darkness.", category: "color", examples: ['(ink black)', "(wipe black)"] }, gray: { signature: "gray", description: "The color gray (128, 128, 128). Neutral midtone.", category: "color", examples: ['(ink gray)', "(wipe gray)"] }, navy: { signature: "navy", description: "The color navy (0, 0, 128). Dark blue.", category: "color", examples: ['(ink navy)', "(wipe navy)"] }, teal: { signature: "teal", description: "The color teal (0, 128, 128). Dark cyan.", category: "color", examples: ['(ink teal)', "(wipe teal)"] }, coral: { signature: "coral", description: "The color coral (255, 127, 80). Warm orange-pink.", category: "color", examples: ['(ink coral)', "(wipe coral)"] }, gold: { signature: "gold", description: "The color gold (255, 215, 0). Shiny and precious!", category: "color", examples: ['(ink gold)', "(wipe gold)"] }, // === SPECIAL GLOBALS === mouseX: { signature: "mouseX", description: "The current X position of the mouse/touch pointer. Updates every frame.", category: "input", examples: ["(circle mouseX mouseY 10)", "(line 0 0 mouseX mouseY)"] }, mouseY: { signature: "mouseY", description: "The current Y position of the mouse/touch pointer. Updates every frame.", category: "input", examples: ["(circle mouseX mouseY 10)", "(plot mouseX mouseY)"] }, lastX: { signature: "lastX", description: "The previous frame's X position of the mouse/touch. Useful for drawing lines.", category: "input", examples: ["(draw (line lastX lastY mouseX mouseY))"] }, lastY: { signature: "lastY", description: "The previous frame's Y position of the mouse/touch. Useful for drawing lines.", category: "input", examples: ["(draw (line lastX lastY mouseX mouseY))"] }, // === BEAT/MUSIC === beat: { signature: "(beat [division])", description: "Returns true on musical beats. Division can be 1, 2, 4, 8, 16 for whole, half, quarter notes etc.", category: "audio", examples: ["(if (beat) (wipe))", "(if (beat 4) (circle 100 100 20))"] }, bpm: { signature: "bpm", description: "The current beats per minute for timing. Can be set or read.", category: "audio", examples: ["bpm", "(set bpm 120)"] } }; // Make docs globally accessible for external use window.KIDLISP_DOCS = KIDLISP_DOCS; // Helper function to get the word at cursor position function getWordAtPosition(model, position) { const word = model.getWordAtPosition(position); if (word) return word.word; // Check for special characters (operators like +, -, *, /, etc.) const lineContent = model.getLineContent(position.lineNumber); const col = position.column - 1; const char = lineContent[col]; // Check if it's a special operator character if (['+', '-', '*', '/', '%', '=', '<', '>', '!'].includes(char)) { // Check for two-char operators if (col + 1 < lineContent.length) { const twoChar = char + lineContent[col + 1]; if (['>=', '<=', '!='].includes(twoChar)) { return twoChar; } } return char; } return null; } // Create documentation tooltip popup function createDocTooltip() { let tooltip = document.getElementById('kidlisp-doc-tooltip'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.id = 'kidlisp-doc-tooltip'; tooltip.style.cssText = `position: fixed; z-index: 10000; display: none;`; document.body.appendChild(tooltip); } return tooltip; } // Escape HTML for safe display function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Show documentation for a function function showDocumentation(funcName, x, y) { const doc = KIDLISP_DOCS[funcName]; if (!doc) return false; const tooltip = createDocTooltip(); // Build HTML content using CSS classes let html = `<div style="margin-bottom: 8px;"> <strong style="color: #FF69B4; font-size: 15px;">${escapeHtml(funcName)}</strong> <code class="doc-signature">${escapeHtml(doc.signature)}</code> </div>`; html += `<div style="margin-bottom: 8px; opacity: 0.9;">${escapeHtml(doc.description)}</div>`; if (doc.category) { html += `<div style="margin-bottom: 8px;"> <span class="doc-category">${escapeHtml(doc.category)}</span> </div>`; } if (doc.examples && doc.examples.length > 0) { html += `<div style="margin-top: 8px; border-top: 1px solid var(--border-subtle, #3a3530); padding-top: 8px;"> <span style="color: var(--text-tertiary, #888); font-size: 11px;">Examples:</span> <div style="margin-top: 4px;">`; doc.examples.slice(0, 3).forEach(ex => { html += `<code class="doc-example">${escapeHtml(ex)}</code>`; }); html += `</div></div>`; } tooltip.innerHTML = html; tooltip.style.display = 'block'; // Position tooltip near cursor but ensure it stays on screen const rect = tooltip.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let posX = x + 10; let posY = y + 10; if (posX + rect.width > viewportWidth - 20) { posX = x - rect.width - 10; } if (posY + rect.height > viewportHeight - 20) { posY = y - rect.height - 10; } tooltip.style.left = posX + 'px'; tooltip.style.top = posY + 'px'; return true; } // Hide documentation tooltip function hideDocumentation() { const tooltip = document.getElementById('kidlisp-doc-tooltip'); if (tooltip) { tooltip.style.display = 'none'; } } // Add context menu action for documentation lookup editor.addAction({ id: 'kidlisp-docs-lookup', label: '📖 KidLisp Docs', keybindings: [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD ], contextMenuGroupId: 'navigation', contextMenuOrder: 0, run: function(ed) { const position = ed.getPosition(); const model = ed.getModel(); const word = getWordAtPosition(model, position); if (word && KIDLISP_DOCS[word]) { // Get cursor screen position const coords = ed.getScrolledVisiblePosition(position); const editorDom = ed.getDomNode(); const editorRect = editorDom.getBoundingClientRect(); const x = editorRect.left + (coords ? coords.left : 100); const y = editorRect.top + (coords ? coords.top : 100); showDocumentation(word, x, y); // Auto-hide after 8 seconds or on next click/keypress const hideHandler = () => { hideDocumentation(); document.removeEventListener('click', hideHandler); document.removeEventListener('keydown', hideHandler); }; setTimeout(hideHandler, 8000); setTimeout(() => { document.addEventListener('click', hideHandler); document.addEventListener('keydown', hideHandler); }, 100); } else if (word) { // Show "not found" message briefly const tooltip = createDocTooltip(); tooltip.innerHTML = `<div style="color: #e06c75;">No documentation found for: <strong>${escapeHtml(word)}</strong></div> <div style="color: var(--text-tertiary, #888); font-size: 11px; margin-top: 8px;">This might be a variable or custom function.</div>`; tooltip.style.display = 'block'; const coords = ed.getScrolledVisiblePosition(position); const editorDom = ed.getDomNode(); const editorRect = editorDom.getBoundingClientRect(); tooltip.style.left = (editorRect.left + (coords ? coords.left : 100) + 10) + 'px'; tooltip.style.top = (editorRect.top + (coords ? coords.top : 100) + 10) + 'px'; setTimeout(hideDocumentation, 3000); } } }); // Add "Insert Example" submenu action editor.addAction({ id: 'kidlisp-insert-example', label: '✨ Insert Example', contextMenuGroupId: 'navigation', contextMenuOrder: 1, run: function(ed) { const position = ed.getPosition(); const model = ed.getModel(); const word = getWordAtPosition(model, position); if (word && KIDLISP_DOCS[word] && KIDLISP_DOCS[word].examples && KIDLISP_DOCS[word].examples.length > 0) { const example = KIDLISP_DOCS[word].examples[0]; ed.executeEdits('kidlisp-example', [{ range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), text: '\n' + example, forceMoveMarkers: true }]); } } }); // AI Assist - show/hide the input bar from context menu editor.addAction({ id: 'kidlisp-ai-assist', label: '✨ Wish', keybindings: [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG ], contextMenuGroupId: 'navigation', contextMenuOrder: 2, run: function(ed) { const bar = document.getElementById('ai-assist-bar'); if (bar) { // Toggle visibility if (bar.style.display === 'none' || bar.style.display === '') { bar.style.display = 'flex'; const input = document.getElementById('ai-assist-input'); if (input) { // Prefill with "I wish " if empty if (!input.value.trim()) { input.value = 'I wish '; } input.focus(); // Move cursor to end input.setSelectionRange(input.value.length, input.value.length); } } else { bar.style.display = 'none'; } } } }); // Toggle Stage Mode from context menu (two actions with preconditions) editor.addAction({ id: 'kidlisp-enter-stage', label: '🎭 Enter Stage Mode', contextMenuGroupId: 'navigation', contextMenuOrder: 3, precondition: 'kidlisp.studioMode', run: function() { window.setUiMode('stage'); updateStageContext(); } }); editor.addAction({ id: 'kidlisp-exit-stage', label: '🎭 Exit Stage Mode', contextMenuGroupId: 'navigation', contextMenuOrder: 3, precondition: 'kidlisp.stageMode', run: function() { window.setUiMode('studio'); updateStageContext(); } }); function updateStageContext() { editor.createContextKey('kidlisp.stageMode', window.isStageMode()); editor.createContextKey('kidlisp.studioMode', !window.isStageMode()); } updateStageContext();
// ==================== END KIDLISP DOCUMENTATION CONTEXT MENU ====================
// Log editor initialization logEditorInit('Monaco', { theme: initialTheme, language: 'kidlisp' }); logBoot('editor', 'Monaco ready');
// Eagerly load learn iframe so Hello tab content is ready { const learnIframe = document.getElementById('learn-iframe'); if (learnIframe && !learnIframe.hasAttribute('src')) { learnIframe.src = learnIframe.dataset.src; } // Auto-open Learn tab when editor is blank (new/empty session) const editorCode = editor.getValue().trim(); if (!editorCode) { const guideTabEl = document.getElementById('guide-tab'); if (guideTabEl) setTimeout(() => guideTabEl.click(), 100); } }
// Remove decrypting state after a brief delay for syntax to initialize setTimeout(() => { const editorEl = document.getElementById('kidlisp-editor'); if (editorEl) { editorEl.classList.remove('decrypting'); } }, 300); // Initialize auto-height for single column mode setTimeout(() => { if (window.updateEditorAutoHeight) { window.updateEditorAutoHeight(); } }, 100);
// Check URL path for a kidlisp code (e.g., /kidlisp.com/$46k or /$46k or /roz) // The $ prefix is optional - /roz works the same as /$roz const urlPath = window.location.pathname; const codeMatch = urlPath.match(/\/\$?([a-zA-Z0-9]+)$/); // Only treat as a code if it's not just the root path if (codeMatch && codeMatch[1]) { const code = codeMatch[1]; log.code.log(`Detected code in URL: $${code}`); // Set title immediately to match URL document.title = `KidLisp.com/$${code}`; setRouteCodeInUI(code, { immediate: true, animate: false }); // Clear editor and localStorage immediately while we load from DB // This prevents old cached code from flashing or being used isSettingValueProgrammatically = true; editor.setValue(''); isSettingValueProgrammatically = false; editorUserModified = false; // Not user-edited, loaded from URL localStorage.removeItem('kidlisp-code'); localStorage.removeItem('kidlisp-current-code'); // Load code from database loadKidlispFromDatabase(code).then(async (source) => { if (source && editor) { logCodeOperation('load', source, code); // Store the code identifier BEFORE animation starts if (currentPlatform === 'aesthetic-computer') { localStorage.setItem('kidlisp-current-code', code); } // Start decryption animation (will keep scrambling until signaled) const editorEl = document.getElementById('kidlisp-editor'); if (editorEl) editorEl.classList.add('decrypting'); // Start animation in background - don't await yet! // Animation will complete when kidlisp-trace signal is received const animationPromise = window.decryptIntoEditor(source, editor, { mode: 'initial' }); // Mark that we've handled the initial preview hasInitialPreviewRun = true; shouldAutoPlay = true; // IMMEDIATELY trigger auto-play if iframe is ready // The animation is scrambling, but updatePreview will use pendingDecryptText if (pieceReady && !isPlaying && currentPlatform === 'aesthetic-computer') { log.playback.log('Auto-playing (code loaded, piece ready, animation will reveal when running)...'); updatePreview(); // This will use getPendingDecryptText() for the real code isPlaying = true; isPaused = false; sendButton.classList.add('playing'); stopButton.style.display = 'flex'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); setLiveState('running'); shouldAutoPlay = false; } // Now wait for animation to complete (will be signaled by kidlisp-trace) await animationPromise; if (editorEl) editorEl.classList.remove('decrypting'); // Save to localStorage for persistence localStorage.setItem('kidlisp-code', source); // Add to console addStateEntry('load', code); addCodeEntry(source); // Update URL and title (only on AC platform) if (currentPlatform === 'aesthetic-computer') { updateURLForCode(code); // Update route code tab (no animation for initial load) setRouteCodeInUI(code, { immediate: true, animate: false }); } } else { log.code.error(`Failed to load code: $${code}`); addConsoleEntry(`Could not load $${code} - code not found`, 'error'); } }); } else { // No code in URL (at root /) // If the editor has content but wasn't user-edited (e.g., from Top Hits auto-load), // clear it so user starts fresh at root if (!editorUserModified && editor.getValue().trim()) { log.code.debug('Clearing non-user-edited content at root'); isSettingValueProgrammatically = true; editor.setValue(''); isSettingValueProgrammatically = false; localStorage.removeItem('kidlisp-code'); localStorage.removeItem('kidlisp-current-code'); } // Only restore saved code if user had previously edited const savedCode = localStorage.getItem('kidlisp-current-code'); if (savedCode && editor.getValue().trim() && currentPlatform === 'aesthetic-computer') { log.code.debug(`Restored code from localStorage: $${savedCode}`); // Update URL and title to match the saved code updateURLForCode(savedCode); // Update route code tab (no animation for restore) setRouteCodeInUI(savedCode, { immediate: true, animate: false }); } else { setRouteCodeInUI(null, { immediate: true, animate: false }); } }
// Line limit button handler // Removed - now using fixed 16 lines // Update editor vertical padding based on line count function updateEditorPadding() { const model = editor.getModel(); if (!model) return; const currentContent = model.getValue(); const currentLineCount = model.getLineCount(); if (currentLineCount < maxLines) { // Add empty lines to reach maxLines const linesToAdd = maxLines - currentLineCount; const newContent = currentContent + '\n'.repeat(linesToAdd); // Get current cursor position const position = editor.getPosition(); // Update content model.setValue(newContent); // Restore cursor position if (position) { editor.setPosition(position); } } } // Enforce line limit function enforceLineLimit() { const model = editor.getModel(); if (!model) return; const lineCount = model.getLineCount(); if (lineCount > maxLines) { // Trim to maxLines immediately const lastLine = model.getLineContent(maxLines); const range = new monaco.Range(maxLines + 1, 1, lineCount + 1, 1); // Remove extra lines model.pushEditOperations([], [{ range: range, text: null }], () => null); // Move cursor to end of last allowed line const endOfLastLine = lastLine.length + 1; editor.setPosition({ lineNumber: maxLines, column: endOfLastLine }); } } // Stop decryption animation immediately when user starts typing // This ensures no garbage code can remain if user interrupts the animation editor.onKeyDown((e) => { // Any key press (except navigation keys) should stop decryption const isNavigationKey = [ monaco.KeyCode.LeftArrow, monaco.KeyCode.RightArrow, monaco.KeyCode.UpArrow, monaco.KeyCode.DownArrow, monaco.KeyCode.Home, monaco.KeyCode.End, monaco.KeyCode.PageUp, monaco.KeyCode.PageDown, monaco.KeyCode.Shift, monaco.KeyCode.Ctrl, monaco.KeyCode.Alt, monaco.KeyCode.Meta ].includes(e.keyCode); if (!isNavigationKey && window.isDecrypting && window.isDecrypting()) { // Stop animation and set final text immediately window.stopDecryption(true); const editorEl = document.getElementById('kidlisp-editor'); if (editorEl) editorEl.classList.remove('decrypting'); } }); // Also stop decryption if user clicks to focus/interact with editor editor.onMouseDown(() => { if (window.isDecrypting && window.isDecrypting()) { window.stopDecryption(true); const editorEl = document.getElementById('kidlisp-editor'); if (editorEl) editorEl.classList.remove('decrypting'); } }); // Prevent Enter key when at line limit editor.onKeyDown((e) => { if (e.keyCode === monaco.KeyCode.Enter) { const model = editor.getModel(); if (model && model.getLineCount() >= maxLines) { e.preventDefault(); e.stopPropagation(); } } }); // Independent zoom controls for Monaco editor (Cmd/Ctrl + Plus/Minus/0) let editorFontSize = 16; // Default font size const EDITOR_FONT_MIN = 8; const EDITOR_FONT_MAX = 32; const EDITOR_FONT_DEFAULT = 16; editor.onKeyDown((e) => { const isMeta = e.metaKey || e.ctrlKey; if (!isMeta) return; // Check for Plus (= or NumpadAdd), Minus (- or NumpadSubtract), or 0 const isPlus = e.keyCode === monaco.KeyCode.Equal || e.keyCode === monaco.KeyCode.NumpadAdd; const isMinus = e.keyCode === monaco.KeyCode.Minus || e.keyCode === monaco.KeyCode.NumpadSubtract; const isZero = e.keyCode === monaco.KeyCode.Digit0 || e.keyCode === monaco.KeyCode.Numpad0; if (isPlus || isMinus || isZero) { e.preventDefault(); e.stopPropagation(); if (isPlus && editorFontSize < EDITOR_FONT_MAX) { editorFontSize += 2; } else if (isMinus && editorFontSize > EDITOR_FONT_MIN) { editorFontSize -= 2; } else if (isZero) { editorFontSize = EDITOR_FONT_DEFAULT; } editor.updateOptions({ fontSize: editorFontSize }); console.log(`Editor zoom: ${editorFontSize}px`); } }); // Listen for cursor position changes to update Learn tab documentation editor.onDidChangeCursorPosition((e) => { // Only react to genuine user cursor movement. Programmatic content // changes — initial restore, code-load, and the startup scramble/decrypt // reveal (which calls setValue every frame) — fire with reason // ContentFlush and would otherwise flip the Learn tab rapidly through // half-formed/non-existent functions while Monaco chunks in. const ContentFlush = window.monaco?.editor?.CursorChangeReason?.ContentFlush ?? 1; if (e.reason === ContentFlush) return; if (isSettingValueProgrammatically) return; const model = editor.getModel(); if (!model) return;
const position = e.position; const word = model.getWordAtPosition(position); const wordText = word ? word.word : null;
// Extract all identifiers from the code const code = model.getValue(); const identifiers = extractIdentifiersFromCode(code);
// Find current word index and neighbors let currentIndex = -1; if (wordText && identifiers.includes(wordText)) { // Find the closest instance of this word to cursor position currentIndex = identifiers.findIndex(id => id === wordText); }
const prevWord = currentIndex > 0 ? identifiers[currentIndex - 1] : null; const nextWord = currentIndex >= 0 && currentIndex < identifiers.length - 1 ? identifiers[currentIndex + 1] : null;
// Only update if word changed if (wordText !== currentCursorWord) { currentCursorWord = wordText; // Update Learn documentation with ticker updateLearnDocumentation(wordText, prevWord, nextWord); // Send identifier to Learn iframe via postMessage const learnIframe = document.getElementById('learn-iframe'); if (learnIframe?.contentWindow) { learnIframe.contentWindow.postMessage({ type: 'kidlisp-identify', word: wordText }, '*'); } } }); // Listen for content changes to enforce line limit editor.onDidChangeModelContent(() => { enforceLineLimit(); // Update editor auto-height in single column mode if (window.updateEditorAutoHeight) { window.updateEditorAutoHeight(); } // Update play button state based on content if (window.updatePlayButtonState) { window.updatePlayButtonState(); } });
// Note: OS theme change listener is in applyTheme section below // to avoid duplicate listeners and respect manual theme selection
// Track decorations for execution trace highlighting (separate from syntax highlighting) let highlightTraceDecorations = []; let errorDecorations = []; // Track error underlines separately // Reassign the stub functions to real implementations addErrorDecoration = function(word, loc) { if (!editor || !loc || !loc.line) return; const model = editor.getModel(); if (!model) return; // Find the word in the line const lineContent = model.getLineContent(loc.line); const col = loc.col || lineContent.indexOf(word) + 1; const endCol = col + (word ? word.length : 1); // Create error decoration const decoration = { range: new monaco.Range(loc.line, col, loc.line, endCol), options: { inlineClassName: 'kidlisp-error-word', hoverMessage: { value: `Unknown word: ${word}` }, glyphMarginClassName: 'kidlisp-error-glyph' } }; // Add to error decorations errorDecorations = editor.deltaDecorations(errorDecorations, [...errorDecorations.map(() => ({})), decoration].slice(-10)); // Also scroll to the error editor.revealLineInCenter(loc.line); }; // Reassign clearErrorDecorations to real implementation clearErrorDecorations = function() { if (editor) { errorDecorations = editor.deltaDecorations(errorDecorations, []); } }; // Edit mode for timing blinks is controlled dynamically based on playback state kidlisp.isEditMode = false; // Syntax highlighting via shared MonacoKidLispHighlighter module const syntaxHighlighter = new MonacoKidLispHighlighter(editor, { kidlispInstance: kidlisp }); // Apply color decorations on content change editor.onDidChangeModelContent(() => { updateLineNumbers(); // Track if this was a user edit (not programmatic) if (!isSettingValueProgrammatically && !(window.isDecrypting && window.isDecrypting())) { editorUserModified = true; } // Debounce decoration updates to avoid overwhelming the editor if (window.decorationTimeout) { clearTimeout(window.decorationTimeout); } window.decorationTimeout = setTimeout(() => { syntaxHighlighter.applyDecorations(true); }, 250); // Debounce localStorage writes to avoid blocking the main thread on every keystroke if (window.localStorageSaveTimeout) { clearTimeout(window.localStorageSaveTimeout); } window.localStorageSaveTimeout = setTimeout(() => { // Don't save scrambled animation content to localStorage if (!(window.isDecrypting && window.isDecrypting())) { localStorage.setItem('kidlisp-code', editor.getValue()); } }, 500); // Clear URL and code identifier when content changes const currentPath = window.location.pathname; if (currentPath !== '/kidlisp.com' && currentPath.startsWith('/kidlisp.com/')) { window.history.pushState({}, 'KidLisp.com', '/kidlisp.com'); document.title = 'KidLisp.com'; localStorage.removeItem('kidlisp-current-code'); setRouteCodeInUI(null, { immediate: true, animate: false }); // Reset preview title to current platform name (using global helper) const previewTitle = document.getElementById('preview-title'); if (previewTitle) { const savedTab = document.getElementById('route-code-tab'); const platformOption = document.querySelector(`.platform-option[data-platform="${currentPlatform}"]`); const platformName = platformOption?.querySelector('.platform-opt-name')?.textContent || 'Aesthetic.Computer'; previewTitle.innerHTML = getPlatformTitleHTML(currentPlatform, platformName); if (savedTab) previewTitle.appendChild(savedTab); } } }); // Initial application after a short delay to ensure editor is ready setTimeout(() => { syntaxHighlighter.applyDecorations(true); }, 100);
// Continuously update decorations for timing blinks (60fps) syntaxHighlighter.startTimingBlinksLoop(() => isPlaying && !isPaused);
// Update line numbers based on maxLines setting function updateLineNumbers() { // Show line numbers only when maxLines > 1 editor.updateOptions({ lineNumbers: maxLines > 1 ? 'on' : 'off', lineNumbersMinChars: maxLines > 1 ? 3 : 0, lineDecorationsWidth: maxLines > 1 ? 10 : 0 }); } updateLineNumbers();
// Update preview on Cmd/Ctrl+Enter (play/run) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { if (sendButton && !sendButton.disabled) { sendButton.click(); } }); // Stop playback on Cmd/Ctrl+. (period) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Period, () => { if (stopButton && stopButton.style.display !== 'none') { stopButton.click(); } }); // Pause/Resume on Cmd/Ctrl+P editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyP, () => { if (isPlaying && sendButton) { sendButton.click(); // Toggle pause } }); // Clear editor on Cmd/Ctrl+K editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => { if (clearButton && clearButton.style.display !== 'none') { clearButton.click(); } });
// Increment/decrement number at cursor with Ctrl+] / Ctrl+[ function adjustNumberAtCursor(delta) { const position = editor.getPosition(); const model = editor.getModel(); const lineContent = model.getLineContent(position.lineNumber); const col = position.column - 1; // 0-based
// Find number under or adjacent to cursor const numRegex = /-?\d+\.?\d*/g; let match; let bestMatch = null; while ((match = numRegex.exec(lineContent)) !== null) { const start = match.index; const end = start + match[0].length; // Cursor is inside or immediately adjacent to the number if (col >= start && col <= end) { bestMatch = match; break; } } if (!bestMatch) return;
const original = bestMatch[0]; const startCol = bestMatch.index + 1; // 1-based const endCol = startCol + original.length; const dotIndex = original.indexOf('.');
let newValue; if (dotIndex === -1) { // Integer: adjust by 1 newValue = String(parseInt(original, 10) + delta); } else { // Float: adjust by its smallest decimal place const decimals = original.length - dotIndex - 1; const step = Math.pow(10, -decimals); const result = parseFloat(original) + delta * step; newValue = result.toFixed(decimals); }
const range = new monaco.Range( position.lineNumber, startCol, position.lineNumber, endCol ); editor.executeEdits('adjust-number', [{ range, text: newValue, forceMoveMarkers: true }]);
// Keep cursor within the new number const newCol = Math.min( startCol + newValue.length, startCol + Math.max(0, col - bestMatch.index) ); editor.setPosition({ lineNumber: position.lineNumber, column: newCol }); }
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.BracketRight, () => { adjustNumberAtCursor(1); });
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.BracketLeft, () => { adjustNumberAtCursor(-1); });
// Clear button handler const clearButton = document.getElementById('clear-button'); const stopButton = document.getElementById('stop-button'); // No need for manual positioning - flexbox handles it automatically window.updateClearButtonPosition = function() { // Flexbox with gap handles spacing automatically when buttons show/hide } function updateClearButtonVisibility() { if (clearButton && editor) { const hasContent = editor.getValue().trim().length > 0; clearButton.style.display = hasContent ? 'flex' : 'none'; } } if (clearButton) { clearButton.addEventListener('click', () => { if (editor) { // Confirm before clearing to prevent accidental data loss const confirmed = confirm('Clear the editor? This will erase all your code.'); if (!confirmed) { return; // User cancelled, don't clear }
isSettingValueProgrammatically = true; editor.setValue(''); isSettingValueProgrammatically = false; editorUserModified = false; // Reset on clear editor.focus(); // Also stop playback and show boot screen when clearing if (isPlaying || isPaused) { stopPlayback(); } } }); } // Update clear/slide button visibility on content change // Note: updateLineNumbers and applyDecorations are already handled by other // onDidChangeModelContent handlers with proper debouncing — don't duplicate here. editor.onDidChangeModelContent(() => { updateClearButtonVisibility(); if (window.updateSlideButtonVisibility) window.updateSlideButtonVisibility(); }); // Initial visibility check updateClearButtonVisibility(); // ==================== SLIDE MODE ==================== // Slide mode allows dragging numbers to adjust their values in real-time const slideButton = document.getElementById('slide-button'); const slideSetBtn = document.getElementById('slide-set-btn'); const slideActions = document.getElementById('slide-actions'); // Legacy, hidden const slideDragOverlay = document.getElementById('slide-drag-overlay'); const slideValueTooltip = document.getElementById('slide-value-tooltip'); let slideMode = false; let slideOriginalCode = ''; // Code before slide mode started let slideDecorations = []; let slideWidgets = []; let slideNumbers = []; // Array of { range, value, step, decorationId } let slideDragging = null; // Currently dragging number info let slideDragStartX = 0; let slideDragStartValue = 0; let slideHasChanges = false; // Check if code contains numbers (for slide button visibility) function codeHasNumbers(code) { if (!code) return false; // Check for numeric literals (integers or floats) return /\b-?\d+\.?\d*\b/.test(code); } // Update slide button visibility based on: has numbers + is playing + not paused function updateSlideButtonVisibility() { if (!slideButton) return; const code = editor ? editor.getValue() : ''; const hasNumbers = codeHasNumbers(code); const shouldShow = hasNumbers && isPlaying && !isPaused && !isSoftStopped; if (shouldShow) { slideButton.classList.add('available'); } else { slideButton.classList.remove('available'); // If hiding and slide mode is active, exit slide mode if (slideMode) { exitSlideMode(); } } } // Expose globally for playback state changes window.updateSlideButtonVisibility = updateSlideButtonVisibility; // Get step size based on number precision function getSlideStep(numStr) { // Handle negative numbers const absStr = numStr.replace(/^-/, ''); if (!absStr.includes('.')) return 1; const decimals = absStr.split('.')[1].length; return Math.pow(10, -decimals); } // Format number preserving original precision function formatSlideNumber(value, step) { if (step >= 1) return Math.round(value).toString(); const decimals = Math.round(-Math.log10(step)); return value.toFixed(decimals); } // Find all numeric literals in code with their positions function findSlideNumbers(code) { const numbers = []; // Regex to find numeric literals (integers and floats, positive and negative) // But not inside strings or comments const lines = code.split('\n'); lines.forEach((line, lineIndex) => { // Skip comment lines if (line.trim().startsWith(';')) return; // Find numbers, avoiding those in strings let inString = false; let stringChar = null; let i = 0; while (i < line.length) { const char = line[i]; // Track string state if ((char === '"' || char === "'") && (i === 0 || line[i-1] !== '\\')) { if (!inString) { inString = true; stringChar = char; } else if (char === stringChar) { inString = false; stringChar = null; } i++; continue; } // Skip if in string if (inString) { i++; continue; } // Check for comment start if (char === ';') break; // Look for number start if (/[\d\-]/.test(char)) { // Check if this could be a number (not part of identifier) const prevChar = i > 0 ? line[i-1] : ' '; if (/[a-zA-Z_]/.test(prevChar)) { i++; continue; } // For minus sign, check if followed by digit if (char === '-') { if (i + 1 >= line.length || !/\d/.test(line[i+1])) { i++; continue; } } // Extract the full number const numMatch = line.slice(i).match(/^-?\d+\.?\d*/); if (numMatch) { const numStr = numMatch[0]; const value = parseFloat(numStr); // Skip if NaN or if it's just a minus sign if (!isNaN(value) && numStr !== '-') { numbers.push({ lineNumber: lineIndex + 1, startColumn: i + 1, endColumn: i + 1 + numStr.length, value: value, originalStr: numStr, step: getSlideStep(numStr) }); } i += numStr.length; continue; } } i++; } }); return numbers; } // Apply slide mode decorations to highlight numbers function applySlideDecorations() { if (!editor || !slideMode) return; const code = editor.getValue(); slideNumbers = findSlideNumbers(code); const decorations = slideNumbers.map(num => ({ range: new monaco.Range(num.lineNumber, num.startColumn, num.lineNumber, num.endColumn), options: { inlineClassName: 'slide-number-highlight', hoverMessage: { value: `Drag to adjust (step: ${num.step})` } } })); slideDecorations = editor.deltaDecorations(slideDecorations, decorations); } // Clear slide mode decorations function clearSlideDecorations() { if (editor) { slideDecorations = editor.deltaDecorations(slideDecorations, []); } slideNumbers = []; } // Find number at mouse position function findNumberAtPosition(e) { if (!editor) return null; const target = editor.getTargetAtClientPoint(e.clientX, e.clientY); if (!target || !target.position) return null; const pos = target.position; for (let i = 0; i < slideNumbers.length; i++) { const num = slideNumbers[i]; if (pos.lineNumber === num.lineNumber && pos.column >= num.startColumn && pos.column <= num.endColumn) { return { ...num, index: i }; } } return null; } // Update a number value in the editor function updateSlideNumber(numInfo, newValue) { if (!editor) return; const model = editor.getModel(); if (!model) return; const newStr = formatSlideNumber(newValue, numInfo.step); const range = new monaco.Range( numInfo.lineNumber, numInfo.startColumn, numInfo.lineNumber, numInfo.endColumn ); // Temporarily disable readOnly to allow our edit editor.updateOptions({ readOnly: false }); // Apply edit without triggering full re-evaluation isSettingValueProgrammatically = true; editor.executeEdits('slide-mode', [{ range: range, text: newStr, forceMoveMarkers: true }]); isSettingValueProgrammatically = false; // Re-enable readOnly editor.updateOptions({ readOnly: true }); // Update the number info for future drags const lengthDiff = newStr.length - (numInfo.endColumn - numInfo.startColumn); numInfo.endColumn = numInfo.startColumn + newStr.length; numInfo.value = newValue; numInfo.originalStr = newStr; // Adjust subsequent numbers on the same line for (let i = numInfo.index + 1; i < slideNumbers.length; i++) { if (slideNumbers[i].lineNumber === numInfo.lineNumber) { slideNumbers[i].startColumn += lengthDiff; slideNumbers[i].endColumn += lengthDiff; } } slideHasChanges = true; // Show Set button when changes are made if (slideSetBtn) slideSetBtn.classList.add('visible'); // Get the updated code const newCode = editor.getValue(); // Update lastCode so auto-reload doesn't see this as a mismatch // (lastCode is used by the onDidChangeModelContent handler) if (typeof lastCode !== 'undefined') { lastCode = newCode.trim(); } // Broadcast slide update to PJ sessions via WebSocket (low latency) if (typeof broadcastSlideToChannel === 'function') { broadcastSlideToChannel(newCode); } // Trigger preview update with slide flag to avoid buffer reset if (typeof sendToIframe === 'function') { sendToIframe({ type: 'kidlisp-slide', code: newCode }); } } // Mouse handlers for slide dragging function handleSlideMouseDown(e) { if (!slideMode || !editor) return; // Always prevent default in slide mode to avoid cursor movement e.preventDefault(); e.stopPropagation(); const numInfo = findNumberAtPosition(e); if (!numInfo) return; slideDragging = numInfo; slideDragStartX = e.clientX; slideDragStartValue = numInfo.value; // Show overlay to capture all mouse movement if (slideDragOverlay) slideDragOverlay.style.display = 'block'; // Update tooltip if (slideValueTooltip) { slideValueTooltip.textContent = formatSlideNumber(numInfo.value, numInfo.step); slideValueTooltip.style.display = 'block'; slideValueTooltip.style.left = (e.clientX + 15) + 'px'; slideValueTooltip.style.top = (e.clientY - 10) + 'px'; } document.body.style.cursor = 'ew-resize'; document.body.style.userSelect = 'none'; } function handleSlideMouseMove(e) { if (!slideDragging) return; const dx = e.clientX - slideDragStartX; // Sensitivity: 1px = 0.1 * step (so 10px = 1 step) const sensitivity = 0.1; const delta = dx * sensitivity * slideDragging.step; const newValue = slideDragStartValue + delta; // Update tooltip if (slideValueTooltip) { slideValueTooltip.textContent = formatSlideNumber(newValue, slideDragging.step); slideValueTooltip.style.left = (e.clientX + 15) + 'px'; slideValueTooltip.style.top = (e.clientY - 10) + 'px'; } updateSlideNumber(slideDragging, newValue); } function handleSlideMouseUp(e) { if (!slideDragging) return; slideDragging = null; if (slideDragOverlay) slideDragOverlay.style.display = 'none'; if (slideValueTooltip) slideValueTooltip.style.display = 'none'; document.body.style.cursor = ''; document.body.style.userSelect = ''; // Re-apply decorations after edit applySlideDecorations(); } // Toggle slide mode function toggleSlideMode() { if (slideMode) { exitSlideMode(); } else { enterSlideMode(); } } // Enter slide mode let slideSavedCursorPosition = null; let slideSavedScrollTop = null; function enterSlideMode() { if (slideMode) return; slideMode = true; if (slideButton) { slideButton.classList.add('active'); } // Save cursor position and scroll state if (editor) { slideSavedCursorPosition = editor.getPosition(); slideSavedScrollTop = editor.getScrollTop(); } // Add slide-mode-active class to editor for CSS styling const editorContainer = document.getElementById('kidlisp-editor'); if (editorContainer) { editorContainer.classList.add('slide-mode-active'); } // Hide other playback controls const playbackControls = document.getElementById('playback-controls'); if (playbackControls) { playbackControls.classList.add('slide-mode-controls-hidden'); } // Insert curtain overlay into Monaco editor (inside the view-lines container) const viewOverlays = document.querySelector('#kidlisp-editor .monaco-editor .view-overlays'); if (viewOverlays && !document.getElementById('slide-curtain-overlay')) { const curtain = document.createElement('div'); curtain.id = 'slide-curtain-overlay'; viewOverlays.parentElement.insertBefore(curtain, viewOverlays); } // Entering slide mode slideOriginalCode = editor ? editor.getValue() : ''; slideHasChanges = false; applySlideDecorations(); // Make editor read-only except for our programmatic edits if (editor) { editor.updateOptions({ readOnly: true }); // Hide the cursor editor.updateOptions({ cursorStyle: 'line', cursorBlinking: 'hidden' }); } // Add mouse handlers to editor (use capture phase to intercept before Monaco) const editorDom = document.querySelector('#kidlisp-editor .monaco-editor'); if (editorDom) { editorDom.addEventListener('mousedown', handleSlideMouseDown, true); } log.editor.log('Slide mode enabled'); } // Exit slide mode (without discarding - keeps current values) function exitSlideMode() { if (!slideMode) return; slideMode = false; if (slideButton) { slideButton.classList.remove('active'); } // Hide Set button if (slideSetBtn) { slideSetBtn.classList.remove('visible'); } // Remove slide-mode-active class from editor const editorContainer = document.getElementById('kidlisp-editor'); if (editorContainer) { editorContainer.classList.remove('slide-mode-active'); } // Show other playback controls again const playbackControls = document.getElementById('playback-controls'); if (playbackControls) { playbackControls.classList.remove('slide-mode-controls-hidden'); } // Remove curtain overlay const curtain = document.getElementById('slide-curtain-overlay'); if (curtain) { curtain.remove(); } clearSlideDecorations(); if (editor) { editor.updateOptions({ readOnly: false }); // Restore cursor visibility editor.updateOptions({ cursorStyle: 'line', cursorBlinking: 'blink' }); // Restore cursor position and scroll if (slideSavedCursorPosition) { editor.setPosition(slideSavedCursorPosition); } if (slideSavedScrollTop !== null) { editor.setScrollTop(slideSavedScrollTop); } } // Remove mouse handlers (must match capture phase flag) const editorDom = document.querySelector('#kidlisp-editor .monaco-editor'); if (editorDom) { editorDom.removeEventListener('mousedown', handleSlideMouseDown, true); } // Current values become the new baseline (no discard on exit) slideOriginalCode = editor ? editor.getValue() : ''; slideHasChanges = false; // Sync play button state with actual playback state // (exiting slide mode shouldn't change playback, but button might need sync) const sendButton = document.getElementById('send-button'); if (sendButton && typeof isPlaying !== 'undefined' && typeof isPaused !== 'undefined') { if (isPlaying && !isPaused) { sendButton.classList.add('playing'); sendButton.classList.remove('paused'); } else if (isPlaying && isPaused) { sendButton.classList.remove('playing'); sendButton.classList.add('paused'); } else { sendButton.classList.remove('playing'); sendButton.classList.remove('paused'); } } log.editor.log('Slide mode disabled'); } // Set slide changes (commit current values as the new original) function setSlideChanges() { slideHasChanges = false; if (slideSetBtn) slideSetBtn.classList.remove('visible'); slideOriginalCode = editor ? editor.getValue() : ''; log.editor.log('Slide values set'); } // Keep slide changes (legacy - alias for setSlideChanges) function keepSlideChanges() { setSlideChanges(); } // Discard slide changes (restore original code) - legacy, not used in new UI function discardSlideChanges() { if (editor && slideOriginalCode) { // Need to temporarily disable readOnly if we're in slide mode if (slideMode) editor.updateOptions({ readOnly: false }); isSettingValueProgrammatically = true; editor.setValue(slideOriginalCode); isSettingValueProgrammatically = false; if (slideMode) editor.updateOptions({ readOnly: true }); // Trigger preview update - it reads from editor.getValue() if (typeof updatePreview === 'function') { updatePreview(); } } slideHasChanges = false; if (slideSetBtn) slideSetBtn.classList.remove('visible'); log.editor.log('Slide changes discarded'); } // Set up event listeners if (slideButton) { slideButton.addEventListener('click', toggleSlideMode); } if (slideSetBtn) { slideSetBtn.addEventListener('click', () => { setSlideChanges(); // Stay in slide mode after setting (don't exit) }); } // Global mouse handlers for drag document.addEventListener('mousemove', handleSlideMouseMove); document.addEventListener('mouseup', handleSlideMouseUp); if (slideDragOverlay) { slideDragOverlay.addEventListener('mousemove', handleSlideMouseMove); slideDragOverlay.addEventListener('mouseup', handleSlideMouseUp); } // Expose for debugging window.slideMode = { toggle: toggleSlideMode, enter: enterSlideMode, exit: exitSlideMode, set: setSlideChanges, discard: discardSlideChanges, isActive: () => slideMode }; // ==================== END SLIDE MODE ==================== // Language list for preferences modal const languages = [ { code: 'en', name: 'English', flagClass: 'fi fi-us' }, { code: 'es', name: 'Español', flagClass: 'fi fi-es' }, { code: 'zh', name: '中文', flagClass: 'fi fi-cn' }, { code: 'da', name: 'Dansk', flagClass: 'fi fi-dk' } ]; // Initialize UI language updateUILanguage(); // Theme functionality (toggle removed from header, now in preferences modal) let currentTheme = 'system'; // Start with system preference window.manualThemeSet = false; // Session-only tracking - resets on refresh window.applyTheme = function(theme) { currentTheme = theme; // Don't persist to localStorage - theme override is session-only // Determine the actual visual theme to apply let visualTheme; if (theme === 'system') { visualTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } else { visualTheme = theme; } // Always set data-theme to ensure consistent styling // This overrides the @media (prefers-color-scheme) fallbacks document.documentElement.setAttribute('data-theme', visualTheme); // Update Monaco editor theme if (editor) { monaco.editor.setTheme(visualTheme === 'dark' ? 'kidlisp-dark' : 'kidlisp-light'); } // Send theme to iframe if (previewIframe) { sendToIframe({ type: 'kidlisp-theme', theme: visualTheme }); } // Re-apply decorations with new theme if (editor) { syntaxHighlighter.applyDecorations(true); } // Regenerate QR code with new theme colors if (currentQRCode) { generateAndDisplayQR(currentQRCode); } } // Always start with system preference on page load (no persistence) window.applyTheme('system'); // Listen for system theme changes - only respond if user hasn't manually toggled this session window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { // Only respond to OS theme if user hasn't manually set a preference this session if (!window.manualThemeSet) { window.applyTheme('system'); // Re-apply to update theme } });
// Stochastic coloring for KidLisp title const kidlispTitle = document.querySelector('.baby-colors'); if (kidlispTitle) { const letters = kidlispTitle.querySelectorAll('span:not(.com-part)'); // Saturated Easter palette const colors = ['#FF7096', '#FFB870', '#FFEA70', '#8AFF8A', '#70D6FF', '#C596FF', '#FF96C5']; let lastColorIndex = -1; letters.forEach(letter => { let randomIndex; do { randomIndex = Math.floor(Math.random() * colors.length); } while (randomIndex === lastColorIndex && colors.length > 1); lastColorIndex = randomIndex; const randomColor = colors[randomIndex]; letter.style.color = randomColor; // Calculate intelligent shadow color (darker version for contrast) const rgb = randomColor.match(/\w\w/g); if (rgb) { const r = Math.max(0, parseInt(rgb[0], 16) - 80); const g = Math.max(0, parseInt(rgb[1], 16) - 80); const b = Math.max(0, parseInt(rgb[2], 16) - 80); letter.style.textShadow = `1px 1px 0px rgb(${r}, ${g}, ${b})`; } letter.style.animation = 'none'; // Ensure animation is off }); }
// Book - platform-specific guide bookContainer = document.getElementById('book-container'); renderBook(currentPlatform); window.addEventListener('resize', fitBookFrame); window.addEventListener('resize', fitBookTypography); // Resize observer keeps the frame fitting when panels are dragged on mobile if (window.ResizeObserver && bookContainer) { const ro = new ResizeObserver(() => { fitBookFrame(); fitBookTypography(); }); ro.observe(bookContainer); } // Auto-reload when code changes while playing if (editor) { let debounceTimer = null; let pendingUpdate = false;
// Count top-level parenthesized expressions, skipping ; comments and // quoted strings so a string like "(foo)" doesn't throw off the count. // Used to decide slide-vs-reload below; see REPORT-kidlisp-realtime-state.md. function countTopLevelExpressions(code) { if (!code) return 0; let count = 0, depth = 0, i = 0; while (i < code.length) { const ch = code[i]; if (ch === ';') { while (i < code.length && code[i] !== '\n') i++; continue; } if (ch === '"' || ch === "'") { const q = ch; i++; while (i < code.length && code[i] !== q) { if (code[i] === '\\') i++; i++; } i++; continue; } if (ch === '(') { if (depth === 0) count++; depth++; } else if (ch === ')') depth--; i++; } return count; }
editor.onDidChangeModelContent(() => { // Skip updates during decryption animation - the animation is changing the content if (window.isDecrypting && window.isDecrypting()) { return; } // Skip updates during programmatic edits (slide mode, etc.) // Slide mode sends its own 'kidlisp-slide' message for live updates if (isSettingValueProgrammatically) { return; } const currentCode = editor.getValue().trim(); // If soft-stopped and code changes, kill the $code tab (full stop) if (isSoftStopped && currentCode !== lastCode) { log.playback.debug('Code changed while soft-stopped, killing tab'); sendToIframe({ type: 'kidlisp-stop' }); // Full stop - kill the tab isSoftStopped = false; lastCode = ''; setLiveState('idle'); // Remove red outline localStorage.removeItem('kidlisp-current-code'); return; } if (isPlaying && !isPaused && currentCode !== lastCode) { // Immediately mark code as stale when user types (POOF animation!) const codeId = localStorage.getItem('kidlisp-current-code'); if (codeId) { markCodeAsStale(); // Clear the localStorage code since it's now invalid localStorage.removeItem('kidlisp-current-code'); }
// Immediately broadcast every keystroke to PJ / popout (no debounce) if (typeof broadcastSlideToChannel === 'function') { broadcastSlideToChannel(currentCode); }
// Cancel any pending update if (debounceTimer) { clearTimeout(debounceTimer); pendingUpdate = true; }
// Shorter debounce in stage mode for immediate feedback const delay = (window.isStageMode && window.isStageMode()) ? 150 : 1000; debounceTimer = setTimeout(() => { // If the top-level expression count is unchanged, route through // the slide-update path. slideUpdate() in kidlisp.mjs re-parses // the AST but preserves globalDef, frameCount, onceExecuted, // melodies, and timers — so accumulated graphics + animation // state survive value tweaks (numbers, colors). Full reload // still runs when expressions are added/removed. const oldExpr = countTopLevelExpressions(lastCode); const newExpr = countTopLevelExpressions(currentCode); if (oldExpr > 0 && oldExpr === newExpr) { sendToIframe({ type: 'kidlisp-slide', code: currentCode }); } else { updatePreview(); } lastCode = currentCode; pendingUpdate = false; debounceTimer = null; }, delay); } }); }
// Initial preview - only if we haven't already loaded from URL if (!hasInitialPreviewRun) { // If there's code in the editor (from localStorage), auto-play it const initialCode = editor?.getValue().trim(); if (initialCode && currentPlatform === 'aesthetic-computer') { // Don't send code yet - wait for pieceReady to trigger auto-play // This prevents double-run (once now, once when pieceReady fires) shouldAutoPlay = true; } else { // No code or not AC platform - just load the iframe updatePreview(); } hasInitialPreviewRun = true; } // App is ready - trigger the splash animation window.kidlispReady = true; if (window.triggerSplashAnimation) { window.triggerSplashAnimation(); }
}, 0); }); // end deferred init (rAF + setTimeout yield) });
let isInitialLoad = true; let hasInitialPreviewRun = false; // Guard against duplicate initial runs // Debug: Track iframe src changes let iframeSrcSetCount = 0; const originalSrc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'src'); Object.defineProperty(previewIframe, 'src', { get: function() { return originalSrc.get.call(this); }, set: function(value) { iframeSrcSetCount++; return originalSrc.set.call(this, value); } }); // Load KidLisp with empty code (should show default pattern/checkerboard) previewIframe.src = `${aestheticUrl}/kidlisp?nogap=true&nolabel=true&noauth=true`; // Pending messages queue (iframeReady is defined earlier) let pendingMessages = []; function sendToIframe(message) { // When popout is active, skip sending to the local preview iframe if (localPreviewPausedForPopout) return; if (iframeReady && previewIframe?.contentWindow) { try { previewIframe.contentWindow.postMessage(message, aestheticUrl); } catch (e) { console.warn('🔲 iframe postMessage failed:', e.message); } } else { // Queue message until iframe is ready pendingMessages.push(message); } } // Mark iframe as ready when it loads and send queued messages previewIframe.addEventListener('load', function onFirstLoad() { // Small delay to ensure iframe's message handler is set up setTimeout(() => { iframeReady = true; pendingMessages.forEach(msg => { try { previewIframe.contentWindow?.postMessage(msg, aestheticUrl); } catch (e) { console.warn('🔲 iframe Failed to send queued message:', e.message); } }); pendingMessages = []; // Update play button state now that iframe is ready if (window.updatePlayButtonState) { window.updatePlayButtonState(); } }, 100); }, { once: true }); let isPlaying = false; let isPaused = false; let isSoftStopped = false; // Track if soft-stopped (paused but tab still open) let userStopped = false; // Track if user explicitly stopped (prevent auto-play) let lastCode = ''; // Track last code for auto-reload
// 📊 FPS tracker — measures intervals between kidlisp-trace messages const fpsTracker = { times: [], // recent frame timestamps (last 60) fps: 0, // smoothed FPS peak: 0, // peak FPS seen frameCount: 0, // total frames tick() { const now = performance.now(); this.times.push(now); if (this.times.length > 60) this.times.shift(); this.frameCount++; if (this.times.length > 1) { const elapsed = (now - this.times[0]) / 1000; this.fps = Math.round((this.times.length - 1) / elapsed); if (this.fps > this.peak) this.peak = this.fps; } }, reset() { this.times = []; this.fps = 0; this.frameCount = 0; }, }; // 📏 Pixel Rulers System (shown when paused) const rulerSystem = { initialized: false, visible: false, frozen: false, // Whether canvas size is frozen (during pause) canvasWidth: 640, // AC logical canvas size (viewport / density) canvasHeight: 360, // Default density = 2, so 1280/2 = 640 frozenWidth: null, // Frozen pixel dimensions (for CSS) frozenHeight: null, // Frozen pixel dimensions (for CSS) density: 2, // AC screen density (default) resizeObserver: null, // ResizeObserver for frozen position updates init() { if (this.initialized) return; this.initialized = true; const overlay = document.getElementById('preview-mouse-overlay'); const rulers = document.getElementById('preview-rulers'); const coordDisplay = document.getElementById('coord-display'); const crosshairH = document.getElementById('crosshair-h'); const crosshairV = document.getElementById('crosshair-v'); const crosshairHBottom = document.getElementById('crosshair-h-bottom'); const crosshairVRight = document.getElementById('crosshair-v-right'); const previewCrosshairH = document.getElementById('preview-crosshair-h'); const previewCrosshairV = document.getElementById('preview-crosshair-v'); const iframeContainer = document.getElementById('preview-iframe-container'); if (!overlay || !rulers) return; // Set up resize observer to update frozen position when container resizes if (iframeContainer && window.ResizeObserver) { this.resizeObserver = new ResizeObserver(() => { if (this.frozen) { this.updateFrozenPosition(); } }); this.resizeObserver.observe(iframeContainer); } // Generate initial tick marks this.generateTicks(); // Mouse tracking on overlay overlay.addEventListener('mousemove', (e) => { if (!this.visible) return; const rect = overlay.getBoundingClientRect(); const relX = e.clientX - rect.left; const relY = e.clientY - rect.top; // Calculate canvas coordinates (scale from overlay size to canvas size) const scaleX = this.canvasWidth / rect.width; const scaleY = this.canvasHeight / rect.height; const canvasX = Math.floor(relX * scaleX); const canvasY = Math.floor(relY * scaleY); // Update coordinate display if (coordDisplay) { coordDisplay.textContent = `x: ${canvasX}, y: ${canvasY}`; coordDisplay.classList.add('visible'); } // Update crosshair positions on all rulers (in ruler pixel space) const rulerH = document.getElementById('ruler-h'); const rulerV = document.getElementById('ruler-v'); const rulerHBottom = document.getElementById('ruler-h-bottom'); const rulerVRight = document.getElementById('ruler-v-right'); if (rulerH) { const rulerScale = rulerH.offsetWidth / this.canvasWidth; if (crosshairH) crosshairH.style.left = `${canvasX * rulerScale}px`; if (crosshairHBottom) crosshairHBottom.style.left = `${canvasX * rulerScale}px`; } if (rulerV) { const rulerScale = rulerV.offsetHeight / this.canvasHeight; if (crosshairV) crosshairV.style.top = `${canvasY * rulerScale}px`; if (crosshairVRight) crosshairVRight.style.top = `${canvasY * rulerScale}px`; } // Update full preview crosshairs (military reticle) if (previewCrosshairH) { // Position horizontal line at mouse Y (relative to preview area) previewCrosshairH.style.top = `${16 + relY}px`; // 16px = ruler height offset } if (previewCrosshairV) { // Position vertical line at mouse X (relative to preview area) previewCrosshairV.style.left = `${20 + relX}px`; // 20px = ruler width offset } }); overlay.addEventListener('mouseleave', () => { if (coordDisplay) coordDisplay.classList.remove('visible'); // Cancel any pending zoom if (this.zoomTimeout) { clearTimeout(this.zoomTimeout); this.zoomTimeout = null; } this.endZoom(); }); // Pixel zoom on click+hold const getZoomPosition = (e) => { const rect = overlay.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const relX = clientX - rect.left; const relY = clientY - rect.top; // Convert to percentage for transform-origin const percentX = (relX / rect.width) * 100; const percentY = (relY / rect.height) * 100; return { percentX, percentY, clientX, clientY }; }; const startZoomHold = (e) => { if (!this.visible) return; const pos = getZoomPosition(e); this.zoomPosition = pos; // Zoom immediately on tap (no delay) this.startZoom(pos.percentX, pos.percentY); }; const moveWhileHolding = (e) => { if (!this.visible || !this.isZooming) return; const pos = getZoomPosition(e); this.updateZoomPosition(pos.percentX, pos.percentY); }; const endZoomHold = () => { if (this.zoomTimeout) { clearTimeout(this.zoomTimeout); this.zoomTimeout = null; } this.endZoom(); }; overlay.addEventListener('mousedown', startZoomHold); overlay.addEventListener('mousemove', (e) => { // Also update zoom position while dragging if (this.isZooming) moveWhileHolding(e); }); overlay.addEventListener('mouseup', endZoomHold); // Touch support overlay.addEventListener('touchstart', startZoomHold, { passive: true }); overlay.addEventListener('touchmove', moveWhileHolding, { passive: true }); overlay.addEventListener('touchend', endZoomHold); overlay.addEventListener('touchcancel', endZoomHold); // Update on window resize window.addEventListener('resize', () => { if (this.visible) this.generateTicks(); }); }, // Zoom methods zoomTimeout: null, zoomPosition: null, isZooming: false, startZoom(percentX, percentY) { const iframeContainer = document.getElementById('preview-iframe-container'); const iframe = document.getElementById('preview-iframe'); if (!iframeContainer || !iframe) return; // Set transform origin to cursor position iframe.style.setProperty('--zoom-x', `${percentX}%`); iframe.style.setProperty('--zoom-y', `${percentY}%`); iframeContainer.classList.add('pixel-zoom'); this.isZooming = true; }, updateZoomPosition(percentX, percentY) { const iframe = document.getElementById('preview-iframe'); if (!iframe || !this.isZooming) return; iframe.style.setProperty('--zoom-x', `${percentX}%`); iframe.style.setProperty('--zoom-y', `${percentY}%`); }, endZoom() { const iframeContainer = document.getElementById('preview-iframe-container'); if (iframeContainer) { iframeContainer.classList.remove('pixel-zoom'); } this.isZooming = false; }, generateTicks() { const rulerHTicks = document.getElementById('ruler-h-ticks'); const rulerVTicks = document.getElementById('ruler-v-ticks'); const rulerHBottomTicks = document.getElementById('ruler-h-bottom-ticks'); const rulerVRightTicks = document.getElementById('ruler-v-right-ticks'); const rulerH = document.getElementById('ruler-h'); const rulerV = document.getElementById('ruler-v'); if (!rulerH || !rulerV) return; // Clear existing ticks if (rulerHTicks) rulerHTicks.innerHTML = ''; if (rulerVTicks) rulerVTicks.innerHTML = ''; if (rulerHBottomTicks) rulerHBottomTicks.innerHTML = ''; if (rulerVRightTicks) rulerVRightTicks.innerHTML = ''; const hWidth = rulerH.offsetWidth; const vHeight = rulerV.offsetHeight; // Calculate scale (pixels per canvas unit) const scaleH = hWidth / this.canvasWidth; const scaleV = vHeight / this.canvasHeight; // Determine tick interval based on scale let majorInterval = 100; let minorInterval = 10; if (scaleH < 0.3) { majorInterval = 200; minorInterval = 50; } else if (scaleH < 0.8) { majorInterval = 100; minorInterval = 20; } else if (scaleH > 1.5) { majorInterval = 50; minorInterval = 10; } // Generate horizontal ticks (top ruler) if (rulerHTicks) { for (let i = 0; i <= this.canvasWidth; i += minorInterval) { const isMajor = i % majorInterval === 0; const tick = document.createElement('div'); tick.className = `ruler-tick ${isMajor ? 'major' : 'minor'}`; tick.style.left = `${i * scaleH}px`; rulerHTicks.appendChild(tick); if (isMajor && i > 0) { const label = document.createElement('div'); label.className = 'ruler-label'; label.textContent = i; label.style.left = `${i * scaleH + 2}px`; rulerHTicks.appendChild(label); } } } // Generate horizontal ticks (bottom ruler - mirrored) if (rulerHBottomTicks) { for (let i = 0; i <= this.canvasWidth; i += minorInterval) { const isMajor = i % majorInterval === 0; const tick = document.createElement('div'); tick.className = `ruler-tick ${isMajor ? 'major' : 'minor'}`; tick.style.left = `${i * scaleH}px`; rulerHBottomTicks.appendChild(tick); if (isMajor && i > 0) { const label = document.createElement('div'); label.className = 'ruler-label'; label.textContent = i; label.style.left = `${i * scaleH + 2}px`; rulerHBottomTicks.appendChild(label); } } } // Generate vertical ticks (left ruler) if (rulerVTicks) { for (let i = 0; i <= this.canvasHeight; i += minorInterval) { const isMajor = i % majorInterval === 0; const tick = document.createElement('div'); tick.className = `ruler-tick ${isMajor ? 'major' : 'minor'}`; tick.style.top = `${i * scaleV}px`; rulerVTicks.appendChild(tick); if (isMajor && i > 0) { const label = document.createElement('div'); label.className = 'ruler-label'; label.textContent = i; label.style.top = `${i * scaleV + 2}px`; rulerVTicks.appendChild(label); } } } // Generate vertical ticks (right ruler - mirrored) if (rulerVRightTicks) { for (let i = 0; i <= this.canvasHeight; i += minorInterval) { const isMajor = i % majorInterval === 0; const tick = document.createElement('div'); tick.className = `ruler-tick ${isMajor ? 'major' : 'minor'}`; tick.style.top = `${i * scaleV}px`; rulerVRightTicks.appendChild(tick); if (isMajor && i > 0) { const label = document.createElement('div'); label.className = 'ruler-label'; label.textContent = i; label.style.top = `${i * scaleV + 2}px`; rulerVRightTicks.appendChild(label); } } } }, show() { this.init(); const rulers = document.getElementById('preview-rulers'); const overlay = document.getElementById('preview-mouse-overlay'); const iframeContainer = document.getElementById('preview-iframe-container'); // Calculate AC logical canvas size from iframe using density const iframe = document.getElementById('preview-iframe'); if (iframe) { const iframeRect = iframe.getBoundingClientRect(); // AC uses density (default 2) - logical pixels = viewport / density // This gives us the actual AC coordinate system this.canvasWidth = Math.round(iframeRect.width / this.density); this.canvasHeight = Math.round(iframeRect.height / this.density); // Store the frozen pixel dimensions for CSS this.frozenWidth = iframeRect.width; this.frozenHeight = iframeRect.height; this.frozen = true; // Apply frozen CSS variables to maintain this size if (iframeContainer) { iframeContainer.style.setProperty('--frozen-width', `${this.frozenWidth}px`); iframeContainer.style.setProperty('--frozen-height', `${this.frozenHeight}px`); iframeContainer.classList.add('canvas-frozen'); // Initial position calculation this.updateFrozenPosition(); } } if (rulers) { rulers.classList.add('visible'); this.visible = true; this.generateTicks(); // Regenerate for current size } if (overlay) overlay.style.display = 'block'; if (iframeContainer) iframeContainer.classList.add('rulers-active'); }, // Update the centered position of frozen iframe when container resizes updateFrozenPosition() { if (!this.frozen || !this.frozenWidth || !this.frozenHeight) return; const iframeContainer = document.getElementById('preview-iframe-container'); if (!iframeContainer) return; const containerRect = iframeContainer.getBoundingClientRect(); // Available space within ruler inset (16px top/bottom, 20px left/right) const availWidth = containerRect.width - 40; const availHeight = containerRect.height - 32; // Clamp frozen size to available space const displayWidth = Math.min(this.frozenWidth, availWidth); const displayHeight = Math.min(this.frozenHeight, availHeight); // Calculate centered position within inset area const centeredTop = 16 + (availHeight - displayHeight) / 2; const centeredLeft = 20 + (availWidth - displayWidth) / 2; iframeContainer.style.setProperty('--frozen-top', `${Math.max(16, centeredTop)}px`); iframeContainer.style.setProperty('--frozen-left', `${Math.max(20, centeredLeft)}px`); }, hide() { const rulers = document.getElementById('preview-rulers'); const overlay = document.getElementById('preview-mouse-overlay'); const coordDisplay = document.getElementById('coord-display'); const iframeContainer = document.getElementById('preview-iframe-container'); if (rulers) { rulers.classList.remove('visible'); this.visible = false; } if (overlay) overlay.style.display = 'none'; if (coordDisplay) coordDisplay.classList.remove('visible'); if (iframeContainer) { iframeContainer.classList.remove('rulers-active'); iframeContainer.classList.remove('pixel-zoom'); // Unfreeze canvas iframeContainer.classList.remove('canvas-frozen'); iframeContainer.style.removeProperty('--frozen-width'); iframeContainer.style.removeProperty('--frozen-height'); iframeContainer.style.removeProperty('--frozen-top'); iframeContainer.style.removeProperty('--frozen-left'); } // Unfreeze state this.frozen = false; this.frozenWidth = null; this.frozenHeight = null; // Clean up zoom state this.endZoom(); if (this.zoomTimeout) { clearTimeout(this.zoomTimeout); this.zoomTimeout = null; } }, // Update canvas dimensions (call when we know the actual AC canvas size) setCanvasSize(width, height) { // Don't update if frozen (preserves paused state during resize) if (this.frozen) return; this.canvasWidth = width; this.canvasHeight = height; if (this.visible) this.generateTicks(); }, // Update density (call if AC sends density info) setDensity(density) { this.density = density || 2; // Don't recalculate canvas size if frozen if (this.frozen) return; // Recalculate canvas size const iframe = document.getElementById('preview-iframe'); if (iframe) { const iframeRect = iframe.getBoundingClientRect(); this.canvasWidth = Math.round(iframeRect.width / this.density); this.canvasHeight = Math.round(iframeRect.height / this.density); } if (this.visible) this.generateTicks(); } };
// Helper to set live state on editor panel (for visibility across all viz views) function setLiveState(state) { const editorPanel = document.getElementById('editor-panel'); const kidlispEditor = document.getElementById('kidlisp-editor'); const previewTitle = document.getElementById('preview-title'); const previewContainer = document.querySelector('.preview-iframe-container'); // Remove all states first editorPanel?.classList.remove('live-running', 'live-paused', 'live-stopped'); kidlispEditor?.classList.remove('live-running', 'live-paused', 'live-stopped'); previewTitle?.classList.remove('state-running', 'state-paused', 'state-stopped'); previewContainer?.classList.remove('stopped'); // Trigger bump animation on state change if (state && previewTitle) { previewTitle.classList.add('state-changed'); setTimeout(() => previewTitle.classList.remove('state-changed'), 250); } // Add new state if specified if (state === 'running') { editorPanel?.classList.add('live-running'); kidlispEditor?.classList.add('live-running'); previewTitle?.classList.add('state-running'); } else if (state === 'paused') { editorPanel?.classList.add('live-paused'); kidlispEditor?.classList.add('live-paused'); previewTitle?.classList.add('state-paused'); // Show pixel rulers when paused rulerSystem.show(); } else if (state === 'stopped') { editorPanel?.classList.add('live-stopped'); kidlispEditor?.classList.add('live-stopped'); previewTitle?.classList.add('state-stopped'); previewContainer?.classList.add('stopped'); // Hide rulers when stopped rulerSystem.hide(); } // 'idle' or null removes all classes (already done above) // Hide rulers when running, idle, or no state (covers platform switches when paused) if (state === 'running' || state === 'idle' || !state) { rulerSystem.hide(); } // Sync mini playback buttons when state changes if (window.syncMiniPlaybackButtons) window.syncMiniPlaybackButtons(); } // Track current platform globally (always starts as aesthetic-computer, not persisted) let currentPlatform = 'aesthetic-computer'; // Global helper to generate platform title HTML with short/full variants for responsive collapse function getACTitleHTML() { // Wrap each letter in spans for rainbow animation during boot const wrapLetters = (word) => word.split('').map(c => `<span class="title-letter">${c}</span>`).join(''); return `<span class="title-full">${wrapLetters('Aesthetic')}</span><span class="title-short">prompt</span><span class="bounce-dot">.</span><span class="title-full">${wrapLetters('Computer')}</span><span class="title-short">AC</span>`; }
function setRouteCodeInUI(code, options = {}) { const { immediate = false, animate = true } = options; const cleanCode = code ? (code.startsWith('$') ? code.slice(1) : code) : ''; document.body?.classList.toggle('has-route-code', Boolean(cleanCode)); if (currentPlatform !== 'aesthetic-computer') return;
const previewTitle = document.getElementById('preview-title'); if (!previewTitle) return;
// Save #route-code-tab before innerHTML replaces children const routeCodeTab = document.getElementById('route-code-tab');
if (!cleanCode) { previewTitle.innerHTML = getACTitleHTML(); if (routeCodeTab) { routeCodeTab.classList.remove('visible'); routeCodeTab.innerHTML = ''; previewTitle.appendChild(routeCodeTab); } return; }
const codeChars = `/$${cleanCode}` .split('') .map((c, i) => `<span class="code-char${i === 0 ? ' slash' : i === 1 ? ' dollar' : ' code'}">${c}</span>`) .join(''); previewTitle.innerHTML = getACTitleHTML(); if (routeCodeTab) { routeCodeTab.classList.add('visible'); routeCodeTab.innerHTML = `<span class="code-display">${codeChars}</span>`; previewTitle.appendChild(routeCodeTab); }
if (!immediate) { const codeDisplay = routeCodeTab?.querySelector('.code-display'); if (codeDisplay && animate && typeof scrambleToCode === 'function') { scrambleToCode(cleanCode, codeDisplay); } } }
window.setRouteCodeInUI = setRouteCodeInUI;
window.getInitialRouteCode = function() { const urlPath = window.location.pathname; const match = urlPath.match(/\/\$?([a-zA-Z0-9]+)$/); return match?.[1] || ''; };
// Helper to generate platform title HTML (with logo for FF1, short/full for AC) function getPlatformTitleHTML(platformId, platformName) { if (platformId === 'aesthetic-computer') { return getACTitleHTML(); } if (platformId === 'ff1') { return `<img src="https://assets.aesthetic.computer/kidlisp.com/feralfile-logo.png" alt="" class="platform-logo" style="height: 16px; margin-right: 6px; vertical-align: middle;"> ${platformName}`; } if (platformId === 'ableton') { return `<svg class="platform-icon ableton-logo" viewBox="0 0 51 24" xmlns="http://www.w3.org/2000/svg" style="width: 20px; height: 14px; margin-right: 6px; vertical-align: middle;"><path fill="currentColor" d="M0 0h3v24H0V0zm6 0h3v24H6V0zm6 0h3v24h-3V0zm6 0h3v24h-3V0zm9 0h24v3H27V0zm0 7h24v3H27V7zm0 7h24v3H27v-3zm0 7h24v3H27v-3z"/></svg> ${platformName}`; } return platformName.replace('.', '<span class="bounce-dot">.</span>'); }
const urlParams = new URLSearchParams(window.location.search); const isVSCode = urlParams.get('vscode') === 'true'; const isDeviceMode = window.location.pathname.includes('/device'); // Device mode: immediately add class to body for canvas-only view if (isDeviceMode) { document.body.classList.add('device-mode'); // Make sure body is visible in device mode document.body.style.opacity = '1'; console.log('🎹 Device mode active - canvas-only view'); // Ensure preview container doesn't have stopped class const previewContainer = document.querySelector('.preview-iframe-container'); if (previewContainer) { previewContainer.classList.remove('stopped'); } } let externalSessionHydrated = false; let pendingHistoryTabSwitch = false; // Flag to switch to History when DOM ready // Forward declare auth and keeps state (initialized later) let acAuth0Client = null; let acToken = null; let acUser = null; let acHandle = null; // The user's AC handle (@username)
function applyExternalSession(session, source = 'external') { if (!session) { acToken = null; acUser = null; acHandle = null; externalSessionHydrated = false; updateACLoginUI?.(); return; }
try { acToken = session.accessToken || session.access_token || null; const email = session.account?.label || session.account?.id || session.user?.email || session.email; const sub = session.account?.id || session.user?.sub || session.id || 'ac-external'; acUser = email ? { email, sub } : session.user || { sub: 'ac-external' }; acHandle = session.account?.label?.includes('@') ? session.account.label.split('@')[0] : session.account?.label || null; externalSessionHydrated = true; currentPlatform = 'aesthetic-computer'; updateACLoginUI?.(); // Try to switch to History tab; if DOM not ready yet, set pending flag if (typeof switchToHistoryTab === 'function') { switchToHistoryTab(); } else { pendingHistoryTabSwitch = true; } if (log?.auth) log.auth.log(`Hydrated session (${source})`, acHandle || email || 'user'); } catch (e) { console.warn('Failed to apply external session', e); } }
const encodedSession = urlParams.get('session-aesthetic'); if (encodedSession && encodedSession !== 'null') { try { const decoded = JSON.parse(atob(encodedSession)); applyExternalSession(decoded, 'query-param'); } catch (e) { console.warn('Failed to parse VS Code session', e); } } // Show the boot screen for a specific platform function showBootScreen(platform) { // Hide all boot screens first document.querySelectorAll('.boot-screen').forEach(screen => { screen.classList.add('hidden'); }); // Map platform to boot screen ID const screenId = platform === 'aesthetic-computer' ? 'boot-screen' : `boot-screen-${platform}`; const bootScreen = document.getElementById(screenId); if (bootScreen) { bootScreen.classList.remove('hidden'); } else { // Fallback to main boot screen document.getElementById('boot-screen').classList.remove('hidden'); } } // Hide all boot screens function hideAllBootScreens() { document.querySelectorAll('.boot-screen').forEach(screen => { screen.classList.add('hidden'); }); } // Initialize: show the correct boot screen for the saved platform showBootScreen(currentPlatform);
// Platform selector dropdown (function initPlatformSelector() { const selector = document.getElementById('platform-selector'); const previewTitle = document.getElementById('preview-title'); const dropdown = document.querySelector('.platform-dropdown'); const options = document.querySelectorAll('.platform-option');
if (!selector || !dropdown) return;
// Toggle dropdown on click selector.addEventListener('click', (e) => { e.stopPropagation(); // Position dropdown BEFORE opening to prevent flash if (!selector.classList.contains('open') && dropdown) { const rect = selector.getBoundingClientRect(); dropdown.style.top = (rect.bottom + 4) + 'px'; dropdown.style.left = rect.left + 'px'; } selector.classList.toggle('open'); });
// Handle platform selection options.forEach(option => { option.addEventListener('click', (e) => { e.stopPropagation(); const platform = option.dataset.platform; const isComingSoon = option.classList.contains('coming-soon'); const isExperimental = option.classList.contains('experimental'); const name = option.querySelector('.platform-opt-name')?.textContent || 'Unknown';
if (isComingSoon) { showToast(`${name} coming soon`, 'info'); return; } if (isExperimental) { showToast(`${name} is experimental`, 'warning'); }
// Update title (save/restore route-code-tab) if (previewTitle) { const savedTab = document.getElementById('route-code-tab'); previewTitle.innerHTML = getPlatformTitleHTML(platform, name); previewTitle.dataset.platform = platform; if (savedTab) previewTitle.appendChild(savedTab); }
// Update panel data-platform for header color styling const previewPanel = document.getElementById('preview-panel'); if (previewPanel) previewPanel.dataset.platform = platform;
// Update active state options.forEach(o => o.classList.remove('active')); option.classList.add('active');
// Update global platform and stop playback currentPlatform = platform; stopPlayback();
// Close dropdown selector.classList.remove('open');
// Emit event for platform change window.dispatchEvent(new CustomEvent('kidlisp-platform-change', { detail: { platform, name, isExperimental } }));
// Log platform switch const previousPlatform = localStorage.getItem('kidlisp-previous-platform') || 'aesthetic-computer'; logPlatformSwitch(previousPlatform, platform, { comingSoon: isComingSoon, experimental: isExperimental }); localStorage.setItem('kidlisp-previous-platform', platform); }); }); })();
// Header user menu - click to toggle dropdown const headerUserMenu = document.getElementById('header-user-menu'); if (headerUserMenu) { const userDropdown = document.getElementById('header-user-dropdown'); headerUserMenu.addEventListener('click', (e) => { e.stopPropagation(); headerUserMenu.classList.toggle('open'); if (headerUserMenu.classList.contains('open') && userDropdown) { const rect = headerUserMenu.getBoundingClientRect(); userDropdown.style.top = (rect.bottom + 4) + 'px'; userDropdown.style.right = (window.innerWidth - rect.right) + 'px'; } }); }
// Close dropdowns when clicking elsewhere document.addEventListener('click', () => { if (headerUserMenu) headerUserMenu.classList.remove('open'); const platformSelector = document.getElementById('platform-selector'); if (platformSelector) platformSelector.classList.remove('open'); });
// Listen for sessions coming from the VS Code extension window.addEventListener('message', (event) => { if (event.data?.type === 'setSession' && event.data?.tenant === 'aesthetic') { applyExternalSession(event.data.session, 'postMessage'); } });
// Keep book content in sync with platform changes window.addEventListener('kidlisp-platform-change', (event) => { const platform = event?.detail?.platform || currentPlatform; renderBook(platform); }); // Reusable stop playback function (full stop - kills the $code tab) function stopPlayback() { log.playback.debug('Stopping playback (full stop)'); // Send stop message to the iframe if (previewIframe.contentWindow) { previewIframe.contentWindow.postMessage({ type: 'kidlisp-stop' }, aestheticUrl); } // Reset state - including auto-play flag to prevent re-triggering isPlaying = false; isPaused = false; isSoftStopped = false; // Clear soft-stopped state on full stop fpsTracker.reset(); shouldAutoPlay = false; // IMPORTANT: prevent auto-play from re-triggering lastCode = ''; sendButton.classList.remove('playing'); sendButton.classList.remove('paused'); // Remove yellow outline on stop stopButton.style.display = 'none'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); if (window.updateSlideButtonVisibility) window.updateSlideButtonVisibility(); setLiveState('idle'); // No outline for full stop // Hide QR code when not playing const previewQR = document.getElementById('preview-qr'); if (previewQR) previewQR.style.display = 'none'; // Show the current platform's boot screen showBootScreen(currentPlatform); // Clear title document.title = 'KidLisp.com'; localStorage.removeItem('kidlisp-current-code'); } // Disable controls initially until iframe signals ready sendButton.disabled = true; stopButton.disabled = true; sendButton.style.opacity = '0.4'; stopButton.style.opacity = '0.4'; sendButton.style.cursor = 'not-allowed'; stopButton.style.cursor = 'not-allowed'; log.iframe.debug('Controls disabled until iframe ready');
function reloadIframe() { log.iframe.warn('Reloading iframe to reinitialize piece...'); iframeReady = false; pieceReady = false; previewIframe.src = `${aestheticUrl}/kidlisp?nogap=true&nolabel=true&noauth=true&t=${Date.now()}`; }
function updatePreview(shouldCreateCode = false) { if (!editor) return; // CRITICAL: Don't send scrambled animation text to KidLisp! // If decryption is in progress, use the pending target text instead if (window.isDecrypting && window.isDecrypting()) { const pendingText = window.getPendingDecryptText && window.getPendingDecryptText(); if (pendingText) { // Use the actual target code, not the scrambled animation log.playback.debug('Using pending decrypt text instead of scrambled editor content'); } else { // No pending text - skip this update, wait for decryption to complete log.playback.debug('Decryption in progress, skipping updatePreview'); return; } } // Clear previous error decorations when running new code clearErrorDecorations(); // Get the actual code - use pending decrypt text if decrypting, otherwise editor value const pendingCode = window.getPendingDecryptText && window.getPendingDecryptText(); const code = (window.isDecrypting && window.isDecrypting() && pendingCode) ? pendingCode.trim() : editor.getValue().trim(); log.playback.verbose(`Sending code (${code.length} chars)`); if (code) { // Get the current code identifier if we have one const codeId = localStorage.getItem('kidlisp-current-code') || null; // Detect if code has changed from what we last sent const codeChanged = code !== lastSentCodeSource; // Create new $code when: // 1. Explicitly requested (shouldCreateCode=true) // 2. Code changed AND we're in a playing state (live editing) // 3. Code changed AND this is the first send (lastSentCodeSource is empty) const needsNewCode = shouldCreateCode || (codeChanged && (isPlaying || !lastSentCodeSource)); // Mark old code as stale when code changes if (codeChanged && codeId && isPlaying) { markCodeAsStale(); } // Update last sent code lastSentCodeSource = code; // Always enable trace - user may switch to tree/space view while running const needsTrace = true; // Clear any existing reload timeout if (pendingReloadTimeout) { clearTimeout(pendingReloadTimeout); pendingReloadTimeout = null; } // Send code via postMessage for instant reload (no network!) sendToIframe({ type: 'kidlisp-reload', code: code, codeId: codeId, // Pass the $code identifier so bios can populate cache createCode: needsNewCode, // Create new code when source changes authToken: acToken || null, // Pass auth token from kidlisp.com login enableTrace: needsTrace // Enable execution trace for visualization }); logCodeOperation('run', code);
// Set a timeout to detect if the piece didn't respond // Skip when popout is active — local iframe is intentionally disabled if (!localPreviewPausedForPopout) { pendingReloadTimeout = setTimeout(() => { if (reloadAttempts < MAX_RELOAD_ATTEMPTS) { reloadAttempts++; log.iframe.warn(`Piece not responding after 3s (attempt ${reloadAttempts}/${MAX_RELOAD_ATTEMPTS}), reloading iframe...`); showToast('Reinitializing preview...', 'info'); reloadIframe(); // After iframe reloads, try sending code again setTimeout(() => { if (isPlaying) { updatePreview(shouldCreateCode); } }, 2000); } else { log.iframe.error('Max reload attempts reached, piece may be stuck'); showToast('Preview not responding. Try refreshing the page.', 'error'); } }, 3000); } } else { log.playback.debug('No code to run'); } }
sendButton.addEventListener('click', () => { // Don't respond if disabled if (sendButton.disabled) { log.playback.verbose('Play button disabled'); return; } // Trigger dip animation for visual feedback sendButton.classList.remove('dipping'); // Force reflow to restart animation void sendButton.offsetWidth; sendButton.classList.add('dipping'); setTimeout(() => sendButton.classList.remove('dipping'), 150); // If editor is empty and not already playing, pop open the editor so user can write something const currentCode = editor.getValue().trim(); const editorPanel = document.getElementById('editor-panel'); const isEditorCollapsed = editorPanel && (editorPanel.dataset.collapsed === 'true' || editorPanel.classList.contains('panel-collapsed')); if (!currentCode && !isPlaying && isEditorCollapsed) { // Only pop open if editor is collapsed - this is the "empty outline button" case editorPanel.dataset.collapsed = 'false'; editorPanel.classList.remove('panel-collapsed'); // Set exactly 1-line height const HEADER_HEIGHT = 40; const LINE_HEIGHT = 19; const PADDING = 24; const singleLineHeight = HEADER_HEIGHT + LINE_HEIGHT + PADDING; editorPanel.style.height = `${singleLineHeight}px`; editorPanel.style.flex = `0 0 ${singleLineHeight}px`; editorPanel.dataset.savedHeight = singleLineHeight; // Update mini controls visibility (will hide since editor now has 1 line) if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); // Update play button state (will disable main, update mini empty state) if (window.updatePlayButtonState) window.updatePlayButtonState(); // Focus the editor so user can start typing setTimeout(() => { editor.focus(); }, 50); log.playback.debug('Editor empty - opening editor for user input'); return; // Don't try to play empty code } // Only AC platform supports live playback for now if (currentPlatform !== 'aesthetic-computer') { // Ableton platform sends code to DAW devices via session-server if (currentPlatform === 'ableton') { const code = editor.getValue().trim(); if (code && sessionWs && sessionWs.readyState === WebSocket.OPEN) { sessionWs.send(JSON.stringify({ type: 'daw:code', content: { code } })); showToast('Code sent to Ableton device', 'success'); log.playback.log('Sent code to DAW channel'); // Update UI to show "sent" state sendButton.classList.add('playing'); setTimeout(() => sendButton.classList.remove('playing'), 1000); } else if (!sessionWs || sessionWs.readyState !== WebSocket.OPEN) { showToast('Not connected to session server', 'error'); } else { showToast('No code to send', 'info'); } return; } // FF1 Art Computer - send URL to device if (currentPlatform === 'ff1') { sendToFF1Device(); return; } const platformNames = { 'playdate': 'playdate', 'gameboy': 'Game Boy', 'n64': 'N64' }; const name = platformNames[currentPlatform] || currentPlatform; showToast(`${name} preview coming soon!`, 'info'); return; } if (!isPlaying) { // Check if we're resuming from soft-stopped state if (isSoftStopped) { // Resume from soft-stop - just send resume, don't reload log.playback.log('Resuming from soft-stop'); addStateEntry('play'); sendToIframe({ type: 'kidlisp-resume' }); isPlaying = true; isSoftStopped = false; userStopped = false; // Clear user-stopped flag when playing vizSystem.isPlaying = true; sendButton.classList.add('playing'); stopButton.style.display = 'flex'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); setLiveState('running'); // Show QR code when playing on aesthetic-computer platform if (currentPlatform === 'aesthetic-computer') { const previewQR = document.getElementById('preview-qr'); if (previewQR && previewQR.src && previewQR.src !== window.location.href) { previewQR.style.display = 'block'; } } return; // Early return - we're done } // Start playing - run the current code // Only create a NEW code if we don't already have one stored const existingCode = localStorage.getItem('kidlisp-current-code'); const shouldCreateCode = !existingCode; // Only create if no existing code // Get code safely - use pending decrypt text if animation is running const pendingCode = window.getPendingDecryptText && window.getPendingDecryptText(); const codeToRun = (window.isDecrypting && window.isDecrypting() && pendingCode) ? pendingCode.trim() : editor.getValue().trim(); // Only log if there's code to run if (codeToRun) { log.playback.log(existingCode ? `Playing $${existingCode}` : 'Starting new playback'); // Add to console addStateEntry('play', existingCode); addCodeEntry(codeToRun); } // Hide boot screen when starting playback hideAllBootScreens(); updatePreview(shouldCreateCode); isPlaying = true; isPaused = false; isSoftStopped = false; // Clear soft-stopped state userStopped = false; // Clear user-stopped flag when playing vizSystem.isPlaying = true; vizSystem.isPaused = false; sendButton.classList.add('playing'); sendButton.classList.remove('paused'); // Remove yellow outline when playing stopButton.style.display = 'flex'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); if (window.updateSlideButtonVisibility) window.updateSlideButtonVisibility(); // Add live-running class to show green outline, remove paused state setLiveState('running'); // Show QR code when playing on aesthetic-computer platform if (currentPlatform === 'aesthetic-computer') { const previewQR = document.getElementById('preview-qr'); if (previewQR && previewQR.src && previewQR.src !== window.location.href) { previewQR.style.display = 'block'; } } } else if (isPaused) { // Resume log.playback.debug('Resuming'); addStateEntry('play'); // Resume shows as play sendToIframe({ type: 'kidlisp-resume' }); broadcastPauseToChannel(false); // Resume remote PJ windows isPaused = false; vizSystem.isPaused = false; sendButton.classList.add('playing'); // Ensure playing style (Pause icon) sendButton.classList.remove('paused'); // Remove yellow outline // Switch from paused (yellow) to running (green) outline setLiveState('running'); if (window.updateSlideButtonVisibility) window.updateSlideButtonVisibility(); } else { // Pause - but NOT if slide mode is active if (window.slideMode && window.slideMode.isActive()) { log.playback.debug('Cannot pause while in slide mode'); return; } log.playback.debug('Pausing'); addStateEntry('pause'); // 🎯 Skip sending pause if we already froze the frame on mousedown // This ensures we use the exact frame from when user started pressing if (!prePauseFrozeFrame) { sendToIframe({ type: 'kidlisp-pause' }); } broadcastPauseToChannel(true); // Pause remote PJ windows isPaused = true; vizSystem.isPaused = true; sendButton.classList.remove('playing'); // Revert to Play icon (Green) sendButton.classList.add('paused'); // Add yellow outline to play button // Switch from running (green) to paused (yellow) outline setLiveState('paused'); if (window.updateSlideButtonVisibility) window.updateSlideButtonVisibility(); // Keep stop button visible stopButton.style.display = 'flex'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); } }); // Pre-play/pause animation: preview the target state while holding button, cancels if drag off let prePlayActive = false; let prePlayRulersWereVisible = false; // Track if rulers were showing before press let prePauseActive = false; // Track if we're previewing pause (shrink for rulers) let prePauseFrozeFrame = false; // Track if we froze the frame on mousedown for pause preview const startPrePlay = (e) => { if (sendButton.disabled) return; const iframeContainer = document.getElementById('preview-iframe-container'); if (isPlaying && !isPaused) { // Currently playing - preview PAUSE state (shrink to show ruler gap) prePauseActive = true; if (iframeContainer) iframeContainer.classList.add('rulers-active'); // 🎯 IMMEDIATE FRAME FREEZE: Send pause NOW to capture the exact frame // This ensures embedded layers ($cow, etc.) are captured at this moment // If user cancels (drags off), we'll resume immediately sendToIframe({ type: 'kidlisp-pause' }); prePauseFrozeFrame = true; log.playback.debug('Pre-pause: frame frozen on mousedown'); } else if (isPaused || !isPlaying) { // Currently paused or stopped - preview PLAY state (hide rulers) prePlayActive = true; if (rulerSystem.visible) { prePlayRulersWereVisible = true; rulerSystem.hide(); } } }; const endPrePlay = () => { // Just clean up state - the click handler will do the actual action prePlayActive = false; prePlayRulersWereVisible = false; prePauseActive = false; prePauseFrozeFrame = false; // Will be handled by click handler }; const cancelPrePlay = () => { const iframeContainer = document.getElementById('preview-iframe-container'); if (prePlayActive) { // Was previewing play - restore rulers if they were visible if (prePlayRulersWereVisible && isPaused) { rulerSystem.show(); } } if (prePauseActive) { // Was previewing pause - remove the ruler gap preview if (iframeContainer) iframeContainer.classList.remove('rulers-active'); // 🎯 CANCEL FRAME FREEZE: Resume if we froze on mousedown but user cancelled if (prePauseFrozeFrame) { sendToIframe({ type: 'kidlisp-resume' }); log.playback.debug('Pre-pause cancelled: frame unfrozen'); } } prePlayActive = false; prePlayRulersWereVisible = false; prePauseActive = false; prePauseFrozeFrame = false; }; // Track if pointer leaves button while held sendButton.addEventListener('mousedown', startPrePlay); sendButton.addEventListener('touchstart', startPrePlay, { passive: true }); sendButton.addEventListener('mouseup', endPrePlay); sendButton.addEventListener('touchend', endPrePlay); // Cancel if drag off the button sendButton.addEventListener('mouseleave', cancelPrePlay); sendButton.addEventListener('touchcancel', cancelPrePlay);
// Stop button - sends stop message to iframe (full reset, not just pause) if (stopButton) { log.ui.verbose('Stop button initialized'); stopButton.addEventListener('click', (e) => { // Don't respond if disabled if (stopButton.disabled) { log.playback.verbose('Stop button disabled'); return; } // Don't allow stopping while in slide mode if (window.slideMode && window.slideMode.isActive()) { log.playback.debug('Cannot stop while in slide mode'); return; } log.playback.log('Stopping (full reset)'); addStateEntry('stop'); // Prevent any bubbling e.stopPropagation(); e.preventDefault(); // Send full stop message - this clears all kidlisp state/buffers // When play is pressed again, it will start fresh sendToIframe({ type: 'kidlisp-stop' }); // Also stop any Jukebox or Strudel audio that may be playing if (typeof stopJukebox === 'function') stopJukebox(); if (typeof stopStrudel === 'function') stopStrudel(); // Update state - fully stopped (will restart fresh on play) isPlaying = false; isPaused = false; isSoftStopped = false; // Not soft-stopped - will start fresh userStopped = true; // User explicitly stopped - prevent auto-play on next ready vizSystem.isPlaying = false; vizSystem.isPaused = false; vizSystem.frameCount = 0; // Reset frame count for fresh start lastCode = ''; // Clear last code so play starts fresh sendButton.classList.remove('playing'); sendButton.classList.remove('paused'); // Remove yellow outline on stop stopButton.style.display = 'none'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); if (window.updateSlideButtonVisibility) window.updateSlideButtonVisibility(); // Show purple outline to indicate stopped state setLiveState('stopped'); // Re-enable play button if there's content updatePlayButtonState(); }); } else { log.ui.error('Stop button not found in DOM'); }
// Mini playback controls (shown when main controls are obscured in mobile view) const miniSendButton = document.getElementById('mini-send-button'); const miniStopButton = document.getElementById('mini-stop-button'); if (miniSendButton) { miniSendButton.addEventListener('click', (e) => { e.stopPropagation(); // Don't respond if mini button itself is disabled if (miniSendButton.disabled) return; // Trigger dip animation miniSendButton.classList.remove('dipping'); void miniSendButton.offsetWidth; // Force reflow miniSendButton.classList.add('dipping'); setTimeout(() => miniSendButton.classList.remove('dipping'), 150); // Check if this is the "empty collapsed" case - need to handle directly const editorPanel = document.getElementById('editor-panel'); const isEditorCollapsed = editorPanel && (editorPanel.dataset.collapsed === 'true' || editorPanel.classList.contains('panel-collapsed')); const currentCode = editor.getValue().trim(); if (!currentCode && !isPlaying && isEditorCollapsed) { // Open editor to 1 line and focus editorPanel.dataset.collapsed = 'false'; editorPanel.classList.remove('panel-collapsed'); const HEADER_HEIGHT = 40; const LINE_HEIGHT = 19; const PADDING = 24; const singleLineHeight = HEADER_HEIGHT + LINE_HEIGHT + PADDING; editorPanel.style.height = `${singleLineHeight}px`; editorPanel.style.flex = `0 0 ${singleLineHeight}px`; editorPanel.dataset.savedHeight = singleLineHeight; // Update controls and button states if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); if (window.updatePlayButtonState) window.updatePlayButtonState(); setTimeout(() => editor.focus(), 50); log.playback.debug('Mini button: Editor empty - opening editor for user input'); return; } // Otherwise trigger the main send button click if (sendButton && !sendButton.disabled) { sendButton.click(); } }); } if (miniStopButton) { miniStopButton.addEventListener('click', (e) => { e.stopPropagation(); // Trigger the main stop button click if (stopButton) { stopButton.click(); } }); } // Sync mini button state with main buttons window.syncMiniPlaybackButtons = function() { if (miniSendButton) { // Sync playing state if (isPlaying && !isPaused) { miniSendButton.classList.add('playing'); miniSendButton.classList.remove('paused'); } else if (isPaused) { miniSendButton.classList.remove('playing'); miniSendButton.classList.add('paused'); } else { miniSendButton.classList.remove('playing'); miniSendButton.classList.remove('paused'); } } if (miniStopButton) { // Show mini stop button when main stop button is visible AND we're playing const shouldShowStop = isPlaying && stopButton && stopButton.style.display !== 'none'; miniStopButton.style.display = shouldShowStop ? 'flex' : 'none'; } // Note: Mini button disabled state is managed by updatePlayButtonState() // which allows mini button to be enabled when collapsed+empty (to open editor) };
// Enable KidLisp-only console messages from the preview iframe let hasRequestedKidlispConsole = false; let iframeLoadCount = 0; previewIframe.addEventListener('load', () => { iframeLoadCount++; log.iframe.debug('Iframe loaded, checking readiness'); // Tell the iframe to suppress noisy logs (we show them in KidLisp.com console only) try { if (previewIframe.contentWindow) { previewIframe.contentWindow.KIDLISP_SUPPRESS_SNAPSHOT_LOGS = true; previewIframe.contentWindow.KIDLISP_SUPPRESS_FONT_LOGS = true; } } catch (e) { // Ignore if cross-origin or not ready } // Send ping to check if aesthetic.computer is ready sendToIframe({ type: 'kidlisp-ping' }); // Send current visual theme to iframe const initialTheme = document.documentElement.getAttribute('data-theme') || 'dark'; sendToIframe({ type: 'kidlisp-theme', theme: initialTheme });
// Ask the iframe to enable the KidLisp-only console channel if (!hasRequestedKidlispConsole) { hasRequestedKidlispConsole = true; sendToIframe({ type: 'kidlisp-console-enable' }); }
// Send stored GPU rendering config to iframe try { const gpuStored = localStorage.getItem('kidlisp-gpu-config'); if (gpuStored) { sendToIframe({ type: 'kidlisp-gpu-config', config: JSON.parse(gpuStored) }); } } catch {} });
// Update initial console message updateUILanguage(); // 🧪 Test harness message handler function handleTestMessage(data, source) { const respond = (response) => { if (source && source.postMessage) { source.postMessage({ type: 'test:response', originalType: data.type, ...response }, '*'); } }; switch (data.type) { case 'test:setCode': if (editor && data.code !== undefined) { editor.setValue(data.code); respond({ success: true, length: data.code.length }); } else { respond({ success: false, error: 'Editor not ready or no code provided' }); } break; case 'test:getCode': if (editor) { respond({ success: true, code: editor.getValue() }); } else { respond({ success: false, error: 'Editor not ready' }); } break; case 'test:play': if (sendButton && !sendButton.disabled) { sendButton.click(); log.message.debug('Test: Play clicked'); respond({ success: true }); } else { respond({ success: false, error: 'Play button not ready' }); } break; case 'test:stop': if (stopButton) { stopButton.click(); log.message.debug('Test: Stop clicked'); respond({ success: true }); } else { respond({ success: false, error: 'Stop button not found' }); } break; case 'test:clear': if (editor) { editor.setValue(''); log.message.debug('Test: Editor cleared'); respond({ success: true }); } else { respond({ success: false, error: 'Editor not ready' }); } break; case 'test:getConsole': const consoleOutput = document.getElementById('console-output'); if (consoleOutput) { const entries = Array.from(consoleOutput.querySelectorAll('.console-entry')).map(el => ({ text: el.textContent, level: el.classList.contains('error') ? 'error' : el.classList.contains('warn') ? 'warn' : 'log' })); respond({ success: true, entries }); } else { respond({ success: false, error: 'Console not found' }); } break; case 'test:clearConsole': const consoleEl = document.getElementById('console-output'); if (consoleEl) { consoleEl.innerHTML = ''; log.message.debug('Test: Console cleared'); respond({ success: true }); } else { respond({ success: false, error: 'Console not found' }); } break; case 'test:togglePanel': const panelMap = { 'editor': 'editor-panel', 'preview': 'preview-panel', 'console': 'console-panel', 'reference': 'reference-panel' }; const panelId = panelMap[data.panel]; if (panelId) { const panel = document.getElementById(panelId); if (panel) { const header = panel.querySelector('.editor-header, .docs-header'); if (header) { header.click(); log.message.debug('Test: Panel toggled:', data.panel); respond({ success: true, panel: data.panel }); } else { respond({ success: false, error: 'Panel header not found' }); } } else { respond({ success: false, error: 'Panel not found' }); } } else { respond({ success: false, error: 'Unknown panel: ' + data.panel }); } break; case 'test:switchLanguage': // Use global setLanguage function if (typeof setLanguage === 'function') { setLanguage(data.lang); log.message.debug('Test: Language switched:', data.lang); respond({ success: true, lang: data.lang }); } else { respond({ success: false, error: 'Language function not found' }); } break; case 'test:toggleTheme': // Toggle between light and dark using global applyTheme if (typeof window.applyTheme === 'function') { const currentVisual = document.documentElement.getAttribute('data-theme'); window.manualThemeSet = true; window.applyTheme(currentVisual === 'dark' ? 'light' : 'dark'); log.message.debug('Test: Theme toggled'); respond({ success: true }); } else { respond({ success: false, error: 'Theme toggle not available' }); } break; case 'test:getUIState': respond({ success: true, state: { theme: document.documentElement.getAttribute('data-theme') || 'auto', lang: document.documentElement.lang || 'en', isPlaying: isPlaying, isPaused: isPaused, editorReady: !!editor, code: editor?.getValue() || '' } }); break; case 'test:getStatus': respond({ success: true, status: { editorReady: !!editor, playButtonEnabled: sendButton && !sendButton.disabled, isPlaying: isPlaying, isPaused: isPaused, pieceReady: pieceReady } }); break; default: log.message.warn('Unknown test message type:', data.type); respond({ success: false, error: 'Unknown test message type: ' + data.type }); } } // Listen for console messages and syntax highlighting signals from iframe window.addEventListener('message', (event) => { if (isSafari) perfCounters.messages++; // Ignore setCode messages - they're for other pieces, not kidlisp if (event.data?.type === 'setCode') { return; } // 🧪 Test harness message handlers if (event.data?.type?.startsWith('test:')) { log.message.debug('Test message:', event.data.type); handleTestMessage(event.data, event.source); return; }
// Some runtimes send messages via BIOS relay as { type: 'post-to-parent', content: {...} }. // Normalize those so the rest of this handler can treat them as direct events. if (event.data?.type === 'post-to-parent' && event.data?.content?.type) { console.log('📬 Normalizing post-to-parent:', event.data.content.type); event = { ...event, data: event.data.content }; } const messageType = event.data?.type; // Debug: log all message types if (messageType && messageType.startsWith('kidlisp-')) { console.log('📨 Message type:', messageType); }
if (messageType === 'setSession' && event.data?.tenant === 'aesthetic') { applyExternalSession(event.data.session, 'postMessage'); return; } // 📸 Snapshots are for the KidLisp.com console only—do not log to browser console if (messageType === 'kidlisp-console-image') { addConsoleImageEntry(event.data); return; } // Handle piece load requests from learn iframe if (messageType === 'kidlisp-load-piece' && event.data?.source) { if (editor) { if (typeof window.setSettingValueProgrammatically === 'function') window.setSettingValueProgrammatically(true); editor.setValue(event.data.source); if (typeof window.setSettingValueProgrammatically === 'function') window.setSettingValueProgrammatically(false); if (typeof window.setEditorUserModified === 'function') { window.setEditorUserModified(false); } if (event.data.code) { if (currentPlatform === 'aesthetic-computer') { updateURLForCode(event.data.code); localStorage.setItem('kidlisp-current-code', event.data.code); setRouteCodeInUI(event.data.code, { immediate: true, animate: false }); } } updatePreview(); // Expand editor panel if collapsed (mobile layout) const editorPanel = document.getElementById('editor-panel'); if (editorPanel?.classList.contains('panel-collapsed')) { const header = editorPanel.querySelector('.editor-header'); if (header) header.click(); } } return; }
log.message.verbose('Message:', messageType);
// Handle ready signal from iframe if (messageType === 'kidlisp-ready') { log.iframe.success('Iframe ready - enabling controls'); logBoot('iframe', 'Preview ready'); iframeReady = true; // Enable stop button unconditionally stopButton.disabled = false; stopButton.style.opacity = '1'; stopButton.style.cursor = 'pointer'; // Enable send button only if there's content updatePlayButtonState(); // Show welcome message in console (only once) showWelcomeMessage(); // Send current theme to iframe const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark'; sendToIframe({ type: 'kidlisp-theme', theme: currentTheme });
// Always (re-)send enable when iframe signals ready - the worker is now definitely wired. sendToIframe({ type: 'kidlisp-console-enable' });
// Re-send GPU config on iframe ready try { const gpuStored = localStorage.getItem('kidlisp-gpu-config'); if (gpuStored) sendToIframe({ type: 'kidlisp-gpu-config', config: JSON.parse(gpuStored) }); } catch {}
// Mark that we want to auto-play, but wait for piece to be ready // Only auto-play on aesthetic-computer platform // Don't auto-play if user explicitly stopped const initialCode = editor?.getValue().trim(); if (initialCode && !isPlaying && !userStopped && currentPlatform === 'aesthetic-computer') { shouldAutoPlay = true; // If piece is already ready, play immediately (but only once) if (pieceReady && hasInitialPreviewRun) { log.playback.log('Auto-playing (iframe ready)...'); updatePreview(); isPlaying = true; isPaused = false; sendButton.classList.add('playing'); stopButton.style.display = 'flex'; if (window.updateClearButtonPosition) window.updateClearButtonPosition(); setLiveState('running'); shouldAutoPlay = false; // Clear the flag so we don't auto-play again } } } else if (messageType === 'kidlisp-console-enabled') { // Intentionally do not write connection status to the Console tab. } else if (messageType === 'kidlisp-console') { addConsoleEntry(event.data.message, event.data.level, event.data.loc, { embeddedSource: event.data.embeddedSource, embeddedSourceCode: event.data.embeddedSourceCode }); // If it's an error with a location, highlight it in the editor if (event.data.level === 'error' && event.data.loc) { // Extract the word from the error message const wordMatch = event.data.message.match(/Unknown KidLisp word: (\w+)/); if (wordMatch) { addErrorDecoration(wordMatch[1], event.data.loc); } } } else if (messageType === 'kidlisp-highlight') { // Syntax highlighting signals from KidLisp execution if (editor && event.data.highlights) { applyHighlightDecorations(event.data.highlights); } } else if (messageType === 'kidlisp-first-frame') { // First frame fired! Signal the decryption animation to reveal the code log.playback.debug('First frame fired - revealing code'); if (window.signalDecryptionReady) { window.signalDecryptionReady(); } } else if (messageType === 'kidlisp-trace') { // Execution trace from KidLisp interpreter // Also signal decryption ready (fallback if first-frame wasn't received) if (window.signalDecryptionReady) { window.signalDecryptionReady(); } fpsTracker.tick(); if (event.data.trace && Array.isArray(event.data.trace)) { vizSystem.executionTrace = event.data.trace; vizSystem.frameCount++; // Update pills visualization if visible if (vizSystem.currentView === 'pills') { // Flash pills based on trace flashPillsFromTrace(event.data.trace); } } } else if (messageType === 'kidlisp-code-created') { // Code was created with $ identifier - update URL and preview title (AC platform only) log.code.success(`Code created: $${event.data.code}`); if (event.data.code && currentPlatform === 'aesthetic-computer') { // Clear stale state clearStaleCode(); // Update browser URL and title updateURLForCode(event.data.code); // Store the code identifier in localStorage localStorage.setItem('kidlisp-current-code', event.data.code); // Update preview pane title with scramble animation window.setRouteCodeInUI?.(event.data.code); } if (event.data.qr) { const previewQR = document.getElementById('preview-qr'); // Display QR code as data URI previewQR.src = event.data.qr; // Only show QR on aesthetic-computer platform if (currentPlatform === 'aesthetic-computer') { previewQR.style.display = 'block'; } }
// Update local/session history immediately addHistoryEntry({ code: event.data.code, source: window.__acCurrentKidlispCode || '' }); } else if (event.data && event.data.type === 'keep-mint-prepared') { // Mint preparation response from iframe log.wallet.debug('Mint prepared:', event.data.success); if (event.data.success && pendingMintResolve) { pendingMintResolve(event.data); } else if (!event.data.success && pendingMintReject) { pendingMintReject(event.data.error || 'Mint preparation failed'); } } else if (event.data && event.data.type === 'keep-mint-progress') { // Progress update from iframe during mint preparation const statusEl = document.getElementById('keeps-status'); if (statusEl && event.data.message) { statusEl.textContent = event.data.message; } } else if (event.data && event.data.type === 'keep-mint-error') { // Error from iframe during mint preparation log.wallet.error('Mint error:', event.data.error); if (pendingMintReject) { pendingMintReject(event.data.error || 'Mint preparation failed'); } } });
// Apply syntax highlighting decorations based on kidlisp execution state function applyHighlightDecorations(highlights) { if (!editor) return; const decorations = []; const model = editor.getModel(); if (!model) return;
// highlights is an array of { token, color, line, column } highlights.forEach(hl => { const lineContent = model.getLineContent(hl.line); const startColumn = lineContent.indexOf(hl.token, hl.column - 1) + 1; if (startColumn > 0) { const endColumn = startColumn + hl.token.length; // Map color to CSS let cssColor = hl.color; if (hl.color.includes(',')) { cssColor = `rgb(${hl.color})`; } decorations.push({ range: new monaco.Range(hl.line, startColumn, hl.line, endColumn), options: { inlineClassName: hl.color === 'red' || hl.color === 'lime' ? 'blink-highlight' : null, beforeContentClassName: null, afterContentClassName: null, color: cssColor, } }); } });
// Apply decorations highlightTraceDecorations = editor.deltaDecorations(highlightTraceDecorations, decorations); }
// Populate reference grids automatically const structureWords = ['(', 'def', 'repeat', 'later', 'if', 'once']; const drawingWords = ['wipe', 'ink', 'line', 'circle', 'box', 'point', 'write', 'paste', 'stamp', 'plot', 'tri', 'quad']; const effectsWords = ['scroll', 'flip', 'spin', 'blur', 'zoom', 'fade', 'wiggle', 'contrast', 'invert', 'noise', 'half', 'third', 'fourth']; const colorWords = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple', 'pink', 'white', 'black', 'gray', 'lime', 'navy', 'magenta', 'brown', 'rainbow', 'zebra']; const numberWords = ['0', '1', '10', '50', '100', '255', '0.5', '1s...', '2s', 'width', 'height', 'frame']; function populateGrid(gridId, words) { const grid = document.getElementById(gridId); if (!grid) return; // Grid is optional - silently skip if not present words.forEach(word => { const item = document.createElement('div'); item.className = 'function-item'; item.setAttribute('data-word', word); item.textContent = word === '(' ? '( )' : word; grid.appendChild(item); }); } // Populate grids BEFORE attaching event listeners populateGrid('structure-grid', structureWords); populateGrid('drawing-grid', drawingWords); populateGrid('effects-grid', effectsWords); populateGrid('colors-grid', colorWords); populateGrid('numbers-grid', numberWords);
// Click function words to insert at cursor document.querySelectorAll('.function-item').forEach(item => { item.addEventListener('click', () => { const word = item.getAttribute('data-word'); if (word && editor) { const position = editor.getPosition(); if (word === '(') { // Insert parentheses with cursor inside editor.executeEdits('', [{ range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), text: '()', }]); // Move cursor between the parentheses editor.setPosition({ lineNumber: position.lineNumber, column: position.column + 1 }); } else { editor.executeEdits('', [{ range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), text: word + ' ', }]); } editor.focus(); } }); // Apply KidLisp syntax highlighting color to each function word const word = item.getAttribute('data-word'); if (word) { try { // Tokenize and get color from KidLisp using the global instance const tokens = tokenize(word); kidlisp.initializeSyntaxHighlighting(word); let color = kidlisp.getTokenColor(tokens[0], tokens, 0); // Apply high-contrast colors for light mode if (kidlisp.lightModeHighContrast) { const lightModeColorMap = { 'yellow': '#cc9900', 'cyan': '#0099cc', 'lime': '#00aa00', 'pink': '#cc0066', 'orange': '#cc6600', 'magenta': '#cc00cc', 'gray': '#666666', }; if (lightModeColorMap[color]) { color = lightModeColorMap[color]; } } // Apply the color if (color && color !== 'RAINBOW' && color !== 'ZEBRA' && !color.startsWith('COMPOUND:')) { const cssColor = color.includes(',') ? `rgb(${color})` : color; item.style.color = cssColor; } } catch (e) { // If coloring fails, just leave default } } });
// Tab functionality const referenceContent = document.getElementById('reference-content'); const guideContent = document.getElementById('guide-content'); const listContent = document.getElementById('list-content'); const jukeboxContent = document.getElementById('jukebox-content'); const jockeyContent = document.getElementById('jockey-content'); const keepContent = document.getElementById('keep-content'); const mobileConsoleContent = document.getElementById('mobile-console-content'); const codeTab = document.getElementById('reference-tab'); const guideTab = document.getElementById('guide-tab'); const listTab = document.getElementById('list-tab'); const jukeboxTab = document.getElementById('jukebox-tab'); const jockeyTab = document.getElementById('jockey-tab'); const keepTab = document.getElementById('keep-tab'); const consoleTab = document.getElementById('console-tab'); let examplesLoaded = false; let recentLoaded = false; let currentListSubtab = 'examples'; // Track which subtab is active const codeListLastConfigs = new Map();
// Helper to hide all tab contents function hideAllTabContents() { referenceContent?.classList.add('hidden'); guideContent?.classList.add('hidden'); listContent?.classList.add('hidden'); jukeboxContent?.classList.add('hidden'); jockeyContent?.classList.add('hidden'); keepContent?.classList.add('hidden'); mobileConsoleContent?.classList.add('hidden'); }
// Helper to deactivate all tabs function deactivateAllTabs() { codeTab?.classList.remove('active'); guideTab?.classList.remove('active'); listTab?.classList.remove('active'); jukeboxTab?.classList.remove('active'); jockeyTab?.classList.remove('active'); keepTab?.classList.remove('active'); consoleTab?.classList.remove('active'); } // Helper to show List subtab content function showListSubtab(subtab) { currentListSubtab = subtab; // Update sub-tab active states document.querySelectorAll('.list-subtab').forEach(el => { el.classList.toggle('active', el.dataset.subtab === subtab); }); // Hide all subcontents document.querySelectorAll('.list-subcontent').forEach(el => el.classList.add('hidden')); // Show the corresponding subcontent and load data const subcontent = document.getElementById(`${subtab}-subcontent`); if (subcontent) subcontent.classList.remove('hidden'); if (subtab === 'examples') { if (!examplesLoaded) loadExamples(); } else if (subtab === 'recent') { if (!recentLoaded) loadRecent(); } else if (subtab === 'history') { loadHistory(); }
const listIdBySubtab = { examples: 'examples-list', recent: 'recent-list', history: 'history-list' }; const activeListId = listIdBySubtab[subtab]; Object.values(listIdBySubtab).forEach(listId => { if (listId === activeListId) return; const hiddenList = document.getElementById(listId); if (!hiddenList) return; hiddenList.innerHTML = ''; });
if (!activeListId) return; const activeListEl = document.getElementById(activeListId); if (activeListEl && activeListEl.children.length === 0) { const cachedConfig = codeListLastConfigs.get(activeListId); if (cachedConfig && Array.isArray(cachedConfig.items) && cachedConfig.items.length > 0) { renderCodeList(cachedConfig); return; } } } // Helper to programmatically switch to History tab function switchToHistoryTab() { if (!listTab || !listContent) return; deactivateAllTabs(); hideAllTabContents(); listTab.classList.add('active'); listContent.classList.remove('hidden'); showListSubtab('history'); } if (guideTab && listTab && guideContent && listContent) { // Helper to expand reference panel on mobile if collapsed (never collapse from tab tap) function expandReferencePanelOnMobile() { if (window.innerWidth <= 768) { const refPanel = document.getElementById('reference-panel'); if (!refPanel) return; // Only expand if collapsed - tapping tabs should never collapse if (refPanel.dataset.collapsed === 'true') { refPanel.dataset.collapsed = 'false'; refPanel.classList.remove('panel-collapsed'); // Use redistributeFlex if available (set up in setupMobileHeaders) if (window.redistributeFlex) { window.redistributeFlex(); } if (window.updateCheckerboard) window.updateCheckerboard(); if (window.updateMiniPlaybackControls) window.updateMiniPlaybackControls(); } } } codeTab?.addEventListener('click', function(e) { e.stopPropagation(); log.tabs.debug('Book tab'); deactivateAllTabs(); hideAllTabContents(); codeTab.classList.add('active'); referenceContent.classList.remove('hidden'); expandReferencePanelOnMobile(); // Refit cards when tab becomes visible (ResizeObserver may not fire immediately) setTimeout(() => { fitBookFrame(); fitBookTypography(); }, 50); }); guideTab.addEventListener('click', function(e) { if (typeof window.isStageMode === 'function' && window.isStageMode()) { e.preventDefault(); return; } e.stopPropagation(); e.preventDefault(); log.tabs.debug('Guide tab');
deactivateAllTabs(); hideAllTabContents(); guideTab.classList.add('active'); guideContent.classList.remove('hidden'); expandReferencePanelOnMobile(); // Lazy-load learn iframe on first click const learnIframe = document.getElementById('learn-iframe'); if (learnIframe && !learnIframe.hasAttribute('src')) { learnIframe.src = learnIframe.dataset.src; } }); // List tab - shows the list content with dropdown inside listTab.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); log.tabs.debug('List tab'); deactivateAllTabs(); hideAllTabContents(); listTab.classList.add('active'); listContent.classList.remove('hidden'); showListSubtab(currentListSubtab); expandReferencePanelOnMobile(); }); // Jukebox tab - shows audio player and Strudel (lazy-init on first click) jukeboxTab?.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); log.tabs.debug('Jukebox tab');
deactivateAllTabs(); hideAllTabContents(); jukeboxTab.classList.add('active'); jukeboxContent.classList.remove('hidden'); expandReferencePanelOnMobile();
// Lazy-init jukebox/strudel UI on first tab open if (typeof window._initJukeboxLazy === 'function') window._initJukeboxLazy();
// Load Strudel when Music tab is clicked (not just when playing) if (typeof loadStrudel === 'function') { loadStrudel().catch(err => console.warn('🌀 Strudel preload failed:', err)); } }); jockeyTab?.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); deactivateAllTabs(); hideAllTabContents(); jockeyTab.classList.add('active'); jockeyContent.classList.remove('hidden'); expandReferencePanelOnMobile(); updateJockeyPanel(); });
// Keep tab - embedded focused-keep panel for the current editor piece keepTab?.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); log.tabs.debug('Keep tab'); deactivateAllTabs(); hideAllTabContents(); keepTab.classList.add('active'); keepContent.classList.remove('hidden'); expandReferencePanelOnMobile(); // Lazy-load keeps iframe on first open; otherwise re-target at the // piece that's open now. const keepsIframe = document.getElementById('keeps-iframe'); if (keepsIframe && !keepsIframe.hasAttribute('src')) { console.log('📦 [KEEPS] Lazy-loading keeps iframe'); keepsIframe.addEventListener('load', postCurrentPieceToKeeps, { once: true }); keepsIframe.src = keepsIframe.dataset.src; } else { postCurrentPieceToKeeps(); } });
// List sub-tab click handlers document.querySelectorAll('.list-subtab').forEach(subtab => { subtab.addEventListener('click', function(e) { e.stopPropagation(); const subtabType = this.dataset.subtab; log.tabs.debug('List subtab clicked:', subtabType); showListSubtab(subtabType); }); }); // Console tab click handler (mobile only - console is merged into reference panel) if (consoleTab && mobileConsoleContent) { consoleTab.addEventListener('click', function(e) { if (typeof window.isStageMode === 'function' && window.isStageMode()) { e.preventDefault(); return; } log.tabs.debug('Console tab (mobile)'); e.stopPropagation(); e.preventDefault(); deactivateAllTabs(); hideAllTabContents(); consoleTab.classList.add('active'); mobileConsoleContent.classList.remove('hidden'); expandReferencePanelOnMobile(); }); } // === Auth0 Login/Logout === let _auth0SDKPromise = null; function loadAuth0SDK() { if (window.auth0) return Promise.resolve(); if (_auth0SDKPromise) return _auth0SDKPromise; _auth0SDKPromise = new Promise((resolve, reject) => { // Hide Monaco's AMD define() so the UMD bundle takes the global path. const savedDefine = window.define; try { window.define = undefined; } catch (e) {} const restore = () => { try { window.define = savedDefine; } catch (e) {} }; const s = document.createElement('script'); s.src = '/aesthetic.computer/dep/auth0-spa-js.production.js'; s.onload = () => { restore(); window.__auth0SDKReady = true; resolve(); }; s.onerror = (e) => { restore(); _auth0SDKPromise = null; reject(e); }; document.head.appendChild(s); }); return _auth0SDKPromise; }
async function initAuth0() { try { console.log('[AUTH] initAuth0 called, externalSessionHydrated:', externalSessionHydrated, 'window.auth0:', typeof window.auth0); if (externalSessionHydrated && acUser && acToken) { console.log('[AUTH] External session already hydrated, skipping'); updateACLoginUI(); logBoot('auth', 'session'); return; } // Load the Auth0 SDK (UMD). The editor has Monaco's AMD loader, which // would otherwise capture this UMD via define() and never set // window.auth0 — the root of the "SDK still loading" login hang. We // load it ourselves with define hidden so it sets the global. if (!window.auth0) { try { await loadAuth0SDK(); } catch (e) { console.warn('[AUTH] Auth0 SDK load failed:', e?.message || e); } } if (!window.auth0) { console.warn('[AUTH] window.auth0 SDK failed to load'); updateACLoginUI(); logBoot('auth', 'unavailable'); return; } const isKidlispDomain = window.location.hostname === 'kidlisp.com' || window.location.hostname === 'www.kidlisp.com'; const redirectUri = isKidlispDomain ? window.location.origin : window.location.origin + '/kidlisp.com'; acAuth0Client = await window.auth0.createAuth0Client({ domain: 'aesthetic.us.auth0.com', clientId: 'LVdZaMbyXctkGfZDnpzDATB5nR0ZhmMt', authorizationParams: { redirect_uri: redirectUri, audience: 'https://aesthetic.us.auth0.com/api/v2/' }, cacheLocation: 'localstorage', useRefreshTokens: true, useRefreshTokensFallback: true }); if (window.location.search.includes('code=') || window.location.search.includes('error=')) { try { const cbResult = await acAuth0Client.handleRedirectCallback(); const returnTo = cbResult?.appState?.returnTo; // Land back on the piece the user logged in from. Auth0 redirects // to the registered origin, so the $code path lives in appState. if (returnTo && returnTo !== '/' && returnTo !== (window.location.pathname + window.location.search + window.location.hash)) { window.location.replace(returnTo); return; // navigating away; initAuth0 re-runs on the fresh load } window.history.replaceState({}, document.title, window.location.pathname); } catch (e) { /* auth callback error */ } } let isAuthenticated = await acAuth0Client.isAuthenticated(); if (!isAuthenticated) { try { await acAuth0Client.getTokenSilently(); isAuthenticated = await acAuth0Client.isAuthenticated(); } catch (e) { /* silent auth failed */ } } if (isAuthenticated) { acUser = await acAuth0Client.getUser(); acToken = await acAuth0Client.getTokenSilently(); try { const userRes = await fetch(`${aestheticUrl}/user?from=${encodeURIComponent(acUser.email)}&withHandle=true`); const userData = await userRes.json(); if (userData.handle) acHandle = userData.handle; } catch (e) { /* could not fetch handle */ } logAuth('restore', acHandle || acUser?.email?.split('@')[0]); if (typeof switchToHistoryTab === 'function') switchToHistoryTab(); } updateACLoginUI(); logBoot('auth', isAuthenticated ? 'authenticated' : 'guest'); } catch (e) { updateACLoginUI(); logBoot('auth', 'error'); } }
function updateACLoginUI() { const headerAuthButtons = document.getElementById('header-auth-buttons'); const headerUserMenu = document.getElementById('header-user-menu'); const headerUserHandle = document.getElementById('header-user-handle'); const isACPlatform = currentPlatform === 'aesthetic-computer'; if (acUser) { if (headerAuthButtons) headerAuthButtons.style.display = 'none'; if (headerUserMenu) { headerUserMenu.style.display = isACPlatform ? 'flex' : 'none'; const displayName = acHandle ? `@${acHandle}` : acUser.email?.split('@')[0] || 'User'; if (headerUserHandle) headerUserHandle.textContent = displayName; } } else { if (headerAuthButtons) headerAuthButtons.style.display = isACPlatform ? 'flex' : 'none'; if (headerUserMenu) headerUserMenu.style.display = 'none'; } }
async function acLogin() { try { console.log('[AUTH] acLogin called, isVSCode:', isVSCode, 'acAuth0Client:', !!acAuth0Client); if (isVSCode) { console.log('[AUTH] VSCode mode — posting message'); window.parent?.postMessage({ type: 'kidlisp:login' }, '*'); window.parent?.postMessage({ type: 'login', tenant: 'aesthetic' }, '*'); return; } if (!acAuth0Client) { // SDK may still be loading. initAuth0() waits up to 10s for it; a // full-page redirect (below) doesn't need a popup user-gesture, so // awaiting here is safe and avoids the "try again in a moment" dead end. await initAuth0(); } if (!acAuth0Client) { console.warn('[AUTH] Auth0 unavailable — SDK failed to load'); return; } console.log('[AUTH] Redirecting to Auth0 login...'); logAuth('check'); // Carry the current piece path through the Auth0 roundtrip so login // returns to /$code instead of the app root. await acAuth0Client.loginWithRedirect({ authorizationParams: { prompt: 'login' }, appState: { returnTo: window.location.pathname + window.location.search + window.location.hash }, }); } catch (e) { console.error('[AUTH] acLogin error:', e); } }
async function acLogout() { if (isVSCode) { window.parent?.postMessage({ type: 'kidlisp:logout' }, '*'); window.parent?.postMessage({ type: 'logout', tenant: 'aesthetic' }, '*'); acUser = null; acToken = null; acHandle = null; externalSessionHydrated = false; updateACLoginUI(); return; } if (!acAuth0Client) return; logAuth('logout', acHandle || acUser?.email?.split('@')[0]); acUser = null; acToken = null; acHandle = null; const isKidlispDomain = window.location.hostname === 'kidlisp.com' || window.location.hostname === 'www.kidlisp.com'; const returnTo = isKidlispDomain ? window.location.origin : window.location.origin + '/kidlisp.com'; await acAuth0Client.logout({ logoutParams: { returnTo } }); }
function initACLogin() { const headerLoginBtn = document.getElementById('header-login-btn'); console.log('[AUTH] initACLogin — headerLoginBtn:', !!headerLoginBtn); if (headerLoginBtn) headerLoginBtn.addEventListener('click', acLogin);
const headerSignupBtn = document.getElementById('header-signup-btn'); if (headerSignupBtn) { headerSignupBtn.addEventListener('click', async () => { try { if (isVSCode) { window.parent?.postMessage({ type: 'kidlisp:login' }, '*'); window.parent?.postMessage({ type: 'login', tenant: 'aesthetic' }, '*'); return; } if (!acAuth0Client) await initAuth0(); if (!acAuth0Client) return; logAuth('check'); await acAuth0Client.loginWithRedirect({ authorizationParams: { prompt: 'login', screen_hint: 'signup' }, appState: { returnTo: window.location.pathname + window.location.search + window.location.hash }, }); } catch (e) { /* signup error */ } }); }
const headerLogoutBtn = document.getElementById('header-logout-btn'); if (headerLogoutBtn) { headerLogoutBtn.addEventListener('click', (e) => { e.stopPropagation(); acLogout(); }); }
// Initialize Auth0 (async — doesn't block, resolves acAuth0Client for later use) initAuth0().then(() => { console.log('[AUTH] initAuth0 resolved, acAuth0Client:', !!acAuth0Client); }); }
initACLogin(); // Preload Top Hits in the background so they're ready when the tab is opened setTimeout(() => { if (!examplesLoaded) { loadExamples(); } }, 1000); // Set initial tab based on viewport: Console for mobile, Learn (Guide) for desktop const isMobileView = window.innerWidth <= 768; if (typeof window.isStageMode === 'function' && window.isStageMode()) { deactivateAllTabs(); hideAllTabContents(); if (listTab && listContent) { listTab.classList.add('active'); listContent.classList.remove('hidden'); showListSubtab(currentListSubtab); } } else if (isMobileView && consoleTab && mobileConsoleContent) { deactivateAllTabs(); hideAllTabContents(); consoleTab.classList.add('active'); mobileConsoleContent.classList.remove('hidden'); } else { // Desktop: start with Learn tab (Guide) - more welcoming than History deactivateAllTabs(); hideAllTabContents(); guideTab.classList.add('active'); guideContent.classList.remove('hidden'); } // If session was hydrated before DOM was ready, switch to History now if (pendingHistoryTabSwitch && acUser) { switchToHistoryTab(); pendingHistoryTabSwitch = false; } // Boot complete! logBoot('complete'); } else { log.boot.error('Missing tab elements'); }
// ═══════════════════════════════════════════════════════════════════════════ // GUIDES SYSTEM - Tutorial guides list // ═══════════════════════════════════════════════════════════════════════════ // Guides coming soon - donation amounts const GUIDE_AMOUNTS = [5, 10, 25, 50, 100, 250]; let selectedGuideAmount = 25; // Default // Dev/prod URL for give page const giveBaseUrl = (window.location.hostname === 'localhost' || window.location.hostname.includes('local.')) ? 'https://localhost:8888/give.aesthetic.computer' : 'https://give.aesthetic.computer'; // Track current cursor word for live docs let currentCursorWord = null; // Enhanced syntax highlighting for Learn docs using actual KidLisp tokenizer (Monaco engine) function highlightKidlispCodeForLearn(code, isSignature = false) { // Check if KidLisp tokenizer is available (loaded after Monaco init) if (typeof tokenize !== 'function' || typeof KidLisp !== 'function') { // Fallback to simple highlighting if KidLisp not loaded yet return highlightKidlispCodeForLearnSimple(code, isSignature); }
try { // Create a dedicated KidLisp instance for Learn tab highlighting const learnKidlisp = new KidLisp(); learnKidlisp.isEditMode = true; // Edit mode for static display learnKidlisp.initializeSyntaxHighlighting(code); const coloredString = learnKidlisp.buildColoredKidlispString();
// Convert KidLisp color codes to HTML spans with CSS classes // The KidLisp tokenizer outputs format: \color\text\color\text... let highlightedHtml = coloredString.replace(/\\([^\\]+)\\([^\\]*)/g, (match, color, text) => { if (!text) return '';
// Escape HTML entities in text const escapedText = text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>');
// Map KidLisp RGB colors to CSS classes for theme support // Parse RGB color from tokenizer let cssClass = 'kl-var'; // default
// Handle RGB colors from tokenizer if (color.match(/^\d+,\s*\d+,\s*\d+(,\s*\d+)?$/)) { const parts = color.split(',').map(n => parseInt(n.trim())); const [r, g, b] = parts;
// Map RGB to semantic CSS classes based on KidLisp color scheme // Cyan-ish (0-255, 180-255, 180-255) -> functions if (r < 100 && g > 150 && b > 150) { cssClass = 'kl-fn'; } // Yellow-ish (200-255, 200-255, 0-100) -> strings else if (r > 200 && g > 200 && b < 100) { cssClass = 'kl-str'; } // Pink-ish (200-255, 100-200, 180-255) -> numbers else if (r > 200 && g > 100 && g < 200 && b > 180) { cssClass = 'kl-num'; } // Orange-ish (200-255, 100-200, 0-100) -> variables/parameters else if (r > 200 && g > 100 && g < 200 && b < 100) { cssClass = isSignature ? 'kl-param' : 'kl-var'; } // Green-ish (0-100, 200-255, 0-100) -> math operators else if (r < 100 && g > 200 && b < 100) { cssClass = 'kl-math'; } // Red-ish (200-255, 50-150, 50-150) -> parentheses else if (r > 200 && g > 50 && g < 150 && b > 50 && b < 150) { cssClass = 'kl-paren'; } // Gray-ish -> brackets or comments else if (Math.abs(r - g) < 30 && Math.abs(g - b) < 30 && Math.abs(r - b) < 30 && r < 180) { cssClass = 'kl-bracket'; } // Handle special tokens by content else if (text.match(/^\d*\.?\d+[sf]\.\.\.?$/) || text.match(/^\d*\.?\d+[sf]!?$/)) { cssClass = 'kl-timing'; } else if (text === 'rainbow') { cssClass = 'kl-rainbow'; } else if (text === 'zebra') { cssClass = 'kl-special'; } // Check if it's a color name else if (/^(red|blue|green|yellow|purple|orange|pink|cyan|magenta|lime|white|black|gray|grey|navy|teal|coral|gold|silver|maroon|olive|aqua|fuchsia|indigo|violet|brown|tan|salmon|crimson|turquoise|plum|khaki|tomato|peru|sienna|chocolate|firebrick|hotpink|deeppink)$/i.test(text)) { // For color names, use kl-color class and inline style for actual color return `<span class="kl-color" style="color: rgb(${r},${g},${b});">${escapedText}</span>`; } }
return `<span class="${cssClass}">${escapedText}</span>`; });
return highlightedHtml; } catch (e) { // Fallback to simple highlighting on error return highlightKidlispCodeForLearnSimple(code, isSignature); } }
// Fallback simple highlighting (when KidLisp tokenizer not available) function highlightKidlispCodeForLearnSimple(code, isSignature = false) { // Escape HTML first const escapeHtml = (str) => str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); // CSS color name to RGB mapping (common colors for highlighting) const learnColorMap = { red: [255, 0, 0], blue: [0, 0, 255], green: [0, 128, 0], yellow: [255, 255, 0], purple: [128, 0, 128], orange: [255, 165, 0], pink: [255, 192, 203], cyan: [0, 255, 255], magenta: [255, 0, 255], lime: [0, 255, 0], white: [255, 255, 255], black: [0, 0, 0], gray: [128, 128, 128], grey: [128, 128, 128], navy: [0, 0, 128], teal: [0, 128, 128], coral: [255, 127, 80], gold: [255, 215, 0], silver: [192, 192, 192], maroon: [128, 0, 0], olive: [128, 128, 0], aqua: [0, 255, 255], fuchsia: [255, 0, 255], indigo: [75, 0, 130], violet: [238, 130, 238], brown: [165, 42, 42], tan: [210, 180, 140], salmon: [250, 128, 114], crimson: [220, 20, 60], turquoise: [64, 224, 208], plum: [221, 160, 221], khaki: [240, 230, 140], tomato: [255, 99, 71], peru: [205, 133, 63], sienna: [160, 82, 45], chocolate: [210, 105, 30], firebrick: [178, 34, 34], hotpink: [255, 105, 180], deeppink: [255, 20, 147] }; // Known functions for highlighting const knownFunctions = new Set([ 'wipe', 'ink', 'line', 'box', 'flood', 'circle', 'write', 'paste', 'stamp', 'point', 'poly', 'embed', 'print', 'debug', 'random', 'sin', 'cos', 'tan', 'floor', 'ceil', 'round', 'tri', 'shape', 'noise', 'choose', '?', '...', '..', 'overtone', 'mic', 'amplitude', 'tone', 'beep', 'melody', 'resolution', 'wiggle', 'scroll', 'flip', 'spin', 'resetSpin', 'smoothspin', 'zoom', 'blur', 'contrast', 'fade', 'bake', 'mask', 'unmask', 'label', 'len', 'now', 'once', 'die', 'jump', 'plot', 'tap', 'draw', 'not', 'range', 'if', 'def', 'set', 'later', 'repeat', 'bunch', 'loop', 'list', 'get', 'first', 'rest', 'push', 'pop', 'fill', 'outline', 'stroke', 'nofill', 'nostroke', 'and', 'or', 'beat' ]); // Math operators const mathOps = new Set(['+', '-', '*', '/', '%', 'mod', '=', '>', '<', '>=', '<=', 'abs', 'sqrt', 'min', 'max', 'lerp', 'map', 'constrain', 'dist', 'pow']); // Color names (common CSS colors) const colorNames = new Set([ 'red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'cyan', 'magenta', 'lime', 'white', 'black', 'gray', 'grey', 'navy', 'teal', 'coral', 'gold', 'silver', 'maroon', 'olive', 'aqua', 'fuchsia', 'indigo', 'violet', 'brown', 'tan', 'salmon', 'crimson', 'turquoise', 'plum', 'khaki', 'tomato', 'peru', 'sienna', 'chocolate', 'firebrick', 'hotpink', 'deeppink' ]); // Global variables const globals = new Set(['width', 'height', 'time', 'elapsed', 'frame', 'delta', 'fps', 'mic', 'mouseX', 'mouseY', 'lastX', 'lastY', 'bpm', 'amplitude']); let result = ''; let i = 0; while (i < code.length) { const char = code[i]; // Handle parentheses if (char === '(' || char === ')') { result += `<span class="kl-paren">${char}</span>`; i++; continue; } // Handle brackets (optional params in signatures) if (char === '[' || char === ']') { result += `<span class="kl-bracket">${char}</span>`; i++; continue; } // Handle strings if (char === '"') { let str = char; i++; while (i < code.length && code[i] !== '"') { str += code[i]; i++; } if (i < code.length) str += code[i++]; result += `<span class="kl-str">${escapeHtml(str)}</span>`; continue; } // Handle whitespace if (/\s/.test(char)) { result += char; i++; continue; } // Collect a word/token let token = ''; while (i < code.length && !/[\s()\[\]"]/.test(code[i])) { token += code[i]; i++; } if (!token) continue; // Determine token type and apply class if (token === 'rainbow') { result += `<span class="kl-rainbow">${escapeHtml(token)}</span>`; } else if (token === 'zebra') { result += `<span class="kl-special">${escapeHtml(token)}</span>`; } else if (/^\d*\.?\d+[sf]\.\.\.?$/.test(token) || /^\d*\.?\d+[sf]!?$/.test(token)) { // Timing patterns like 1s, 0.5s, 1s..., 2f result += `<span class="kl-timing">${escapeHtml(token)}</span>`; } else if (/^-?\d+(\.\d+)?$/.test(token)) { // Numbers result += `<span class="kl-num">${escapeHtml(token)}</span>`; } else if (mathOps.has(token)) { result += `<span class="kl-math">${escapeHtml(token)}</span>`; } else if (knownFunctions.has(token)) { result += `<span class="kl-fn">${escapeHtml(token)}</span>`; } else if (colorNames.has(token.toLowerCase())) { // Show color names in their actual color const colorRgb = learnColorMap[token.toLowerCase()]; if (colorRgb) { const [r, g, b] = colorRgb; // Add text shadow for dark colors const luminance = 0.299 * r + 0.587 * g + 0.114 * b; const shadow = luminance < 80 ? 'text-shadow: 1px 1px 0 rgba(255,255,255,0.5);' : ''; result += `<span class="kl-color" style="color: rgb(${r},${g},${b}); ${shadow}">${escapeHtml(token)}</span>`; } else { result += `<span class="kl-color">${escapeHtml(token)}</span>`; } } else if (globals.has(token)) { result += `<span class="kl-var">${escapeHtml(token)}</span>`; } else if (isSignature && /^[a-z]/.test(token)) { // In signatures, lowercase words are likely parameters result += `<span class="kl-param">${escapeHtml(token)}</span>`; } else { // Default - likely a variable result += `<span class="kl-var">${escapeHtml(token)}</span>`; } } return result; } // Extract all identifiers from KidLisp code for ticker navigation function extractIdentifiersFromCode(code) { const identifiers = []; const seen = new Set();
// Match words (identifiers) - anything that's not whitespace, parens, or quotes const wordPattern = /[^\s()"']+/g; let match;
while ((match = wordPattern.exec(code)) !== null) { const word = match[0]; // Only include documented identifiers and avoid duplicates if (KIDLISP_DOCS[word] && !seen.has(word)) { identifiers.push(word); seen.add(word); } }
return identifiers; }
function updateLearnDocumentation(word, prevWord = null, nextWord = null) { const docSection = document.getElementById('learn-doc-section'); if (!docSection) return;
// Ticker navigation HTML const tickerHtml = (prevWord || nextWord) ? ` <div class="learn-doc-ticker"> <span class="learn-doc-ticker-nav ${!prevWord ? 'disabled' : ''}">${prevWord || '---'}</span> <span class="learn-doc-ticker-current">${word || '---'}</span> <span class="learn-doc-ticker-nav ${!nextWord ? 'disabled' : ''}">${nextWord || '---'}</span> </div> ` : '';
if (!word || !KIDLISP_DOCS[word]) { // Show empty state with hint docSection.innerHTML = ` ${tickerHtml} <div class="learn-doc-empty"> Place your cursor on a KidLisp function to see its documentation here. </div> <div class="learn-doc-hint"> Try: wipe, ink, box, circle, line, wiggle, beat, purple, ?... </div> `; return; } const doc = KIDLISP_DOCS[word]; // Syntax highlight the signature const signatureHtml = doc.signature ? highlightKidlispCodeForLearn(doc.signature, true) : ''; // Syntax highlight each example const examplesHtml = doc.examples && doc.examples.length > 0 ? `<div class="learn-doc-examples-title">Examples</div> ${doc.examples.map(ex => `<div class="learn-doc-example" data-example="${encodeURIComponent(ex)}">${highlightKidlispCodeForLearn(ex)}</div>`).join('')}` : ''; // Color map for doc name coloring const docColorMap = { red: [255, 0, 0], blue: [0, 0, 255], green: [0, 128, 0], yellow: [255, 255, 0], purple: [128, 0, 128], orange: [255, 165, 0], pink: [255, 192, 203], cyan: [0, 255, 255], magenta: [255, 0, 255], lime: [0, 255, 0], white: [255, 255, 255], black: [0, 0, 0], gray: [128, 128, 128], navy: [0, 0, 128], teal: [0, 128, 128], coral: [255, 127, 80], gold: [255, 215, 0], silver: [192, 192, 192] }; // Determine name color based on category let nameColor = 'rgb(205, 92, 155)'; // Default pink if (doc.category === 'color') { // Show color name in its actual color const colorRgb = docColorMap[word.toLowerCase()]; if (colorRgb) { nameColor = `rgb(${colorRgb[0]},${colorRgb[1]},${colorRgb[2]})`; } } else if (doc.category === 'timing') { nameColor = '#ffff00'; // Yellow for timing } else if (doc.category === 'math' || doc.category === 'logic') { nameColor = '#00ff00'; // Green for math/logic } else if (word === 'rainbow') { nameColor = 'transparent'; } // Special rainbow treatment for the name const nameStyle = word === 'rainbow' ? 'background: linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet); -webkit-background-clip: text; -webkit-text-fill-color: transparent;' : `color: ${nameColor};`; docSection.innerHTML = ` ${tickerHtml} <div class="learn-doc-content"> <div class="learn-doc-name" style="${nameStyle}">${word}</div> ${doc.signature ? `<div class="learn-doc-signature">${signatureHtml}</div>` : ''} ${doc.category ? `<div class="learn-doc-category">${doc.category}</div>` : ''} <div class="learn-doc-description">${doc.description || 'No description available.'}</div> ${examplesHtml} </div> `; // Add click handlers for examples docSection.querySelectorAll('.learn-doc-example').forEach(el => { el.addEventListener('click', () => { const example = decodeURIComponent(el.getAttribute('data-example')); if (editor && example) { // Insert at cursor or replace selection const selection = editor.getSelection(); editor.executeEdits('learn-insert', [{ range: selection, text: example, forceMoveMarkers: true }]); editor.focus(); } }); }); } function initGuides() { const guidesList = document.getElementById('guides-list'); if (!guidesList) return; // Render Learn tab with live docs section and fund section guidesList.innerHTML = ` <div class="learn-container"> <div class="learn-doc-section" id="learn-doc-section"> <div class="learn-doc-empty"> Place your cursor on a KidLisp function to see its documentation here. </div> <div class="learn-doc-hint"> Try: wipe, ink, box, circle, wiggle, purple, ?, 1s... </div> </div> <div class="guides-coming-soon"> <div class="guides-coming-text">Guides from <span class="author">@jeffrey</span> coming soon. Fund development:</div> <div class="guides-fund-row"> ${GUIDE_AMOUNTS.map((amt, i) => `<button class="guides-amount-btn${i === 2 ? ' selected' : ''}" data-amount="${amt}">$${amt}</button>` ).join('')} <button class="guides-give-btn" id="guides-give-btn">Give</button> </div> </div> </div> `; // Set up preset button interaction const amountBtns = guidesList.querySelectorAll('.guides-amount-btn'); const giveBtn = document.getElementById('guides-give-btn'); amountBtns.forEach(btn => { btn.addEventListener('click', () => { amountBtns.forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); selectedGuideAmount = parseInt(btn.getAttribute('data-amount')); }); }); if (giveBtn) { giveBtn.addEventListener('click', () => { openExternal(`${giveBaseUrl}?amount=${selectedGuideAmount}`); }); } } // Initialize guides on load initGuides(); // ═══════════════════════════════════════════════════════════════════════════ // AI ASSIST - Generate KidLisp from natural language descriptions // ═══════════════════════════════════════════════════════════════════════════ let aiAssistAbort = null; function initAIAssist() { const input = document.getElementById('ai-assist-input'); const sendBtn = document.getElementById('ai-assist-send'); if (!input || !sendBtn) return; // Handle Enter key input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); generateFromPrompt(); } if (e.key === 'Escape') { input.blur(); if (aiAssistAbort) { aiAssistAbort(); aiAssistAbort = null; sendBtn.classList.remove('loading'); sendBtn.disabled = false; } } }); // Handle send button click sendBtn.addEventListener('click', generateFromPrompt); async function generateFromPrompt() { const prompt = input.value.trim(); if (!prompt) return; // Abort any in-progress request if (aiAssistAbort) { aiAssistAbort(); aiAssistAbort = null; } sendBtn.classList.add('loading'); sendBtn.disabled = true; try { // Load the KidLisp prompt template from aesthetic.computer const templateResp = await fetch(`${aestheticUrl}/aesthetic.computer/prompts/kidlisp-make.prompt`); let template = await templateResp.text(); // Check for HTML response (wrong path) if (template.includes('<!doctype html>') || template.includes('<!DOCTYPE html>')) { throw new Error('Could not load prompt template'); } // Get current code from editor for context const currentCode = editor ? editor.getValue().trim() : ''; // Replace placeholder with user prompt const systemPrompt = template.replace(/\{\{USER_PROMPT\}\}/g, prompt); // Build user message with current code context if available let userMessage = prompt; if (currentCode) { userMessage = `Current KidLisp code:\n\`\`\`\n${currentCode}\n\`\`\`\n\nWish: ${prompt}`; } // Prepare messages for the API const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage } ]; // Create abort controller const controller = new AbortController(); aiAssistAbort = () => controller.abort(); // Call the ask API on aesthetic.computer with Claude Haiku 4.5 (fast & cheap) const response = await fetch(`${aestheticUrl}/api/ask`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages, hint: 'code:claude-haiku-4-5-20251001' }), signal: controller.signal }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } // Stream the response const reader = response.body.getReader(); const decoder = new TextDecoder(); let generatedCode = ''; // Clear editor and start streaming if (editor) { editor.setValue(''); } while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); generatedCode += chunk; // Update editor with streaming content if (editor) { if (typeof window.setSettingValueProgrammatically === 'function') window.setSettingValueProgrammatically(true); editor.setValue(generatedCode); if (typeof window.setSettingValueProgrammatically === 'function') window.setSettingValueProgrammatically(false); // Move cursor to end const model = editor.getModel(); if (model) { const lastLine = model.getLineCount(); const lastCol = model.getLineMaxColumn(lastLine); editor.setPosition({ lineNumber: lastLine, column: lastCol }); } } } // Clear input on success input.value = ''; // Auto-play the generated code setTimeout(() => { if (sendButton && !sendButton.disabled) { if (isPlaying) stopButton?.click(); sendButton?.click(); } }, 100); } catch (err) { if (err.name !== 'AbortError') { console.error('AI Assist error:', err); // Show error in editor if (editor) { editor.setValue(`; Error generating code: ${err.message}\n; Try again with a different prompt.`); } } } finally { aiAssistAbort = null; sendBtn.classList.remove('loading'); sendBtn.disabled = false; } } } // Initialize AI assist on load (hidden until triggered via context menu) initAIAssist();
async function loadExamples() { try { const examplesList = document.getElementById('examples-list'); examplesList.innerHTML = `<div class="code-list-loading">${t('examplesLoading')}</div>`; const response = await fetch(`${aestheticUrl}/api/tv?types=kidlisp&sort=hits&limit=25`); const data = await response.json(); if (data.media && data.media.kidlisp) { // Sort by hits (descending) and map to standard format const sortedItems = [...data.media.kidlisp] .sort((a, b) => (b.hits || 0) - (a.hits || 0)) .map(item => ({ code: item.code, source: item.source || '', assetUrl: item.assetUrl || item.thumbnail || null, animatedUrl: item.animatedUrl || null, hits: item.hits || 0, owner: item.owner?.handle || item.handle || null, ts: item.when ? Date.parse(item.when) : null, })); renderCodeList({ listId: 'examples-list', items: sortedItems, emptyMessage: t('examplesNone'), syntaxHighlight: true, tickerAnimation: true, showHits: true, showOwner: true, showTimestamp: true, groupByOwner: true, // Group similar items by author for diversity maxPerOwner: 2, // Show max 2 per author, then "... and X more" }); examplesLoaded = true; } else { examplesList.innerHTML = `<div class="code-list-empty">${t('examplesNone')}</div>`; } } catch (e) { log.code.error('Failed to load examples:', e.message); document.getElementById('examples-list').innerHTML = `<div class="code-list-empty">${t('examplesError')}</div>`; } } // ═══════════════════════════════════════════════════════════════════════════ // SLIDESHOW MODE // Opens the Top 100 playlist on device.kidlisp.com in a new tab // ═══════════════════════════════════════════════════════════════════════════ function initPlayOnTvCard() { // This will be called after renderCodeList adds the card const playOnTvCard = document.querySelector('.play-on-tv-card'); if (!playOnTvCard) return; playOnTvCard.addEventListener('click', () => { // Use nice short URL path const deviceUrl = 'https://top.kidlisp.com'; console.log('📺 Opening slideshow:', deviceUrl); // Open in new tab/window (fullscreen for best effect) window.open(deviceUrl, '_blank'); }); }
// ═══════════════════════════════════════════════════════════════════════════ // SHARED CODE LIST COMPONENT // Reusable scrollable list for Top Hits, Recent, and History tabs // ═══════════════════════════════════════════════════════════════════════════ // Helper to format time ago (moved here for shared use) function formatTimeAgo(ts) { const now = Date.now(); const diff = now - ts; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ago`; if (hours > 0) return `${hours}h ago`; if (minutes > 0) return `${minutes}m ago`; return 'just now'; } // Helper to check if text color is dark (needs light shadow) function isDarkColor(colorStr) { if (!colorStr) return false; let rgb; if (colorStr.startsWith('rgb(')) { const match = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (match) rgb = [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]; } else if (colorStr.match(/^\d+,\s*\d+,\s*\d+/)) { rgb = colorStr.split(',').map(n => parseInt(n.trim())); } else { const darkColors = { black: [0,0,0], darkblue: [0,0,139], navy: [0,0,128], maroon: [128,0,0], purple: [128,0,128], indigo: [75,0,130], darkgreen: [0,100,0], darkred: [139,0,0], brown: [139,69,19], darkgray: [169,169,169], dimgray: [105,105,105], gray: [128,128,128] }; rgb = darkColors[colorStr.toLowerCase()]; } if (!rgb || rgb.length < 3) return false; const luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]; return luminance < 120; } // Helper to check if token is a timing expression function isTimingToken(text) { return /^\d*\.?\d+[sf](\.\.\.?)?$/.test(text.trim()); } // Apply KidLisp syntax highlighting to code string function applySyntaxHighlighting(code) { try { const kl = new KidLisp(); kl.isEditMode = true; kl.initializeSyntaxHighlighting(code); const coloredString = kl.buildColoredKidlispString(); return coloredString.replace(/\\([^\\]+)\\([^\\]*)/g, (match, color, text) => { if (!text) return ''; let cssColor = color; let extraClass = ''; if (color.includes(',0') && color.split(',').length === 4) { cssColor = 'rgb(128,128,128)'; extraClass += ' dark-text-shadow'; } if (color.match(/^\d+,\s*\d+,\s*\d+(,\s*\d+)?$/)) { const parts = color.split(',').map(n => parseInt(n.trim())); if (parts.length === 4 && parts[3] === 0) { cssColor = 'rgb(180,180,180)'; extraClass += ' dark-text-shadow'; } else { cssColor = `rgb(${parts.slice(0,3).join(',')})`; } } const dark = isDarkColor(color) || isDarkColor(cssColor); if (isTimingToken(text)) extraClass += ' timing-token'; if (dark) extraClass += ' dark-text-shadow'; if (['yellow', 'lime', 'cyan', 'orange', 'pink', 'magenta', 'white', 'lightyellow', 'lightgreen'].includes(color.toLowerCase())) { extraClass += ' glow-text'; } return `<span style="color: ${cssColor}; font-weight: bold;" class="${extraClass.trim()}">${text}</span>`; }); } catch (e) { return code; // Return plain text on error } } // ═══════════════════════════════════════════════════════════════════════════ // OVEN STATUS - Thumbnail generation status from oven.aesthetic.computer // ═══════════════════════════════════════════════════════════════════════════ const OVEN_URL = 'https://oven.aesthetic.computer'; let ovenStatusCache = { queue: [], progress: null, active: [], lastFetch: 0 }; const OVEN_CACHE_TTL = 5000; // 5 seconds async function fetchOvenStatus() { const now = Date.now(); if (now - ovenStatusCache.lastFetch < OVEN_CACHE_TTL) { return ovenStatusCache; } try { const resp = await fetch(`${OVEN_URL}/grab-status`, { mode: 'cors' }); const data = await resp.json(); ovenStatusCache = { ...data, lastFetch: now }; return ovenStatusCache; } catch (e) { console.warn('Failed to fetch oven status:', e); return ovenStatusCache; } } function getOvenStatusForPiece(code) { // Check if this piece is in oven queue or being processed if (!code) return null; const { queue = [], progress, active = [] } = ovenStatusCache; // Check if currently processing if (progress?.piece === code) { return { status: 'processing', stage: progress.stage, percent: progress.percent }; } // Check if in queue const queueItem = queue.find(q => q.piece === code); if (queueItem) { return { status: 'queued', position: queueItem.position, estimatedWait: queueItem.estimatedWait }; } // Check active grabs const activeItem = active.find(a => a.piece === code); if (activeItem) { return { status: 'processing', stage: activeItem.stage }; } return null; } function getOvenStatusLabel(ovenStatus) { if (!ovenStatus) return 'WebP pending'; if (ovenStatus.status === 'processing') { if (typeof ovenStatus.percent === 'number') return `Baking ${Math.round(ovenStatus.percent)}%`; if (ovenStatus.stage) return `Baking ${ovenStatus.stage}`; return 'Baking'; } if (ovenStatus.status === 'queued') { if (ovenStatus.position) return `Queued #${ovenStatus.position}`; return 'Queued'; } if (ovenStatus.status === 'cached') return 'Cached'; return 'WebP pending'; }
function applyOvenStatusToPlaceholder(wrapper, code) { if (!wrapper || !code) return; wrapper.querySelectorAll('.thumb-oven-status, .thumb-oven-label').forEach(el => el.remove()); const ovenStatus = getOvenStatusForPiece(code); const statusBar = document.createElement('div'); statusBar.className = `thumb-oven-status ${ovenStatus?.status || 'pending'}`; const statusLabel = document.createElement('div'); statusLabel.className = 'thumb-oven-label'; statusLabel.textContent = getOvenStatusLabel(ovenStatus); if (ovenStatus?.position) { statusBar.title = `Queue position: ${ovenStatus.position}`; } else if (ovenStatus?.stage) { statusBar.title = `Processing: ${ovenStatus.stage}`; } else { statusBar.title = 'Waiting for oven thumbnail'; } wrapper.appendChild(statusBar); wrapper.appendChild(statusLabel); }
// Create static square fallback (cheap to render) with optional oven status. function createNoisePlaceholder(code = null, { showOvenStatus = true } = {}) { const wrapper = document.createElement('div'); wrapper.className = 'thumb-noise'; if (code) wrapper.dataset.pieceCode = code; const square = document.createElement('div'); square.className = 'thumb-fallback-square'; wrapper.appendChild(square); if (showOvenStatus && code) applyOvenStatusToPlaceholder(wrapper, code); return wrapper; }
function refreshVisibleOvenStatuses(listEl) { if (!listEl) return; listEl.querySelectorAll('.thumb-noise[data-piece-code]').forEach(wrapper => { const code = wrapper.dataset.pieceCode; if (code) applyOvenStatusToPlaceholder(wrapper, code); }); }
function scheduleOvenStatusRefresh(listEl, items = []) { const hasCodeItems = items.some(item => item?.code); if (!hasCodeItems) return; fetchOvenStatus().then(() => refreshVisibleOvenStatuses(listEl)); }
/** * Render a single code list item * @param {Object} item - The code item data * @param {Object} options - Rendering options * @returns {HTMLElement} The rendered item element */ function renderCodeListItem(item, options = {}) { const { syntaxHighlight = false, tickerAnimation = false, showOwner = false, showTimeAgo = false, showTimestamp = false, showHits = true, pendingThumb = false, rank = null, } = options; const el = document.createElement('div'); el.className = 'example-item'; // Get code snippet (never use item.code as fallback - that's the short identifier) const sourceCode = item.source || ''; const snippet = sourceCode.slice(0, 80).replace(/\n/g, ' '); const truncated = snippet.length === 80; // Apply syntax highlighting if requested (cache on the item to avoid recompute while scrolling) let displayCode = ''; if (syntaxHighlight) { if (item._syntaxSourceCache === sourceCode && item._syntaxHighlightCache) { displayCode = item._syntaxHighlightCache; } else { displayCode = applySyntaxHighlighting(sourceCode); item._syntaxSourceCache = sourceCode; item._syntaxHighlightCache = displayCode; } } else { displayCode = snippet || '(empty)'; if (truncated) displayCode += '…'; } // Ticker animation - will be activated by JS after render if content overflows // Use data attribute to mark for ticker processing, don't duplicate content yet const tickerEnabled = tickerAnimation; if (tickerEnabled) { // Wrap in ticker-wrapper, JS will add ticker-scroll class and duplicate if needed displayCode = `<span class="ticker-wrapper" data-ticker-content="${encodeURIComponent(displayCode)}">${displayCode}</span>`; } // Build metadata lines const codeLabel = item.code ? `$${item.code}` : 'draft'; // Handle already includes @ prefix from API, so strip it if present to avoid @@ const ownerClean = item.owner ? item.owner.replace(/^@/, '') : null; const ownerText = showOwner && ownerClean ? ` by @${ownerClean}` : ''; const handleText = item.handle ? item.handle : ''; let timeLine = ''; if (showTimeAgo && item.ts) { timeLine = formatTimeAgo(item.ts); } else if (showTimestamp && item.ts) { const d = new Date(item.ts); const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const month = months[d.getMonth()]; const day = d.getDate(); const year = d.getFullYear(); const hour = d.getHours(); const minute = d.getMinutes().toString().padStart(2, '0'); const ampm = hour >= 12 ? 'PM' : 'AM'; const hour12 = hour % 12 || 12; timeLine = `${month} ${day}, ${year} at ${hour12}:${minute} ${ampm}`; } // Build thumbnail (always render a fallback square underneath the image) const thumbClass = pendingThumb ? 'example-thumb-placeholder history-thumb-pending' : 'example-thumb-placeholder'; const itemCode = item.code || null; const useNoisePlaceholder = typeof createNoisePlaceholder === 'function'; // Build hits display const hitsHtml = showHits ? `<div class="example-hits">${item.hits || 0} hits</div>` : ''; const rankHtml = typeof rank === 'number' ? `<div class="example-rank">#${rank}</div>` : ''; const rightMetaHtml = (rankHtml || hitsHtml) ? `<div class="example-meta-right">${rankHtml}${hitsHtml}</div>` : ''; // Assemble HTML el.innerHTML = ` <div class="thumb-container" data-code="${itemCode || ''}" data-asset="${item.assetUrl || ''}"></div> <div style="display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0;"> <div class="example-code">${displayCode}</div> <div style="font-size: 10px; color: var(--text-tertiary); opacity: 0.7;">${codeLabel}${ownerText}</div> ${handleText && !ownerText ? `<div style="font-size: 10px; color: var(--text-tertiary); opacity: 0.7;">${handleText}</div>` : ''} ${timeLine ? `<div style="font-size: 10px; color: var(--text-tertiary); opacity: 0.7;">${timeLine}</div>` : ''} </div> ${rightMetaHtml} `; // Now populate the thumbnail container const thumbContainer = el.querySelector('.thumb-container'); if (thumbContainer) { const hasValidAsset = !!(item.assetUrl && item.assetUrl.trim().length > 0); let fallback = null;
if (useNoisePlaceholder) { fallback = createNoisePlaceholder(itemCode, { showOvenStatus: !hasValidAsset || pendingThumb }); } else { fallback = document.createElement('div'); fallback.className = thumbClass; } thumbContainer.appendChild(fallback);
if (hasValidAsset) { // Load still image first, then upgrade to animated WebP. const img = document.createElement('img'); img.className = 'example-thumb'; img.src = item.assetUrl; img.alt = ''; img.loading = 'lazy'; img.decoding = 'async'; img.onload = () => { img.classList.add('loaded'); // Upgrade to animated WebP in background if (item.animatedUrl) { const anim = new Image(); anim.src = item.animatedUrl; anim.onload = () => { img.src = item.animatedUrl; }; } }; img.onerror = () => { img.remove(); if (fallback && itemCode && fallback.classList.contains('thumb-noise')) { applyOvenStatusToPlaceholder(fallback, itemCode); } }; thumbContainer.appendChild(img); } } // Store data for click handler el._itemData = item; el._highlightedCode = syntaxHighlight ? displayCode : null; return el; } /** * Common click handler for code list items * @param {HTMLElement} el - The clicked element */ async function handleCodeItemClick(el) { if (!editor) return; const item = el._itemData; if (!item) return; // Never use item.code as fallback - that's the short identifier like "h6g", not source code const sourceCode = item.source || ''; const snippet = (sourceCode).slice(0, 80).replace(/\n/g, ' ') || '(empty)'; // Add to history if (typeof addHistoryEntry === 'function') { addHistoryEntry({ code: item.code || null, source: sourceCode, assetUrl: item.assetUrl || null, hits: item.hits || 0, ts: Date.now() }); } // Flying code animation const codeEl = el.querySelector('.example-code'); if (codeEl && window.flyCodeToEditor) { await window.flyCodeToEditor(codeEl, el._highlightedCode || snippet, sourceCode); } else { if (typeof window.setSettingValueProgrammatically === 'function') window.setSettingValueProgrammatically(true); editor.setValue(sourceCode); if (typeof window.setSettingValueProgrammatically === 'function') window.setSettingValueProgrammatically(false); } // Mark as not user-edited since this was loaded from list if (typeof window.setEditorUserModified === 'function') { window.setEditorUserModified(false); } const editorEl = document.getElementById('kidlisp-editor'); if (editorEl) editorEl.classList.remove('decrypting'); // Update URL and title (only on AC platform) if (currentPlatform === 'aesthetic-computer' && item.code) { updateURLForCode(item.code); localStorage.setItem('kidlisp-current-code', item.code); setRouteCodeInUI(item.code, { immediate: true, animate: false }); } updatePreview(); // Auto-play - ensure button state is updated before checking if (window.updatePlayButtonState) { window.updatePlayButtonState(); } // Use requestAnimationFrame to ensure DOM/state updates have propagated requestAnimationFrame(() => { if (sendButton && !sendButton.disabled) { if (isPlaying) stopButton.click(); sendButton.click(); } }); } /** * Render a scrollable code list * @param {Object} config - List configuration */ function renderCodeList(config) { const { listId, // ID of the list container element items, // Array of code items emptyMessage = 'No items yet.', syntaxHighlight = false, tickerAnimation = false, showOwner = false, showTimeAgo = false, showTimestamp = false, showHits = true, pendingThumb = false, hitsLabel = 'hits', groupByOwner = false, // Group similar items by owner maxPerOwner = 2, // Max items shown per owner before collapsing } = config; const listEl = document.getElementById(listId); if (!listEl) return; codeListLastConfigs.set(listId, config); if (!items || items.length === 0) { listEl.innerHTML = `<div class="code-list-empty">${emptyMessage}</div>`; return; }
// Avoid expensive syntax/image work while this list is in a hidden tab. if (listEl.offsetParent === null) { listEl.innerHTML = ''; return; }
listEl.innerHTML = '';
if (listId === 'examples-list') { const playOnTvCard = document.createElement('div'); playOnTvCard.className = 'play-on-tv-card'; playOnTvCard.innerHTML = ` <div class="play-on-tv-icon"> <div class="play-on-tv-stack"> <span class="play-on-tv-slide back"></span> <span class="play-on-tv-slide mid"></span> <span class="play-on-tv-slide front"></span> </div> </div> <div class="play-on-tv-content"> <div class="play-on-tv-title">Play on your TV</div> <div class="play-on-tv-subtitle">Watch <span class="play-on-tv-highlight">Top 100</span> as a playlist slideshow</div> </div> <div class="play-on-tv-arrow">→</div> `; playOnTvCard.addEventListener('click', () => { const deviceUrl = 'https://top.kidlisp.com'; console.log('📺 Opening slideshow:', deviceUrl); window.open(deviceUrl, '_blank'); }); listEl.appendChild(playOnTvCard); } // Process items with optional owner grouping let processedItems = items; let ownerHiddenCounts = {}; // Track hidden items per owner // Helper: calculate similarity ratio between two strings (0-1) function codeSimilarity(a, b) { if (!a || !b) return 0; const strA = a.trim(); const strB = b.trim(); if (strA === strB) return 1; if (strA.length === 0 || strB.length === 0) return 0; // Simple Levenshtein-based similarity for short codes // For longer codes, use character frequency comparison (faster) const maxLen = Math.max(strA.length, strB.length); if (maxLen > 500) { // Character frequency comparison for longer codes const freqA = {}, freqB = {}; for (const c of strA) freqA[c] = (freqA[c] || 0) + 1; for (const c of strB) freqB[c] = (freqB[c] || 0) + 1; const allChars = new Set([...Object.keys(freqA), ...Object.keys(freqB)]); let diff = 0; for (const c of allChars) { diff += Math.abs((freqA[c] || 0) - (freqB[c] || 0)); } return 1 - (diff / (strA.length + strB.length)); } // Levenshtein distance for shorter codes const matrix = []; for (let i = 0; i <= strB.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= strA.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= strB.length; i++) { for (let j = 1; j <= strA.length; j++) { if (strB[i-1] === strA[j-1]) { matrix[i][j] = matrix[i-1][j-1]; } else { matrix[i][j] = Math.min( matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1 ); } } } return 1 - (matrix[strB.length][strA.length] / maxLen); } // Helper: check if two items are "similar" (same owner + close in time OR similar code) function areSimilar(itemA, itemB) { // Must be same owner if ((itemA.owner || '_anonymous') !== (itemB.owner || '_anonymous')) return false; // Check if created within 30 minutes of each other const timeA = itemA.timestamp ? new Date(itemA.timestamp).getTime() : 0; const timeB = itemB.timestamp ? new Date(itemB.timestamp).getTime() : 0; const thirtyMinutes = 30 * 60 * 1000; if (timeA && timeB && Math.abs(timeA - timeB) < thirtyMinutes) { return true; } // Check if code is 90%+ similar if (itemA.code && itemB.code && codeSimilarity(itemA.code, itemB.code) >= 0.9) { return true; } return false; } if (groupByOwner) { // Group items into "similarity clusters" per owner const clusters = []; // Array of { owner, items: [...], representative } items.forEach(item => { const owner = item.owner || '_anonymous'; // Find an existing cluster this item is similar to let foundCluster = null; for (const cluster of clusters) { if (cluster.owner === owner && areSimilar(cluster.representative, item)) { foundCluster = cluster; break; } } if (foundCluster) { foundCluster.items.push(item); } else { // Start a new cluster with this item as representative clusters.push({ owner, items: [item], representative: item }); } }); // Build processedItems: show up to maxPerOwner from each cluster, track hidden processedItems = []; clusters.forEach(cluster => { const shown = cluster.items.slice(0, maxPerOwner); const hidden = cluster.items.length - shown.length; shown.forEach((item, idx) => { processedItems.push(item); // Mark the last shown item as having hidden siblings if (idx === shown.length - 1 && hidden > 0) { ownerHiddenCounts[item.owner || '_anonymous'] = ownerHiddenCounts[item.owner || '_anonymous'] || {}; ownerHiddenCounts[item.owner || '_anonymous'][cluster.representative.code || 'cluster'] = { count: hidden, allItems: cluster.items }; } }); }); }
function renderItem(item, rank = null) { const el = renderCodeListItem(item, { syntaxHighlight, tickerAnimation, showOwner, showTimeAgo, showTimestamp, showHits, pendingThumb, rank, }); el.addEventListener('click', () => handleCodeItemClick(el)); return el; }
function renderClusterRows(clusterItems, ownerClean) { listEl.innerHTML = ''; const backEl = document.createElement('div'); backEl.className = 'owner-back-indicator'; backEl.innerHTML = `<span class="back-text">← Back to all</span><span class="owner-filter">Showing ${clusterItems.length} similar from @${ownerClean}</span>`; backEl.addEventListener('click', () => renderCodeList(config)); listEl.appendChild(backEl); const clusterFrag = document.createDocumentFragment(); clusterItems.forEach(clusterItem => clusterFrag.appendChild(renderItem(clusterItem))); listEl.appendChild(clusterFrag); if (tickerAnimation) requestAnimationFrame(() => processTickerAnimations(listEl)); scheduleOvenStatusRefresh(listEl, clusterItems); }
const frag = document.createDocumentFragment(); processedItems.forEach((item, index) => { frag.appendChild(renderItem(item, listId === 'examples-list' ? index + 1 : null));
// Add "... and X more similar" indicator after items with hidden cluster siblings const owner = item.owner || '_anonymous'; const ownerClusters = ownerHiddenCounts[owner]; if (!ownerClusters) return;
for (const key of Object.keys(ownerClusters)) { const cluster = ownerClusters[key]; if (!cluster.allItems.some(ci => ci.code === item.code)) continue; const hiddenCount = cluster.count; const ownerClean = owner.replace(/^@/, ''); const moreEl = document.createElement('div'); moreEl.className = 'owner-more-indicator'; moreEl.innerHTML = `<span class="more-count">... and ${hiddenCount} more similar from @${ownerClean}</span>`; moreEl.addEventListener('click', (event) => { event.stopPropagation(); renderClusterRows(cluster.allItems, ownerClean); }); frag.appendChild(moreEl); delete ownerClusters[key]; // Only show once per cluster break; } });
listEl.appendChild(frag); if (tickerAnimation) requestAnimationFrame(() => processTickerAnimations(listEl)); scheduleOvenStatusRefresh(listEl, processedItems); } /** * Process ticker elements - only animate if content overflows container * Adds separator and duplicate content for seamless infinite scroll */ function processTickerAnimations(container) { const tickers = container.querySelectorAll('.ticker-wrapper[data-ticker-content]'); tickers.forEach(ticker => { // Skip tickers already processed (avoids redundant layout thrashing) if (ticker.classList.contains('ticker-scroll') || ticker.classList.contains('ticker-static')) return;
const parent = ticker.closest('.example-code'); if (!parent) return;
// Get the encoded original content const encodedContent = ticker.getAttribute('data-ticker-content'); if (!encodedContent) return;
// Temporarily allow overflow to get true content width const originalOverflow = parent.style.overflow; parent.style.overflow = 'visible'; const containerWidth = parent.clientWidth; const contentWidth = ticker.scrollWidth; // Restore overflow parent.style.overflow = originalOverflow || ''; // Debug: uncomment to see measurements // console.log('Ticker:', ticker.textContent.substring(0, 30), 'container:', containerWidth, 'content:', contentWidth); // If content fits (with small buffer for rounding), no animation needed if (contentWidth <= containerWidth + 2) { ticker.classList.add('ticker-static'); return; } // Content overflows - set up infinite scroll with separator const originalContent = ticker.innerHTML; const separator = '<span class="ticker-separator"></span>'; // Duplicate content with separator for seamless loop ticker.innerHTML = originalContent + separator + originalContent + separator; // Calculate animation duration based on content length (pixels per second) const pixelsPerSecond = 40; // Adjust speed here const totalWidth = ticker.scrollWidth / 2; // Half because we duplicated const duration = totalWidth / pixelsPerSecond; ticker.style.setProperty('--ticker-duration', `${duration}s`); // Enable scrolling animation ticker.classList.add('ticker-scroll'); }); }
// ─────────────────────────────────────────── // History (per-user or per-session) tab const HISTORY_KEY = 'kidlisp-history-v1'; let historyLoading = false;
function readLocalHistory() { try { const raw = localStorage.getItem(HISTORY_KEY); if (!raw) return []; const arr = JSON.parse(raw); return Array.isArray(arr) ? arr : []; } catch (_) { return []; } }
function writeLocalHistory(items) { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(items.slice(0, 50))); } catch (_) { /* ignore */ } }
function renderHistory(items, { loggedIn } = { loggedIn: false }) { const listEl = document.getElementById('history-list'); if (!listEl) return;
// Sort by timestamp (most recent first) const sorted = [...(items || [])].sort((a, b) => (b.ts || 0) - (a.ts || 0)); renderCodeList({ listId: 'history-list', items: sorted, emptyMessage: 'No history yet.', syntaxHighlight: true, tickerAnimation: true, showTimeAgo: true, showHits: true, showOwner: true, }); }
async function loadHistory() { if (historyLoading) return; historyLoading = true; const loggedIn = !!acUser;
// Try remote history when logged in if (loggedIn) { try { const ownerParam = acHandle || acUser?.email || ''; const resp = await fetch(`${aestheticUrl}/api/tv?types=kidlisp&limit=50${ownerParam ? `&owner=${encodeURIComponent(ownerParam)}` : ''}`); const data = await resp.json(); const records = data?.media?.kidlisp || []; const mapped = records.map(r => ({ code: r.code, source: r.source || '', assetUrl: r.assetUrl || r.thumbnail || null, animatedUrl: r.animatedUrl || null, hits: r.hits || 0, owner: r.owner?.handle || r.handle || null, ts: r.when ? Date.parse(r.when) : Date.now() })); writeLocalHistory(mapped); renderHistory(mapped, { loggedIn: true }); historyLoading = false; return; } catch (e) { // Fallback to local } }
const local = readLocalHistory(); renderHistory(local, { loggedIn: false }); historyLoading = false; }
function addHistoryEntry(entry) { const now = Date.now(); const item = { code: entry.code || null, source: entry.source || '', assetUrl: entry.assetUrl || null, hits: entry.hits || 0, ts: entry.ts || now }; const local = readLocalHistory(); // De-dup by code (if available) or source const filtered = local.filter(it => (item.code && it.code === item.code) ? false : (item.source && it.source === item.source ? false : true)); filtered.unshift(item); writeLocalHistory(filtered); // If history subtab is active, refresh UI const historySubtabActive = listTab?.classList.contains('active') && currentListSubtab === 'history'; if (historySubtabActive) { renderHistory(filtered, { loggedIn: !!acUser }); } }
// ═══════════════════════════════════════════════════════════════════════════ // RECENT FEED - Shows everyone's recent $codes (auto-refreshing) // ═══════════════════════════════════════════════════════════════════════════ let recentLoading = false; let recentItems = []; // Store current items to detect new ones let recentAutoRefreshInterval = null; function renderRecent(items, { showNewIndicator = false, newCount = 0 } = {}) { const listEl = document.getElementById('recent-list'); const newIndicator = document.getElementById('recent-new-indicator'); if (!listEl) return; // Handle new items indicator if (newIndicator) { if (showNewIndicator && newCount > 0) { newIndicator.textContent = `↑ ${newCount} new $code${newCount > 1 ? 's' : ''}`; newIndicator.classList.remove('hidden'); newIndicator.onclick = () => { listEl.scrollTop = 0; newIndicator.classList.add('hidden'); }; } else { newIndicator.classList.add('hidden'); } }
renderCodeList({ listId: 'recent-list', items: items || [], emptyMessage: 'No recent codes yet.', syntaxHighlight: true, tickerAnimation: true, showOwner: true, showTimeAgo: true, showHits: true, groupByOwner: true, // Group similar items by author for diversity maxPerOwner: 2, // Show max 2 per author, then "... and X more" }); }
async function loadRecent({ silent = false } = {}) { if (recentLoading) return; recentLoading = true;
try { const resp = await fetch(`${aestheticUrl}/api/tv?types=kidlisp&limit=50`); const data = await resp.json(); const records = data?.media?.kidlisp || []; const mapped = records.map(r => ({ code: r.code, source: r.source || '', assetUrl: r.assetUrl || r.thumbnail || null, animatedUrl: r.animatedUrl || null, hits: r.hits || 0, owner: r.owner?.handle || r.handle || null, ts: r.when ? Date.parse(r.when) : Date.now() })); mapped.sort((a, b) => (b.ts || 0) - (a.ts || 0)); // Check for new items (compare by code) const oldCodes = new Set(recentItems.map(i => i.code)); const newItems = mapped.filter(i => i.code && !oldCodes.has(i.code)); const hasNewItems = silent && recentItems.length > 0 && newItems.length > 0; recentItems = mapped; renderRecent(mapped, { showNewIndicator: hasNewItems, newCount: newItems.length }); recentLoaded = true; } catch (e) { console.error('Failed to load recent:', e); if (!silent) renderRecent([]); } recentLoading = false; } // Auto-refresh recent every 30 seconds when tab is active function startRecentAutoRefresh() { if (recentAutoRefreshInterval) return; recentAutoRefreshInterval = setInterval(() => { const recentActive = currentListSubtab === 'recent' && listTab?.classList.contains('active'); if (recentActive && !document.hidden) { loadRecent({ silent: true }); } }, 30000); } function stopRecentAutoRefresh() { if (recentAutoRefreshInterval) { clearInterval(recentAutoRefreshInterval); recentAutoRefreshInterval = null; } } // Start auto-refresh when page loads startRecentAutoRefresh();
// ═══════════════════════════════════════════════════════════════════════════ // VISUALIZATION SYSTEM - text/tree/space views // Shows EXECUTION PATH from the actual KidLisp interpreter // ═══════════════════════════════════════════════════════════════════════════ const vizSystem = { currentView: 'text', ast: null, executionTrace: [], // Real trace from KidLisp interpreter traceEnabled: false, isPlaying: false, isPaused: false, frameCount: 0, // Increments each frame when playing // 3D Pills visualization state pills: { canvas: null, ctx: null, particles: [], constraints: [], lastCode: '', animationId: null, // 3D rotation state rotation: { x: 0, y: 0, z: 0 }, targetRotation: { x: 0, y: 0, z: 0 }, autoRotate: true, zoom: 1, pan: { x: 0, y: 0 }, dragging: false, dragStart: { x: 0, y: 0 }, lastMouse: { x: 0, y: 0 } } }; // KidLisp syntax colors - matching the interpreter's getTokenColor const syntaxColors = { comment: '#888888', string: '#FFD700', // yellow number: '#FFB6C1', // pink timing: '#FFFF00', // yellow for timing like "1s", "2s..." timingActive: '#00FF00', // lime when active function: '#40E0D0', // turquoise/cyan for functions special: '#FF69B4', // hotpink for special forms operator: '#FF69B4', // hotpink for operators paren: '#C0C0C0', // silver for parentheses symbol: '#87CEEB', // skyblue for symbols dollarCode: '#32CD32', // limegreen for $codes hashCode: '#FFA500', // orange for #codes colorName: null, // will be actual color default: '#D4D4D4' // light gray }; // Get color for a token using KidLisp-style syntax highlighting function getTokenSyntaxColor(token, prevToken, nextToken) { if (!token) return syntaxColors.default; // Comments if (token.startsWith(';')) return syntaxColors.comment; // Strings if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'"))) { return syntaxColors.string; } // $codes if (token.startsWith('$') && /^\$[0-9A-Za-z]+$/.test(token)) { return syntaxColors.dollarCode; } // #codes (painting refs) if (token.startsWith('#') && /^#[0-9A-Za-z]{1,8}$/.test(token)) { return syntaxColors.hashCode; } // Timing patterns if (/^\d*\.?\d+[sf]!?\.{0,3}$/.test(token)) { return syntaxColors.timing; } // Numbers if (/^-?\d+(\.\d+)?$/.test(token)) { return syntaxColors.number; } // Parentheses if (token === '(' || token === ')') { return syntaxColors.paren; } // Special forms const specialForms = ['def', 'if', 'cond', 'later', 'once', 'lambda', 'let', 'do', 'repeat', 'bunch', 'tap', 'drag']; if (specialForms.includes(token)) { return syntaxColors.special; } // Operators const operators = ['+', '-', '*', '/', '%', 'mod', '=', '>', '<', '>=', '<=', 'abs', 'sqrt', 'min', 'max', 'and', 'or', 'not']; if (operators.includes(token)) { return syntaxColors.operator; } // Functions (after opening paren) if (prevToken === '(') { return syntaxColors.function; } // Known graphics functions const graphicsFns = ['wipe', 'ink', 'line', 'box', 'circle', 'write', 'paste', 'stamp', 'point', 'poly', 'shape', 'flood']; if (graphicsFns.includes(token)) { return syntaxColors.function; } // CSS color names - return actual color const cssColorNames = ['red', 'blue', 'green', 'yellow', 'orange', 'purple', 'pink', 'cyan', 'magenta', 'white', 'black', 'gray', 'lime', 'teal', 'navy', 'maroon']; if (cssColorNames.includes(token.toLowerCase())) { return token.toLowerCase(); } return syntaxColors.symbol; } // Tokenize a line of KidLisp for syntax highlighting function tokenizeLine(line) { const tokens = []; let i = 0; while (i < line.length) { // Whitespace - preserve it if (/\s/.test(line[i])) { let ws = ''; while (i < line.length && /\s/.test(line[i])) { ws += line[i]; i++; } tokens.push(ws); continue; } // Comment - rest of line if (line[i] === ';') { tokens.push(line.substring(i)); break; } // String if (line[i] === '"' || line[i] === "'") { const quote = line[i]; let str = quote; i++; while (i < line.length && line[i] !== quote) { if (line[i] === '\\' && i + 1 < line.length) { str += line[i] + line[i + 1]; i += 2; } else { str += line[i]; i++; } } if (i < line.length) { str += line[i]; i++; } tokens.push(str); continue; } // Parentheses if (line[i] === '(' || line[i] === ')') { tokens.push(line[i]); i++; continue; } // Word/symbol/number let word = ''; while (i < line.length && !/[\s()\[\]{}]/.test(line[i])) { word += line[i]; i++; } if (word) { tokens.push(word); } } return tokens; } // Node type colors matching the plan const nodeColors = { 'function': '#9370DB', 'number': '#FF9F43', 'string': '#2ECC71', 'symbol': '#4ECDC4', 'list': '#CD5C9B', 'special': '#F8B500', 'comment': '#888888', 'error': '#E74C3C', 'root': '#FFFFFF' }; // Parse KidLisp code into AST for visualization function parseToAST(code) { const tokens = []; let i = 0; while (i < code.length) { // Skip whitespace if (/\s/.test(code[i])) { i++; continue; } // Comments if (code[i] === ';') { let comment = ''; while (i < code.length && code[i] !== '\n') comment += code[i++]; tokens.push({ type: 'comment', value: comment, pos: i }); continue; } // Strings if (code[i] === '"') { let str = '"'; i++; while (i < code.length && code[i] !== '"') { if (code[i] === '\\' && i + 1 < code.length) str += code[i++]; str += code[i++]; } if (i < code.length) str += code[i++]; tokens.push({ type: 'string', value: str, pos: i }); continue; } // Parens if (code[i] === '(') { tokens.push({ type: 'lparen', pos: i }); i++; continue; } if (code[i] === ')') { tokens.push({ type: 'rparen', pos: i }); i++; continue; } // Numbers or symbols let word = ''; const start = i; while (i < code.length && !/[\s()";\[\]]/.test(code[i])) word += code[i++]; if (word) { const isNum = /^-?\d+(\.\d+)?$/.test(word); tokens.push({ type: isNum ? 'number' : 'symbol', value: word, pos: start }); } } // Parse tokens into tree let ti = 0; function parseExpr() { if (ti >= tokens.length) return null; const tok = tokens[ti]; if (tok.type === 'lparen') { ti++; const children = []; while (ti < tokens.length && tokens[ti].type !== 'rparen') { const child = parseExpr(); if (child) children.push(child); } ti++; // skip rparen const nodeType = children.length > 0 && children[0].type === 'symbol' ? 'function' : 'list'; return { type: nodeType, children, value: children[0]?.value || 'list' }; } ti++; return { type: tok.type, value: tok.value, children: [] }; } const expressions = []; while (ti < tokens.length) { const expr = parseExpr(); if (expr) expressions.push(expr); } return { type: 'root', value: 'program', children: expressions }; } // Initialize visualization tab switching function initVizTabs() { const tabs = document.querySelectorAll('.viz-tab'); const containers = document.querySelectorAll('.viz-container'); tabs.forEach(tab => { tab.addEventListener('click', () => { const vizType = tab.dataset.viz; // Update tab states tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); // Update container visibility containers.forEach(c => c.classList.remove('active')); const container = document.getElementById(`viz-${vizType}`); if (container) { container.classList.add('active'); } const previousView = vizSystem.currentView; vizSystem.currentView = vizType; // Initialize visualization if needed (delay to let CSS take effect) setTimeout(() => { if (vizType === 'pills') initPillsView(); // If switching TO pills from text, re-run code with tracing enabled if (previousView === 'text' && vizType === 'pills') { // Re-send code to iframe with trace enabled if (isPlaying && editor) { const code = editor.getValue(); if (code) { console.log('💊 [PILLS] Re-running code with trace enabled'); const codeId = localStorage.getItem('kidlisp-current-code') || null; sendToIframe({ type: 'kidlisp-reload', code: code, codeId: codeId, enableTrace: true }); } } } // Update with current code updateVisualization(); }, 50); }); }); } // Initialize Tree View (Canvas 2D) function initTreeView() { const canvas = document.getElementById('tree-canvas'); console.log('🌳 [TREE] initTreeView called, canvas:', canvas); if (!canvas) return; // Only initialize once if (vizSystem.tree.ctx) { console.log('🌳 [TREE] Already initialized, resizing'); // Already initialized, just resize and render with DPR requestAnimationFrame(() => { const container = document.getElementById('viz-tree'); if (container) { const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; vizSystem.tree.dpr = dpr; console.log('🌳 [TREE] Container rect:', rect, 'dpr:', dpr); canvas.width = (rect.width || 400) * dpr; canvas.height = Math.max(100, rect.height || 300) * dpr; canvas.style.width = (rect.width || 400) + 'px'; canvas.style.height = Math.max(100, rect.height || 300) + 'px'; vizSystem.tree.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); renderTree(); } }); return; } console.log('🌳 [TREE] First init, creating context'); vizSystem.tree.canvas = canvas; vizSystem.tree.ctx = canvas.getContext('2d'); vizSystem.tree.dpr = window.devicePixelRatio || 1; // Handle resize with devicePixelRatio for crisp rendering function resizeCanvas() { requestAnimationFrame(() => { const container = document.getElementById('viz-tree'); if (container) { const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; vizSystem.tree.dpr = dpr; // Set actual canvas size scaled by DPR canvas.width = (rect.width || 400) * dpr; canvas.height = Math.max(100, rect.height || 300) * dpr; // Set display size via CSS canvas.style.width = (rect.width || 400) + 'px'; canvas.style.height = Math.max(100, rect.height || 300) + 'px'; // Scale context to match DPR vizSystem.tree.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); renderTree(); } }); } resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Pan with mouse drag let dragging = false; let lastX, lastY; canvas.addEventListener('mousedown', (e) => { dragging = true; lastX = e.clientX; lastY = e.clientY; canvas.style.cursor = 'grabbing'; }); let treeRafPending = false; canvas.addEventListener('mousemove', (e) => { if (!dragging) return; vizSystem.tree.pan.x += e.clientX - lastX; vizSystem.tree.pan.y += e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; if (!treeRafPending) { treeRafPending = true; requestAnimationFrame(() => { treeRafPending = false; renderTree(); }); } }); canvas.addEventListener('mouseup', () => { dragging = false; canvas.style.cursor = 'grab'; }); canvas.addEventListener('mouseleave', () => { dragging = false; canvas.style.cursor = 'grab'; }); // Zoom with wheel canvas.addEventListener('wheel', (e) => { e.preventDefault(); const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; vizSystem.tree.zoom = Math.max(0.1, Math.min(5, vizSystem.tree.zoom * zoomFactor)); renderTree(); }); canvas.style.cursor = 'grab'; } // Layout tree nodes using simple algorithm function layoutTree(node, depth = 0, index = 0, siblingCount = 1) { if (!node) return []; const nodeWidth = 80; const nodeHeight = 32; const levelHeight = 60; const minSpacing = 20; // Calculate positions recursively let nodes = []; let childNodes = []; let totalWidth = 0; if (node.children && node.children.length > 0) { node.children.forEach((child, i) => { const childLayout = layoutTree(child, depth + 1, i, node.children.length); childNodes = childNodes.concat(childLayout); }); // Get width of all children const childXPositions = childNodes.filter(n => n.depth === depth + 1).map(n => n.x); if (childXPositions.length > 0) { totalWidth = Math.max(...childXPositions) - Math.min(...childXPositions); } } // Position this node centered above its children const childrenAtNextLevel = childNodes.filter(n => n.depth === depth + 1); let x; if (childrenAtNextLevel.length > 0) { const minX = Math.min(...childrenAtNextLevel.map(n => n.x)); const maxX = Math.max(...childrenAtNextLevel.map(n => n.x)); x = (minX + maxX) / 2; } else { x = index * (nodeWidth + minSpacing); } const thisNode = { ...node, x, y: depth * levelHeight, width: nodeWidth, height: nodeHeight, depth, color: nodeColors[node.type] || nodeColors.symbol }; return [thisNode, ...childNodes]; } // Render Tree View - Shows EXECUTION PATH with source code lines function renderTree() { const { canvas, ctx, pan, zoom, dpr } = vizSystem.tree; if (!ctx || !canvas) return; // Use display dimensions (not canvas buffer dimensions) const width = canvas.width / (dpr || 1); const height = canvas.height / (dpr || 1); // Dark background ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, width, height); // Get current code from editor const code = editor?.getValue() || ''; const lines = code.split('\n'); // If we have an execution trace, show it const trace = vizSystem.executionTrace; if (!trace || trace.length === 0) { // Show source code lines with syntax highlighting ctx.font = '13px monospace'; ctx.textAlign = 'left'; const lineHeight = 20; const startY = 40 + pan.y; const startX = 60 + pan.x; // Header ctx.fillStyle = '#4ECDC4'; ctx.fillText('EXECUTION PATH', 20, 20); ctx.fillStyle = '#666'; ctx.fillText('(run code to see trace)', 150, 20); // Draw line numbers and code with proper syntax highlighting lines.forEach((line, lineIdx) => { const y = startY + lineIdx * lineHeight * zoom; if (y < 0 || y > height) return; // Line number ctx.fillStyle = '#666'; ctx.fillText(String(lineIdx + 1).padStart(3, ' '), 10, y); // Tokenize and color each token const tokens = tokenizeLine(line); let x = startX; tokens.forEach((token, i) => { const prevToken = i > 0 ? tokens[i-1] : null; const nextToken = i < tokens.length - 1 ? tokens[i+1] : null; const color = getTokenSyntaxColor(token, prevToken, nextToken); ctx.fillStyle = color; ctx.fillText(token, x, y); x += ctx.measureText(token).width; }); // Draw connecting line for nested expressions const depth = (line.match(/^\s*/)?.[0].length || 0) / 2; if (depth > 0) { ctx.strokeStyle = 'rgba(147, 112, 219, 0.2)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(startX - 10 - depth * 8, y - lineHeight / 2); ctx.lineTo(startX - 10 - depth * 8, y + 4); ctx.lineTo(startX - 5, y + 4); ctx.stroke(); } }); return; } // We have a trace! Show execution flow const lineHeight = 22; const startY = 50 + pan.y; const startX = 20 + pan.x; // Header ctx.fillStyle = '#4ECDC4'; ctx.font = 'bold 14px monospace'; ctx.fillText(`EXECUTION TRACE (${trace.length} steps)`, 20, 25); // Type colors const typeColors = { 'enter': '#9370DB', 'exit': '#2ECC71', 'function-call': '#FF9F43', 'special-form': '#CD5C9B', 'number': '#FF9F43', 'string': '#2ECC71', 'symbol': '#4ECDC4' }; // Draw trace entries ctx.font = '11px monospace'; let displayIndex = 0; trace.forEach((entry, i) => { const y = startY + displayIndex * lineHeight * zoom; if (y < 0 || y > height) { displayIndex++; return; } const indent = entry.depth * 16; const x = startX + indent; // Draw depth line if (entry.depth > 0) { ctx.strokeStyle = 'rgba(147, 112, 219, 0.3)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(startX + indent - 8, y - lineHeight); ctx.lineTo(startX + indent - 8, y + 4); ctx.lineTo(startX + indent - 2, y + 4); ctx.stroke(); } // Entry type indicator const typeColor = typeColors[entry.type] || typeColors[entry.exprType] || '#AAAAAA'; if (entry.type === 'enter') { // Arrow for entering expression ctx.fillStyle = typeColor; ctx.fillText('→', x, y); ctx.fillText(String(entry.expr), x + 16, y); } else if (entry.type === 'exit') { // Arrow for exiting with result ctx.fillStyle = '#2ECC71'; ctx.fillText('←', x, y); ctx.fillStyle = '#888'; const result = entry.result !== undefined ? ` = ${entry.result}` : ''; ctx.fillText(`${entry.expr}${result}`, x + 16, y); } displayIndex++; }); } // Initialize Space View (Canvas 2D with 3D projection) function initSpaceView() { const canvas = document.getElementById('space-canvas'); console.log('🌌 [SPACE] initSpaceView called, canvas:', canvas); if (!canvas) return; // Only initialize once if (vizSystem.space.initialized) { console.log('🌌 [SPACE] Already initialized, resizing'); // Already initialized, just resize and render with DPR requestAnimationFrame(() => { const container = document.getElementById('viz-space'); if (container) { const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; vizSystem.space.dpr = dpr; console.log('🌌 [SPACE] Container rect:', rect, 'dpr:', dpr); canvas.width = (rect.width || 400) * dpr; canvas.height = Math.max(100, rect.height || 300) * dpr; canvas.style.width = (rect.width || 400) + 'px'; canvas.style.height = Math.max(100, rect.height || 300) + 'px'; const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); renderSpace(); } }); return; } console.log('🌌 [SPACE] First init, setting up'); vizSystem.space.canvas = canvas; vizSystem.space.initialized = true; vizSystem.space.dpr = window.devicePixelRatio || 1; // Handle resize - delay to ensure container is visible function resizeCanvas() { requestAnimationFrame(() => { const container = document.getElementById('viz-space'); if (container) { const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; vizSystem.space.dpr = dpr; // Set actual canvas size scaled by DPR canvas.width = (rect.width || 400) * dpr; canvas.height = Math.max(100, rect.height || 300) * dpr; // Set display size via CSS canvas.style.width = (rect.width || 400) + 'px'; canvas.style.height = Math.max(100, rect.height || 300) + 'px'; // Scale context to match DPR const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); renderSpace(); } }); } resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Camera controls let dragging = false; let lastX, lastY; canvas.addEventListener('mousedown', (e) => { dragging = true; lastX = e.clientX; lastY = e.clientY; }); canvas.addEventListener('mousemove', (e) => { if (!dragging) return; vizSystem.space.camera.x += (e.clientX - lastX) * 1; vizSystem.space.camera.y -= (e.clientY - lastY) * 1; lastX = e.clientX; lastY = e.clientY; renderSpace(); }); canvas.addEventListener('mouseup', () => dragging = false); canvas.addEventListener('mouseleave', () => dragging = false); canvas.addEventListener('wheel', (e) => { e.preventDefault(); vizSystem.space.camera.z += e.deltaY * 0.5; vizSystem.space.camera.z = Math.max(-100, Math.min(300, vizSystem.space.camera.z)); renderSpace(); }); } // Simple 3D point projection - retro style with wider FOV function project3D(x, y, z, camera, width, height) { const cx = camera.x; const cy = camera.y; const cz = camera.z; // Wider field of view for retro feel const fov = 400; const scale = fov / (z + cz + 10); const px = (x - cx) * scale + width / 2; const py = (y - cy) * scale + height / 2; return { x: px, y: py, scale: scale / 50, depth: z + cz }; } // Layout nodes in 3D space - spread out more function layout3D(node, depth = 0, index = 0, siblingCount = 1) { if (!node) return []; const spacing = 80; // Much wider spacing const depthSpacing = 60; // More depth between levels let nodes = []; // Position based on depth (z), sibling index (x), and type grouping (y) const x = (index - (siblingCount - 1) / 2) * spacing; const y = 0; const z = depth * depthSpacing; nodes.push({ ...node, x, y, z, color: nodeColors[node.type] || nodeColors.symbol }); if (node.children) { node.children.forEach((child, i) => { const childNodes = layout3D(child, depth + 1, i, node.children.length); // Offset children relative to parent childNodes.forEach(cn => { cn.x += x; }); nodes = nodes.concat(childNodes); }); } return nodes; } // Render Space View - Retro wireframe showing EXECUTION FLOW in 3D function renderSpace() { const { canvas, camera, dpr } = vizSystem.space; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Use display dimensions (not canvas buffer dimensions) const width = canvas.width / (dpr || 1); const height = canvas.height / (dpr || 1); // Pure black background ctx.fillStyle = '#000'; ctx.fillRect(0, 0, width, height); // Draw retro grid floor ctx.strokeStyle = '#1a3a1a'; ctx.lineWidth = 1; const gridSize = 50; const gridExtent = 400; // Horizontal grid lines (going into distance) for (let z = 0; z <= gridExtent; z += gridSize) { const p1 = project3D(-gridExtent, 100, z, camera, width, height); const p2 = project3D(gridExtent, 100, z, camera, width, height); if (p1.depth > 0 && p2.depth > 0) { ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } } // Vertical grid lines (left to right) for (let x = -gridExtent; x <= gridExtent; x += gridSize) { const p1 = project3D(x, 100, 0, camera, width, height); const p2 = project3D(x, 100, gridExtent, camera, width, height); if (p1.depth > 0 && p2.depth > 0) { ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } } // Get execution trace const trace = vizSystem.executionTrace; if (!trace || trace.length === 0) { // Draw "WAITING FOR TRACE" text ctx.fillStyle = '#0f0'; ctx.font = '16px monospace'; ctx.textAlign = 'center'; ctx.fillText('[ WAITING FOR TRACE ]', width / 2, height / 2); ctx.fillStyle = '#888'; ctx.font = '12px monospace'; ctx.fillText('run code to see execution flow', width / 2, height / 2 + 24); // Draw corner brackets drawCornerBrackets(ctx, width, height); return; } // Wireframe colors by type - neon retro palette const wireColors = { 'enter': '#ff00ff', // Magenta - entering expression 'exit': '#00ff00', // Green - returning result 'function-call': '#ffff00', // Yellow 'special-form': '#ff8800', // Orange 'number': '#ffff00', // Yellow 'string': '#00ff00', // Green 'symbol': '#00ffff', // Cyan }; // Layout trace entries in 3D space // X = horizontal spread based on expression // Y = vertical (up is shallower depth) // Z = time/sequence (deeper = later in trace) const nodes = []; const spacing = 60; const depthSpacing = 40; let maxDepth = 0; trace.forEach((entry, i) => { // Position based on sequence (z), depth (y), and some horizontal spread (x) const x = (entry.depth - 2) * spacing; const y = -entry.depth * 30; // Higher = shallower call depth const z = i * depthSpacing; maxDepth = Math.max(maxDepth, entry.depth); nodes.push({ ...entry, x, y, z, nodeIndex: i }); }); // Sort by z for painter's algorithm (far to near) const sortedNodes = [...nodes].sort((a, b) => b.z - a.z); // Draw connection lines between sequential entries ctx.lineWidth = 1; for (let i = 1; i < nodes.length; i++) { const prev = nodes[i - 1]; const curr = nodes[i]; const p1 = project3D(prev.x, prev.y, prev.z, camera, width, height); const p2 = project3D(curr.x, curr.y, curr.z, camera, width, height); if (p1.depth > 0 && p2.depth > 0) { // Color based on transition const isEnter = curr.type === 'enter'; ctx.strokeStyle = isEnter ? 'rgba(255, 0, 255, 0.4)' : 'rgba(0, 255, 0, 0.4)'; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } } // Draw nodes sortedNodes.forEach(node => { const p = project3D(node.x, node.y, node.z, camera, width, height); if (p.depth <= 0) return; const size = Math.max(4, Math.min(20, 12 * p.scale)); const color = wireColors[node.type] || wireColors[node.exprType] || '#00ffff'; ctx.strokeStyle = color; ctx.lineWidth = 2; // Shape based on type if (node.type === 'enter') { // Right-pointing arrow/chevron for enter ctx.beginPath(); ctx.moveTo(p.x - size, p.y - size); ctx.lineTo(p.x + size, p.y); ctx.lineTo(p.x - size, p.y + size); ctx.stroke(); } else if (node.type === 'exit') { // Left-pointing arrow for exit ctx.beginPath(); ctx.moveTo(p.x + size, p.y - size); ctx.lineTo(p.x - size, p.y); ctx.lineTo(p.x + size, p.y + size); ctx.stroke(); } else { // Diamond for others ctx.beginPath(); ctx.moveTo(p.x, p.y - size); ctx.lineTo(p.x + size, p.y); ctx.lineTo(p.x, p.y + size); ctx.lineTo(p.x - size, p.y); ctx.closePath(); ctx.stroke(); } // Label if (size > 6) { ctx.fillStyle = color; ctx.font = `${Math.max(8, size * 0.8)}px monospace`; ctx.textAlign = 'center'; const label = String(node.expr).substring(0, 12); ctx.fillText(label, p.x, p.y + size + 10); } }); // Scanline overlay ctx.fillStyle = 'rgba(0,0,0,0.03)'; for (let y = 0; y < height; y += 3) { ctx.fillRect(0, y, width, 1); } // Corner brackets drawCornerBrackets(ctx, width, height); // Status display ctx.fillStyle = '#0f0'; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.fillText(`TRACE: ${trace.length} steps`, 25, 35); ctx.fillText(`DEPTH: ${maxDepth}`, 25, 47); } function drawCornerBrackets(ctx, width, height) { ctx.strokeStyle = '#0f0'; ctx.lineWidth = 1; const m = 20; const b = 30; ctx.beginPath(); ctx.moveTo(m, m + b); ctx.lineTo(m, m); ctx.lineTo(m + b, m); ctx.stroke(); ctx.beginPath(); ctx.moveTo(width - m - b, m); ctx.lineTo(width - m, m); ctx.lineTo(width - m, m + b); ctx.stroke(); ctx.beginPath(); ctx.moveTo(m, height - m - b); ctx.lineTo(m, height - m); ctx.lineTo(m + b, height - m); ctx.stroke(); ctx.beginPath(); ctx.moveTo(width - m - b, height - m); ctx.lineTo(width - m, height - m); ctx.lineTo(width - m, height - m - b); ctx.stroke(); } // Update visualization with current code function updateVisualization() { // Get code from Monaco editor let code = ''; if (typeof monaco !== 'undefined' && window.editor) { code = window.editor.getValue(); } // Fallback: try to get code from any Monaco model if (!code && typeof monaco !== 'undefined') { const models = monaco.editor.getModels(); if (models.length > 0) { code = models[0].getValue(); } } if (code) { vizSystem.ast = parseToAST(code); } // Pills is the only visualization view now if (vizSystem.currentView === 'pills') { // Pills has its own animation loop, just ensure it's initialized if (!vizSystem.pills.initialized) initPillsView(); } } // Bead type styles (Calder-inspired palette) - used by pills too const BEAD_STYLES = { state: { color: '#DC3C3C', symbol: '○' }, // red - wipe, ink, def draw: { color: '#3C64DC', symbol: '●' }, // blue - line, box, circle, point effect: { color: '#DCB428', symbol: '◆' }, // yellow - scroll, flip, blur, zoom timing: { color: '#B450B4', symbol: '◇' }, // purple - 1s, 0.5s, later branch: { color: '#28B478', symbol: '◈' }, // green - if, ? loop: { color: '#FF8C28', symbol: '⬡' }, // orange - repeat atom: { color: '#969696', symbol: '·' }, // gray - numbers, strings }; // Command type classification const COMMAND_TYPES = { 'wipe': 'state', 'ink': 'state', 'def': 'state', 'let': 'state', 'line': 'draw', 'box': 'draw', 'circle': 'draw', 'point': 'draw', 'write': 'draw', 'oval': 'draw', 'tri': 'draw', 'draw': 'draw', 'scroll': 'effect', 'flip': 'effect', 'blur': 'effect', 'zoom': 'effect', 'pan': 'effect', 'coat': 'effect', 'fade': 'effect', 'noise': 'effect', '1s': 'timing', '0.5s': 'timing', '0.3s': 'timing', '2s': 'timing', 'later': 'timing', 'every': 'timing', 'if': 'branch', '?': 'branch', 'cond': 'branch', 'repeat': 'loop', 'loop': 'loop', 'for': 'loop', }; // Color names for detecting wipe colors const COLOR_NAMES = ['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'cyan', 'magenta', 'white', 'black', 'gray', 'navy', 'brown', 'pink', 'rainbow']; // ═══════════════════════════════════════════════════════════════════════════ // 3D PILLS VISUALIZATION - Matter floating in space // ═══════════════════════════════════════════════════════════════════════════ // 3D Pill particle with physics class PillParticle { constructor(x, y, z, pill, index) { this.x = x; this.y = y; this.z = z; this.oldX = x; this.oldY = y; this.oldZ = z; this.vx = (Math.random() - 0.5) * 0.5; this.vy = (Math.random() - 0.5) * 0.5; this.vz = (Math.random() - 0.5) * 0.5; this.pill = pill; this.index = index; this.pinned = false; this.flash = 0; // Ideal position for soft constraint this.idealX = x; this.idealY = y; this.idealZ = z; // Pill dimensions based on text this.width = Math.max(60, pill.name.length * 10 + 20); this.height = 24; this.depth = 16; } update(damping = 0.96) { if (this.pinned) return; const vx = (this.x - this.oldX) * damping; const vy = (this.y - this.oldY) * damping; const vz = (this.z - this.oldZ) * damping; this.oldX = this.x; this.oldY = this.y; this.oldZ = this.z; this.x += vx; this.y += vy; this.z += vz; // Soft spring back to ideal position const dx = this.idealX - this.x; const dy = this.idealY - this.y; const dz = this.idealZ - this.z; this.x += dx * 0.02; this.y += dy * 0.02; this.z += dz * 0.02; // Decay flash if (this.flash > 0) this.flash *= 0.9; } applyForce(fx, fy, fz) { if (this.pinned) return; this.x += fx; this.y += fy; this.z += fz; } } // Spring constraint between pills class PillConstraint { constructor(p1, p2, restLength, stiffness = 0.1) { this.p1 = p1; this.p2 = p2; this.restLength = restLength; this.stiffness = stiffness; } solve() { const dx = this.p2.x - this.p1.x; const dy = this.p2.y - this.p1.y; const dz = this.p2.z - this.p1.z; const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.001; const diff = (dist - this.restLength) / dist; const offsetX = dx * diff * this.stiffness; const offsetY = dy * diff * this.stiffness; const offsetZ = dz * diff * this.stiffness; if (!this.p1.pinned) { this.p1.x += offsetX * 0.5; this.p1.y += offsetY * 0.5; this.p1.z += offsetZ * 0.5; } if (!this.p2.pinned) { this.p2.x -= offsetX * 0.5; this.p2.y -= offsetY * 0.5; this.p2.z -= offsetZ * 0.5; } } } // Project 3D point to 2D screen with rotation (for pills) function projectPill3D(x, y, z, rotation, zoom, cx, cy) { // Apply rotation around Y axis (horizontal spin) const cosY = Math.cos(rotation.y); const sinY = Math.sin(rotation.y); let x1 = x * cosY - z * sinY; let z1 = x * sinY + z * cosY; // Apply rotation around X axis (vertical tilt) const cosX = Math.cos(rotation.x); const sinX = Math.sin(rotation.x); let y1 = y * cosX - z1 * sinX; let z2 = y * sinX + z1 * cosX; // Perspective projection const fov = 400; const scale = fov / (fov + z2) * zoom; return { x: cx + x1 * scale, y: cy + y1 * scale, z: z2, scale: scale }; } // Initialize Pills View function initPillsView() { const canvas = document.getElementById('pills-canvas'); console.log('💊 [PILLS] initPillsView called, canvas:', canvas); if (!canvas) return; const pills = vizSystem.pills; pills.canvas = canvas; pills.ctx = canvas.getContext('2d'); pills.dpr = window.devicePixelRatio || 1; function resizeCanvas() { const container = document.getElementById('viz-pills'); if (!container) return; const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; pills.dpr = dpr; const w = rect.width || 400; const h = Math.max(100, rect.height || 300); canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; pills.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); rebuildPillsPhysics(); } resizeCanvas(); // ResizeObserver for panel resize const resizeObserver = new ResizeObserver(() => resizeCanvas()); const container = document.getElementById('viz-pills'); if (container) resizeObserver.observe(container); window.addEventListener('resize', resizeCanvas); // Mouse interaction - drag to rotate, click pills, scroll zoom canvas.addEventListener('mousedown', (e) => { const rect = canvas.getBoundingClientRect(); pills.dragging = true; pills.autoRotate = false; pills.lastMouse = { x: e.clientX, y: e.clientY }; // Check if clicking on a pill (simplified - uses 2D projection) const mx = e.clientX - rect.left; const my = e.clientY - rect.top; const cx = (canvas.width / pills.dpr) / 2; const cy = (canvas.height / pills.dpr) / 2; for (const p of pills.particles) { const proj = projectPill3D(p.x, p.y, p.z, pills.rotation, pills.zoom, cx, cy); const dx = mx - proj.x; const dy = my - proj.y; if (Math.abs(dx) < p.width * proj.scale / 2 && Math.abs(dy) < p.height * proj.scale / 2) { // Flash the clicked pill p.flash = 1.5; // Bounce it p.applyForce((Math.random() - 0.5) * 20, -15, (Math.random() - 0.5) * 20); return; } } }); canvas.addEventListener('mousemove', (e) => { if (pills.dragging) { const dx = e.clientX - pills.lastMouse.x; const dy = e.clientY - pills.lastMouse.y; pills.targetRotation.y += dx * 0.01; pills.targetRotation.x += dy * 0.01; // Clamp X rotation (don't flip upside down) pills.targetRotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pills.targetRotation.x)); pills.lastMouse = { x: e.clientX, y: e.clientY }; } }); canvas.addEventListener('mouseup', () => { pills.dragging = false; }); canvas.addEventListener('mouseleave', () => { pills.dragging = false; }); // Double-click to toggle auto-rotate canvas.addEventListener('dblclick', () => { pills.autoRotate = !pills.autoRotate; }); // Zoom with scroll canvas.addEventListener('wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 0.92 : 1.08; pills.zoom = Math.max(0.3, Math.min(3, pills.zoom * delta)); }); pills.initialized = true; startPillsPhysicsLoop(); } // Build/rebuild 3D pills from code function rebuildPillsPhysics() { const pills = vizSystem.pills; const { canvas, dpr } = pills; if (!canvas) return; const code = editor?.getValue() || ''; const pillData = parseToPills(code); const n = pillData.length; const width = canvas.width / (dpr || 1); const height = canvas.height / (dpr || 1); // Arrange pills in a horizontal line (like reading code left to right) // with slight depth variation for visual interest const spacing = Math.min(100, (width * 0.8) / Math.max(1, n)); const startX = -(n - 1) * spacing / 2; // Preserve old particle states const oldParticles = pills.particles; pills.particles = []; pills.constraints = []; for (let i = 0; i < n; i++) { // Horizontal line arrangement - left to right like code const x = startX + i * spacing; const y = 0; // All on same horizontal plane const z = Math.sin(i * 0.5) * 15; // Slight wave for depth const p = new PillParticle(x, y, z, pillData[i], i); // Preserve momentum from old particle if (oldParticles[i]) { const old = oldParticles[i]; p.oldX = p.x - (old.x - old.oldX) * 0.5; p.oldY = p.y - (old.y - old.oldY) * 0.5; p.oldZ = p.z - (old.z - old.oldZ) * 0.5; p.flash = old.flash; } pills.particles.push(p); } // Create spring constraints between sequential pills only (not looping) for (let i = 0; i < n - 1; i++) { const p1 = pills.particles[i]; const p2 = pills.particles[i + 1]; pills.constraints.push(new PillConstraint(p1, p2, spacing, 0.2)); } pills.lastCode = code; } // Flash pills from execution trace function flashPillsFromTrace(trace) { const particles = vizSystem.pills.particles; if (!particles.length || !trace?.length) return; for (const entry of trace.slice(-10)) { if (entry.type === 'enter' || entry.type === 'function-call') { const exprStr = String(entry.expr || '').toLowerCase(); for (const p of particles) { const pillName = p.pill.name.toLowerCase(); if (exprStr.includes(pillName) || pillName.includes(exprStr.split(' ')[0])) { p.flash = Math.max(p.flash, 1.0); // Bounce the pill p.applyForce(0, -5, 0); if (p.pill.type === 'timing') p.flash = 1.5; } } } } // Sequential flash based on frame count const frameIdx = vizSystem.frameCount % particles.length; if (particles[frameIdx]) { particles[frameIdx].flash = Math.max(particles[frameIdx].flash, 0.6); } } // Physics loop for pills function startPillsPhysicsLoop() { const pills = vizSystem.pills; if (pills.animationId) return; function loop() { if (vizSystem.currentView !== 'pills') { pills.animationId = null; return; } // Check if code changed const code = editor?.getValue() || ''; if (code !== pills.lastCode) { rebuildPillsPhysics(); } // Auto-rotate if (pills.autoRotate && !pills.dragging) { pills.targetRotation.y += 0.008; } // Smooth rotation interpolation pills.rotation.x += (pills.targetRotation.x - pills.rotation.x) * 0.1; pills.rotation.y += (pills.targetRotation.y - pills.rotation.y) * 0.1; pills.rotation.z += (pills.targetRotation.z - pills.rotation.z) * 0.1; // When paused, add stronger damping const damping = vizSystem.isPaused ? 0.8 : 0.96; // Update particles for (const p of pills.particles) { p.update(damping); } // Solve constraints for (let iter = 0; iter < 2; iter++) { for (const c of pills.constraints) { c.solve(); } } renderPills(); pills.animationId = requestAnimationFrame(loop); } pills.animationId = requestAnimationFrame(loop); } // Parse code into pill data function parseToPills(code) { const pills = []; let current = ''; let inString = false; let parenDepth = 0; for (let i = 0; i < code.length; i++) { const char = code[i]; if (char === '"') { inString = !inString; current += char; } else if (inString) { current += char; } else if (char === '(') { if (parenDepth === 0 && current.trim()) { pills.push(parsePillToken(current.trim())); current = ''; } parenDepth++; current += char; } else if (char === ')') { parenDepth--; current += char; if (parenDepth === 0) { pills.push(parsePillToken(current.trim())); current = ''; } } else if (parenDepth > 0) { current += char; } else if (char === ',' || char === '\n') { if (current.trim()) pills.push(parsePillToken(current.trim())); current = ''; } else { current += char; } } if (current.trim()) pills.push(parsePillToken(current.trim())); return pills; } function parsePillToken(token) { if (token.startsWith('(') && token.endsWith(')')) { const inner = token.slice(1, -1).trim(); // Find the head - first token before space or nested paren let head = ''; let i = 0; while (i < inner.length && inner[i] !== ' ' && inner[i] !== '(') { head += inner[i]; i++; } head = head.trim(); // Check for timing pattern like "1s", "0.3s", "1s!" if (/^\d*\.?\d+s!?\.{0,3}$/.test(head)) { return { type: 'timing', name: head, token, style: BEAD_STYLES.timing }; } const type = COMMAND_TYPES[head] || 'state'; return { type, name: head || '(…)', token, style: BEAD_STYLES[type] }; } const parts = token.split(/\s+/); const head = parts[0]; if (COLOR_NAMES.includes(head) && parts.length === 1) { return { type: 'state', name: head, token, style: BEAD_STYLES.state, isColor: true }; } const type = COMMAND_TYPES[head] || 'state'; return { type, name: head, token, style: BEAD_STYLES[type] }; } // Render 3D pills function renderPills() { const pills = vizSystem.pills; const { canvas, ctx, dpr, particles, constraints } = pills; if (!ctx || !canvas) return; const width = canvas.width / (dpr || 1); const height = canvas.height / (dpr || 1); const cx = width / 2; const cy = height / 2; // Theme detection const isLightMode = document.documentElement.getAttribute('data-theme') === 'light' || (!document.documentElement.getAttribute('data-theme') && window.matchMedia?.('(prefers-color-scheme: light)').matches); // Theme-aware colors const theme = isLightMode ? { bgInner: '#f8f8fa', bgOuter: '#e8e8ec', textEmpty: '#888', textHint: '#aaa', headerColor: '#2AA1A1', lineColor: 'rgba(100, 100, 140, 0.4)', shadowColor: 'rgba(0, 0, 0, 0.15)', legendText: '#666' } : { bgInner: '#1a1a2e', bgOuter: '#0a0a14', textEmpty: '#666', textHint: '#444', headerColor: '#4ECDC4', lineColor: 'rgba(120, 120, 160, 0.5)', shadowColor: 'rgba(0, 0, 0, 0.3)', legendText: '#888' }; // Gradient background const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(width, height) * 0.7); bgGrad.addColorStop(0, theme.bgInner); bgGrad.addColorStop(1, theme.bgOuter); ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, width, height); const n = particles.length; if (n === 0) { ctx.fillStyle = theme.textEmpty; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Write some KidLisp to see the pills', cx, cy); ctx.fillStyle = theme.textHint; ctx.font = '11px monospace'; ctx.fillText('drag to rotate • scroll to zoom • dbl-click for auto-rotate', cx, cy + 24); return; } // Header ctx.fillStyle = theme.headerColor; ctx.font = 'bold 12px monospace'; ctx.textAlign = 'left'; ctx.fillText(`💊 PILLS (${n} expressions)`, 16, 20); // Status indicator let status, statusColor; if (vizSystem.isPlaying && !vizSystem.isPaused) { status = '● running'; statusColor = '#4CAF50'; } else if (vizSystem.isPaused) { status = '◐ paused'; statusColor = '#FFD93D'; } else { status = '○ stopped'; statusColor = '#666'; } ctx.fillStyle = statusColor; ctx.fillText(status, 180, 20); if (vizSystem.isPlaying) { ctx.fillStyle = theme.textHint; ctx.font = '9px monospace'; ctx.fillText(`frame ${vizSystem.frameCount}`, 260, 20); } ctx.fillStyle = theme.textHint; ctx.font = '10px monospace'; ctx.fillText(pills.autoRotate ? '◉ auto-rotate' : '○ drag to rotate', 16, 34); // Sort particles by Z (back to front) const projected = particles.map(p => { const proj = projectPill3D(p.x, p.y, p.z, pills.rotation, pills.zoom, cx, cy); return { particle: p, proj }; }).sort((a, b) => b.proj.z - a.proj.z); // Draw connecting lines between sequential pills (not looping) ctx.lineWidth = 2; for (let i = 0; i < n - 1; i++) { const p1 = particles[i]; const p2 = particles[i + 1]; const proj1 = projectPill3D(p1.x, p1.y, p1.z, pills.rotation, pills.zoom, cx, cy); const proj2 = projectPill3D(p2.x, p2.y, p2.z, pills.rotation, pills.zoom, cx, cy); // Fade based on depth const avgZ = (proj1.z + proj2.z) / 2; const alpha = Math.max(0.2, Math.min(0.7, 0.5 - avgZ * 0.001)); const lineRgb = isLightMode ? '100, 100, 140' : '120, 120, 160'; ctx.strokeStyle = `rgba(${lineRgb}, ${alpha})`; ctx.beginPath(); ctx.moveTo(proj1.x, proj1.y); ctx.lineTo(proj2.x, proj2.y); ctx.stroke(); } // Draw pills (back to front) for (const { particle: p, proj } of projected) { const pill = p.pill; const baseColor = pill.style.color; // Parse base color const r = parseInt(baseColor.slice(1, 3), 16); const g = parseInt(baseColor.slice(3, 5), 16); const b = parseInt(baseColor.slice(5, 7), 16); // Depth-based lighting const depthFactor = Math.max(0.4, Math.min(1, 0.8 - proj.z * 0.002)); const flash = p.flash; const finalR = Math.min(255, r * depthFactor + flash * 150); const finalG = Math.min(255, g * depthFactor + flash * 150); const finalB = Math.min(255, b * depthFactor + flash * 150); const w = p.width * proj.scale; const h = p.height * proj.scale; const x = proj.x; const y = proj.y; // Glow for flashing pills if (flash > 0.1) { const glow = ctx.createRadialGradient(x, y, 0, x, y, w); glow.addColorStop(0, `rgba(${finalR}, ${finalG}, ${finalB}, ${flash * 0.5})`); glow.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = glow; ctx.fillRect(x - w, y - w, w * 2, w * 2); } // Shadow const shadowAlpha = isLightMode ? 0.15 * depthFactor : 0.3 * depthFactor; ctx.fillStyle = `rgba(0, 0, 0, ${shadowAlpha})`; drawPillShape(ctx, x + 2, y + 2, w, h); ctx.fill(); // Pill body const pillGrad = ctx.createLinearGradient(x - w/2, y - h/2, x + w/2, y + h/2); pillGrad.addColorStop(0, `rgb(${Math.min(255, finalR + 30)}, ${Math.min(255, finalG + 30)}, ${Math.min(255, finalB + 30)})`); pillGrad.addColorStop(0.5, `rgb(${finalR}, ${finalG}, ${finalB})`); pillGrad.addColorStop(1, `rgb(${Math.max(0, finalR - 40)}, ${Math.max(0, finalG - 40)}, ${Math.max(0, finalB - 40)})`); ctx.fillStyle = pillGrad; drawPillShape(ctx, x, y, w, h); ctx.fill(); // Highlight ctx.fillStyle = `rgba(255, 255, 255, ${0.3 * depthFactor})`; drawPillShape(ctx, x, y - h * 0.15, w * 0.8, h * 0.4); ctx.fill(); // Inner content: just the name, colored by type const fontSize = Math.max(9, 13 * proj.scale); ctx.font = `bold ${fontSize}px monospace`; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; // Name color - matches pill type, flashes bright white const nameR = Math.min(255, r + flash * 150); const nameG = Math.min(255, g + flash * 150); const nameB = Math.min(255, b + flash * 150); // Truncate long names const name = pill.name.length > 8 ? pill.name.substring(0, 7) + '…' : pill.name; // Shadow const textShadowAlpha = isLightMode ? 0.3 * depthFactor : 0.6 * depthFactor; ctx.fillStyle = `rgba(0, 0, 0, ${textShadowAlpha})`; ctx.fillText(name, x + 1, y + 1); // Name text ctx.fillStyle = `rgba(${nameR}, ${nameG}, ${nameB}, ${depthFactor})`; ctx.fillText(name, x, y); } // Legend at bottom const legendY = height - 16; const legendItems = [ { type: 'state', label: 'state' }, { type: 'draw', label: 'draw' }, { type: 'effect', label: 'effect' }, { type: 'timing', label: 'timing' }, { type: 'loop', label: 'loop' } ]; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; let legendX = 16; for (const item of legendItems) { const style = BEAD_STYLES[item.type]; ctx.fillStyle = style.color; drawPillShape(ctx, legendX + 12, legendY, 24, 10); ctx.fill(); ctx.fillStyle = theme.legendText; ctx.fillText(item.label, legendX + 28, legendY); legendX += ctx.measureText(item.label).width + 40; } } // Draw a pill/capsule shape function drawPillShape(ctx, x, y, w, h) { const r = h / 2; ctx.beginPath(); ctx.moveTo(x - w/2 + r, y - h/2); ctx.lineTo(x + w/2 - r, y - h/2); ctx.arc(x + w/2 - r, y, r, -Math.PI/2, Math.PI/2); ctx.lineTo(x - w/2 + r, y + h/2); ctx.arc(x - w/2 + r, y, r, Math.PI/2, -Math.PI/2); ctx.closePath(); } // Legacy render function (calls pills version) function renderPills_legacy() { if (!vizSystem.pills.initialized) { initPillsView(); } if (!vizSystem.pills.animationId) { startPillsPhysicsLoop(); } } // Initialize visualization system initVizTabs(); // Update visualizations when code changes (hook into Monaco later) setInterval(() => { if (vizSystem.currentView !== 'text') { updateVisualization(); } }, 500);
// ========================================== // PJ Broadcasting (silent background sync) // ==========================================
let codeChannelId = null; let isBroadcasting = false; const logPj = (...args) => console.log('🧩 PJ:', ...args); // PJ WebSocket - prefers production, falls back to localhost // This allows localhost development to broadcast to pj.kidlisp.com viewers let pjWs = null; let pjWsPendingChannel = null; let pjIsProduction = true; // Track if connected to production or localhost let pjWsKeepAlive = null; // Keepalive interval const PJ_PROD_URL = 'wss://session-server.aesthetic.computer'; const PJ_LOCAL_URL = 'wss://localhost:8889'; const PJ_WS_KEEPALIVE_MS = 25000; // Ping every 25s to keep connection alive
function connectPjWebSocket(forceLocal = false) { if (pjWs && pjWs.readyState === WebSocket.OPEN) return; // Clean up any stale connection if (pjWs) { try { pjWs.close(); } catch(e) {} pjWs = null; } if (pjWsKeepAlive) { clearInterval(pjWsKeepAlive); pjWsKeepAlive = null; }
const url = forceLocal ? PJ_LOCAL_URL : PJ_PROD_URL; pjIsProduction = !forceLocal; logPj('connect websocket', { url, forceLocal, pjIsProduction, codeChannelId });
try { pjWs = new WebSocket(url); pjWs.onopen = () => { log.socket.log(`PJ WebSocket connected to ${pjIsProduction ? 'production' : 'localhost'}`); // (Re)subscribe to active channel const subChannel = pjWsPendingChannel || codeChannelId; if (subChannel) { pjWs.send(JSON.stringify({ type: 'code-channel:sub', content: subChannel })); pjWsPendingChannel = null; logPj('subscribed on connect', { subChannel }); } updatePjUI();
// Handle viewer count responses pjWs.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.type === 'code-channel:info' && msg.content) { const count = msg.content.viewers || 0; const ddViewers = document.getElementById('pj-dropdown-viewers'); const jockeyViewers = document.getElementById('jockey-viewers'); if (ddViewers) ddViewers.textContent = `${count} viewer${count !== 1 ? 's' : ''}`; if (jockeyViewers) jockeyViewers.textContent = `${count} viewer${count !== 1 ? 's' : ''}`; } } catch (err) {} };
// Send current code to server for late joiners (if playing) if (codeChannelId && isPlaying && editor) { const code = editor.getValue().trim(); if (code) { pjWs.send(JSON.stringify({ type: 'code', content: { piece: code, source: code, codeChannel: codeChannelId } })); log.socket.debug('Sent current code to server on reconnect'); } }
// Start keepalive pings to prevent idle timeout pjWsKeepAlive = setInterval(() => { if (pjWs?.readyState === WebSocket.OPEN) { pjWs.send(JSON.stringify({ type: 'ping' })); } }, PJ_WS_KEEPALIVE_MS); }; pjWs.onclose = () => { log.socket.debug('PJ WebSocket disconnected'); pjWs = null; if (pjWsKeepAlive) { clearInterval(pjWsKeepAlive); pjWsKeepAlive = null; } // Reconnect if broadcasting if (isBroadcasting || codeChannelId) setTimeout(() => connectPjWebSocket(forceLocal), 2000); }; pjWs.onerror = () => { pjWs?.close(); logPj('websocket error', { forceLocal, pjIsProduction }); // If production failed, try localhost if (!forceLocal) { log.socket.warn('Production PJ server unreachable, trying localhost...'); setTimeout(() => connectPjWebSocket(true), 500); } }; } catch (e) { log.socket.warn('PJ WebSocket connection failed:', e.message); // If production failed, try localhost if (!forceLocal) { setTimeout(() => connectPjWebSocket(true), 500); } } } // PJ UDP (geckos.io) - lowest latency for audio data let pjUdpChannel = null; let pjUdpConnected = false; let pjUdpReconnectTimer = null; let pjUdpAudioCounter = 0; let GeckosClient = null;
async function connectPjUDP(forceLocal = false) { if (pjUdpConnected || pjUdpChannel) return;
// Lazy-load geckos.io client if (!GeckosClient) { try { const mod = await import(`${aestheticUrl}/aesthetic.computer/dep/geckos.io-client.3.0.2.min.js`); GeckosClient = mod.default; } catch (e) { console.warn('🩰 PJ UDP: Failed to load geckos.io client:', e); return; } }
const url = forceLocal ? 'https://localhost' : 'https://session-server.aesthetic.computer'; const port = forceLocal ? 8889 : 443; const isLocalDev = forceLocal || url.includes('localhost'); logPj('UDP connecting', { url, port, forceLocal });
try { const iceServers = isLocalDev ? [ { urls: 'stun:localhost:3478' }, { urls: 'turn:localhost:3478', username: 'aesthetic', credential: 'computer123' }, { urls: 'turn:localhost:3478?transport=tcp', username: 'aesthetic', credential: 'computer123' }, ] : [{ urls: 'stun:stun.l.google.com:19302' }];
pjUdpChannel = GeckosClient({ url, port, iceServers, iceTransportPolicy: isLocalDev ? 'relay' : 'all', }); } catch (e) { console.warn('🩰 PJ UDP: Failed to create geckos channel:', e); pjUdpChannel = null; return; }
pjUdpChannel.onConnect((error) => { if (error) { logPj('UDP error', { error: error.message || error, forceLocal }); pjUdpChannel = null; if (!forceLocal) { setTimeout(() => connectPjUDP(true), 500); } else { pjUdpReconnectTimer = setTimeout(() => connectPjUDP(forceLocal), 3000); } return; }
pjUdpConnected = true; logPj('UDP connected', { url, port });
pjUdpChannel.emit('udp:identity', JSON.stringify({ role: 'kidlisp-editor', codeChannel: codeChannelId })); });
pjUdpChannel.onDisconnect(() => { logPj('UDP disconnected'); pjUdpConnected = false; pjUdpChannel = null; if (codeChannelId) { pjUdpReconnectTimer = setTimeout(() => connectPjUDP(forceLocal), 3000); } }); }
function disconnectPjUDP() { if (pjUdpReconnectTimer) { clearTimeout(pjUdpReconnectTimer); pjUdpReconnectTimer = null; } if (pjUdpChannel) { pjUdpChannel.close(); pjUdpChannel = null; } pjUdpConnected = false; }
// Get the current PJ share URL based on connection state function getPjShareUrl() { if (!codeChannelId) return null; if (pjIsProduction) { // Use path-based URL for better DNS compatibility return `https://kidlisp.com/pj/${codeChannelId}`; } else { // Localhost mode - mirror production path structure return `${window.location.origin}/kidlisp.com/pj/${codeChannelId}`; } } const popoutBtn = document.getElementById('popout-btn'); const previewIframeContainer = document.getElementById('preview-iframe-container'); const pjDropdown = document.getElementById('pj-dropdown'); let pjViewerPollInterval = null; let pjPopoutWindow = null; // Tracks the local popout window (postMessage only) let localPreviewPausedForPopout = false; // True when local preview is paused for popout
// Generate a pronounceable PJ channel code (CVCV pattern) function generateCodeChannelId() { const consonants = 'bdfghjklmnprstvwz'; const vowels = 'aeiou'; const pick = (str) => str[Math.floor(Math.random() * str.length)]; return pick(consonants) + pick(vowels) + pick(consonants) + pick(vowels); }
// Draw QR code onto a canvas element function drawPjQr(canvasId, url, size) { try { const canvas = document.getElementById(canvasId); if (!canvas || !url) return; const cells = window.qr(url, { errorCorrectLevel: window.ErrorCorrectLevel.L }).modules; const cellSize = Math.floor(size / cells.length); const actualSize = cells.length * cellSize; canvas.width = actualSize; canvas.height = actualSize; const ctx = canvas.getContext('2d'); for (let y = 0; y < cells.length; y++) { for (let x = 0; x < cells.length; x++) { ctx.fillStyle = cells[y][x] ? '#000000' : '#ffffff'; ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); } } } catch (e) { console.error('Failed to draw PJ QR code:', e); } }
function copyPjUrl() { const url = getPjShareUrl(); if (url) { navigator.clipboard.writeText(url).then(() => { showToast('URL copied!', 'success'); }).catch(() => { showToast('Failed to copy', 'error'); }); } }
// Start PJ broadcasting (silent background — preview stays local) function startPjBroadcast() { if (isBroadcasting) return; if (!codeChannelId) { codeChannelId = generateCodeChannelId(); localStorage.setItem('kidlisp-code-channel', codeChannelId); logPj('generated channel', { codeChannelId }); } isBroadcasting = true; localStorage.setItem('kidlisp-pj-active', '1'); subscribeToCodeChannel(codeChannelId); popoutBtn?.classList.add('active'); popoutBtn.title = 'PJ on — click for info'; // Send current code if playing if (editor && isPlaying) { const code = editor.getValue().trim(); if (code) broadcastCodeToChannel(code); } updatePjUI(); startViewerPolling(); showToast(`PJ on • ${codeChannelId}`, 'success'); logPj('broadcast started', { codeChannelId }); }
// Stop PJ broadcasting function stopPjBroadcast() { isBroadcasting = false; codeChannelId = null; localStorage.removeItem('kidlisp-code-channel'); localStorage.removeItem('kidlisp-pj-active'); popoutBtn?.classList.remove('active'); popoutBtn.title = 'Start PJ broadcast'; hidePjDropdown(); stopViewerPolling(); updatePjUI(); showToast('PJ off', 'info'); logPj('broadcast stopped'); }
// Update all PJ UI elements (dropdown + Jockey tab) function updatePjUI() { const url = getPjShareUrl(); // Dropdown const ddChannel = document.getElementById('pj-dropdown-channel'); if (ddChannel) ddChannel.textContent = codeChannelId || '—'; if (url) { drawPjQr('pj-dropdown-qr', url, 120); } // Show/hide Jockey tab based on PJ state const jockeyTab = document.getElementById('jockey-tab'); if (jockeyTab) { jockeyTab.style.display = isBroadcasting ? '' : 'none'; // If hiding jockey tab while it's active, switch to Learn if (!isBroadcasting && jockeyTab.classList.contains('active')) { jockeyTab.classList.remove('active'); document.getElementById('jockey-content')?.classList.add('hidden'); document.getElementById('guide-tab')?.classList.add('active'); document.getElementById('guide-content')?.classList.remove('hidden'); } } // Jockey tab updateJockeyPanel(); }
function updateJockeyPanel() { const offMsg = document.getElementById('jockey-off-msg'); const liveDiv = document.getElementById('jockey-live'); const jockeyChannel = document.getElementById('jockey-channel'); const jockeyUrl = document.getElementById('jockey-url'); if (!offMsg || !liveDiv) return; if (isBroadcasting && codeChannelId) { offMsg.style.display = 'none'; liveDiv.style.display = 'flex'; if (jockeyChannel) jockeyChannel.textContent = codeChannelId; const url = getPjShareUrl(); if (jockeyUrl && url) jockeyUrl.textContent = url; drawPjQr('jockey-qr', url, 160); } else { offMsg.style.display = ''; liveDiv.style.display = 'none'; } }
// Dropdown toggle function showPjDropdown() { if (!pjDropdown) return; updatePjUI(); pjDropdown.classList.remove('hidden'); requestViewerCount(); } function hidePjDropdown() { pjDropdown?.classList.add('hidden'); }
// Viewer count polling function requestViewerCount() { if (!codeChannelId) return; if (pjWs?.readyState === WebSocket.OPEN) { pjWs.send(JSON.stringify({ type: 'code-channel:info', content: codeChannelId })); } } function startViewerPolling() { stopViewerPolling(); pjViewerPollInterval = setInterval(requestViewerCount, 5000); } function stopViewerPolling() { if (pjViewerPollInterval) { clearInterval(pjViewerPollInterval); pjViewerPollInterval = null; } } // Subscribe to code channel via production session server (for PJ broadcasting) function subscribeToCodeChannel(channelId) { logPj('subscribe channel', { channelId }); // Ensure PJ WebSocket is connected to production connectPjWebSocket(); // Also start UDP for low-latency audio connectPjUDP(); if (pjWs && pjWs.readyState === WebSocket.OPEN) { pjWs.send(JSON.stringify({ type: 'code-channel:sub', content: channelId })); log.socket.log(`Subscribed to PJ channel: ${channelId}`); } else { // Will subscribe when pjWs connects pjWsPendingChannel = channelId; log.socket.debug('PJ WebSocket connecting, will subscribe to:', channelId); } } function hasActivePjSession() { return isBroadcasting; } // Broadcast code to channel subscribers // Always sends to WebSocket (for late joiner state), but only sends to local // BroadcastChannel/postMessage when there's an active PJ session function broadcastCodeToChannel(code) { // Always forward to local popout window (independent of channel broadcasting) postToPopout({ type: 'kidlisp-code-update', code }); if (!codeChannelId) return; const hasActiveSession = hasActivePjSession(); logPj('broadcast code', { codeChannelId, length: code?.length, pjIsProduction, hasActiveSession }); // Broadcast via BroadcastChannel for same-origin windows (only if active session) if (hasActiveSession && window.BroadcastChannel) { const bc = new BroadcastChannel(`kidlisp-channel-${codeChannelId}`); bc.postMessage({ type: 'code-update', code: code, channelId: codeChannelId, timestamp: Date.now() }); bc.close(); } // Broadcast via WebSocket to store state for late joiners // The session server keeps track of the latest code for each channel if (pjWs && pjWs.readyState === WebSocket.OPEN) { pjWs.send(JSON.stringify({ type: 'code', content: { piece: code, source: code, codeChannel: codeChannelId } })); log.socket.debug('Broadcast code via PJ WebSocket to channel:', codeChannelId); } else { // Try to connect and send - important for late joiners connectPjWebSocket(); log.socket.debug('PJ WebSocket not ready, reconnecting...'); } } // Broadcast slide update to channel (optimized for low latency) function broadcastSlideToChannel(code) { postToPopout({ type: 'kidlisp-slide-update', code }); if (!codeChannelId || !hasActivePjSession()) return; logPj('broadcast slide', { codeChannelId, length: code?.length, pjIsProduction });
// 1. BroadcastChannel for same-origin windows (zero latency) if (window.BroadcastChannel) { const bc = new BroadcastChannel(`kidlisp-channel-${codeChannelId}`); bc.postMessage({ type: 'slide-update', code: code, channelId: codeChannelId, timestamp: Date.now() }); bc.close(); }
// 2. UDP (geckos.io) — lowest latency for cross-origin if (pjUdpConnected && pjUdpChannel) { pjUdpChannel.emit('slide:code', JSON.stringify({ codeChannel: codeChannelId, code: code, timestamp: Date.now() })); return; // Skip WebSocket if UDP is available }
// 3. WebSocket fallback (cross-origin) if (pjWs && pjWs.readyState === WebSocket.OPEN) { pjWs.send(JSON.stringify({ type: 'slide', content: { piece: code, source: code, codeChannel: codeChannelId } })); } } // Broadcast pause/resume to channel subscribers function broadcastPauseToChannel(paused) { postToPopout({ type: paused ? 'kidlisp-pause' : 'kidlisp-resume' }); if (!codeChannelId || !hasActivePjSession()) return; logPj('broadcast pause', { codeChannelId, paused }); // Broadcast via BroadcastChannel for same-origin windows if (window.BroadcastChannel) { const bc = new BroadcastChannel(`kidlisp-channel-${codeChannelId}`); bc.postMessage({ type: paused ? 'pause' : 'resume', channelId: codeChannelId, timestamp: Date.now() }); bc.close(); } // Broadcast via production PJ WebSocket (cross-origin) if (pjWs && pjWs.readyState === WebSocket.OPEN) { pjWs.send(JSON.stringify({ type: paused ? 'pause' : 'resume', content: { codeChannel: codeChannelId } })); } } // Antenna button: toggle PJ on/off + dropdown popoutBtn?.addEventListener('click', (e) => { e.stopPropagation(); if (!isBroadcasting) { // Turn ON startPjBroadcast(); } else { // Already on — toggle dropdown if (pjDropdown?.classList.contains('hidden')) { showPjDropdown(); } else { hidePjDropdown(); } } });
// Close dropdown when clicking outside document.addEventListener('click', (e) => { if (pjDropdown && !pjDropdown.classList.contains('hidden') && !pjDropdown.contains(e.target) && e.target !== popoutBtn && !popoutBtn?.contains(e.target)) { hidePjDropdown(); } });
// Dropdown: popout window button let pjPopoutClosePoller = null;
function openPjPopout() { // Focus existing popout if (pjPopoutWindow && !pjPopoutWindow.closed) { pjPopoutWindow.focus(); return; } // Open popup loading pj.html without a channel (postMessage-only mode) const isLocal = window.location.hostname === 'localhost' || window.location.hostname.includes('local.'); const popoutUrl = isLocal ? `${window.location.origin}/kidlisp.com/pj/` : `https://kidlisp.com/pj/`; pjPopoutWindow = window.open( popoutUrl, 'kidlisp-pj-popout', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no' ); if (!pjPopoutWindow) { showToast('Popup blocked — allow popups for this site', 'error'); return; } logPj('popout opened');
// Pause the local preview to save performance if (isPlaying && !isPaused) { sendToIframe({ type: 'kidlisp-pause' }); localPreviewPausedForPopout = true; logPj('local preview paused for popout'); }
// Send current code once the popout is ready let initialSent = false; const sendInitialCode = () => { if (initialSent) return; if (editor) { const code = editor.getValue().trim(); if (code && pjPopoutWindow && !pjPopoutWindow.closed) { pjPopoutWindow.postMessage({ type: 'kidlisp-code-update', code }, '*'); initialSent = true; logPj('sent initial code to popout'); } } }; // Listen for the ready signal from popout window.addEventListener('message', function onReady(e) { if (e.source === pjPopoutWindow && e.data?.type === 'pj-popout-ready') { sendInitialCode(); window.removeEventListener('message', onReady); } }); // Fallback: retry a few times in case the ready message is missed setTimeout(sendInitialCode, 1500); setTimeout(sendInitialCode, 3000);
// Poll for popout close to resume local preview if (pjPopoutClosePoller) clearInterval(pjPopoutClosePoller); pjPopoutClosePoller = setInterval(() => { if (!pjPopoutWindow || pjPopoutWindow.closed) { clearInterval(pjPopoutClosePoller); pjPopoutClosePoller = null; pjPopoutWindow = null; // Resume local preview if we paused it if (localPreviewPausedForPopout) { localPreviewPausedForPopout = false; // Clear flag first so sendToIframe works sendToIframe({ type: 'kidlisp-resume' }); logPj('local preview resumed (popout closed)'); } } }, 500);
showToast('Popout window opened — local preview paused', 'success'); }
document.getElementById('pj-dropdown-popout')?.addEventListener('click', () => { openPjPopout(); hidePjDropdown(); });
// Forward code/slide/pause/audio to popout window via postMessage function postToPopout(msg) { if (pjPopoutWindow && !pjPopoutWindow.closed) { pjPopoutWindow.postMessage(msg, '*'); } }
// Reconnect to an existing popout after editor refresh. // The popout pings window.opener every 2s with 'pj-popout-ready'. window.addEventListener('message', (e) => { if (e.data?.type === 'pj-popout-ready' && !pjPopoutWindow) { pjPopoutWindow = e.source; logPj('reconnected to existing popout'); // Pause local preview and start close poller if (isPlaying && !isPaused && !localPreviewPausedForPopout) { sendToIframe({ type: 'kidlisp-pause' }); localPreviewPausedForPopout = true; } if (!pjPopoutClosePoller) { pjPopoutClosePoller = setInterval(() => { if (!pjPopoutWindow || pjPopoutWindow.closed) { clearInterval(pjPopoutClosePoller); pjPopoutClosePoller = null; pjPopoutWindow = null; if (localPreviewPausedForPopout) { localPreviewPausedForPopout = false; sendToIframe({ type: 'kidlisp-resume' }); logPj('local preview resumed (popout closed)'); } } }, 500); } // Send current code to the popout if (editor) { const code = editor.getValue().trim(); if (code) postToPopout({ type: 'kidlisp-code-update', code }); } showToast('Popout reconnected', 'success'); } });
// Dropdown: stop button document.getElementById('pj-dropdown-stop')?.addEventListener('click', () => stopPjBroadcast()); // Dropdown: copy URL on QR / channel click document.getElementById('pj-dropdown-qr')?.addEventListener('click', () => copyPjUrl()); document.getElementById('pj-dropdown-channel')?.addEventListener('click', () => copyPjUrl()); // Jockey tab: stop button + copy handlers document.getElementById('jockey-stop')?.addEventListener('click', () => stopPjBroadcast()); document.getElementById('jockey-qr')?.addEventListener('click', () => copyPjUrl()); document.getElementById('jockey-channel')?.addEventListener('click', () => copyPjUrl()); document.getElementById('jockey-url')?.addEventListener('click', () => copyPjUrl()); // Hook into updatePreview to broadcast code changes const originalUpdatePreview = window.updatePreview || updatePreview; window.updatePreviewWithBroadcast = function(shouldCreateCode) { // Call original if (typeof originalUpdatePreview === 'function') { originalUpdatePreview(shouldCreateCode); } // Broadcast to pop-out if (codeChannelId && editor) { const code = editor.getValue().trim(); broadcastCodeToChannel(code); } }; // Patch updatePreview to include broadcast (function() { const _updatePreview = updatePreview; updatePreview = function(shouldCreateCode) { _updatePreview(shouldCreateCode); if (editor) { const code = editor.getValue().trim(); // Forward to local popout window (always, independent of PJ channel) postToPopout({ type: 'kidlisp-code-update', code }); // Broadcast to PJ channel subscribers if (codeChannelId) broadcastCodeToChannel(code); } }; })(); // Auto-restore PJ if it was active before refresh const savedChannel = localStorage.getItem('kidlisp-code-channel'); const savedPjActive = localStorage.getItem('kidlisp-pj-active'); if (savedChannel && savedPjActive) { codeChannelId = savedChannel; startPjBroadcast(); // Restores everything: subscribe, button state, UI logPj('restored channel', { savedChannel }); setTimeout(() => showToast(`PJ restored • ${savedChannel}`, 'info'), 1000); }
// Session server connection with connectivity feedback let sessionWs = null; let reconnectInterval = null; const connectivityDot = document.querySelector('.connectivity-dot'); function setConnectivityState(state) { if (!connectivityDot) return; connectivityDot.className = 'connectivity-dot ' + state; } function connectToSessionServer() { if (reconnectInterval) { clearInterval(reconnectInterval); reconnectInterval = null; } // Determine connection URL let connectionUrl = "wss://localhost:8889"; if (window.location.host === "local.aesthetic.computer") { connectionUrl = "wss://session.local.aesthetic.computer"; } else if (window.location.host === "kidlisp.com") { connectionUrl = "wss://session-server.aesthetic.computer"; } setConnectivityState('connecting'); log.socket.debug("Connecting to session server:", connectionUrl); try { sessionWs = new WebSocket(connectionUrl); } catch (error) { log.socket.warn("Connection failed:", error.message); setConnectivityState('disconnected'); scheduleReconnect(); return; }
sessionWs.onopen = () => { log.socket.log("Connected to session server"); setConnectivityState('connected'); // In device mode, join the DAW channel to receive code if (isDeviceMode && sessionWs) { sessionWs.send(JSON.stringify({ type: 'daw:join' })); log.socket.log("Device mode: joined DAW channel"); } // Re-subscribe to code channel if we have one (restored or active) if (codeChannelId) { subscribeToCodeChannel(codeChannelId); log.socket.log(`Re-subscribed to code channel: ${codeChannelId}`); } };
sessionWs.onmessage = (e) => { // Blink the dot when receiving a message setConnectivityState('receiving'); setTimeout(() => { setConnectivityState('connected'); }, 400); try { const msg = JSON.parse(e.data); log.socket.verbose("Received message:", msg); // Handle DAW code messages in device mode if (isDeviceMode && msg.type === "daw:code" && msg.content?.code) { log.socket.log("Device mode: received code from IDE"); // Get current codeId from localStorage const codeId = localStorage.getItem('kidlisp-current-code') || null; // Send code to the kidlisp iframe using the same format as normal playback if (iframeReady && previewIframe?.contentWindow) { previewIframe.contentWindow.postMessage({ type: 'kidlisp-reload', code: msg.content.code, codeId: codeId, createCode: false, enableTrace: false }, aestheticUrl); log.socket.log("Device mode: sent code to preview iframe"); } else { log.socket.warn("Device mode: iframe not ready, queuing code"); // Queue for later when iframe is ready pendingMessages.push({ type: 'kidlisp-reload', code: msg.content.code, codeId: codeId, createCode: false, enableTrace: false }); } } // Handle reload messages for full page refresh if (msg.type === "reload") { log.socket.log("Reload message received:", msg.content.piece); if (msg.content.piece === "*refresh*" || msg.content.piece === "kidlisp.com") { log.socket.log("Reloading entire page..."); setTimeout(() => { console.clear(); window.location.reload(); }, 150); // Wait a moment for backend to be ready } } } catch (error) { log.socket.warn("Error parsing message:", error.message); } };
sessionWs.onerror = (error) => { log.socket.warn("WebSocket error"); setConnectivityState('disconnected'); };
sessionWs.onclose = () => { log.socket.debug("Disconnected from session server"); setConnectivityState('disconnected'); sessionWs = null; scheduleReconnect(); }; } function scheduleReconnect() { if (!reconnectInterval) { setConnectivityState('waiting'); reconnectInterval = setInterval(() => { if (!sessionWs || sessionWs.readyState === WebSocket.CLOSED) { connectToSessionServer(); } }, 2000); // Try to reconnect every 2 seconds } } // Start connection on page load connectToSessionServer(); // ═══════════════════════════════════════════════════════════════════════════ // 🎵 Jukebox - Audio player with real-time analysis for KidLisp // ═══════════════════════════════════════════════════════════════════════════ const JUKEBOX_ASSET_BASE = 'https://aesthetic.computer/assets/oskie/wipppps/'; let jukeboxAudioContext = null; let jukeboxAudioElement = null; let jukeboxGainNode = null; let jukeboxAnalyser = null; let jukeboxSource = null; let jukeboxWorkletNode = null; let jukeboxAnimationFrame = null; let jukeboxCurrentTrack = null; let jukeboxIsPlaying = false; let jukeboxEnergyHistory = []; const JUKEBOX_ENERGY_HISTORY_SIZE = 10; const JUKEBOX_BEAT_SENSITIVITY = 1.12; const JUKEBOX_BEAT_COOLDOWN = 60; let jukeboxLastBeatTime = 0; let jukeboxCurrentBeat = false; // AudioWorklet processor code (runs on audio thread for sample-accurate timing) const audioAnalyzerWorkletCode = ` class AudioAnalyzerProcessor extends AudioWorkletProcessor { constructor() { super(); this.sampleCount = 0; this.sumSquares = 0; this.blockSize = 128; // Process every 128 samples (~2.7ms at 48kHz) this.energyHistory = new Float32Array(10); this.historyIndex = 0; this.lastBeatFrame = 0; this.beatCooldownFrames = Math.floor(sampleRate * 0.06 / 128); // 60ms cooldown } process(inputs, outputs, parameters) { const input = inputs[0]; if (!input || !input[0]) return true; const channelData = input[0]; const channelDataR = input[1] || channelData; // Pass through audio for (let c = 0; c < outputs[0].length; c++) { outputs[0][c].set(inputs[0][c] || inputs[0][0]); } // Calculate RMS for this block let sum = 0; let sumL = 0; let sumR = 0; for (let i = 0; i < channelData.length; i++) { sum += channelData[i] * channelData[i]; sumL += channelData[i] * channelData[i]; sumR += channelDataR[i] * channelDataR[i]; } const rms = Math.sqrt(sum / channelData.length); const rmsL = Math.sqrt(sumL / channelData.length); const rmsR = Math.sqrt(sumR / channelDataR.length); const amp = Math.min(1, rms * 1.5); const leftAmp = Math.min(1, rmsL * 1.5); const rightAmp = Math.min(1, rmsR * 1.5); // Simple bass energy estimation (low frequencies have larger samples) let bassEnergy = 0; let prevSample = 0; for (let i = 0; i < channelData.length; i++) { // Low-pass approximation: samples that don't change much = bass const diff = Math.abs(channelData[i] - prevSample); bassEnergy += channelData[i] * channelData[i] * (1 - Math.min(1, diff * 10)); prevSample = channelData[i]; } bassEnergy = Math.sqrt(bassEnergy / channelData.length); // Beat detection this.energyHistory[this.historyIndex] = bassEnergy; this.historyIndex = (this.historyIndex + 1) % this.energyHistory.length; let avgEnergy = 0; for (let i = 0; i < this.energyHistory.length; i++) avgEnergy += this.energyHistory[i]; avgEnergy /= this.energyHistory.length; let kick = 0; if (bassEnergy > avgEnergy * 1.12 && currentFrame - this.lastBeatFrame > this.beatCooldownFrames) { kick = 1; this.lastBeatFrame = currentFrame; } // Send data to main thread this.port.postMessage({ amp, leftAmp, rightAmp, kick, bass: bassEnergy * 4, beat: kick }); return true; } } registerProcessor('audio-analyzer', AudioAnalyzerProcessor); `; async function createAudioWorklet(ctx) { const blob = new Blob([audioAnalyzerWorkletCode], { type: 'application/javascript' }); const url = URL.createObjectURL(blob); try { await ctx.audioWorklet.addModule(url); return true; } catch (e) { console.warn('AudioWorklet not supported, falling back to setInterval:', e); return false; } finally { URL.revokeObjectURL(url); } } function initJukebox() { const playPauseBtn = document.getElementById('jukebox-play-pause'); const prevBtn = document.getElementById('jukebox-prev'); const nextBtn = document.getElementById('jukebox-next'); const playIcon = document.getElementById('jukebox-play-icon'); const pauseIcon = document.getElementById('jukebox-pause-icon'); const seekSlider = document.getElementById('jukebox-seek'); const volumeSlider = document.getElementById('jukebox-volume'); const trackList = document.getElementById('jukebox-tracklist'); if (!playPauseBtn || !trackList) return; // Get all tracks for prev/next navigation function getAllTracks() { return Array.from(trackList.querySelectorAll('.jukebox-track')); } function getCurrentTrackIndex() { const tracks = getAllTracks(); return tracks.findIndex(t => t.dataset.track === jukeboxCurrentTrack); } // Update play/pause icon state function updatePlayPauseIcon() { if (playIcon && pauseIcon) { playIcon.style.display = jukeboxIsPlaying ? 'none' : 'block'; pauseIcon.style.display = jukeboxIsPlaying ? 'block' : 'none'; } } trackList.addEventListener('click', (e) => { const trackEl = e.target.closest('.jukebox-track'); if (trackEl) loadJukeboxTrack(trackEl.dataset.track); }); playPauseBtn.addEventListener('click', () => { if (jukeboxIsPlaying) { pauseJukebox(); } else if (jukeboxAudioElement?.src) { playJukebox(); } else if (jukeboxCurrentTrack) { loadJukeboxTrack(jukeboxCurrentTrack); } else { // Auto-play first track if nothing selected const tracks = getAllTracks(); if (tracks.length > 0) loadJukeboxTrack(tracks[0].dataset.track); } updatePlayPauseIcon(); }); prevBtn?.addEventListener('click', () => { const tracks = getAllTracks(); const currentIndex = getCurrentTrackIndex(); if (currentIndex > 0) { loadJukeboxTrack(tracks[currentIndex - 1].dataset.track); } else if (tracks.length > 0) { // Wrap to last track loadJukeboxTrack(tracks[tracks.length - 1].dataset.track); } }); nextBtn?.addEventListener('click', () => { const tracks = getAllTracks(); const currentIndex = getCurrentTrackIndex(); if (currentIndex < tracks.length - 1) { loadJukeboxTrack(tracks[currentIndex + 1].dataset.track); } else if (tracks.length > 0) { // Wrap to first track loadJukeboxTrack(tracks[0].dataset.track); } }); seekSlider.addEventListener('input', (e) => { if (jukeboxAudioElement?.duration) { jukeboxAudioElement.currentTime = (e.target.value / 100) * jukeboxAudioElement.duration; } }); volumeSlider.addEventListener('input', (e) => { const vol = e.target.value / 100; if (jukeboxGainNode) jukeboxGainNode.gain.value = vol; if (jukeboxAudioElement) jukeboxAudioElement.volume = vol; }); // Store updatePlayPauseIcon for external use window.updateJukeboxPlayPauseIcon = updatePlayPauseIcon; } let jukeboxUseWorklet = false; // Will be set to true if worklet loads successfully async function initJukeboxAudio() { if (jukeboxAudioContext) return; jukeboxAudioContext = new (window.AudioContext || window.webkitAudioContext)(); jukeboxAudioElement = new Audio(); jukeboxAudioElement.crossOrigin = 'anonymous'; jukeboxSource = jukeboxAudioContext.createMediaElementSource(jukeboxAudioElement); jukeboxGainNode = jukeboxAudioContext.createGain(); // Try to use AudioWorklet for sample-accurate analysis jukeboxUseWorklet = await createAudioWorklet(jukeboxAudioContext); if (jukeboxUseWorklet) { // Worklet path: source -> gain -> worklet -> destination jukeboxWorkletNode = new AudioWorkletNode(jukeboxAudioContext, 'audio-analyzer'); jukeboxWorkletNode.port.onmessage = (e) => { const data = e.data; // Update beat state if (data.kick) { jukeboxCurrentBeat = true; jukeboxLastBeatTime = performance.now(); } else if (performance.now() - jukeboxLastBeatTime > 50) { jukeboxCurrentBeat = false; } // Send to preview immediately (worklet fires at audio rate!) const audioData = { amp: data.amp, leftAmp: data.leftAmp, rightAmp: data.rightAmp, beat: data.beat, kick: data.kick, bass: data.bass, mid: data.bass * 0.6, // Approximate treble: data.amp * 0.3, // Approximate highMid: data.bass * 0.4, presence: data.amp * 0.25 }; sendJukeboxAudioToPreview(audioData); broadcastJukeboxAudioToPJ(audioData); // Throttle UI updates if (!jukeboxAnimationFrame) { jukeboxAnimationFrame = requestAnimationFrame(() => { document.getElementById('jukebox-amp-meter').style.width = `${data.amp*100}%`; document.getElementById('jukebox-amp-value').textContent = data.amp.toFixed(1); document.getElementById('jukebox-kick-indicator').classList.toggle('active', jukeboxCurrentBeat); jukeboxAnimationFrame = null; }); } }; jukeboxSource.connect(jukeboxGainNode); jukeboxGainNode.connect(jukeboxWorkletNode); jukeboxWorkletNode.connect(jukeboxAudioContext.destination); console.log('🎵 Jukebox: Using AudioWorklet for sample-accurate analysis'); } else { // Fallback: AnalyserNode with setInterval jukeboxAnalyser = jukeboxAudioContext.createAnalyser(); jukeboxAnalyser.fftSize = 256; jukeboxAnalyser.smoothingTimeConstant = 0; jukeboxSource.connect(jukeboxGainNode); jukeboxGainNode.connect(jukeboxAnalyser); jukeboxAnalyser.connect(jukeboxAudioContext.destination); console.log('🎵 Jukebox: Using AnalyserNode fallback'); } const vol = document.getElementById('jukebox-volume'); if (vol) jukeboxGainNode.gain.value = vol.value / 100; jukeboxAudioElement.addEventListener('timeupdate', updateJukeboxProgress); jukeboxAudioElement.addEventListener('ended', stopJukebox); jukeboxAudioElement.addEventListener('loadedmetadata', () => { updateJukeboxTime(); document.getElementById('jukebox-seek').disabled = false; }); } async function loadJukeboxTrack(trackFile) { await initJukeboxAudio(); if (jukeboxAudioContext.state === 'suspended') await jukeboxAudioContext.resume(); jukeboxCurrentTrack = trackFile; document.getElementById('jukebox-track-name').textContent = trackFile.replace('.wav', '').replace('_master', ''); document.getElementById('jukebox-status').textContent = 'Loading...'; document.querySelectorAll('.jukebox-track').forEach(el => { el.classList.toggle('playing', el.dataset.track === trackFile); }); jukeboxAudioElement.src = JUKEBOX_ASSET_BASE + trackFile; await jukeboxAudioElement.load(); playJukebox(); } function playJukebox() { if (!jukeboxAudioElement) return; jukeboxAudioElement.play().then(() => { jukeboxIsPlaying = true; document.getElementById('jukebox-status').textContent = 'Playing'; if (window.updateJukeboxPlayPauseIcon) window.updateJukeboxPlayPauseIcon(); startJukeboxAnalysis(); }); } function pauseJukebox() { if (!jukeboxAudioElement) return; jukeboxAudioElement.pause(); jukeboxIsPlaying = false; document.getElementById('jukebox-status').textContent = 'Paused'; if (window.updateJukeboxPlayPauseIcon) window.updateJukeboxPlayPauseIcon(); stopJukeboxAnalysis(); } function stopJukebox() { if (!jukeboxAudioElement) return; jukeboxAudioElement.pause(); jukeboxAudioElement.currentTime = 0; jukeboxIsPlaying = false; document.getElementById('jukebox-seek').value = 0; document.getElementById('jukebox-status').textContent = 'Stopped'; if (window.updateJukeboxPlayPauseIcon) window.updateJukeboxPlayPauseIcon(); stopJukeboxAnalysis(); document.getElementById('jukebox-amp-meter').style.width = '0%'; document.getElementById('jukebox-amp-value').textContent = '0.0'; document.getElementById('jukebox-kick-indicator').classList.remove('active'); document.querySelectorAll('.jukebox-track').forEach(el => el.classList.remove('playing')); } function updateJukeboxProgress() { if (!jukeboxAudioElement?.duration) return; document.getElementById('jukebox-seek').value = (jukeboxAudioElement.currentTime / jukeboxAudioElement.duration) * 100; updateJukeboxTime(); } function updateJukeboxTime() { if (!jukeboxAudioElement) return; const fmt = (s) => !isFinite(s) ? '0:00' : `${Math.floor(s/60)}:${Math.floor(s%60).toString().padStart(2,'0')}`; document.getElementById('jukebox-time').textContent = `${fmt(jukeboxAudioElement.currentTime)} / ${fmt(jukeboxAudioElement.duration || 0)}`; } function startJukeboxAnalysis() { // If using AudioWorklet, analysis happens automatically via worklet messages if (jukeboxUseWorklet) return; // Fallback: setInterval-based analysis with AnalyserNode if (jukeboxAnimationFrame) return; if (!jukeboxAnalyser) return; const freqData = new Uint8Array(jukeboxAnalyser.frequencyBinCount); const timeData = new Uint8Array(jukeboxAnalyser.fftSize); const sampleRate = jukeboxAudioContext.sampleRate; const binFreq = sampleRate / jukeboxAnalyser.fftSize; // Throttle UI updates to ~30fps (every ~33ms) to avoid layout thrashing let lastUIUpdate = 0; const UI_UPDATE_INTERVAL = 33; // Helper to get average energy in a frequency range function getBandEnergy(minHz, maxHz) { const startBin = Math.floor(minHz / binFreq); const endBin = Math.min(Math.floor(maxHz / binFreq), freqData.length - 1); let sum = 0; for (let i = startBin; i <= endBin; i++) sum += freqData[i]; return (endBin > startBin) ? (sum / (endBin - startBin + 1)) / 255 : 0; } function analyze() { if (!jukeboxIsPlaying) return; if (isSafari) perfCounters.audioTicks++; jukeboxAnalyser.getByteFrequencyData(freqData); jukeboxAnalyser.getByteTimeDomainData(timeData); let sum = 0; for (let i = 0; i < timeData.length; i++) { const n = (timeData[i] - 128) / 128; sum += n * n; } const amp = Math.min(1, Math.sqrt(sum / timeData.length) * 1.5); // Calculate frequency bands (0-1 scale) const bass = getBandEnergy(20, 250); // Bass (kick, bass guitar) const mid = getBandEnergy(250, 2000); // Mids (vocals, snare) const treble = getBandEnergy(2000, 20000); // Treble (hi-hats, cymbals) const highMid = getBandEnergy(1000, 4000); // High-mids (guitar presence) const presence = getBandEnergy(2500, 5000); // Presence (clarity) const bassEnd = Math.floor(250 * jukeboxAnalyser.fftSize / sampleRate); let bassEnergy = 0; for (let i = 0; i < Math.min(bassEnd, freqData.length); i++) bassEnergy += freqData[i] * freqData[i]; bassEnergy = Math.sqrt(bassEnergy / bassEnd) / 255; jukeboxEnergyHistory.push(bassEnergy); if (jukeboxEnergyHistory.length > JUKEBOX_ENERGY_HISTORY_SIZE) jukeboxEnergyHistory.shift(); let kick = 0; if (jukeboxEnergyHistory.length >= JUKEBOX_ENERGY_HISTORY_SIZE) { const avg = jukeboxEnergyHistory.reduce((a,b)=>a+b,0) / jukeboxEnergyHistory.length; const now = performance.now(); if (bassEnergy > avg * JUKEBOX_BEAT_SENSITIVITY && now - jukeboxLastBeatTime > JUKEBOX_BEAT_COOLDOWN) { kick = 1; jukeboxLastBeatTime = now; jukeboxCurrentBeat = true; } else if (now - jukeboxLastBeatTime > 50) { jukeboxCurrentBeat = false; } } document.getElementById('jukebox-kick-indicator').classList.toggle('active', jukeboxCurrentBeat); const audioData = { amp, leftAmp: amp, rightAmp: amp, beat: kick, kick, bass, mid, treble, highMid, presence }; sendJukeboxAudioToPreview(audioData); broadcastJukeboxAudioToPJ(audioData); // Update UI at throttled rate (DOM updates are expensive) const now = performance.now(); if (now - lastUIUpdate > UI_UPDATE_INTERVAL) { document.getElementById('jukebox-amp-meter').style.width = `${amp*100}%`; document.getElementById('jukebox-amp-value').textContent = amp.toFixed(2); lastUIUpdate = now; } } // Run at ~250Hz (4ms) for fast audio response, much faster than rAF's 60fps jukeboxAnimationFrame = setInterval(analyze, 4); analyze(); // Run immediately } function stopJukeboxAnalysis() { if (jukeboxAnimationFrame) { clearInterval(jukeboxAnimationFrame); jukeboxAnimationFrame = null; } jukeboxEnergyHistory = []; } // Track last log time to avoid spamming console let lastAudioDebugLog = 0; function sendJukeboxAudioToPreview(audioData) { // Forward audio to local popout window postToPopout({ type: 'kidlisp-audio', data: audioData }); // Skip local iframe when popout is active (saves performance) if (localPreviewPausedForPopout) return; if (iframeReady && previewIframe?.contentWindow) { previewIframe.contentWindow.postMessage({ type: 'kidlisp-audio', data: audioData }, aestheticUrl); } else { // Debug: log when events can't be sent (throttled to 1/sec) const now = Date.now(); if (now - lastAudioDebugLog > 1000) { console.log('🎵 Audio NOT sent - iframeReady:', iframeReady, 'iframe:', !!previewIframe, 'contentWindow:', !!previewIframe?.contentWindow, 'url:', aestheticUrl); lastAudioDebugLog = now; } } } function broadcastJukeboxAudioToPJ(audioData) { if (!codeChannelId) return;
// 1. BroadcastChannel (same-origin, zero-latency) — always send if (typeof BroadcastChannel !== 'undefined') { try { const bc = new BroadcastChannel(`kidlisp-channel-${codeChannelId}`); bc.postMessage({ type: 'audio-data', channelId: codeChannelId, data: audioData, timestamp: Date.now() }); bc.close(); } catch (e) {} }
// 2. UDP (geckos.io) — preferred for cross-origin, ~60Hz throttle from 250Hz source if (pjUdpConnected && pjUdpChannel) { pjUdpAudioCounter++; if (pjUdpAudioCounter % 4 === 0) { pjUdpChannel.emit('udp:audio', JSON.stringify({ codeChannel: codeChannelId, ...audioData, timestamp: Date.now() })); } return; // Skip WebSocket when UDP is available }
// 3. WebSocket fallback (when UDP unavailable) if (sessionWs?.readyState === WebSocket.OPEN) { sessionWs.send(JSON.stringify({ type: 'audio', content: { codeChannel: codeChannelId, ...audioData, timestamp: Date.now() } })); } } // ═══════════════════════════════════════════════════════════════════════════ // 🌀 Strudel Integration - HYBRID: Scheduler-based beats + continuous audio stream // ═══════════════════════════════════════════════════════════════════════════ // Uses Strudel's pattern scheduler (.onTrigger) for precise beat timing // PLUS continuous audio analysis for real-time amplitude streaming to KidLisp let strudelLoaded = false; let strudelLoading = false; // Track in-progress loading to prevent duplicates let strudelPlaying = false; let strudelAudioContext = null; let strudelRepl = null; // Store reference to Strudel repl for stop functionality let strudelAnalyser = null; // For continuous amplitude monitoring let strudelStreamInterval = null; // Continuous data stream to KidLisp let strudelSchedulerHooked = false; // Track if we've set up scheduler hooks let strudelLastTriggerTime = 0; let strudelTriggerCooldown = 50; // ms between triggers to debounce let strudelCurrentKick = false; // Set by scheduler, read by stream let strudelKickDecay = 0; // Decay counter for kick indicator // Scheduler-based beat callback - sets kick flag for the continuous stream function handleStrudelTrigger(hap, currentTime, cps, targetTime) { if (!strudelPlaying) return; const now = performance.now(); const value = hap.value || {}; // Determine if this is a "kick" or beat event const sound = value.s || value.sound || ''; const note = value.n || value.note || 0; const isKick = /^(bd|kick|bass|k|808|909)/i.test(sound) || (sound && note < 3) || sound === 'bd'; // Debounce rapid triggers const shouldTrigger = now - strudelLastTriggerTime > strudelTriggerCooldown; if (shouldTrigger && isKick) { strudelLastTriggerTime = now; // Calculate when this sound will actually play const playTime = targetTime * 1000; const latencyMs = Math.max(0, playTime - (currentTime * 1000)); // Schedule the kick flag to be set when the sound plays setTimeout(() => { if (!strudelPlaying) return; strudelCurrentKick = true; strudelKickDecay = 3; // Will decay over 3 stream cycles (~12ms) jukeboxCurrentBeat = true; jukeboxLastBeatTime = performance.now(); }, latencyMs); } } // Continuous audio stream - runs while Strudel plays, sends data to KidLisp function startStrudelStream() { if (strudelStreamInterval) return; // Try to create analyser from Strudel's audio output const ctx = window.getAudioContext?.(); if (ctx) { try { const dest = window.getDestination?.(); if (dest && !strudelAnalyser) { strudelAnalyser = ctx.createAnalyser(); strudelAnalyser.fftSize = 256; strudelAnalyser.smoothingTimeConstant = 0.3; dest.connect(strudelAnalyser); console.log('🌀 Strudel: Analyser connected for continuous amplitude'); } } catch (e) { console.log('🌀 Strudel: Could not connect analyser, using synthetic values'); } } const timeData = strudelAnalyser ? new Uint8Array(strudelAnalyser.fftSize) : null; const freqData = strudelAnalyser ? new Uint8Array(strudelAnalyser.frequencyBinCount) : null; // Run at ~250Hz for smooth streaming (matching songs tab) strudelStreamInterval = setInterval(() => { if (!strudelPlaying) return; if (isSafari) perfCounters.strudelTicks++;
let amp = 0.1; // Base level when no audio let bass = 0.1, mid = 0.1, treble = 0.1; // Get real amplitude if analyser available if (strudelAnalyser && timeData) { try { strudelAnalyser.getByteTimeDomainData(timeData); let sum = 0; for (let i = 0; i < timeData.length; i++) { const n = (timeData[i] - 128) / 128; sum += n * n; } amp = Math.min(1, Math.sqrt(sum / timeData.length) * 2); // Get frequency bands strudelAnalyser.getByteFrequencyData(freqData); const binFreq = (ctx?.sampleRate || 48000) / 256; const getBand = (minHz, maxHz) => { const start = Math.floor(minHz / binFreq); const end = Math.min(Math.floor(maxHz / binFreq), freqData.length - 1); let s = 0; for (let i = start; i <= end; i++) s += freqData[i]; return (end > start) ? (s / (end - start + 1)) / 255 : 0; }; bass = getBand(20, 250); mid = getBand(250, 2000); treble = getBand(2000, 20000); } catch (e) {} } // Decay kick flag if (strudelKickDecay > 0) { strudelKickDecay--; if (strudelKickDecay === 0) { strudelCurrentKick = false; jukeboxCurrentBeat = false; } } const audioData = { amp, leftAmp: amp, rightAmp: amp, beat: strudelCurrentKick ? 1 : 0, kick: strudelCurrentKick ? 1 : 0, bass, mid, treble, highMid: mid * 0.8, presence: treble * 0.6 }; sendJukeboxAudioToPreview(audioData); broadcastJukeboxAudioToPJ(audioData); // Update Strudel UI meters (throttled) const strudelAmpMeter = document.getElementById('strudel-amp-meter'); const strudelAmpValue = document.getElementById('strudel-amp-value'); const strudelKickIndicator = document.getElementById('strudel-kick-indicator'); if (strudelAmpMeter) strudelAmpMeter.style.width = `${amp * 100}%`; if (strudelAmpValue) strudelAmpValue.textContent = amp.toFixed(2); if (strudelKickIndicator) strudelKickIndicator.classList.toggle('active', strudelCurrentKick); }, 4); // ~250Hz } function stopStrudelStream() { if (strudelStreamInterval) { clearInterval(strudelStreamInterval); strudelStreamInterval = null; } strudelCurrentKick = false; strudelKickDecay = 0; } async function loadStrudel() { if (strudelLoaded) return true; if (strudelLoading) { // Wait for current load to complete while (strudelLoading) { await new Promise(r => setTimeout(r, 50)); } return strudelLoaded; } strudelLoading = true; document.getElementById('strudel-status').textContent = 'Loading Strudel...'; try { // Check if script is already loaded to prevent duplicate core warnings const existingScript = document.querySelector('script[src*="@strudel/web"]'); if (!existingScript) { // Load @strudel/web from CDN await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://unpkg.com/@strudel/web@1.3.0'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } else if (!window.initStrudel) { // Script exists but not fully loaded, wait for it await new Promise((resolve) => { const check = setInterval(() => { if (window.initStrudel) { clearInterval(check); resolve(); } }, 50); }); } // Only initialize if not already initialized if (!strudelRepl && window.initStrudel) { // Initialize Strudel with sample loading - returns the repl // Loads both default dirt-samples AND c_robo's sample library strudelRepl = await window.initStrudel({ prebake: () => Promise.all([ window.samples('github:tidalcycles/dirt-samples'), window.samples('https://cc.oooooooooooooooo.cc/strudel.json', 'https://cc.oooooooooooooooo.cc'), ]), }); } // Get Strudel's AudioContext (still needed for audio output) strudelAudioContext = window.getAudioContext?.(); console.log('🌀 Strudel: Loaded with scheduler-based beat detection + c_robo samples (cc.oooooooooooooooo.cc)'); strudelLoaded = true; strudelLoading = false; document.getElementById('strudel-status').textContent = 'Ready (+ c_robo samples)'; return true; } catch (err) { console.error('Failed to load Strudel:', err); strudelLoading = false; document.getElementById('strudel-status').textContent = 'Failed to load'; return false; } } async function playStrudel() { if (!await loadStrudel()) return; if (strudelAudioContext?.state === 'suspended') { await strudelAudioContext.resume(); } let code = document.getElementById('strudel-code').value.trim(); if (!code) { document.getElementById('strudel-status').textContent = 'Enter code first'; return; } // Stop samples if playing stopJukebox(); // Auto-inject .onTrigger() for scheduler-based beat detection // This hooks into the pattern scheduler directly - no audio analysis latency! // We expose a global callback that the pattern can call window.__kidlispStrudelTrigger = handleStrudelTrigger; // Inject onTrigger callback if not already present if (!code.includes('.onTrigger(') && !code.includes('onTrigger(')) { // Insert before .play() or at the end if (code.includes('.play()')) { code = code.replace(/\.play\(\s*\)/, '.onTrigger(window.__kidlispStrudelTrigger, false).play()'); } else { code = code + '.onTrigger(window.__kidlispStrudelTrigger, false)'; } } console.log('🌀 Strudel: Playing with scheduler hooks:', code.substring(0, 100) + '...'); try { // Set playing BEFORE evaluate - events can fire during evaluation strudelPlaying = true; document.getElementById('strudel-play').disabled = true; document.getElementById('strudel-stop').disabled = false; const statusEl = document.getElementById('strudel-status'); statusEl.textContent = '▶ Playing'; statusEl.style.color = 'var(--jukebox-accent)'; // Evaluate and play the pattern await window.evaluate(code, true); // Start continuous audio stream to KidLisp startStrudelStream(); } catch (err) { strudelPlaying = false; console.error('Strudel error:', err); const statusEl = document.getElementById('strudel-status'); statusEl.textContent = `Error: ${err.message}`; statusEl.style.color = '#f87171'; document.getElementById('strudel-play').disabled = false; document.getElementById('strudel-stop').disabled = true; } } function stopStrudel() { // Stop continuous audio stream first stopStrudelStream(); // Try multiple ways to stop Strudel try { // Method 1: Use the repl's stop method directly if (strudelRepl?.stop) { strudelRepl.stop(); console.log('🌀 Strudel stopped via repl.stop()'); } // Method 2: Use strudel.hush() else if (window.strudel?.hush) { window.strudel.hush(); console.log('🌀 Strudel stopped via strudel.hush()'); } // Method 3: Try window.hush else if (window.hush) { window.hush(); console.log('🌀 Strudel stopped via window.hush()'); } } catch (e) { console.warn('Strudel stop error:', e); } strudelPlaying = false; document.getElementById('strudel-play').disabled = false; document.getElementById('strudel-stop').disabled = true; const statusEl = document.getElementById('strudel-status'); statusEl.textContent = 'Stopped'; statusEl.style.color = 'var(--jukebox-text-dim)'; const strudelAmpMeter = document.getElementById('strudel-amp-meter'); const strudelAmpValue = document.getElementById('strudel-amp-value'); const strudelKickIndicator = document.getElementById('strudel-kick-indicator'); if (strudelAmpMeter) strudelAmpMeter.style.width = '0%'; if (strudelAmpValue) strudelAmpValue.textContent = '0.0'; if (strudelKickIndicator) strudelKickIndicator.classList.remove('active'); } // ═══════════════════════════════════════════════════════════════════════════ // 🎤 Aux - Microphone input with real-time analysis for KidLisp // ═══════════════════════════════════════════════════════════════════════════
let auxAudioContext = null; let auxStream = null; let auxSourceNode = null; let auxAnalyser = null; let auxGainNode = null; let auxWorkletNode = null; let auxUseWorklet = false; let auxStreamInterval = null; let auxRafId = null; let auxActive = false; let auxEnergyHistory = []; const AUX_ENERGY_HISTORY_SIZE = 10; const AUX_BEAT_SENSITIVITY = 1.12; const AUX_BEAT_COOLDOWN = 60; let auxLastBeatTime = 0; let auxCurrentBeat = false; let auxLatestAudioData = null; let auxLatestBeatState = false;
const AUX_GRAPH_MAX_POINTS = 180; const AUX_GRAPH_SERIES = [ "amp", "leftAmp", "rightAmp", "bass", "mid", "treble", "highMid", "presence", ]; const AUX_GRAPH_COLORS = { amp: "#ff7b6b", leftAmp: "#ffd166", rightAmp: "#f8961e", bass: "#4cc9f0", mid: "#06d6a0", treble: "#9b5de5", highMid: "#90be6d", presence: "#f72585", }; const auxGraphHistory = Object.fromEntries( AUX_GRAPH_SERIES.map((key) => [key, []]), );
function clamp01(value) { const n = Number(value); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(1, n)); }
function updateAuxMeter(baseId, value, decimals = 2) { const safeValue = clamp01(value); const meterEl = document.getElementById(`${baseId}-meter`); const valueEl = document.getElementById(`${baseId}-value`); if (meterEl) meterEl.style.width = `${safeValue * 100}%`; if (valueEl) valueEl.textContent = safeValue.toFixed(decimals); }
function pushAuxGraphSample(audioData) { for (const key of AUX_GRAPH_SERIES) { const samples = auxGraphHistory[key]; samples.push(clamp01(audioData?.[key])); if (samples.length > AUX_GRAPH_MAX_POINTS) samples.shift(); } }
function drawAuxLiveGraph() { const canvas = document.getElementById("aux-live-graph"); if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return;
const rect = canvas.getBoundingClientRect(); const cssWidth = Math.max(1, Math.floor(rect.width || canvas.clientWidth || canvas.width)); const cssHeight = Math.max(1, Math.floor(rect.height || canvas.clientHeight || canvas.height)); const dpr = window.devicePixelRatio || 1; const pixelWidth = Math.floor(cssWidth * dpr); const pixelHeight = Math.floor(cssHeight * dpr);
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { canvas.width = pixelWidth; canvas.height = pixelHeight; }
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = "rgba(0, 0, 0, 0.16)"; ctx.fillRect(0, 0, cssWidth, cssHeight);
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)"; ctx.lineWidth = 1; for (let i = 1; i < 4; i++) { const y = (cssHeight * i) / 4; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssWidth, y); ctx.stroke(); }
for (const key of AUX_GRAPH_SERIES) { const samples = auxGraphHistory[key]; if (!samples || samples.length < 2) continue; const denominator = Math.max(1, AUX_GRAPH_MAX_POINTS - 1); ctx.beginPath(); for (let i = 0; i < samples.length; i++) { const x = (i / denominator) * cssWidth; const y = cssHeight - samples[i] * cssHeight; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle = AUX_GRAPH_COLORS[key] || "#ffffff"; ctx.lineWidth = key === "amp" ? 1.8 : 1.2; ctx.globalAlpha = key === "amp" ? 0.95 : 0.8; ctx.stroke(); } ctx.globalAlpha = 1; }
function updateAuxDisplay(audioData = {}, beatActive = false) { const normalized = { amp: clamp01(audioData.amp), leftAmp: clamp01(audioData.leftAmp), rightAmp: clamp01(audioData.rightAmp), bass: clamp01(audioData.bass), mid: clamp01(audioData.mid), treble: clamp01(audioData.treble), highMid: clamp01(audioData.highMid), presence: clamp01(audioData.presence), }; const beatValue = beatActive ? 1 : clamp01(audioData.beat); const kickActive = beatActive || clamp01(audioData.kick) > 0.5;
updateAuxMeter("aux-amp", normalized.amp); updateAuxMeter("aux-left-amp", normalized.leftAmp); updateAuxMeter("aux-right-amp", normalized.rightAmp); updateAuxMeter("aux-beat", beatValue, 0); updateAuxMeter("aux-bass", normalized.bass); updateAuxMeter("aux-mid", normalized.mid); updateAuxMeter("aux-treble", normalized.treble); updateAuxMeter("aux-high-mid", normalized.highMid); updateAuxMeter("aux-presence", normalized.presence);
const kickIndicator = document.getElementById("aux-kick-indicator"); if (kickIndicator) kickIndicator.classList.toggle("active", kickActive);
pushAuxGraphSample(normalized); drawAuxLiveGraph(); }
function resetAuxDisplay() { for (const key of AUX_GRAPH_SERIES) { auxGraphHistory[key].length = 0; } updateAuxDisplay( { amp: 0, leftAmp: 0, rightAmp: 0, beat: 0, kick: 0, bass: 0, mid: 0, treble: 0, highMid: 0, presence: 0, }, false, ); }
async function populateAuxDevices() { const select = document.getElementById('aux-device'); if (!select) return; try { const prevValue = select.value; // Preserve current selection const devices = await navigator.mediaDevices.enumerateDevices(); const audioInputs = devices.filter(d => d.kind === 'audioinput'); select.innerHTML = '<option value="">Default</option>'; audioInputs.forEach(d => { const opt = document.createElement('option'); opt.value = d.deviceId; opt.textContent = d.label || `Mic ${select.options.length}`; select.appendChild(opt); }); // Restore previous selection if it still exists if (prevValue && Array.from(select.options).some(o => o.value === prevValue)) { select.value = prevValue; } } catch (e) { console.warn('🎤 Aux: Could not enumerate devices:', e); } }
async function startAux() { if (auxActive) return;
const statusEl = document.getElementById('aux-status'); statusEl.textContent = 'Requesting mic...';
// Stop other audio modes if (jukeboxIsPlaying) stopJukebox(); if (strudelPlaying) stopStrudel();
try { const deviceId = document.getElementById('aux-device')?.value; const audioConstraints = { echoCancellation: false, noiseSuppression: false, autoGainControl: false, latency: 0, }; if (deviceId) audioConstraints.deviceId = { exact: deviceId }; const constraints = { audio: audioConstraints }; auxStream = await navigator.mediaDevices.getUserMedia(constraints);
auxAudioContext = new (window.AudioContext || window.webkitAudioContext)({ latencyHint: 'interactive', }); auxSourceNode = auxAudioContext.createMediaStreamSource(auxStream); auxGainNode = auxAudioContext.createGain();
const gainSlider = document.getElementById('aux-gain'); if (gainSlider) auxGainNode.gain.value = gainSlider.value / 100;
// Try AudioWorklet first (runs on audio thread — ~2.7ms blocks at 48kHz) auxUseWorklet = await createAudioWorklet(auxAudioContext);
if (auxUseWorklet) { auxWorkletNode = new AudioWorkletNode(auxAudioContext, 'audio-analyzer'); auxWorkletNode.port.onmessage = (e) => { const data = e.data; if (data.kick) { auxCurrentBeat = true; auxLastBeatTime = performance.now(); } else if (performance.now() - auxLastBeatTime > 50) { auxCurrentBeat = false; } const audioData = { amp: data.amp, leftAmp: data.leftAmp, rightAmp: data.rightAmp, beat: data.beat, kick: data.kick, bass: data.bass, mid: data.bass * 0.6, treble: data.amp * 0.3, highMid: data.bass * 0.4, presence: data.amp * 0.25 }; sendJukeboxAudioToPreview(audioData); broadcastJukeboxAudioToPJ(audioData); auxLatestAudioData = audioData; auxLatestBeatState = auxCurrentBeat; // Throttle UI to rAF if (!auxRafId) { auxRafId = requestAnimationFrame(() => { updateAuxDisplay(auxLatestAudioData || audioData, auxLatestBeatState); auxRafId = null; }); } }; // source -> gain -> worklet -> silent destination (no speaker output) auxSourceNode.connect(auxGainNode); auxGainNode.connect(auxWorkletNode); // Connect worklet to a silent gain node to keep the graph alive const silentGain = auxAudioContext.createGain(); silentGain.gain.value = 0; auxWorkletNode.connect(silentGain); silentGain.connect(auxAudioContext.destination); console.log('🎤 Aux: Using AudioWorklet (sample-accurate, ~2.7ms latency)'); } else { // Fallback: AnalyserNode + setInterval auxAnalyser = auxAudioContext.createAnalyser(); auxAnalyser.fftSize = 256; auxAnalyser.smoothingTimeConstant = 0; auxSourceNode.connect(auxGainNode); auxGainNode.connect(auxAnalyser); // Don't connect to destination — avoids feedback console.log('🎤 Aux: Using AnalyserNode fallback (setInterval)'); }
auxActive = true; auxEnergyHistory = []; auxLatestAudioData = null; auxLatestBeatState = false; resetAuxDisplay();
document.getElementById('aux-start').disabled = true; document.getElementById('aux-stop').disabled = false; statusEl.textContent = auxUseWorklet ? '● Live (worklet)' : '● Live'; statusEl.style.color = '#e85d4a';
populateAuxDevices();
// Only start polling loop for the fallback path if (!auxUseWorklet) startAuxAnalysisFallback(); console.log('🎤 Aux: Microphone started'); } catch (err) { console.error('🎤 Aux: Failed to start:', err); statusEl.textContent = err.name === 'NotAllowedError' ? 'Mic denied' : `Error: ${err.message}`; statusEl.style.color = '#f87171'; } }
function stopAux() { stopAuxAnalysis();
if (auxStream) { auxStream.getTracks().forEach(t => t.stop()); auxStream = null; } if (auxSourceNode) { auxSourceNode.disconnect(); auxSourceNode = null; } if (auxGainNode) { auxGainNode.disconnect(); auxGainNode = null; } if (auxWorkletNode) { auxWorkletNode.disconnect(); auxWorkletNode = null; } if (auxAnalyser) { auxAnalyser.disconnect(); auxAnalyser = null; } if (auxAudioContext) { auxAudioContext.close(); auxAudioContext = null; }
auxActive = false; auxUseWorklet = false; auxEnergyHistory = []; auxLatestAudioData = null; auxLatestBeatState = false;
document.getElementById('aux-start').disabled = false; document.getElementById('aux-stop').disabled = true; const statusEl = document.getElementById('aux-status'); statusEl.textContent = 'Stopped'; statusEl.style.color = 'var(--jukebox-text-dim)'; resetAuxDisplay();
console.log('🎤 Aux: Microphone stopped'); }
// Fallback analysis for browsers without AudioWorklet support function startAuxAnalysisFallback() { if (auxStreamInterval) return; if (!auxAnalyser) return;
const freqData = new Uint8Array(auxAnalyser.frequencyBinCount); const timeData = new Uint8Array(auxAnalyser.fftSize); const sampleRate = auxAudioContext.sampleRate; const binFreq = sampleRate / auxAnalyser.fftSize; let lastUIUpdate = 0; const UI_UPDATE_INTERVAL = 33;
function getBandEnergy(minHz, maxHz) { const startBin = Math.floor(minHz / binFreq); const endBin = Math.min(Math.floor(maxHz / binFreq), freqData.length - 1); let sum = 0; for (let i = startBin; i <= endBin; i++) sum += freqData[i]; return (endBin > startBin) ? (sum / (endBin - startBin + 1)) / 255 : 0; }
function analyze() { if (!auxActive || !auxAnalyser) return; auxAnalyser.getByteFrequencyData(freqData); auxAnalyser.getByteTimeDomainData(timeData);
let sum = 0; for (let i = 0; i < timeData.length; i++) { const n = (timeData[i] - 128) / 128; sum += n * n; } const amp = Math.min(1, Math.sqrt(sum / timeData.length) * 2);
const bass = getBandEnergy(20, 250); const mid = getBandEnergy(250, 2000); const treble = getBandEnergy(2000, 20000); const highMid = getBandEnergy(1000, 4000); const presence = getBandEnergy(2500, 5000);
const bassEnd = Math.floor(250 * auxAnalyser.fftSize / sampleRate); let bassEnergy = 0; for (let i = 0; i < Math.min(bassEnd, freqData.length); i++) bassEnergy += freqData[i] * freqData[i]; bassEnergy = Math.sqrt(bassEnergy / bassEnd) / 255;
auxEnergyHistory.push(bassEnergy); if (auxEnergyHistory.length > AUX_ENERGY_HISTORY_SIZE) auxEnergyHistory.shift();
let kick = 0; if (auxEnergyHistory.length >= AUX_ENERGY_HISTORY_SIZE) { const avg = auxEnergyHistory.reduce((a, b) => a + b, 0) / auxEnergyHistory.length; const now = performance.now(); if (bassEnergy > avg * AUX_BEAT_SENSITIVITY && now - auxLastBeatTime > AUX_BEAT_COOLDOWN) { kick = 1; auxLastBeatTime = now; auxCurrentBeat = true; } else if (now - auxLastBeatTime > 50) { auxCurrentBeat = false; } }
const audioData = { amp, leftAmp: amp, rightAmp: amp, beat: kick, kick, bass, mid, treble, highMid, presence }; sendJukeboxAudioToPreview(audioData); broadcastJukeboxAudioToPJ(audioData);
const now = performance.now(); if (now - lastUIUpdate > UI_UPDATE_INTERVAL) { updateAuxDisplay(audioData, auxCurrentBeat); lastUIUpdate = now; } }
auxStreamInterval = setInterval(analyze, 4); analyze(); }
function stopAuxAnalysis() { if (auxStreamInterval) { clearInterval(auxStreamInterval); auxStreamInterval = null; } if (auxRafId) { cancelAnimationFrame(auxRafId); auxRafId = null; } auxEnergyHistory = []; }
function initAuxUI() { document.getElementById('aux-start')?.addEventListener('click', startAux); document.getElementById('aux-stop')?.addEventListener('click', stopAux); document.getElementById('aux-gain')?.addEventListener('input', (e) => { if (auxGainNode) auxGainNode.gain.value = e.target.value / 100; }); document.getElementById('aux-device')?.addEventListener('change', () => { // Restart with new device if already active if (auxActive) { stopAux(); startAux(); } }); resetAuxDisplay(); }
function initStrudelUI() { // Jukebox subtab switching (similar to list subtabs) let currentJukeboxSubtab = 'songs'; function showJukeboxSubtab(subtab) { currentJukeboxSubtab = subtab; // Update sub-tab active states document.querySelectorAll('.jukebox-subtab').forEach(el => { el.classList.toggle('active', el.dataset.subtab === subtab); }); // Hide all subcontents document.querySelectorAll('.jukebox-subcontent').forEach(el => el.classList.add('hidden')); // Show the corresponding subcontent const subcontent = document.getElementById(`${subtab}-subcontent`); if (subcontent) subcontent.classList.remove('hidden'); // Stop other modes when switching if (subtab === 'songs') { if (strudelPlaying) stopStrudel(); if (auxActive) stopAux(); } if (subtab === 'strudel') { if (jukeboxIsPlaying) stopJukebox(); if (auxActive) stopAux(); } if (subtab === 'aux') { if (jukeboxIsPlaying) stopJukebox(); if (strudelPlaying) stopStrudel(); } } // Subtab click handlers document.querySelectorAll('.jukebox-subtab').forEach(subtab => { subtab.addEventListener('click', function(e) { e.stopPropagation(); const subtabType = this.dataset.subtab; showJukeboxSubtab(subtabType); }); }); // Strudel controls document.getElementById('strudel-play')?.addEventListener('click', playStrudel); document.getElementById('strudel-stop')?.addEventListener('click', stopStrudel); // Volume control (Strudel doesn't expose easy volume control, this is a stub) document.getElementById('strudel-volume')?.addEventListener('input', (e) => { const vol = e.target.value / 100; // Note: Strudel uses destination directly, volume control would require // intercepting the audio graph. For now this is just a UI placeholder. console.log('🌀 Strudel volume slider:', vol); }); // Preset buttons document.querySelectorAll('.strudel-preset').forEach(btn => { btn.addEventListener('click', () => { const code = btn.dataset.code; document.getElementById('strudel-code').value = code; }); }); // Keyboard shortcut: Ctrl/Cmd+Enter to play document.getElementById('strudel-code')?.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); playStrudel(); } }); } // Lazy init: jukebox/strudel UI only initializes when music tab is first opened let jukeboxInitialized = false; window._initJukeboxLazy = function() { if (jukeboxInitialized) return; jukeboxInitialized = true; initJukebox(); initStrudelUI(); initAuxUI(); }; </script> <!-- Slide mode overlays --> <div id="slide-drag-overlay"></div> <div id="slide-value-tooltip"></div></body>
</html>