Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
288 kB · 7599 lines
HTML
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600<!DOCTYPE html><!-- KidLisp.com Keeps - Index of KidLisp $codes cached via store-kidlisp --><!-- URL: keep.kidlisp.com --><html lang="en"><head> <meta charset="utf-8"> <title>KidLisp Keeps</title> <meta name="description" content="KidLisp Keeps is a generative art marketplace on Tezos. Browse, collect, and trade interactive artworks written in KidLisp — a minimal Lisp for creative computing." />
<!-- Open Graph / Social Preview --> <meta property="og:type" content="website" /> <meta property="og:url" content="https://keep.kidlisp.com" /> <meta property="og:title" content="KidLisp Keeps" /> <meta property="og:description" content="Browse, collect, and trade interactive generative artworks written in KidLisp — a minimal Lisp for creative computing on Aesthetic.Computer." /> <meta property="og:image" content="https://oven.aesthetic.computer/kidlisp-og/site/keeps.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 Keeps" /> <meta name="twitter:description" content="Browse, collect, and trade interactive generative artworks written in KidLisp on Tezos." /> <meta name="twitter:image" content="https://oven.aesthetic.computer/kidlisp-og/site/keeps.png" />
<link rel="icon" type="image/gif" href="https://assets.aesthetic.computer/kidlisp-favicon.gif" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <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"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&text=%EA%9C%A9&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<!-- Auth0 SPA SDK --> <script src="https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js"></script>
<style> /* ======================================== FONTS ======================================== */ @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; }
/* ======================================== CSS VARIABLES (from kidlisp.com) ======================================== */ :root { --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; --ac-purple: rgb(205, 92, 155); --ac-purple-light: rgb(240, 235, 250); --ac-purple-dark: rgb(48, 43, 58); --font-mono: 'Noto Sans Mono', 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; --font-display: 'YWFTProcessing-Regular', monospace; --font-fun: 'Comic Relief', 'Comic Sans MS', cursive; }
@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; } }
/* 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; }
[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; }
/* ======================================== RESET & BASE ======================================== */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--bg-secondary); } ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
* { box-sizing: border-box; }
body { margin: 0; font-family: var(--font-mono); font-size: 14px; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; display: flex; flex-direction: column; cursor: url('https://aesthetic.computer/aesthetic.computer/cursors/precise.svg') 12 12, auto; }
a { color: var(--ac-purple); text-decoration: none; } a:hover { text-decoration: underline; }
/* Colored dot for Aesthetic.Computer links */ .ac-dot { color: #4ECDC4; font-weight: bold; }
@media (prefers-color-scheme: dark) { .ac-dot { color: #70D6FF; } } [data-theme="dark"] .ac-dot { color: #70D6FF; } [data-theme="light"] .ac-dot { color: #4ECDC4; }
/* ======================================== HERO HEADER (logo + /keeps inside castle area) ======================================== */ .keeps-hero-header { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 16px; }
.keeps-logo { font-family: var(--font-fun); font-weight: bold; font-size: 22px; display: flex; align-items: center; gap: 0; text-decoration: none; } .keeps-logo:hover { text-decoration: none; }
.keeps-logo .baby-k { color: #FF6B6B; } .keeps-logo .baby-i { color: #4ECDC4; } .keeps-logo .baby-d { color: #FFE66D; } .keeps-logo .baby-l { color: #A8E6CF; } .keeps-logo .baby-i2 { color: #FF8B94; } .keeps-logo .baby-s { color: #F7DC6F; } .keeps-logo .baby-p { color: #BB8FCE; } .keeps-logo .com-part { color: #70D6FF; } .keeps-logo span { text-shadow: 1px 1px 0px rgba(0,0,0,0.3); } .keeps-logo .com-part { text-shadow: none; }
.keeps-logo .keeps-subdomain { font-family: inherit; font-size: inherit; font-weight: inherit; letter-spacing: 0; } @keyframes keeps-green { 0% { color: #00ff41; } 25% { color: #39ff7a; } 50% { color: #00c832; } 75% { color: #80ffb0; } 100% { color: #00ff41; } } .keeps-logo .kl0 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite; } .keeps-logo .kl1 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -0.6s; } .keeps-logo .kl2 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -1.2s; } .keeps-logo .kl3 { color: #00ff41; animation: keeps-green 3s ease-in-out infinite -1.8s; }
/* ======================================== CASTLE HERO ======================================== */ .keeps-hero { background: linear-gradient(to bottom, #b8dff0 0%, var(--bg-tertiary) 20%); border-bottom: 1px solid var(--border-subtle); padding: 0; display: flex; flex-direction: column; align-items: center; overflow: visible; }
@media (prefers-color-scheme: dark) { .keeps-hero { background: linear-gradient(to bottom, #0d2233 0%, var(--bg-tertiary) 20%); } } [data-theme="dark"] .keeps-hero { background: linear-gradient(to bottom, #0d2233 0%, var(--bg-tertiary) 20%); } [data-theme="light"] .keeps-hero { background: linear-gradient(to bottom, #b8dff0 0%, var(--bg-tertiary) 20%); }
.keeps-hero-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.keeps-about-btn { display: inline-flex; align-items: center; justify-content: center; padding: 4px 10px; background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-color); border-radius: 0; font-family: var(--font-mono); font-size: 12px; cursor: pointer; transition: border-color 0.15s, color 0.15s; line-height: 1; } .keeps-about-btn:hover { border-color: var(--ac-purple); color: var(--ac-purple); }
.keeps-castle-wrap { position: relative; width: 100%; max-width: 580px; height: 280px; display: flex; align-items: flex-end; justify-content: center; order: 2; /* Below about-inline in flex layout */ transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.6s ease; }
#keeps-about-inline { order: 1; /* Above castle in flex layout */ }
/* Castle starts flush at bottom, rises on enter */ .keeps-castle-wrap.grounded { transform: translateY(20px); } .keeps-castle-wrap.risen { transform: translateY(-20px); }
.keeps-castle-wrap svg { width: 280px; height: auto; filter: drop-shadow(0 4px 16px rgba(205, 92, 155, 0.22)); }
/* Floating $codes rising from castle */ .keeps-code-float { position: absolute; font-family: var(--font-mono); font-size: 14px; font-weight: 700; opacity: 0; animation: floatUp 6s ease-in-out infinite; animation-play-state: paused; pointer-events: none; text-shadow: 0 1px 4px rgba(0,0,0,0.12); } /* Codes only animate after castle rises */ .keeps-castle-wrap.risen .keeps-code-float { animation-play-state: running; } .keeps-code-float:nth-child(1) { left: 18%; animation-delay: 0s; color: #FF6B6B; } .keeps-code-float:nth-child(2) { left: 35%; animation-delay: 1.2s; color: #4ECDC4; } .keeps-code-float:nth-child(3) { left: 52%; animation-delay: 0.6s; color: #FFE66D; } .keeps-code-float:nth-child(4) { left: 68%; animation-delay: 2.0s; color: #A8E6CF; } .keeps-code-float:nth-child(5) { left: 80%; animation-delay: 3.2s; color: #BB8FCE; } .keeps-code-float:nth-child(6) { left: 25%; animation-delay: 4.0s; color: #FF8B94; } .keeps-code-float:nth-child(7) { left: 60%; animation-delay: 1.8s; color: #70D6FF; } .keeps-code-float:nth-child(8) { left: 42%; animation-delay: 2.8s; color: #F7DC6F; }
@keyframes floatUp { 0% { opacity: 0; transform: translateY(0); } 10% { opacity: 1; } 80% { opacity: 0.8; } 100% { opacity: 0; transform: translateY(-200px); } }
/* ======================================== TABS SYSTEM ======================================== */ .keeps-tabs-container { background: var(--bg-tertiary); border-bottom: 1px solid var(--border-subtle); }
.keeps-tabs { display: flex; align-items: center; gap: 0; padding: 0; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); overflow: hidden; }
.keeps-tab { flex: 1 1 50%; min-width: 0; padding: 18px 12px; font-family: var(--font-fun); font-size: 20px; font-weight: 700; letter-spacing: 0.01em; text-align: center; color: var(--text-primary); background: var(--bg-tertiary); border: none; border-bottom: 4px solid transparent; cursor: pointer; transition: all 0.15s; user-select: none; }
.keeps-tab:hover { filter: brightness(1.05); }
.keeps-tab[data-tab="unkept"] { color: #ff6b6b; background: rgba(255, 107, 107, 0.12); }
.keeps-tab[data-tab="kept"] { color: #4ade80; background: rgba(74, 222, 128, 0.12); }
.keeps-tab.active[data-tab="unkept"] { background: rgba(255, 107, 107, 0.24); border-bottom-color: #ff6b6b; box-shadow: inset 0 -1px 0 rgba(255, 107, 107, 0.5); }
.keeps-tab.active[data-tab="kept"] { background: rgba(74, 222, 128, 0.24); border-bottom-color: #4ade80; box-shadow: inset 0 -1px 0 rgba(74, 222, 128, 0.5); }
.keeps-tab[data-tab="market"] { color: #f59e0b; background: rgba(245, 158, 11, 0.10); }
.keeps-tab.active[data-tab="market"] { background: rgba(245, 158, 11, 0.22); border-bottom-color: #f59e0b; box-shadow: inset 0 -1px 0 rgba(245, 158, 11, 0.5); }
/* Market tab content */ #tab-market { padding: 0; max-width: none; }
.market-controls { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 10px 20px; border-bottom: 1px solid var(--border-subtle); background: var(--bg-primary); }
.market-controls-left, .market-controls-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.market-sort-label { font-size: 12px; color: var(--text-tertiary); }
.market-sort-btn { padding: 4px 10px; font-size: 12px; font-family: var(--font-fun); border: 1px solid var(--border-color); background: transparent; color: var(--text-secondary); cursor: pointer; border-radius: 0; }
.market-sort-btn.active { background: rgba(245, 158, 11, 0.15); border-color: #f59e0b; color: #f59e0b; }
.market-live-badge { font-size: 11px; font-family: var(--font-mono); letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 7px; border: 1px solid rgba(52, 211, 153, 0.45); color: #34d399; background: rgba(5, 46, 22, 0.35); }
.market-live-badge.stale { color: #f59e0b; border-color: rgba(245, 158, 11, 0.45); background: rgba(120, 53, 15, 0.28); }
.market-live-badge.error { color: #f87171; border-color: rgba(248, 113, 113, 0.45); background: rgba(127, 29, 29, 0.28); }
.market-last-updated { font-size: 11px; color: var(--text-tertiary); min-width: 110px; text-align: right; }
.market-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; padding: 16px 20px; }
.market-card { background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; cursor: pointer; transition: border-color 0.15s; text-decoration: none; }
.market-card:hover { border-color: #f59e0b; text-decoration: none; }
.market-card.sold { border-color: rgba(248, 113, 113, 0.35); background: rgba(127, 29, 29, 0.08); }
.market-card.sold:hover { border-color: #f87171; }
.market-card-thumb { width: 100%; aspect-ratio: 1; background: #000; overflow: hidden; }
.market-card-thumb img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; display: block; }
.market-card-body { padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; }
.market-card-code { font-family: var(--font-mono); font-size: 12px; color: var(--ac-purple); font-weight: bold; }
.market-card-price { font-size: 14px; font-weight: bold; color: #60a5fa; }
.market-card-price.sold { color: #f87171; }
.market-card-status { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 700; }
.market-card-status.active { color: #34d399; }
.market-card-status.sold { color: #f87171; }
.market-card-seller { font-size: 12px; color: var(--text-tertiary); }
.market-card-objkt { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
.market-empty { grid-column: 1 / -1; text-align: center; padding: 48px 20px; color: var(--text-tertiary); font-size: 14px; }
/* Keep Detail Modal (permalink overlay) */ .keep-detail-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); z-index: 19000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
.keep-detail-overlay.open { display: flex; animation: modalFadeIn 0.2s ease-out forwards; }
.keep-detail-modal { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 12px; max-width: 480px; width: 100%; margin: 16px; max-height: 90vh; overflow-y: auto; box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4); }
.keep-detail-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--border-subtle); }
.keep-detail-title { font-family: var(--font-mono); font-size: 16px; font-weight: bold; color: var(--ac-purple); }
.keep-detail-close { background: none; border: none; color: var(--text-tertiary); font-size: 22px; cursor: pointer; padding: 0 4px; line-height: 1; }
.keep-detail-close:hover { color: var(--text-primary); }
.keep-detail-thumb { width: 100%; aspect-ratio: 1; background: #000; overflow: hidden; }
.keep-detail-thumb img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; display: block; }
.keep-detail-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.keep-detail-row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
.keep-detail-label { color: var(--text-tertiary); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
.keep-detail-value { color: var(--text-primary); font-family: var(--font-mono); font-size: 13px; text-align: right; }
.keep-detail-price { font-size: 20px; font-weight: bold; color: #60a5fa; }
.keep-detail-price.sold { color: #f87171; }
.keep-detail-status { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.keep-detail-status.active { color: #34d399; } .keep-detail-status.sold { color: #f87171; } .keep-detail-status.unlisted { color: var(--text-tertiary); }
.keep-detail-actions { display: flex; flex-direction: column; gap: 8px; padding: 0 16px 16px; }
.keep-detail-btn { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 16px; border: 1px solid var(--border-color); border-radius: 6px; font-family: var(--font-mono); font-size: 13px; cursor: pointer; text-decoration: none; transition: border-color 0.15s, background 0.15s; }
.keep-detail-btn:hover { border-color: #f59e0b; text-decoration: none; }
.keep-detail-btn.primary { background: rgba(245, 158, 11, 0.15); border-color: #f59e0b; color: #f59e0b; font-weight: bold; }
.keep-detail-btn.primary:hover { background: rgba(245, 158, 11, 0.25); }
.keep-detail-btn.secondary { background: transparent; color: var(--text-secondary); }
.keep-detail-btn-row { display: flex; gap: 8px; }
.keep-detail-btn-row .keep-detail-btn { flex: 1; }
.keep-detail-loading { padding: 48px 20px; text-align: center; color: var(--text-tertiary); font-size: 14px; }
@media (max-width: 480px) { .keep-detail-modal { margin: 8px; border-radius: 10px; } .keep-detail-price { font-size: 18px; } }
.keeps-tabs.logged-out { background: var(--bg-secondary); border-bottom-color: var(--border-subtle); pointer-events: none; min-height: 14px; } .keeps-tabs.logged-out .keeps-tab { display: none !important; }
.keeps-tab-content { display: none; padding: 20px 24px; line-height: 1.7; font-size: 14px; color: var(--text-secondary); max-width: 720px; margin: 0 auto; }
.keeps-tab-content.active { display: block; }
/* Index tab is full-width with no padding (search bar + grid have their own) */ #tab-index { max-width: none; padding: 0; }
.keeps-about-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.52); z-index: 30000; display: none; justify-content: center; align-items: center; padding: 20px; overflow-y: auto; overscroll-behavior: contain; }
.keeps-about-overlay.open { display: flex; }
.keeps-about-panel { width: min(480px, calc(100vw - 40px)); background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); overflow: hidden; display: flex; flex-direction: column; }
.keeps-about-panel-header { padding: 10px 14px; border-bottom: 1px solid var(--border-subtle); background: var(--bg-secondary); display: flex; align-items: center; gap: 8px; }
.keeps-about-panel-title { margin: 0; font-family: var(--font-display); font-size: 16px; color: var(--text-primary); letter-spacing: 0.01em; }
.keeps-about-panel-close { margin-left: auto; width: 28px; height: 28px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-tertiary); color: var(--text-secondary); font-size: 18px; line-height: 1; cursor: pointer; } .keeps-about-panel-close:hover { border-color: var(--ac-purple); color: var(--ac-purple); }
.keeps-about-panel-body { overflow: auto; padding: 0 24px 24px; }
body.keeps-about-open { overflow: hidden; }
.keeps-about-panel-body #tab-rules { display: block; max-width: none; padding: 0; margin: 0; }
.keeps-tab-content p { margin: 0 0 10px 0; }
.keeps-about-simple { padding: 20px 0 8px; }
.keeps-about-lede { font-family: var(--font-fun); font-size: 22px; color: var(--text-primary); margin: 0 0 24px 0; line-height: 1.6; display: flex; flex-direction: column; gap: 14px; text-align: left; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); counter-reset: rule; }
.keeps-about-lede > span { display: block; counter-increment: rule; font-size: 1em; font-weight: 600; letter-spacing: 0.01em; }
.keeps-about-lede > span::before { content: counter(rule) ". "; font-weight: 700; opacity: 0.5; }
/* Progressive agreement color ramp across preamble lines */ .keeps-about-lede span:nth-child(1) { color: #ff6b6b; } .keeps-about-lede span:nth-child(2) { color: #f59e0b; } .keeps-about-lede span:nth-child(3) { color: #4ade80; } .keeps-about-lede span:nth-child(4) { color: #60a5fa; }
.keeps-about-lede .keeps-price { color: inherit; font-weight: 700; }
.code-slot { display: inline; font-family: var(--font-mono, monospace); } .code-slot .slot-char { display: inline-block; transition: opacity 0.12s, transform 0.12s; color: limegreen !important; } .code-slot .slot-char.flip { opacity: 0; transform: translateY(-0.3em); } .code-slot .slot-dollar { color: limegreen !important; }
.keeps-price-tez { color: #60a5fa !important; } .keeps-price-tez .tez-prefix { vertical-align: 0.1em; } .keeps-price-usd { color: #4ade80 !important; }
.keeps-about-lede .keeps-fee-detail { font-size: 1em; opacity: 0.7; }
.keeps-enter-btn { font-family: var(--font-fun); font-size: 18px; font-weight: 700; padding: 14px 40px; background: #40c060; color: #fff; border: none; border-radius: 6px; cursor: pointer; margin: 12px auto 20px; display: block; transition: background 0.15s; } .keeps-enter-btn:hover { background: #35a850; }
.keeps-tabs.locked .keeps-tab { pointer-events: none; color: transparent; }
.keeps-about-tech { margin-top: 8px; border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; }
.keeps-about-tech--tezos { border-color: rgba(134, 88, 226, 0.35); } .keeps-about-tech--tezos summary { color: #8658e2; background: rgba(134, 88, 226, 0.07); } .keeps-about-tech--tezos summary::before { color: #8658e2; } .keeps-about-tech--tezos .keeps-about-tech-body { border-top-color: rgba(134, 88, 226, 0.2); }
.keeps-about-tech--security { border-color: rgba(74, 156, 93, 0.35); } .keeps-about-tech--security summary { color: #4a9c5d; background: rgba(74, 156, 93, 0.07); } .keeps-about-tech--security summary::before { color: #4a9c5d; } .keeps-about-tech--security .keeps-about-tech-body { border-top-color: rgba(74, 156, 93, 0.2); }
.keeps-about-tech--ipfs { border-color: rgba(59, 130, 246, 0.35); } .keeps-about-tech--ipfs summary { color: #3b82f6; background: rgba(59, 130, 246, 0.07); } .keeps-about-tech--ipfs summary::before { color: #3b82f6; } .keeps-about-tech--ipfs .keeps-about-tech-body { border-top-color: rgba(59, 130, 246, 0.2); }
.keeps-about-tech summary { padding: 12px 14px; cursor: pointer; font-size: 15px; font-weight: 600; color: var(--text-secondary); background: var(--bg-secondary); user-select: none; list-style: none; display: flex; align-items: center; gap: 6px; }
.keeps-about-tech summary::-webkit-details-marker { display: none; }
.keeps-about-tech summary::before { content: '▶'; font-size: 9px; opacity: 0.5; transition: transform 0.15s; }
.keeps-about-tech[open] summary::before { transform: rotate(90deg); }
.keeps-about-tech-body { padding: 16px; font-size: 16px; color: var(--text-secondary); line-height: 1.65; border-top: 1px solid var(--border-color); }
.keeps-about-tech-body p { margin: 0 0 14px 0; }
.keeps-about-tech-body p:last-child { margin-bottom: 0; }
.keeps-about-tech-section-label { font-size: 13px; font-weight: bold; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-tertiary); margin: 22px 0 10px !important; } .keeps-about-tech-section-label:first-child { margin-top: 0 !important; }
.keeps-about-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin: 14px 0; }
.keeps-about-stat { padding: 12px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 7px; text-align: center; }
.keeps-about-stat-value { font-size: 22px; font-weight: 700; color: var(--ac-purple); font-family: 'Courier New', monospace; }
.keeps-about-stat-label { font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 4px; }
.keeps-about-contract-box { margin: 12px 0; padding: 12px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 7px; }
.keeps-about-contract-label { margin-bottom: 6px; color: var(--text-tertiary); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.keeps-about-links { margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap; }
.keeps-about-link-btn { padding: 5px 10px; border-radius: 5px; text-decoration: none; font-size: 12px; font-weight: 600; border: 1px solid var(--border-color); color: var(--text-primary); background: var(--bg-tertiary); }
.keeps-about-link-btn.primary { background: var(--ac-purple); border-color: var(--ac-purple); color: #fff; }
.keeps-about-note { color: var(--text-tertiary); font-size: 12px; }
.keeps-about-security-list { margin: 0; padding-left: 18px; }
.keeps-about-security-list li { margin-bottom: 10px; line-height: 1.55; }
.keeps-about-security-list li:last-child { margin-bottom: 0; }
.keeps-about-caveat { display: block; margin-top: 4px; color: var(--text-tertiary); font-size: 12px; }
.keeps-tab-content p:last-child { margin-bottom: 0; }
.keeps-tab-content code { background: var(--code-bg); color: var(--code-text); padding: 1px 5px; border-radius: 3px; font-size: 12px; }
.keeps-tab-content a { color: var(--ac-purple); text-decoration: none; }
.keeps-tab-content a:hover { text-decoration: underline; }
.keeps-tab-content h3 { font-family: var(--font-display); font-size: 15px; color: var(--text-primary); margin: 20px 0 10px 0; border-bottom: 1px solid var(--border-subtle); padding-bottom: 6px; }
.keeps-tab-content h3:first-child { margin-top: 0; }
.keeps-tab-content ul { margin: 10px 0; padding-left: 24px; }
.keeps-tab-content li { margin: 6px 0; }
.keeps-endpoint { background: var(--bg-primary); border: 1px solid var(--border-subtle); border-radius: 6px; padding: 12px 16px; margin: 12px 0; }
.keeps-endpoint-name { font-family: var(--font-mono); font-size: 14px; font-weight: 700; color: var(--ac-purple); margin-bottom: 4px; }
.keeps-endpoint-name.v4 { color: #FF8B94; }
.keeps-endpoint-name.v4::after { content: " v4"; color: #FFE66D; font-size: 12px; font-weight: 500; margin-left: 6px; }
.keeps-endpoint-name.v5 { color: #7FDBCA; }
.keeps-endpoint-name.v5::after { content: " v5"; color: #7FDBCA; font-size: 12px; font-weight: 500; margin-left: 6px; }
.keeps-version-select { padding: 8px 12px; font-family: var(--font-mono); font-size: 14px; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; outline: none; }
.keeps-version-select:focus { border-color: var(--ac-purple); }
.keeps-deprecated-banner { padding: 10px 16px; margin-bottom: 20px; background: #3e3520; border: 1px solid #665a30; border-radius: 6px; color: #d4b94a; font-size: 14px; font-weight: 500; }
@media (prefers-color-scheme: light) { .keeps-deprecated-banner { background: #fef9e7; border-color: #e0d39a; color: #8a7520; } } [data-theme="light"] .keeps-deprecated-banner { background: #fef9e7; border-color: #e0d39a; color: #8a7520; }
.keeps-endpoint-name.v6 { color: #4ade80; }
.keeps-endpoint-name.v6::after { content: " v8"; color: #4ade80; font-size: 12px; font-weight: 500; margin-left: 6px; }
/* v6 Launch Steps */ .v6-launch-steps { display: flex; flex-direction: column; gap: 0; }
.v6-step { display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; border-left: 2px solid var(--border-color); position: relative; }
.v6-step.done { border-left-color: #4a9c5d; }
.v6-step.pending { border-left-color: var(--border-color); }
.v6-step-icon { flex-shrink: 0; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; font-size: 14px; border-radius: 50%; }
.v6-step.done .v6-step-icon { color: #4a9c5d; }
.v6-step.pending .v6-step-icon { color: var(--text-tertiary); }
.v6-step.in-progress { border-left-color: #d4a017; background: rgba(255, 193, 7, 0.06); }
.v6-step.in-progress .v6-step-icon { color: #d4a017; }
.v6-step.in-progress .v6-step-title { color: #d4a017; }
.v6-step-content { flex: 1; }
.v6-step-title { font-weight: 600; font-size: 14px; color: var(--text-primary); margin-bottom: 2px; }
.v6-step.done .v6-step-title { color: var(--text-tertiary); }
.v6-step-desc { font-size: 12px; color: var(--text-tertiary); line-height: 1.5; }
.v6-step-desc code { background: var(--code-bg); color: var(--code-text); padding: 1px 5px; border-radius: 3px; font-size: 12px; }
.v6-roadmap-status { margin-bottom: 24px; padding: 16px; background: linear-gradient(135deg, rgba(205, 92, 155, 0.14), rgba(78, 205, 196, 0.09)); border: 1px solid var(--border-color); border-radius: 8px; }
.v6-roadmap-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
.v6-roadmap-kicker { font-size: 12px; color: var(--text-tertiary); letter-spacing: 0.2px; margin-bottom: 2px; }
.v6-roadmap-title { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.v6-roadmap-metric { font-size: 12px; font-weight: 600; color: var(--text-primary); background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 999px; padding: 4px 10px; white-space: nowrap; }
.v6-roadmap-track { width: 100%; height: 8px; border-radius: 999px; background: rgba(0, 0, 0, 0.08); overflow: hidden; }
.v6-roadmap-fill { height: 100%; width: 0; background: linear-gradient(90deg, #4a9c5d, #4ECDC4); transition: width 0.25s ease; }
.v6-roadmap-next { margin-top: 10px; font-size: 12px; color: var(--text-secondary); }
.v6-metadata-sim { margin-bottom: 24px; padding: 16px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; }
.v6-metadata-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.v6-metadata-title { color: var(--text-secondary); font-size: 14px; font-weight: 600; }
.v6-metadata-subtitle { margin-top: 2px; color: var(--text-tertiary); font-size: 12px; line-height: 1.4; }
.v6-sim-btn { padding: 6px 10px; background: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 6px; font-family: var(--font-mono); font-size: 12px; cursor: pointer; }
.v6-sim-btn:hover { border-color: var(--ac-purple); color: var(--ac-purple); }
.v6-sim-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 12px; }
.v6-sim-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--text-tertiary); }
.v6-sim-field input { width: 100%; padding: 7px 8px; font-size: 12px; color: var(--text-primary); background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; outline: none; font-family: var(--font-mono); }
.v6-sim-field select { width: 100%; padding: 7px 8px; font-size: 12px; color: var(--text-primary); background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; outline: none; font-family: var(--font-mono); }
.v6-sim-field input:focus { border-color: var(--ac-purple); }
.v6-sim-field select:focus { border-color: var(--ac-purple); }
.v6-sim-endpoint { margin-bottom: 8px; font-size: 12px; color: var(--text-tertiary); word-break: break-all; }
.v6-sim-textarea { width: 100%; min-height: 100px; padding: 9px 10px; font-size: 12px; color: var(--text-primary); background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; outline: none; font-family: var(--font-mono); resize: vertical; line-height: 1.4; }
.v6-sim-textarea:focus { border-color: var(--ac-purple); }
.v6-sim-actions { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
.v6-sim-status { font-size: 12px; color: var(--text-tertiary); }
.v6-sim-status.running { color: #d4a017; }
.v6-sim-status.ok { color: #4a9c5d; }
.v6-sim-status.error { color: #d9534f; }
.v6-sim-panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 10px; }
.v6-sim-events { margin: 0; padding: 12px; background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 8px; color: var(--text-secondary); font-family: 'Berkeley Mono Variable', var(--font-mono); font-size: 12px; line-height: 1.45; max-height: 360px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
.v6-sim-output { margin: 0; padding: 12px; background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 8px; color: var(--text-secondary); font-family: 'Berkeley Mono Variable', var(--font-mono); font-size: 12px; line-height: 1.45; max-height: 360px; overflow: auto; white-space: pre; }
.keeps-version-docs { display: none; } .keeps-version-docs.active { display: block; }
.keeps-endpoint-desc { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
.keeps-endpoint-auth { font-size: 12px; color: var(--text-tertiary); font-style: italic; }
.keeps-endpoint-auth strong { color: var(--text-secondary); font-style: normal; }
/* ======================================== LOADING / ERROR STATES ======================================== */ .keeps-loading { display: flex; align-items: center; justify-content: center; padding: 60px 20px; gap: 12px; color: var(--text-tertiary); font-size: 14px; }
.keeps-loading .spinner { width: 20px; height: 20px; flex-shrink: 0; aspect-ratio: 1; border: 2px solid var(--border-color); border-top: 2px solid var(--ac-purple); border-radius: 50%; animation: spin 0.8s linear infinite; box-sizing: border-box; } @keyframes spin { to { transform: rotate(360deg); } }
.keeps-error { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; gap: 12px; color: var(--text-tertiary); } .keeps-error .error-icon { font-size: 32px; opacity: 0.5; } .keeps-error .error-msg { font-size: 14px; } .keeps-error .retry-btn { margin-top: 8px; padding: 8px 16px; background: var(--ac-purple); color: white; border: none; border-radius: 6px; cursor: pointer; font-family: var(--font-mono); font-size: 12px; } .keeps-error .retry-btn:hover { filter: brightness(1.1); }
/* ======================================== PAGINATION ======================================== */ .keeps-pagination { display: none; justify-content: center; align-items: center; gap: 16px; padding: 20px; font-family: var(--font-mono); font-size: 14px; } .keeps-pagination .page-btn { background: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 8px 16px; border-radius: 6px; cursor: pointer; font-family: var(--font-mono); font-size: 13px; } .keeps-pagination .page-btn:hover:not(:disabled) { background: var(--ac-purple); color: white; } .keeps-pagination .page-btn:disabled { opacity: 0.3; cursor: default; } .keeps-pagination .page-info { color: var(--text-tertiary); } .keeps-pagination .page-go { display: flex; align-items: center; gap: 4px; margin-left: 8px; } .keeps-pagination .page-go-input { width: 48px; padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); font-size: 13px; text-align: center; -moz-appearance: textfield; } .keeps-pagination .page-go-input::-webkit-outer-spin-button, .keeps-pagination .page-go-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .keeps-pagination .page-go-btn { padding: 6px 10px; }
/* ======================================== SEARCH BAR ======================================== */ .keeps-search-bar { padding: 12px 24px; background: var(--bg-primary); border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 12px; }
.keeps-search-input { flex: 1; padding: 8px 12px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; font-family: var(--font-fun); font-size: 14px; color: var(--text-primary); outline: none; transition: border-color 0.15s; } .keeps-search-input::placeholder { color: var(--text-tertiary); font-family: var(--font-fun); } .keeps-search-input:focus { border-color: var(--ac-purple); }
/* Custom sigil picker */ .keeps-sigil-menu { position: relative; display: flex; align-items: center; cursor: pointer; padding: 6px 10px; background: black; border: 2px solid white; border-radius: 0; flex-shrink: 0; transition: all 0.15s; } .keeps-sigil-menu:hover { background: rgb(30, 30, 30); box-shadow: 0 0 6px rgba(255,255,255,0.2); } .keeps-sigil-label { font-family: var(--font-fun); font-size: 15px; font-weight: bold; color: white; line-height: 1; } .keeps-sigil-dropdown { display: none; position: fixed; background: black; border: 2px solid white; border-radius: 0; box-shadow: 0 4px 16px rgba(0,0,0,0.5); min-width: 220px; z-index: 99999; } .keeps-sigil-menu.open .keeps-sigil-dropdown { display: block; } .keeps-sigil-option { display: flex; align-items: baseline; gap: 8px; padding: 8px 12px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.08); transition: background 0.1s; } .keeps-sigil-option:last-child { border-bottom: none; } .keeps-sigil-option:hover:not(.disabled) { background: rgb(40, 40, 40); } .keeps-sigil-option.disabled { opacity: 0.38; cursor: default; } .keeps-sigil-option.active .keeps-sigil-option-char { color: #00ff41; } .keeps-sigil-option-char { font-family: var(--font-fun); font-size: 15px; font-weight: bold; color: white; flex-shrink: 0; width: 14px; } .keeps-sigil-option-desc { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,0.7); } .keeps-sigil-option-desc em { color: rgba(255,255,255,0.4); font-style: normal; }
.keeps-filter-btn { padding: 8px 12px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); cursor: pointer; transition: all 0.15s; white-space: nowrap; } .keeps-filter-btn:hover { border-color: var(--ac-purple); } .keeps-filter-btn.active { background: rgba(205, 92, 155, 0.15); border-color: var(--ac-purple); color: var(--ac-purple); }
/* ======================================== GRID ======================================== */ .keeps-grid { flex: 1; padding: 16px 24px; display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; align-content: start; overflow-y: auto; }
/* ======================================== CARD ======================================== */ .keeps-card { background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 8px; overflow: hidden; transition: all 0.15s; display: flex; flex-direction: column; } .keeps-card:hover { border-color: var(--ac-purple); box-shadow: 0 2px 12px rgba(205, 92, 155, 0.12); } .keeps-card.kept { border-color: rgba(74, 156, 93, 0.4); } .keeps-card.kept:hover { border-color: #4a9c5d; box-shadow: 0 2px 12px rgba(74, 156, 93, 0.15); }
.keeps-card-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-subtle); }
.keeps-card-code { font-family: var(--font-mono); font-size: 14px; font-weight: 700; color: var(--ac-purple); cursor: pointer; } .keeps-card-code:hover { text-decoration: underline; }
.keeps-card-meta { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-tertiary); }
.keeps-card-handle { color: var(--text-secondary); font-weight: 500; }
.keeps-card-hits { opacity: 0.7; }
.keeps-card-preview { position: relative; aspect-ratio: 4 / 3; background: #0d0d1a; overflow: hidden; }
.keeps-card-preview img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; opacity: 0; transition: opacity 0.3s; } .keeps-card-preview img.loaded { opacity: 0.7; }
.keeps-card:hover .keeps-card-preview img.loaded { opacity: 0.9; }
.keeps-card-preview .preview-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.55) 100%); pointer-events: none; }
/* Animated noise placeholder (oven style) */ .keeps-card-preview .preview-noise { position: absolute; inset: 0; z-index: 0; } .keeps-card-preview .preview-noise canvas { width: 100%; height: 100%; image-rendering: pixelated; } .keeps-card-preview .preview-noise .oven-label { position: absolute; bottom: 8px; left: 0; right: 0; text-align: center; font-family: var(--font-mono); font-size: 12px; color: rgba(255, 255, 255, 0.5); letter-spacing: 1px; pointer-events: none; } .keeps-card-preview .preview-noise .oven-bar { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--ac-purple), #70D6FF); animation: oven-pulse 1.5s ease-in-out infinite; } @keyframes oven-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
.keeps-card-body { padding: 10px 12px; flex: 1; min-height: 0; }
/* Syntax-highlighted source with colored line numbers */ .keeps-card-source { font-family: var(--font-mono); font-size: 12px; line-height: 1.5; color: var(--text-secondary); white-space: pre-wrap; word-break: break-word; max-height: 100px; overflow-y: auto; position: relative; display: flex; }
/* KidLisp syntax highlighting tokens */ .kl-comment { color: #6b7280; font-style: italic; } .kl-string { color: #eab308; } .kl-number { color: #f472b6; } .kl-fn { color: #22d3ee; } .kl-special { color: #c084fc; } .kl-op { color: #84cc16; } .kl-var { color: #fb923c; } .kl-code-ref { color: #4ade80; } .kl-paren { color: #9ca3af; }
@media (prefers-color-scheme: dark) { .kl-comment { color: #6b7280; } .kl-string { color: #e5c07b; } .kl-number { color: #d19a66; } .kl-fn { color: #61afef; } .kl-special { color: #c084fc; } .kl-op { color: #98c379; } .kl-var { color: #e06c75; } .kl-code-ref { color: #98c379; } .kl-paren { color: #858585; } } [data-theme="dark"] .kl-comment { color: #6b7280; } [data-theme="dark"] .kl-string { color: #e5c07b; } [data-theme="dark"] .kl-number { color: #d19a66; } [data-theme="dark"] .kl-fn { color: #61afef; } [data-theme="dark"] .kl-special { color: #c084fc; } [data-theme="dark"] .kl-op { color: #98c379; } [data-theme="dark"] .kl-var { color: #e06c75; } [data-theme="dark"] .kl-code-ref { color: #98c379; } [data-theme="dark"] .kl-paren { color: #858585; }
.keeps-card-source .line-nums { flex-shrink: 0; width: 2.2em; text-align: right; padding-right: 6px; user-select: none; border-right: 1px solid var(--border-subtle); margin-right: 6px; }
.keeps-card-source .line-nums span { display: block; opacity: 0.7; } .keeps-card-source .line-nums span:nth-child(6n+1) { color: #FF6B6B; } .keeps-card-source .line-nums span:nth-child(6n+2) { color: #4ECDC4; } .keeps-card-source .line-nums span:nth-child(6n+3) { color: #FFE66D; } .keeps-card-source .line-nums span:nth-child(6n+4) { color: #A8E6CF; } .keeps-card-source .line-nums span:nth-child(6n+5) { color: #70D6FF; } .keeps-card-source .line-nums span:nth-child(6n+6) { color: #BB8FCE; }
.keeps-card-source .source-text { flex: 1; min-width: 0; }
.keeps-card-actions { display: flex; align-items: center; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--border-subtle); }
.keeps-card-btn { padding: 4px 10px; background: var(--bg-secondary); border: 1px solid var(--border-subtle); border-radius: 4px; font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); cursor: pointer; transition: all 0.15s; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; } .keeps-card-btn:hover { border-color: var(--ac-purple); color: var(--ac-purple); text-decoration: none; }
.keeps-card-btn.kept-badge { background: rgba(74, 156, 93, 0.15); border-color: rgba(74, 156, 93, 0.3); color: #4a9c5d; cursor: default; }
.keeps-card-btn.html-btn:hover { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.5); color: #3b82f6; }
.keeps-card-btn.tezos-btn:hover { background: rgba(74, 156, 93, 0.1); border-color: rgba(74, 156, 93, 0.5); color: #4a9c5d; }
/* ======================================== MODAL SHARED ======================================== */ @keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalScaleIn { from { transform: scale(0.92); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.keeps-modal-close { width: 32px; height: 32px; background: none; border: 1px solid var(--border-color); border-radius: 6px; font-size: 18px; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; } .keeps-modal-close:hover { background: var(--bg-tertiary); border-color: var(--ac-purple); color: var(--ac-purple); }
/* ======================================== AUTH UI ======================================== */ .keeps-auth { display: flex; align-items: center; gap: 8px; }
.keeps-auth-btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px 18px; background: rgb(30, 50, 120); color: white; border: 1px solid white; border-radius: 0; font-family: var(--font-mono); font-size: 14px; cursor: pointer; transition: background 0.15s; text-decoration: none; line-height: 1; } .keeps-auth-btn:hover { background: rgb(50, 70, 150); text-decoration: none; }
.keeps-auth-btn.blinking { animation: login-btn-blink 0.5s ease-in-out 5; } @keyframes login-btn-blink { 0%, 100% { box-shadow: 0 0 0 0 rgba(64, 192, 96, 0); } 50% { box-shadow: 0 0 16px 6px rgba(64, 192, 96, 0.7); } }
.keeps-auth-btn.imnew { background: rgb(30, 90, 50); } .keeps-auth-btn.imnew:hover { background: rgb(50, 120, 70); }
/* Handle dropdown */ .keeps-user-menu { position: relative; display: flex; align-items: center; cursor: pointer; padding: 4px 10px; background: black; border: 2px solid white; border-radius: 0; transition: all 0.15s; } .keeps-user-menu:hover { background: rgb(30, 30, 30); box-shadow: 0 0 6px rgba(255,255,255,0.2); } .keeps-user-handle { font-family: var(--font-mono); font-size: 12px; font-weight: bold; color: white; } .keeps-user-dropdown { display: none; position: fixed; background: black; border: 2px solid white; border-radius: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.4); min-width: 90px; z-index: 99999; } .keeps-user-menu.open .keeps-user-dropdown { display: block; } .keeps-user-logout-btn { width: 100%; padding: 7px 12px; background: black; border: none; text-align: left; font-family: var(--font-mono); font-size: 12px; font-weight: bold; color: white; cursor: pointer; } .keeps-user-logout-btn:hover { background: rgb(50, 50, 50); }
.keeps-auth-btn.wallet { background: var(--bg-secondary); color: #e05555; border: 1px solid rgba(224, 85, 85, 0.4); font-family: var(--font-mono); } .keeps-auth-btn.wallet:hover { border-color: #e05555; color: #ff6b6b; } .keeps-auth-btn.wallet.linked { color: #e05555; border-color: rgba(224, 85, 85, 0.4); } .keeps-auth-btn.wallet.linked:hover { border-color: #e05555; color: #ff6b6b; } .keeps-auth-btn.wallet.connected { color: #40c060; border-color: rgba(64, 192, 96, 0.4); } .keeps-auth-btn.wallet.connected:hover { border-color: #40c060; color: #55dd77; }
/* ======================================== SORT CONTROLS ======================================== */ .keeps-sort-btns { display: flex; gap: 4px; margin-left: auto; } .keeps-sort-btn { padding: 5px 9px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); cursor: pointer; transition: all 0.15s; white-space: nowrap; } .keeps-sort-btn:hover { border-color: var(--ac-purple); } .keeps-sort-btn.active { background: rgba(205, 92, 155, 0.15); border-color: var(--ac-purple); color: var(--ac-purple); }
/* ======================================== KEPT ROW (list view for kept tab) ======================================== */ .keeps-grid.list-view { grid-template-columns: 1fr; gap: 4px; } .keeps-row { background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 6px; display: flex; align-items: center; gap: 12px; padding: 8px 12px; cursor: pointer; transition: border-color 0.12s; } .keeps-row:hover { border-color: var(--ac-purple); } .keeps-row-thumb { width: 28px; height: 21px; object-fit: cover; border-radius: 3px; background: var(--bg-secondary); flex-shrink: 0; } .keeps-row-code { font-family: var(--font-mono); font-size: 14px; font-weight: 700; color: var(--text-primary); min-width: 80px; text-decoration: none; } .keeps-row-code:hover { color: var(--ac-purple); } .keeps-row-handle { font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); } .keeps-row-token { font-family: var(--font-mono); font-size: 12px; color: #8658e2; margin-left: auto; text-decoration: none; } a.keeps-row-token:hover { text-decoration: underline; } .keeps-row-hits { font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); } .keeps-row-time { font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); } .keeps-row-owner { font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); } .keeps-row-owner-link { color: #8658e2; text-decoration: none; } .keeps-row-owner-link:hover { text-decoration: underline; }
/* ======================================== MINT MODAL ======================================== */ .keeps-mint-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); z-index: 20000; display: none; align-items: center; justify-content: center; padding: 20px; } .keeps-mint-overlay.open { display: flex; animation: modalFadeIn 0.2s ease-out forwards; }
.keeps-mint-modal { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 12px; max-width: 520px; width: 100%; height: min(95vh, 860px); overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); animation: modalScaleIn 0.25s ease-out; display: flex; flex-direction: column; }
.mint-header { padding: 16px 20px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); display: flex; align-items: center; justify-content: space-between; }
.mint-header-title { font-family: var(--font-mono); font-size: 16px; font-weight: 700; color: var(--ac-purple); }
.mint-body { padding: 20px; flex: 1; overflow-y: auto; overscroll-behavior: contain; }
.mint-piece-info { margin-bottom: 16px; padding: 12px; background: var(--bg-secondary); border-radius: 8px; font-size: 12px; color: var(--text-secondary); } .mint-piece-info strong { color: var(--text-primary); } .mint-legacy-warning { margin-top: 10px; padding: 8px 10px; border-radius: 6px; border: 1px solid rgba(217, 83, 79, 0.35); background: rgba(217, 83, 79, 0.1); color: #d9534f; font-size: 11px; line-height: 1.4; } .mint-legacy-warning.modern { border-color: rgba(74, 156, 93, 0.35); background: rgba(74, 156, 93, 0.1); color: #4a9c5d; }
.mint-preview-frame { margin-bottom: 12px; border-radius: 8px; overflow: hidden; background: black; aspect-ratio: 4 / 3; position: relative; } .mint-preview-frame iframe { width: 100%; height: 100%; border: none; display: block; } .mint-preview-frame:empty { display: none; } .bake-telemetry { width: 100%; height: 100%; position: relative; display: flex; align-items: center; justify-content: center; background: #000; } .bake-preview-img { width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; position: absolute; inset: 0; } .bake-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,0.85)); padding: 20px 12px 10px; display: flex; flex-direction: column; gap: 4px; } .bake-stage { font-family: var(--font-mono); font-size: 13px; color: #0f0; text-transform: uppercase; letter-spacing: 0.5px; } .bake-bar { width: 100%; height: 4px; background: rgba(255,255,255,0.15); border-radius: 2px; overflow: hidden; } .bake-bar-fill { height: 100%; background: #0f0; transition: width 0.4s ease; width: 0%; } .bake-detail { font-family: var(--font-mono); font-size: 11px; color: rgba(255,255,255,0.6); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Progress overlay — sits inside mint-preview-frame */ .mint-progress-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,0.9)); padding: 24px 14px 12px; display: flex; flex-direction: column; gap: 6px; pointer-events: none; z-index: 2; } .mint-progress-overlay.hidden { display: none; } .mint-progress-stage { font-family: var(--font-mono); font-size: 13px; color: #ccc; letter-spacing: 0.3px; } .mint-progress-stage .step-label { color: #fff; } .mint-progress-stage .step-detail { color: rgba(255,255,255,0.5); margin-left: 6px; font-size: 11px; } .mint-progress-stage.error .step-label { color: #d9534f; } .mint-progress-stage.done .step-label { color: #4a9c5d; } .mint-progress-bar { width: 100%; height: 3px; background: rgba(255,255,255,0.12); border-radius: 2px; overflow: hidden; } .mint-progress-bar-fill { height: 100%; background: var(--ac-purple); transition: width 0.5s ease; width: 0%; } .mint-progress-bar-fill.error { background: #d9534f; } .mint-progress-bar-fill.done { background: #4a9c5d; }
/* Hide old timeline (kept as hidden state tracker) */ .mint-timeline { display: none; }
.mint-actions { display: flex; gap: 8px; padding-top: 10px; border-top: 1px solid var(--border-subtle); position: sticky; bottom: 0; background: var(--bg-primary); z-index: 2; }
.mint-action-btn { flex: 1; padding: 10px 16px; border: 1px solid var(--border-color); border-radius: 6px; font-family: var(--font-mono); font-size: 12px; cursor: pointer; transition: all 0.15s; text-align: center; }
.mint-action-btn.primary { background: #4a9c5d; color: white; border-color: #4a9c5d; } .mint-action-btn.primary:hover { filter: brightness(1.1); } .mint-action-btn.primary:disabled { opacity: 0.5; cursor: not-allowed; }
.mint-action-btn.cancel { background: var(--bg-secondary); color: var(--text-secondary); } .mint-action-btn.cancel:hover { border-color: var(--ac-purple); color: var(--ac-purple); }
.mint-result { margin-top: 16px; padding: 12px; background: rgba(74, 156, 93, 0.1); border: 1px solid rgba(74, 156, 93, 0.3); border-radius: 8px; font-size: 12px; color: #4a9c5d; } .mint-result a { color: #4a9c5d; text-decoration: underline; }
.mint-error-msg { margin-top: 16px; padding: 12px; background: rgba(217, 83, 79, 0.1); border: 1px solid rgba(217, 83, 79, 0.3); border-radius: 8px; font-size: 12px; color: #d9534f; }
.mint-track-log { margin-top: 10px; border-top: 1px solid var(--border-subtle); padding-top: 8px; max-height: 120px; overflow-y: auto; display: flex; flex-direction: column; gap: 3px; } .mint-track-log:empty { display: none; } .mint-track-entry { font-family: var(--font-mono); font-size: 10px; color: var(--text-tertiary); display: flex; gap: 6px; align-items: baseline; } .mint-track-entry .track-time { color: var(--text-tertiary); white-space: nowrap; } .mint-track-entry .track-msg { color: var(--text-secondary); } .mint-track-entry.track-done .track-msg { color: #4a9c5d; } .mint-track-entry.track-error .track-msg { color: #d9534f; } .mint-track-entry.track-active .track-msg { color: var(--ac-purple); }
.mint-artifacts { margin-top: 12px; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-secondary); }
.mint-artifact-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 8px; font-size: 12px; color: var(--text-tertiary); }
.mint-artifact-title { font-size: 12px; color: var(--text-primary); font-weight: 600; }
.mint-artifact-media { display: flex; gap: 10px; align-items: stretch; margin: 10px 0 0; }
.mint-artifact-preview { width: 100px; flex-shrink: 0; aspect-ratio: 1 / 1; background: #000; border-radius: 6px; overflow: hidden; border: 1px solid var(--border-color); }
.mint-artifact-preview img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
.mint-artifact-iframe { flex: 1; min-height: 180px; border-radius: 6px; overflow: hidden; border: 1px solid var(--border-color); background: #000; }
.mint-artifact-iframe iframe { width: 100%; height: 100%; border: none; display: block; }
.mint-artifact-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.mint-link-btn { padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); font-size: 12px; text-decoration: none; cursor: pointer; }
.mint-link-btn:hover { border-color: var(--ac-purple); color: var(--ac-purple); text-decoration: none; }
.mint-link-btn.rebake { background: rgba(180, 120, 40, 0.12); border-color: rgba(180, 120, 40, 0.4); color: #b27828; }
.mint-link-btn.sync-onchain { background: rgba(74, 156, 93, 0.12); border-color: rgba(74, 156, 93, 0.4); color: #4a9c5d; font-weight: 600; }
.mint-link-btn.sync-onchain:hover { background: rgba(74, 156, 93, 0.22); border-color: #4a9c5d; }
.mint-link-btn.sync-onchain:disabled { opacity: 0.5; cursor: not-allowed; }
/* Keep button in card */ .keeps-card-btn.keep-btn { background: rgba(74, 156, 93, 0.1); border-color: rgba(74, 156, 93, 0.3); color: #4a9c5d; font-weight: 600; } .keeps-card-btn.keep-btn:hover { background: rgba(74, 156, 93, 0.2); border-color: #4a9c5d; color: #4a9c5d; }
.keeps-card-btn.rebake-btn { background: rgba(180, 120, 40, 0.12); border-color: rgba(180, 120, 40, 0.4); color: #b27828; font-weight: 600; } .keeps-card-btn.rebake-btn:hover { background: rgba(180, 120, 40, 0.22); border-color: #b27828; color: #b27828; }
/* ======================================== RESPONSIVE ======================================== */ @media (max-width: 768px) { .keeps-tab { padding: 14px 8px; font-size: 16px; } .keeps-hero-header { padding: 6px 10px; } .keeps-castle-wrap { height: 220px; } .keeps-castle-wrap svg { width: 200px; } .keeps-search-bar { padding: 10px 16px; } .keeps-grid { padding: 12px 16px; grid-template-columns: repeat(2, 1fr); } .keeps-about-overlay { padding: 8px; align-items: center; } .keeps-about-panel { width: calc(100vw - 16px); border-radius: 10px; } .keeps-about-panel-body { padding: 0 14px 14px; } .keeps-mint-modal { height: min(96vh, 760px); } .mint-body { padding: 14px; } .mint-progress-stage { font-size: 12px; } .mint-artifact-links { gap: 6px; } .mint-artifact-iframe { min-height: 140px; } }
@media (max-width: 480px) { .keeps-grid { grid-template-columns: 1fr; gap: 10px; } .keeps-card-actions { flex-wrap: wrap; } }
/* ======================================== THEME TOGGLE ======================================== */ /* Embedded in parent editor iframe — hide branding */ .embedded .keeps-hero { display: none; } .embedded .keeps-footer { display: none; } /* Embedded focused-keep mode: hide the browse chrome and the preamble so the tab opens straight onto the current editor piece's keep panel. */ .embedded .keeps-tabs { display: none; } .embedded #index-search-bar { display: none; } .embedded #keeps-grid { display: none; } .embedded #keeps-about-inline { display: none !important; } .embedded #keeps-castle { display: none; } #keeps-embed-empty { display: none; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 40px 20px; text-align: center; color: #8a8a8a; font-size: 13px; line-height: 1.5; min-height: 180px; } #keeps-embed-empty .embed-empty-code { font-size: 22px; opacity: 0.5; } .embedded #keeps-embed-empty { display: flex; } .embedded.has-piece #keeps-embed-empty { display: none; }
/* Embedded keep flow is the tab's content, NOT a modal: no dark backdrop, no floating centered box, no close × / Cancel (you just switch tabs). Fills the panel as one scrolling column, tightened for small viewports. */ .embedded .keeps-mint-overlay { position: absolute; background: var(--bg-primary); padding: 0; z-index: 1; } .embedded .keeps-mint-overlay.open { animation: none; } .embedded .keeps-mint-modal { max-width: none; width: 100%; height: 100%; border: none; border-radius: 0; box-shadow: none; animation: none; } .embedded .keeps-modal-close { display: none; } .embedded .mint-action-btn.cancel { display: none; } .embedded .mint-header { padding: 10px 12px; } .embedded .mint-header-title { font-size: 14px; } .embedded .mint-body { padding: 12px; } .embedded .mint-preview-frame { aspect-ratio: 16 / 10; max-height: 40vh; } @media (max-width: 420px) { .embedded .mint-header { padding: 8px 10px; } .embedded .mint-header-title { font-size: 13px; } .embedded .mint-body { padding: 10px; } }
/* ======================================== FOOTER ======================================== */ .keeps-footer { border-top: 1px solid var(--border-subtle); padding: 24px; text-align: center; font-family: var(--font-mono); font-size: 12px; color: var(--text-tertiary); display: flex; flex-direction: column; gap: 8px; align-items: center; } .keeps-footer a { color: var(--ac-purple); text-decoration: none; } .keeps-footer a:hover { text-decoration: underline; } .keeps-footer .footer-links { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; } .keeps-footer .footer-contract { font-size: 11px; opacity: 0.7; word-break: break-all; } .keeps-footer .footer-buy { display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 4px; } .keeps-footer .footer-buy-label { font-family: var(--font-fun); font-size: 18px; font-weight: bold; color: var(--text-tertiary); } .keeps-footer .footer-buy-link { font-family: var(--font-fun); font-weight: bold; font-size: 18px; text-decoration: none; display: inline-flex; gap: 0; } .keeps-footer .footer-buy-link:hover { text-decoration: none; } .keeps-footer .footer-buy-link span { text-shadow: 1px 1px 0px rgba(0,0,0,0.3); } .keeps-footer .footer-buy-link .buy-b { color: #FF6B6B; } .keeps-footer .footer-buy-link .buy-u { color: #4ECDC4; } .keeps-footer .footer-buy-link .buy-y { color: #FFE66D; } .keeps-footer .footer-buy-link .dot { color: #70D6FF; text-shadow: none; } .keeps-footer .footer-buy-link .baby-k { color: #FF6B6B; } .keeps-footer .footer-buy-link .baby-i { color: #4ECDC4; } .keeps-footer .footer-buy-link .baby-d { color: #FFE66D; } .keeps-footer .footer-buy-link .baby-l { color: #A8E6CF; } .keeps-footer .footer-buy-link .baby-i2 { color: #FF8B94; } .keeps-footer .footer-buy-link .baby-s { color: #F7DC6F; } .keeps-footer .footer-buy-link .baby-p { color: #BB8FCE; } .keeps-footer .footer-buy-link .com-part { color: #70D6FF; text-shadow: none; } </style> <script>if (window.self !== window.top) document.documentElement.classList.add('embedded');</script></head>
<body> <!-- CASTLE HERO --> <section class="keeps-hero"> <div class="keeps-hero-header"> <a href="/" class="keeps-logo" title="Back to KidLisp.com editor"> <span class="keeps-subdomain"><span class="kl0">k</span><span class="kl1">e</span><span class="kl2">e</span><span class="kl3">p</span></span><span class="com-part">.</span><span class="baby-k">K</span><span class="baby-i">i</span><span class="baby-d">d</span><span class="baby-l">L</span><span class="baby-i2">i</span><span class="baby-s">s</span><span class="baby-p">p</span><span class="com-part">.com</span> </a> <div class="keeps-hero-actions"> <!-- Auth UI --> <div class="keeps-auth" id="keeps-auth"> <button class="keeps-auth-btn" id="keeps-login-btn" style="display:none;" onclick="keepsLogin()">Log In</button> <button class="keeps-auth-btn imnew" id="keeps-signup-btn" style="display:none;" onclick="keepsImNew()">I'm New</button> <!-- Handle dropdown (shown when logged in) --> <div class="keeps-user-menu" id="keeps-user-menu" style="display:none;" onclick="toggleUserMenu()"> <span class="keeps-user-handle" id="keeps-auth-handle"></span> <div class="keeps-user-dropdown" id="keeps-user-dropdown"> <button class="keeps-user-logout-btn" onclick="event.stopPropagation(); keepsLogout()">Log Out</button> </div> </div> <button class="keeps-auth-btn wallet" id="keeps-wallet-btn" style="display:none;" onclick="handleWalletBtn()">ꜩ Connect Tezos</button> </div> </div> </div> <div class="keeps-castle-wrap grounded" id="keeps-castle"> <!-- Floating $codes --> <span class="keeps-code-float">$39j</span> <span class="keeps-code-float">$a1b</span> <span class="keeps-code-float">$zyx</span> <span class="keeps-code-float">$k7m</span> <span class="keeps-code-float">$p2q</span> <span class="keeps-code-float">$n4f</span> <span class="keeps-code-float">$h8w</span> <span class="keeps-code-float">$c3v</span> <!-- Castle SVG — illustrative keep fortress --> <svg viewBox="0 0 300 240" fill="none" xmlns="http://www.w3.org/2000/svg"> <defs> <!-- Stone texture pattern (finer brickwork) --> <pattern id="stones" x="0" y="0" width="16" height="10" patternUnits="userSpaceOnUse"> <rect width="16" height="10" fill="none"/> <line x1="0" y1="5" x2="16" y2="5" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="8" y1="0" x2="8" y2="5" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="0" y1="0" x2="0" y2="5" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.12"/> <line x1="4" y1="5" x2="4" y2="10" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="12" y1="5" x2="12" y2="10" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> </pattern> <!-- Glow for sparkles --> <filter id="glow"> <feGaussianBlur stdDeviation="2" result="blur"/> <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge> </filter> <!-- Soft shadow for depth --> <filter id="castleShadow"> <feDropShadow dx="2" dy="3" stdDeviation="3" flood-color="rgba(0,0,0,0.15)"/> </filter> <!-- Grass pattern --> <pattern id="grass" x="0" y="0" width="8" height="6" patternUnits="userSpaceOnUse"> <path d="M1 6 Q1.5 2 2 6" stroke="#4ade80" stroke-width="0.5" fill="none" opacity="0.3"/> <path d="M5 6 Q5.5 3 6 6" stroke="#4ade80" stroke-width="0.5" fill="none" opacity="0.25"/> </pattern> </defs>
<!-- === SKY DETAILS === --> <!-- Clouds --> <ellipse cx="45" cy="22" rx="22" ry="7" fill="var(--text-tertiary)" opacity="0.06"/> <ellipse cx="55" cy="20" rx="16" ry="6" fill="var(--text-tertiary)" opacity="0.08"/> <ellipse cx="255" cy="30" rx="18" ry="5" fill="var(--text-tertiary)" opacity="0.06"/> <ellipse cx="265" cy="28" rx="14" ry="5" fill="var(--text-tertiary)" opacity="0.07"/> <!-- Birds --> <path d="M85 18 Q88 14 91 18" stroke="var(--ac-purple)" stroke-width="0.5" fill="none" opacity="0.15"/> <path d="M95 22 Q97 19 99 22" stroke="var(--ac-purple)" stroke-width="0.5" fill="none" opacity="0.12"/> <path d="M210 15 Q213 11 216 15" stroke="var(--ac-purple)" stroke-width="0.5" fill="none" opacity="0.13"/>
<!-- === GROUND / HILL === --> <!-- Grassy hill --> <ellipse cx="150" cy="238" rx="148" ry="18" fill="#4ade80" opacity="0.08"/> <ellipse cx="150" cy="238" rx="140" ry="16" fill="var(--text-tertiary)" opacity="0.1"/> <!-- Grass blades on hill --> <rect x="10" y="228" width="280" height="12" fill="url(#grass)" opacity="0.6"/> <!-- Path leading to gate --> <path d="M150 238 Q148 242 145 248 Q142 254 150 258 Q158 254 155 248 Q152 242 150 238" fill="var(--text-tertiary)" opacity="0.1"/> <!-- Small bushes --> <ellipse cx="25" cy="234" rx="8" ry="4" fill="#4ade80" opacity="0.12"/> <ellipse cx="275" cy="234" rx="7" ry="3.5" fill="#4ade80" opacity="0.1"/>
<!-- Foundation stone (wider, with moss) --> <rect x="40" y="168" width="220" height="72" rx="4" fill="var(--text-tertiary)" opacity="0.18"/> <!-- Moss on foundation --> <path d="M42 168 Q50 165 58 168 Q65 166 72 168" stroke="#4ade80" stroke-width="1" fill="none" opacity="0.15"/> <path d="M228 168 Q235 165 242 168 Q250 166 258 168" stroke="#4ade80" stroke-width="1" fill="none" opacity="0.12"/>
<!-- === MAIN CURTAIN WALL === --> <rect x="50" y="105" width="200" height="135" rx="2" fill="var(--ac-purple)" opacity="0.2"/> <rect x="52" y="107" width="196" height="131" rx="2" fill="var(--bg-tertiary)" stroke="var(--ac-purple)" stroke-width="1.5" opacity="0.95"/> <!-- Stone texture overlay --> <rect x="52" y="107" width="196" height="131" rx="2" fill="url(#stones)"/> <!-- Horizontal mortar lines --> <line x1="55" y1="127" x2="245" y2="127" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.1"/> <line x1="55" y1="147" x2="245" y2="147" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.1"/> <line x1="55" y1="167" x2="245" y2="167" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.1"/> <line x1="55" y1="187" x2="245" y2="187" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.1"/> <!-- Wall corner stones (quoins) --> <rect x="52" y="107" width="6" height="10" rx="0" fill="var(--ac-purple)" opacity="0.08"/> <rect x="52" y="120" width="6" height="10" rx="0" fill="var(--ac-purple)" opacity="0.06"/> <rect x="52" y="133" width="6" height="10" rx="0" fill="var(--ac-purple)" opacity="0.08"/> <rect x="242" y="107" width="6" height="10" rx="0" fill="var(--ac-purple)" opacity="0.08"/> <rect x="242" y="120" width="6" height="10" rx="0" fill="var(--ac-purple)" opacity="0.06"/> <rect x="242" y="133" width="6" height="10" rx="0" fill="var(--ac-purple)" opacity="0.08"/>
<!-- === GATEHOUSE (more ornate) === --> <!-- Arch recess shadow --> <rect x="116" y="162" width="68" height="78" rx="2" fill="var(--ac-purple)" opacity="0.08"/> <!-- Main arch --> <path d="M118 240 V182 Q118 155 150 155 Q182 155 182 182 V240" fill="var(--ac-purple)" opacity="0.12"/> <path d="M120 240 V183 Q120 158 150 158 Q180 158 180 183 V240" fill="none" stroke="var(--ac-purple)" stroke-width="1.8"/> <!-- Double arch detail --> <path d="M122 240 V184 Q122 161 150 161 Q178 161 178 184 V240" fill="none" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.3"/> <!-- Portcullis (finer grid) --> <line x1="130" y1="158" x2="130" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="137" y1="158" x2="137" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="143" y1="156" x2="143" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="150" y1="155" x2="150" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="157" y1="156" x2="157" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="163" y1="158" x2="163" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="170" y1="158" x2="170" y2="240" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.2"/> <line x1="120" y1="188" x2="180" y2="188" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="120" y1="198" x2="180" y2="198" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="120" y1="208" x2="180" y2="208" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="120" y1="218" x2="180" y2="218" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <line x1="120" y1="228" x2="180" y2="228" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.18"/> <!-- Keystone (larger, decorative) --> <path d="M142 159 L150 150 L158 159" fill="var(--ac-purple)" opacity="0.35"/> <circle cx="150" cy="155" r="2" fill="var(--ac-purple)" opacity="0.2"/> <!-- Gate flanking torches --> <line x1="114" y1="175" x2="114" y2="165" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.3"/> <circle cx="114" cy="163" r="2.5" fill="#FFE66D" opacity="0.3"> <animate attributeName="opacity" values="0.2;0.5;0.2" dur="1.5s" repeatCount="indefinite"/> </circle> <line x1="186" y1="175" x2="186" y2="165" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.3"/> <circle cx="186" cy="163" r="2.5" fill="#FFE66D" opacity="0.3"> <animate attributeName="opacity" values="0.2;0.5;0.2" dur="1.8s" repeatCount="indefinite"/> </circle>
<!-- === LEFT MAIN TOWER === --> <rect x="24" y="62" width="50" height="178" rx="3" fill="var(--ac-purple)" opacity="0.17"/> <rect x="26" y="64" width="46" height="174" rx="3" fill="var(--bg-tertiary)" stroke="var(--ac-purple)" stroke-width="1.4"/> <rect x="26" y="64" width="46" height="174" rx="3" fill="url(#stones)"/> <!-- Left tower conical roof (steeper, with finial) --> <polygon points="49,18 22,60 76,60" fill="var(--ac-purple)" opacity="0.32"/> <polygon points="49,18 22,60 76,60" fill="none" stroke="var(--ac-purple)" stroke-width="1.2"/> <!-- Roof shingle lines --> <line x1="33" y1="45" x2="65" y2="45" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.3"/> <line x1="37" y1="38" x2="61" y2="38" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.25"/> <line x1="41" y1="31" x2="57" y2="31" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.2"/> <!-- Roof finial ball --> <circle cx="49" cy="16" r="2.5" fill="var(--ac-purple)" opacity="0.4"/> <!-- Left tower battlements --> <rect x="22" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="33" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="44" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="55" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="66" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <!-- Left tower windows (arched, with sills) --> <rect x="42" y="80" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.25"/> <rect x="42" y="96" width="12" height="2" rx="0" fill="var(--ac-purple)" opacity="0.15"/> <rect x="42" y="110" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.25"/> <rect x="42" y="126" width="12" height="2" rx="0" fill="var(--ac-purple)" opacity="0.15"/> <rect x="42" y="142" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.25"/> <rect x="42" y="158" width="12" height="2" rx="0" fill="var(--ac-purple)" opacity="0.15"/> <!-- Window warm glow --> <rect x="43" y="81" width="10" height="14" rx="5" fill="#FFE66D" opacity="0.12"/> <rect x="43" y="111" width="10" height="14" rx="5" fill="#70D6FF" opacity="0.1"/> <rect x="43" y="143" width="10" height="14" rx="5" fill="#FF8B94" opacity="0.08"/> <!-- Tower horizontal band --> <line x1="26" y1="100" x2="72" y2="100" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.12"/> <line x1="26" y1="135" x2="72" y2="135" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.12"/>
<!-- === RIGHT MAIN TOWER === --> <rect x="226" y="62" width="50" height="178" rx="3" fill="var(--ac-purple)" opacity="0.17"/> <rect x="228" y="64" width="46" height="174" rx="3" fill="var(--bg-tertiary)" stroke="var(--ac-purple)" stroke-width="1.4"/> <rect x="228" y="64" width="46" height="174" rx="3" fill="url(#stones)"/> <!-- Right tower conical roof --> <polygon points="251,18 224,60 278,60" fill="var(--ac-purple)" opacity="0.32"/> <polygon points="251,18 224,60 278,60" fill="none" stroke="var(--ac-purple)" stroke-width="1.2"/> <line x1="235" y1="45" x2="267" y2="45" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.3"/> <line x1="239" y1="38" x2="263" y2="38" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.25"/> <line x1="243" y1="31" x2="259" y2="31" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.2"/> <!-- Roof finial --> <circle cx="251" cy="16" r="2.5" fill="var(--ac-purple)" opacity="0.4"/> <!-- Right tower battlements --> <rect x="224" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="235" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="246" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="257" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <rect x="268" y="56" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.5"/> <!-- Right tower windows --> <rect x="246" y="80" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.25"/> <rect x="246" y="96" width="12" height="2" rx="0" fill="var(--ac-purple)" opacity="0.15"/> <rect x="246" y="110" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.25"/> <rect x="246" y="126" width="12" height="2" rx="0" fill="var(--ac-purple)" opacity="0.15"/> <rect x="246" y="142" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.25"/> <rect x="246" y="158" width="12" height="2" rx="0" fill="var(--ac-purple)" opacity="0.15"/> <rect x="247" y="81" width="10" height="14" rx="5" fill="#FFE66D" opacity="0.12"/> <rect x="247" y="111" width="10" height="14" rx="5" fill="#70D6FF" opacity="0.1"/> <rect x="247" y="143" width="10" height="14" rx="5" fill="#FF8B94" opacity="0.08"/> <line x1="228" y1="100" x2="274" y2="100" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.12"/> <line x1="228" y1="135" x2="274" y2="135" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.12"/>
<!-- === CENTER KEEP TOWER (tallest) === --> <rect x="125" y="68" width="50" height="92" rx="2" fill="var(--ac-purple)" opacity="0.14"/> <rect x="127" y="70" width="46" height="88" rx="2" fill="var(--bg-tertiary)" stroke="var(--ac-purple)" stroke-width="1.2"/> <rect x="127" y="70" width="46" height="88" rx="2" fill="url(#stones)"/> <!-- Center tower taller conical roof --> <polygon points="150,24 120,66 180,66" fill="var(--ac-purple)" opacity="0.28"/> <polygon points="150,24 120,66 180,66" fill="none" stroke="var(--ac-purple)" stroke-width="1.2"/> <line x1="132" y1="50" x2="168" y2="50" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.3"/> <line x1="137" y1="42" x2="163" y2="42" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.25"/> <line x1="141" y1="35" x2="159" y2="35" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.2"/> <!-- Center finial (taller, with orb) --> <line x1="150" y1="24" x2="150" y2="18" stroke="var(--ac-purple)" stroke-width="1"/> <circle cx="150" cy="16" r="3" fill="var(--ac-purple)" opacity="0.45"/> <circle cx="150" cy="16" r="1.5" fill="#FFE66D" opacity="0.3"/> <!-- Center battlements --> <rect x="121" y="62" width="7" height="8" rx="1" fill="var(--ac-purple)" opacity="0.45"/> <rect x="131" y="62" width="7" height="8" rx="1" fill="var(--ac-purple)" opacity="0.45"/> <rect x="142" y="62" width="7" height="8" rx="1" fill="var(--ac-purple)" opacity="0.45"/> <rect x="152" y="62" width="7" height="8" rx="1" fill="var(--ac-purple)" opacity="0.45"/> <rect x="162" y="62" width="7" height="8" rx="1" fill="var(--ac-purple)" opacity="0.45"/> <rect x="172" y="62" width="7" height="8" rx="1" fill="var(--ac-purple)" opacity="0.45"/> <!-- Grand rosette window --> <circle cx="150" cy="92" r="10" fill="var(--ac-purple)" opacity="0.15"/> <circle cx="150" cy="92" r="8" fill="none" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.35"/> <circle cx="150" cy="92" r="5" fill="none" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.2"/> <line x1="150" y1="82" x2="150" y2="102" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.2"/> <line x1="140" y1="92" x2="160" y2="92" stroke="var(--ac-purple)" stroke-width="0.4" opacity="0.2"/> <line x1="143" y1="85" x2="157" y2="99" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.15"/> <line x1="157" y1="85" x2="143" y2="99" stroke="var(--ac-purple)" stroke-width="0.3" opacity="0.15"/> <!-- Small upper window --> <rect x="145" y="115" width="10" height="14" rx="5" fill="var(--ac-purple)" opacity="0.22"/> <rect x="146" y="116" width="8" height="12" rx="4" fill="#BB8FCE" opacity="0.1"/> <!-- Center horizontal bands --> <line x1="127" y1="108" x2="173" y2="108" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.12"/> <line x1="127" y1="133" x2="173" y2="133" stroke="var(--ac-purple)" stroke-width="0.8" opacity="0.12"/>
<!-- === WALL BATTLEMENTS (between towers) === --> <!-- Left section --> <rect x="72" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="82" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="92" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="102" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="112" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <!-- Right section --> <rect x="180" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="190" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="200" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="210" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/> <rect x="220" y="100" width="7" height="9" rx="1" fill="var(--ac-purple)" opacity="0.4"/>
<!-- === WALL WINDOWS (varied: arrow slits, arched, shield) === --> <!-- Left wall --> <rect x="76" y="125" width="4" height="14" rx="2" fill="var(--ac-purple)" opacity="0.22"/> <rect x="94" y="120" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.2"/> <rect x="95" y="121" width="10" height="14" rx="5" fill="#A8E6CF" opacity="0.06"/> <rect x="76" y="155" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.2"/> <rect x="96" y="155" width="4" height="14" rx="2" fill="var(--ac-purple)" opacity="0.22"/> <!-- Shield emblem on left wall --> <path d="M86 180 L80 185 L80 193 Q80 198 86 200 Q92 198 92 193 L92 185 Z" fill="var(--ac-purple)" opacity="0.1" stroke="var(--ac-purple)" stroke-width="0.5" /> <!-- Right wall --> <rect x="220" y="125" width="4" height="14" rx="2" fill="var(--ac-purple)" opacity="0.22"/> <rect x="194" y="120" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.2"/> <rect x="195" y="121" width="10" height="14" rx="5" fill="#A8E6CF" opacity="0.06"/> <rect x="212" y="155" width="12" height="16" rx="6" fill="var(--ac-purple)" opacity="0.2"/> <rect x="194" y="155" width="4" height="14" rx="2" fill="var(--ac-purple)" opacity="0.22"/> <!-- Shield emblem on right wall --> <path d="M214 180 L208 185 L208 193 Q208 198 214 200 Q220 198 220 193 L220 185 Z" fill="var(--ac-purple)" opacity="0.1" stroke="var(--ac-purple)" stroke-width="0.5" />
<!-- === FLAGS (pennants, more detailed) === --> <!-- Left tower flag --> <line x1="49" y1="16" x2="49" y2="2" stroke="var(--ac-purple)" stroke-width="1"/> <path d="M49 2 L65 7 L49 12" fill="#FF6B6B" opacity="0.8" stroke="#FF6B6B" stroke-width="0.3"> <animateTransform attributeName="transform" type="rotate" values="-3,49,7;3,49,7;-3,49,7" dur="3s" repeatCount="indefinite"/> </path> <!-- Center flag (largest, with pennant tail) --> <line x1="150" y1="16" x2="150" y2="-2" stroke="var(--ac-purple)" stroke-width="1.4"/> <path d="M150 -2 L172 5 L150 12" fill="#4ECDC4" opacity="0.85" stroke="#4ECDC4" stroke-width="0.3"> <animateTransform attributeName="transform" type="rotate" values="-3,150,5;3,150,5;-3,150,5" dur="2.5s" repeatCount="indefinite"/> </path> <!-- Right tower flag --> <line x1="251" y1="16" x2="251" y2="2" stroke="var(--ac-purple)" stroke-width="1"/> <path d="M251 2 L267 7 L251 12" fill="#BB8FCE" opacity="0.8" stroke="#BB8FCE" stroke-width="0.3"> <animateTransform attributeName="transform" type="rotate" values="3,251,7;-3,251,7;3,251,7" dur="3.5s" repeatCount="indefinite"/> </path>
<!-- === DRAWBRIDGE CHAINS === --> <line x1="118" y1="162" x2="112" y2="155" stroke="var(--ac-purple)" stroke-width="0.6" opacity="0.2" stroke-dasharray="1.5,1"/> <line x1="182" y1="162" x2="188" y2="155" stroke="var(--ac-purple)" stroke-width="0.6" opacity="0.2" stroke-dasharray="1.5,1"/>
<!-- === TREASURE SPARKLES (inside gate, richer) === --> <circle cx="150" cy="172" r="3" fill="#FFE66D" opacity="0.8" filter="url(#glow)"> <animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/> </circle> <circle cx="138" cy="192" r="2.5" fill="#A8E6CF" opacity="0.6" filter="url(#glow)"> <animate attributeName="opacity" values="0.2;0.9;0.2" dur="2.5s" repeatCount="indefinite"/> </circle> <circle cx="162" cy="187" r="2.5" fill="#70D6FF" opacity="0.6" filter="url(#glow)"> <animate attributeName="opacity" values="0.4;1;0.4" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="144" cy="208" r="2" fill="#FF8B94" opacity="0.5" filter="url(#glow)"> <animate attributeName="opacity" values="0.3;0.8;0.3" dur="3s" repeatCount="indefinite"/> </circle> <circle cx="156" cy="202" r="2" fill="#BB8FCE" opacity="0.5" filter="url(#glow)"> <animate attributeName="opacity" values="0.2;0.7;0.2" dur="2.2s" repeatCount="indefinite"/> </circle> <circle cx="150" cy="220" r="1.5" fill="#FFE66D" opacity="0.4" filter="url(#glow)"> <animate attributeName="opacity" values="0.1;0.6;0.1" dur="2.8s" repeatCount="indefinite"/> </circle> <!-- Treasure chest silhouette --> <rect x="142" cy="225" y="225" width="16" height="8" rx="2" fill="var(--ac-purple)" opacity="0.08"/> <path d="M142 225 Q150 222 158 225" fill="none" stroke="var(--ac-purple)" stroke-width="0.5" opacity="0.1"/>
<!-- === TOWER WINDOW GLIMMERS === --> <circle cx="48" cy="88" r="1.5" fill="#FFE66D" opacity="0"> <animate attributeName="opacity" values="0;0.6;0" dur="4s" repeatCount="indefinite"/> </circle> <circle cx="252" cy="118" r="1.5" fill="#70D6FF" opacity="0"> <animate attributeName="opacity" values="0;0.5;0" dur="5s" begin="1s" repeatCount="indefinite"/> </circle> <circle cx="150" cy="92" r="2" fill="#F7DC6F" opacity="0"> <animate attributeName="opacity" values="0;0.4;0" dur="3.5s" begin="0.5s" repeatCount="indefinite"/> </circle> <circle cx="48" cy="150" r="1" fill="#FF8B94" opacity="0"> <animate attributeName="opacity" values="0;0.4;0" dur="6s" begin="2s" repeatCount="indefinite"/> </circle> <circle cx="252" cy="88" r="1" fill="#FFE66D" opacity="0"> <animate attributeName="opacity" values="0;0.5;0" dur="4.5s" begin="1.5s" repeatCount="indefinite"/> </circle>
<!-- === SMALL TURRETS on wall corners === --> <!-- Left small turret --> <rect x="48" y="98" width="8" height="14" rx="1" fill="var(--bg-tertiary)" stroke="var(--ac-purple)" stroke-width="0.6" opacity="0.6"/> <polygon points="52,92 46,98 58,98" fill="var(--ac-purple)" opacity="0.2"/> <!-- Right small turret --> <rect x="244" y="98" width="8" height="14" rx="1" fill="var(--bg-tertiary)" stroke="var(--ac-purple)" stroke-width="0.6" opacity="0.6"/> <polygon points="248,92 242,98 254,98" fill="var(--ac-purple)" opacity="0.2"/> </svg> </div>
<!-- About content (shown before Enter, above castle) --> <div id="keeps-about-inline" style="display:none; overflow-y:auto; padding:20px 24px 0; width:100%; max-width:720px;"> <div class="keeps-about-simple"> <div class="keeps-about-lede"> <span>All <span class="kl-brand"><span style="color:#FF6B6B">K</span><span style="color:#4ECDC4">i</span><span style="color:#FFE66D">d</span><span style="color:#A8E6CF">L</span><span style="color:#FF8B94">i</span><span style="color:#F7DC6F">s</span><span style="color:#BB8FCE">p</span><span style="color:#70D6FF">.com</span></span> pieces are given a unique <span class="code-slot" id="code-slot">$code</span>.</span> <span>Keep your KidLisp pieces on Tezos.</span> <span>A KidLisp piece can only be kept by its author.</span> <span>Each keep costs <span class="keeps-price keeps-price-tez"><span class="tez-prefix">ꜩ</span> 2.5</span> <span class="keeps-fee-detail">(<span id="keeps-xtz-usd" class="keeps-price-usd">~$0.91 USD</span>)</span></span> </div>
<button id="keeps-enter-btn" class="keeps-enter-btn" style="display:none;" onclick="enterKeeps()">Enter</button>
<!-- Technology section moved to /technology page --> </div> </div>
</section>
<!-- TABS SYSTEM --> <div class="keeps-tabs-container" id="keeps-tabs-container" style="visibility:hidden;"> <div class="keeps-tabs logged-out"> <button class="keeps-tab active" data-tab="unkept">Yours</button> <button class="keeps-tab" data-tab="kept">Kept</button> </div>
<!-- TAB: Unkept/Kept (search + grid) --> <div class="keeps-tab-content active" id="tab-index"> <div class="keeps-search-bar" id="index-search-bar"> <div class="keeps-sigil-menu" id="sigil-menu" onclick="toggleSigilMenu()"> <span class="keeps-sigil-label" id="sigil-label">$</span> <div class="keeps-sigil-dropdown" id="sigil-dropdown"> <div class="keeps-sigil-option active" data-sigil="$"> <span class="keeps-sigil-option-char">$</span> <span class="keeps-sigil-option-desc">KidLisp — generative art pieces</span> </div> <div class="keeps-sigil-option disabled" data-sigil="!"> <span class="keeps-sigil-option-char">!</span> <span class="keeps-sigil-option-desc">Tapes — video loops <em>(soon)</em></span> </div> <div class="keeps-sigil-option disabled" data-sigil="*"> <span class="keeps-sigil-option-char">*</span> <span class="keeps-sigil-option-desc">Clocks — live time pieces <em>(soon)</em></span> </div> <div class="keeps-sigil-option disabled" data-sigil="#"> <span class="keeps-sigil-option-char">#</span> <span class="keeps-sigil-option-desc">Paintings — pixel art <em>(soon)</em></span> </div> </div> </div> <input type="text" class="keeps-search-input" id="search-input" value="" placeholder="Filter codes..." autocomplete="off" spellcheck="false" /> <div class="keeps-sort-btns" id="keeps-sort-btns"> <button class="keeps-sort-btn" id="sort-btn-recent" onclick="setUnkeptSort('recent')">Recent</button> <button class="keeps-sort-btn active" id="sort-btn-hits" onclick="setUnkeptSort('hits')">Hits</button> </div> </div> <div class="keeps-grid" id="keeps-grid"> <div class="keeps-loading" id="keeps-loading" style="display:none;"> <div class="spinner"></div> <span>Loading your keeps index...</span> </div> </div> <div class="keeps-pagination" id="keeps-pagination"></div> </div> </div>
<!-- TAB: Market --> <div class="keeps-tab-content" id="tab-market"> <div class="market-controls"> <div class="market-controls-left"> <span class="market-sort-label">Sort:</span> <button class="market-sort-btn active" id="market-sort-price" onclick="setMarketSort('price')">Top Price</button> <button class="market-sort-btn" id="market-sort-recent" onclick="setMarketSort('recent')">Recently Listed</button> </div> <div class="market-controls-right"> <span class="market-live-badge" id="market-live-status">LIVE</span> <span class="market-last-updated" id="market-last-updated">waiting...</span> <button class="market-sort-btn" id="market-refresh-btn" onclick="refreshMarketNow()">Refresh</button> </div> </div> <div class="market-grid" id="market-grid"> <div class="market-empty">Loading listings…</div> </div> </div>
<!-- EMBEDDED FOCUSED-KEEP empty state (shown in editor iframe before a piece arrives) --> <div id="keeps-embed-empty"> <div class="embed-empty-code">$</div> <div>Open or run a piece in the editor<br>to keep it on Tezos.</div> </div>
<!-- KEEP MODAL (unified) --> <div class="keeps-mint-overlay" id="mint-overlay"> <div class="keeps-mint-modal"> <div class="mint-header"> <span class="mint-header-title" id="mint-title">Keep $code</span> <button class="keeps-modal-close" onclick="closeMintModal()">×</button> </div> <div class="mint-body"> <div class="mint-piece-info" id="mint-piece-info"></div> <div class="mint-preview-frame" id="mint-preview-frame"></div> <div class="mint-timeline" id="mint-timeline"></div> <div id="mint-result-area"></div> <div class="mint-track-log" id="mint-track-log"></div> <div class="mint-actions" id="mint-actions-start" style="display:none;"> <button class="mint-action-btn cancel" onclick="cancelMint()">Cancel</button> <button class="mint-action-btn primary" id="mint-start-btn" onclick="beginMintFlow()">Keep on Tezos ꜩ</button> </div> <div class="mint-actions" id="mint-actions" style="display:none;"> <button class="mint-action-btn cancel" onclick="cancelMint()">Cancel</button> <button class="mint-action-btn primary" id="mint-confirm-btn" onclick="confirmMint()" disabled>Confirm & Sign</button> </div> </div> </div> </div>
<!-- KEEP DETAIL MODAL (permalink overlay) --> <div class="keep-detail-overlay" id="keep-detail-overlay"> <div class="keep-detail-modal"> <div class="keep-detail-header"> <span class="keep-detail-title" id="keep-detail-title">$code</span> <button class="keep-detail-close" onclick="closeKeepDetailModal()">×</button> </div> <div id="keep-detail-content"> <div class="keep-detail-loading">Loading...</div> </div> </div> </div>
<script> // ========================================= // Theme: auto only (follows system prefers-color-scheme) localStorage.removeItem('keeps-theme'); document.documentElement.removeAttribute('data-theme');
// ========================================= // KidLisp.com Keeps - Index Page // =========================================
const isDev = window.location.hostname === 'localhost' || window.location.hostname === 'local.aesthetic.computer'; const isKidlispDomain = window.location.hostname.includes('kidlisp.com'); // Use page origin on dev (respects http/https), absolute on production const API_BASE = isDev ? window.location.origin : 'https://aesthetic.computer'; const AC_BASE = isDev ? window.location.origin : 'https://aesthetic.computer'; const KL_BASE = isDev ? window.location.origin + '/kidlisp.com' : 'https://kidlisp.com';
// Auth state let acAuth0Client = null; let acUser = null; let acToken = null; let acHandle = null;
// Mint state let mintAbortController = null; let mintPreparedData = null; let mintCurrentPiece = null; let mintCurrentEntry = null; let mintWalletAddress = null; let linkedWalletAddress = null; // Wallet address stored in user's DB profile
// Parent-hosted wallet bridge. When embedded in the kidlisp.com editor the // Beacon DAppClient lives in the PARENT window (full viewport → proper // desktop connect/sign modal, one connection shared app-wide). We delegate // connect / getActiveAccount / requestOperation / disconnect over postMessage // instead of running a Beacon client inside this cramped iframe. const WALLET_VIA_PARENT = (window.self !== window.top); let _walletRpcId = 0; const _walletRpcPending = new Map(); function parentWalletRPC(method, params) { return new Promise((resolve, reject) => { const id = 'wrpc-' + (++_walletRpcId); _walletRpcPending.set(id, { resolve, reject }); console.log('🔑 [iframe wallet] → parent RPC:', method, 'origin=', window.location.origin, 'sameTop=', (window.self === window.top)); try { window.parent.postMessage({ type: 'wallet:rpc', id, method, params: params || null }, window.location.origin); } catch (e) { _walletRpcPending.delete(id); console.warn('🔑 [iframe wallet] postMessage failed', e); reject(e); return; } // Generous timeout — signing can wait on the user in their wallet app. setTimeout(() => { if (_walletRpcPending.has(id)) { _walletRpcPending.delete(id); reject(new Error('Wallet request timed out')); } }, 300000); }); } if (WALLET_VIA_PARENT) { window.addEventListener('message', (e) => { if (e.origin !== window.location.origin) return; const d = e.data; if (d?.type !== 'wallet:rpc:reply') return; console.log('🔑 [iframe wallet] ← parent reply:', d.id, d.ok ? 'OK' : ('ERR ' + d.error)); const pending = _walletRpcPending.get(d.id); if (!pending) return; _walletRpcPending.delete(d.id); if (d.ok) pending.resolve(d.result); else pending.reject(new Error(d.error || 'Wallet error')); }); } let mintOnChainUris = null; let mintLatestUris = null; let mintAutoSyncAfterRebake = false;
// Legacy runtime inspection cache for artifact bundles const legacyRuntimeCache = new Map(); const legacyRuntimeInFlight = new Map(); let mintLegacyCheckNonce = 0;
// Active keeps config (resolved from Mongo-backed API) let keepsConfig = { contractAddress: null, network: 'mainnet', profile: 'v11', version: '11.0.0', rpcUrl: 'https://mainnet.ecadinfra.com', source: 'fallback', }; let keepsConfigPromise = null; function ensureKeepsConfig() { if (!keepsConfigPromise) keepsConfigPromise = loadKeepsConfig(); return keepsConfigPromise; }
function rpcForNetwork(network) { return network === 'mainnet' ? 'https://mainnet.ecadinfra.com' : 'https://ghostnet.ecadinfra.com'; }
function getTezosNetwork() { return keepsConfig.network || 'mainnet'; }
function getKeepsContractAddress() { return keepsConfig.contractAddress || null; }
function getKeepsContractProfile() { return (keepsConfig.profile || '').trim().toLowerCase() || null; }
function getKeepsContractVersion() { return (keepsConfig.version || '').trim() || null; }
function getObjktBase(network = getTezosNetwork()) { return network === 'mainnet' ? 'https://objkt.com' : `https://${network}.objkt.com`; }
function renderActiveKeepsConfig() { const network = getTezosNetwork(); const contract = getKeepsContractAddress();
const addressEl = document.getElementById('v6-active-contract-address'); const metaEl = document.getElementById('v6-active-contract-meta'); const tzktLink = document.getElementById('v6-active-contract-tzkt'); const objktLink = document.getElementById('v6-active-contract-objkt'); const opsLink = document.getElementById('v6-active-contract-ops');
if (addressEl) { addressEl.textContent = contract || 'Unavailable'; }
if (metaEl) { const profile = (keepsConfig.profile || 'unknown').toUpperCase(); const version = keepsConfig.version || 'unknown'; metaEl.textContent = `Profile: ${profile} · Version: ${version} · Network: ${network} · Source: ${keepsConfig.source || 'unknown'}`; }
if (contract) { const tzktBase = network === 'mainnet' ? 'https://tzkt.io' : `https://${network}.tzkt.io`; const objktBase = getObjktBase(network); if (tzktLink) tzktLink.href = `${tzktBase}/${contract}`; if (objktLink) objktLink.href = `${objktBase}/collections/${contract}`; if (opsLink) opsLink.href = `${tzktBase}/${contract}/operations`; }
// Fetch live stats from TzKT if (contract) fetchLiveStats(contract, network); }
async function fetchLiveStats(contract, network) { const tzktApi = network === 'mainnet' ? 'https://api.tzkt.io' : `https://api.${network}.tzkt.io`; try { // Fetch contract info + token balances in parallel const [contractRes, balancesRes] = await Promise.all([ fetch(`${tzktApi}/v1/contracts/${contract}`), fetch(`${tzktApi}/v1/tokens/balances?token.contract=${contract}&balance.gt=0&limit=10000&select=account.address,token.metadata`), ]);
if (contractRes.ok) { const info = await contractRes.json(); // Total tokens from contract's tokensCount const totalKeeps = info.tokensCount || 0; const el = document.getElementById('stat-total-keeps'); if (el) el.textContent = totalKeeps.toLocaleString();
// Contract creation date const ageEl = document.getElementById('stat-contract-age'); if (ageEl && info.firstActivity) { const created = new Date(info.firstActivityTime || info.firstActivity); if (!isNaN(created)) { ageEl.textContent = created.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); } } }
if (balancesRes.ok) { const balances = await balancesRes.json(); // Unique holders const holders = new Set(balances.map(b => b.account?.address || b.account).filter(Boolean)); const holdersEl = document.getElementById('stat-unique-holders'); if (holdersEl) holdersEl.textContent = holders.size.toLocaleString();
// Unique creators (from metadata.creators) const creators = new Set(); for (const b of balances) { const meta = b.token?.metadata; if (meta?.creators) { for (const c of meta.creators) creators.add(c); } } const creatorsEl = document.getElementById('stat-unique-creators'); if (creatorsEl) creatorsEl.textContent = creators.size.toLocaleString();
// Build keepers (authors/creators) leaderboard const keeperCounts = {}; for (const b of balances) { const meta = b.token?.metadata; if (meta?.creators) { for (const c of meta.creators) { if (!keeperCounts[c]) keeperCounts[c] = 0; keeperCounts[c]++; } } } const keepersSorted = Object.entries(keeperCounts) .sort((a, b) => b[1] - a[1]); const keepersEl = document.getElementById('keeps-keepers-board'); if (keepersEl && keepersSorted.length) { // Resolve aliases for keeper addresses const keeperAddrs = keepersSorted.map(k => k[0]); let keeperAliases = {}; try { const aliasRes = await fetch(`${tzktApi}/v1/accounts?address.in=${keeperAddrs.join(',')}&select=address,alias&limit=${keeperAddrs.length}`); if (aliasRes.ok) { const aliasData = await aliasRes.json(); for (const a of aliasData) if (a.alias) keeperAliases[a.address] = a.alias; } } catch (e) {} keepersEl.innerHTML = keepersSorted.map(([addr, count]) => { const label = keeperAliases[addr] || (addr.slice(0, 8) + '\u2026' + addr.slice(-4)); const objktUrl = 'https://objkt.com/profile/' + addr; return `<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--border-subtle);"> <a href="${objktUrl}" target="_blank" rel="noopener" style="color:var(--ac-purple);text-decoration:none;">${escapeHtml(label)}</a> <span style="color:var(--text-tertiary);">${count} kept</span> </div>`; }).join(''); }
// Build buyers (current holders) leaderboard const buyerCounts = {}; for (const b of balances) { const addr = b.account?.address || b.account; if (!addr) continue; const alias = b.account?.alias || null; if (!buyerCounts[addr]) buyerCounts[addr] = { alias, count: 0 }; buyerCounts[addr].count++; } const buyersSorted = Object.entries(buyerCounts) .sort((a, b) => b[1].count - a[1].count); const buyersEl = document.getElementById('keeps-buyers-board'); if (buyersEl && buyersSorted.length) { buyersEl.innerHTML = buyersSorted.map(([addr, info]) => { const label = info.alias || (addr.slice(0, 8) + '\u2026' + addr.slice(-4)); const objktUrl = 'https://objkt.com/profile/' + addr; return `<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--border-subtle);"> <a href="${objktUrl}" target="_blank" rel="noopener" style="color:var(--ac-purple);text-decoration:none;">${escapeHtml(label)}</a> <span style="color:var(--text-tertiary);">${info.count} hold${info.count !== 1 ? 's' : ''}</span> </div>`; }).join(''); } } } catch (err) { console.warn('Failed to fetch live stats:', err.message); } }
async function loadKeepsConfig() { try { const configUrl = `${API_BASE}/api/keeps-config?ts=${Date.now()}`; const response = await fetch(configUrl, { cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json();
if (typeof data.contractAddress === 'string' && data.contractAddress.trim()) { keepsConfig.contractAddress = data.contractAddress.trim(); } if (typeof data.network === 'string' && data.network.trim()) { keepsConfig.network = data.network.trim().toLowerCase(); } if (typeof data.profile === 'string' && data.profile.trim()) { keepsConfig.profile = data.profile.trim(); } if (typeof data.version === 'string' && data.version.trim()) { keepsConfig.version = data.version.trim(); } keepsConfig.rpcUrl = data.rpcUrl || rpcForNetwork(getTezosNetwork()); keepsConfig.source = data.source || keepsConfig.source; } catch (error) { console.warn('Failed to load keeps config, using fallback defaults:', error.message); keepsConfig.rpcUrl = rpcForNetwork(getTezosNetwork()); } finally { renderActiveKeepsConfig(); } }
let allCodes = []; let filteredCodes = []; let activeTab = 'unkept'; let unkeptSort = 'hits'; let searchQuery = ''; let selectedSigil = '$'; let indexReady = false;
// Pagination const PAGE_SIZE = 24; let currentPage = 0; let totalPages = 0;
// Visibility-based image loading — load when visible, unload when not const visibilityObserver = new IntersectionObserver((entries) => { for (const entry of entries) { const img = entry.target; if (entry.isIntersecting) { if (img.dataset.src && !img.src) { img.src = img.dataset.src; } } else { if (img.src) { img.removeAttribute('src'); } } } }, { rootMargin: '200px' }); function observeImage(img) { visibilityObserver.observe(img); }
// DOM const grid = document.getElementById('keeps-grid'); const loading = document.getElementById('keeps-loading'); const indexSearchBar = document.getElementById('index-search-bar'); const sigilSelect = null; // replaced by custom sigil menu const searchInput = document.getElementById('search-input'); const mintOverlay = document.getElementById('mint-overlay');
const walletBtn = document.getElementById('keeps-wallet-btn'); const sortBtnsContainer = document.getElementById('keeps-sort-btns');
// ========================================= // WALLET BUTTON // ========================================= function updateWalletBtn() { if (!walletBtn) return; if (!acUser) { walletBtn.style.display = 'none'; return; } walletBtn.style.display = ''; walletBtn.classList.remove('connected', 'linked'); if (mintWalletAddress) { // Actively connected via Beacon const short = mintWalletAddress.slice(0, 7) + '…' + mintWalletAddress.slice(-4); walletBtn.innerHTML = 'ꜩ ' + short; walletBtn.title = mintWalletAddress + ' — click to disconnect'; walletBtn.classList.add('connected'); } else if (linkedWalletAddress) { // Linked in DB but not actively connected const short = linkedWalletAddress.slice(0, 7) + '…' + linkedWalletAddress.slice(-4); walletBtn.innerHTML = 'ꜩ ' + short + ' (offline)'; walletBtn.title = linkedWalletAddress + ' — linked but not connected, click to reconnect'; walletBtn.classList.add('linked'); } else { walletBtn.innerHTML = 'ꜩ Connect Tezos'; walletBtn.title = 'Connect Tezos wallet'; } }
async function handleWalletBtn() { if (mintWalletAddress) { // Confirm before disconnecting if (!confirm('Disconnect from your Tezos wallet?')) return; // Disconnect if (WALLET_VIA_PARENT) { await parentWalletRPC('disconnect').catch(() => {}); } else if (beaconClient) { await beaconClient.clearActiveAccount(); await beaconClient.destroy().catch(() => {}); beaconClient = null; beaconInitPromise = null; } mintWalletAddress = null; updateWalletBtn(); } else { // Connect try { await connectWallet(); updateWalletBtn(); } catch (e) { console.warn('Wallet connect cancelled', e); } } }
// ========================================= // SORT (unkept tab) // ========================================= function setUnkeptSort(mode) { unkeptSort = mode; document.getElementById('sort-btn-recent')?.classList.toggle('active', mode === 'recent'); document.getElementById('sort-btn-hits')?.classList.toggle('active', mode === 'hits'); applyFilters(); }
function updateSortVisibility() { if (sortBtnsContainer) { sortBtnsContainer.style.display = activeTab === 'unkept' ? '' : 'none'; } }
// ========================================= // FETCH ALL CODES // ========================================= let liveUpdateTimer = null; let lastFetchTime = null; // ISO timestamp of most recent entry
function buildStoreUrl(extra = '') { const contractHint = encodeURIComponent(getKeepsContractAddress() || ''); const contractVersion = encodeURIComponent(getKeepsContractVersion() || ''); const contractProfile = encodeURIComponent(getKeepsContractProfile() || ''); // Push the per-user filter to the server. Without this we'd haul the // whole kidlisp collection (~17K rows) over the wire just to throw out // everything but the current user's pieces in scopeIndexEntries. const handleParam = acHandle ? `&handle=${encodeURIComponent(acHandle)}` : ''; return `${AC_BASE}/api/store-kidlisp?recent=true&contract=${contractHint}&contractVersion=${contractVersion}&contractProfile=${contractProfile}${handleParam}${extra}&ts=${Date.now()}`; }
async function fetchAllCodes(sort = 'recent') { try { // 20000 is a ceiling — @jeffrey already has ~4K pieces and the // server-side handle filter needs headroom, otherwise older // kept pieces get cut off before they reach the client. const url = buildStoreUrl(`&limit=20000&sort=${sort}`); const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const codes = data.recent || []; // Track the newest entry timestamp for live updates if (codes.length > 0) { const newest = codes.reduce((a, b) => (new Date(b.when) > new Date(a.when) ? b : a)); lastFetchTime = newest.when; } else { lastFetchTime = new Date().toISOString(); } return codes; } catch (err) { console.error('Failed to fetch codes:', err); return null; } }
async function fetchNewCodes() { if (!lastFetchTime || !indexReady) return; try { const since = encodeURIComponent(lastFetchTime); const url = buildStoreUrl(`&limit=50&sort=recent&since=${since}`); const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) return; const data = await res.json(); const newCodes = data.recent || []; if (newCodes.length === 0) return;
// Update lastFetchTime to newest entry const newest = newCodes.reduce((a, b) => (new Date(b.when) > new Date(a.when) ? b : a)); lastFetchTime = newest.when;
// Merge: add new entries, update existing ones by code const existingByCode = new Map(allCodes.map(e => [e.code, e])); let added = 0; for (const entry of newCodes) { if (existingByCode.has(entry.code)) { // Update existing entry in-place Object.assign(existingByCode.get(entry.code), entry); } else { allCodes.unshift(entry); added++; } }
// Scope to user's entries and re-render if (added > 0) { const scoped = scopeIndexEntries(allCodes); allCodes = scoped; applyFilters(); console.log(`✨ ${added} new piece${added > 1 ? 's' : ''} appeared`); } } catch (err) { // Silent fail — next poll will retry } }
function startLiveUpdates(intervalMs = 5000) { stopLiveUpdates(); liveUpdateTimer = setInterval(fetchNewCodes, intervalMs); }
function stopLiveUpdates() { if (liveUpdateTimer) { clearInterval(liveUpdateTimer); liveUpdateTimer = null; } }
function hasRegisteredHandle() { return !!(acUser && acHandle && String(acHandle).trim()); }
function normalizeAddress(address) { return typeof address === 'string' ? address.trim().toLowerCase() : ''; }
function normalizeContractProfile(profile) { if (typeof profile !== 'string') return null; const trimmed = profile.trim().toLowerCase(); return trimmed || null; }
function normalizeContractVersion(version) { if (typeof version !== 'string') return null; const trimmed = version.trim(); return trimmed || null; }
function normalizeTokenId(value) { if (value === null || typeof value === 'undefined' || value === '') return null; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 0) return null; return parsed; }
function getKeepRecords(entry) { const list = []; if (Array.isArray(entry?.keptRecords)) { list.push(...entry.keptRecords); } if (entry?.kept) { list.push(entry.kept); }
const deduped = new Map(); for (const item of list) { if (!item) continue; const tokenId = normalizeTokenId(item.tokenId); if (tokenId === null) continue; const network = (item.network || '').trim().toLowerCase() || 'mainnet'; const contractAddress = typeof item.contractAddress === 'string' ? item.contractAddress.trim() : ''; if (!contractAddress) continue; const keptAt = item.keptAt || item.mintedAt || null; const contractProfile = normalizeContractProfile(item.contractProfile || item.profile || null); const contractVersion = normalizeContractVersion(item.contractVersion || item.version || null); const key = `${normalizeAddress(contractAddress)}:${network}:${tokenId}`; const normalized = { ...item, tokenId, network, contractAddress, contractProfile, contractVersion, keptAt, }; if (!deduped.has(key)) { deduped.set(key, normalized); continue; } const existing = deduped.get(key); const existingMs = existing?.keptAt ? new Date(existing.keptAt).getTime() : 0; const normalizedMs = keptAt ? new Date(keptAt).getTime() : 0; if ((Number.isFinite(normalizedMs) ? normalizedMs : 0) > (Number.isFinite(existingMs) ? existingMs : 0)) { deduped.set(key, normalized); } }
return Array.from(deduped.values()).sort((a, b) => { const aMs = a?.keptAt ? new Date(a.keptAt).getTime() : 0; const bMs = b?.keptAt ? new Date(b.keptAt).getTime() : 0; const safeA = Number.isFinite(aMs) ? aMs : 0; const safeB = Number.isFinite(bMs) ? bMs : 0; if (safeA !== safeB) return safeB - safeA; return (b.tokenId || 0) - (a.tokenId || 0); }); }
function getActiveKeepRecord(entry) { const activeContract = getKeepsContractAddress(); if (!activeContract) return null;
const activeNetwork = (getTezosNetwork() || '').trim().toLowerCase(); const activeProfile = normalizeContractProfile(getKeepsContractProfile()); const activeVersion = normalizeContractVersion(getKeepsContractVersion()); const normalizedActiveContract = normalizeAddress(activeContract); const records = getKeepRecords(entry); return records.find(record => { if (!record.contractAddress) return false; const sameContract = normalizeAddress(record.contractAddress) === normalizedActiveContract; if (!sameContract) return false; const sameNetwork = !record.network || !activeNetwork || record.network === activeNetwork; if (!sameNetwork) return false; if (activeProfile && record.contractProfile && normalizeContractProfile(record.contractProfile) !== activeProfile) { return false; } if (activeVersion && record.contractVersion && normalizeContractVersion(record.contractVersion) !== activeVersion) { return false; } return true; }) || null; }
function getPrimaryKeepRecord(entry) { const active = getActiveKeepRecord(entry); if (active) return active; const records = getKeepRecords(entry); return records[0] || null; }
function getNonActiveKeepRecord(entry) { const activeContract = getKeepsContractAddress(); if (!activeContract) return null; const records = getKeepRecords(entry); const active = getActiveKeepRecord(entry); if (!active) return records[0] || null; return records.find(record => normalizeAddress(record.contractAddress) !== normalizeAddress(active.contractAddress) || normalizeTokenId(record.tokenId) !== normalizeTokenId(active.tokenId) ) || null; }
function isKeptOnActiveContract(entry) { return !!getActiveKeepRecord(entry); }
function hasAnyKeepRecord(entry) { return !!getPrimaryKeepRecord(entry); }
function scopeIndexEntries(entries) { const userSub = acUser?.sub; const currentHandle = (acHandle || '').trim().toLowerCase(); return (entries || []).filter(entry => { const isMineBySub = !!userSub && entry.user === userSub; if (isMineBySub) return true; if (!currentHandle) return false; const entryHandle = (entry?.handle || '').trim().toLowerCase(); return entryHandle === currentHandle; }); }
function setIndexSearchVisible(visible) { if (!indexSearchBar) return; indexSearchBar.style.display = visible ? 'flex' : 'none'; }
function revealUI() { const container = document.getElementById('keeps-tabs-container'); if (container) container.style.visibility = ''; }
function showInlineAbout() { const inline = document.getElementById('keeps-about-inline'); const tabIndex = document.getElementById('tab-index'); if (inline) inline.style.display = 'block'; if (tabIndex) tabIndex.style.display = 'none'; }
function hideInlineAbout() { const inline = document.getElementById('keeps-about-inline'); const tabIndex = document.getElementById('tab-index'); if (inline) inline.style.display = 'none'; if (tabIndex) tabIndex.style.display = ''; }
function renderIndexGate(reason = 'login') { revealUI(); setIndexSearchVisible(false); showInlineAbout(); allCodes = []; filteredCodes = [];
grid.innerHTML = ''; }
// ========================================= // RENDER (paginated — renders PAGE_SIZE cards at a time) // ========================================= function createRow(entry) { const handle = entry.handle || 'anon'; const hits = entry.hits || 0; const activeKeepInfo = getActiveKeepRecord(entry); const tokenId = activeKeepInfo?.tokenId; const keptAt = activeKeepInfo?.keptAt ? new Date(activeKeepInfo.keptAt) : null; const createdAt = entry.when ? new Date(entry.when) : null; const keptTimeStr = keptAt ? formatRelativeTime(keptAt) : ''; const createdTimeStr = createdAt ? formatRelativeTime(createdAt) : ''; const thumbUrl = `https://oven.aesthetic.computer/grab/png/56/42/$${entry.code}?density=1&nowait=true&source=gallery`;
const row = document.createElement('div'); row.className = 'keeps-row'; // Prefill owner if already fetched, otherwise show placeholder const knownOwner = tokenId != null ? tokenOwners[String(tokenId)] : null; const ownerLabel = knownOwner ? (knownOwner.alias || knownOwner.address.slice(0, 8) + '…' + knownOwner.address.slice(-4)) : '…'; const ownerHref = knownOwner?.address ? `https://objkt.com/profile/${knownOwner.address}` : null; const objktTokenHref = (activeKeepInfo?.contractAddress && tokenId != null) ? `${getObjktBase(activeKeepInfo.network)}/tokens/${activeKeepInfo.contractAddress}/${tokenId}` : null;
row.innerHTML = ` <img class="keeps-row-thumb" alt="$${entry.code}" loading="lazy" /> <a class="keeps-row-code" href="${KL_BASE}/$${entry.code}" title="Edit in KidLisp.com" onclick="event.stopPropagation()">$${entry.code}</a> <span class="keeps-row-handle">${escapeHtml(handle)}</span> <span class="keeps-row-hits">${hits.toLocaleString()} hit${hits !== 1 ? 's' : ''}</span> ${tokenId != null ? (objktTokenHref ? `<a class="keeps-row-token" href="${objktTokenHref}" target="_blank" rel="noopener" title="View token on objkt.com" onclick="event.stopPropagation()">Token #${tokenId}</a>` : `<span class="keeps-row-token">Token #${tokenId}</span>`) : ''} ${tokenId != null ? `<span class="keeps-row-owner" id="row-owner-${tokenId}">${ownerHref ? `<a class="keeps-row-owner-link" href="${ownerHref}" target="_blank" rel="noopener" onclick="event.stopPropagation()">${escapeHtml(ownerLabel)}</a>` : ownerLabel}</span>` : ''} ${keptTimeStr ? `<span class="keeps-row-time" title="Kept ${keptAt?.toLocaleString() || ''}">kept ${keptTimeStr}</span>` : ''} ${createdTimeStr ? `<span class="keeps-row-time" title="Created ${createdAt?.toLocaleString() || ''}">made ${createdTimeStr}</span>` : ''}`;
const img = row.querySelector('.keeps-row-thumb'); img.dataset.src = thumbUrl; img.onerror = () => { img.style.display = 'none'; }; observeImage(img);
row.addEventListener('click', () => openMintModal(entry)); return row; }
function createCard(entry) { const when = entry.when ? new Date(entry.when) : null; const timeStr = when ? formatRelativeTime(when) : ''; const handle = entry.handle || 'anon'; const hits = entry.hits || 0; const activeKeepInfo = getActiveKeepRecord(entry); const isActiveKept = !!activeKeepInfo; const canManageKeep = !!(acUser && entry.user && acUser.sub === entry.user); const card = document.createElement('div'); card.className = 'keeps-card' + (isActiveKept ? ' kept' : '');
const thumbUrl = `https://oven.aesthetic.computer/grab/png/280/210/$${entry.code}?density=1&nowait=true&source=gallery`;
card.innerHTML = ` <div class="keeps-card-header"> <span class="keeps-card-code" title="Open $${entry.code}">$${entry.code}</span> <div class="keeps-card-meta"> <span class="keeps-card-handle">${escapeHtml(handle)}</span> <span class="keeps-card-hits">${hits} hit${hits !== 1 ? 's' : ''}</span> </div> </div> <div class="keeps-card-preview"> <div class="preview-noise"><div class="oven-label">oven</div><div class="oven-bar"></div></div> <img alt="$${entry.code} preview" loading="lazy" /> <div class="preview-overlay"></div> </div> <div class="keeps-card-actions"> ${isActiveKept ? `<span class="keeps-card-btn kept-badge" title="Kept on Tezos${activeKeepInfo?.tokenId != null ? ` · Token #${activeKeepInfo.tokenId}` : ''}">✔ Kept</span>` : (canManageKeep ? `<button class="keeps-card-btn keep-btn" data-code="${entry.code}" title="Keep on Tezos">ꜩ Keep</button>` : '')} ${timeStr ? `<span style="margin-left: auto; font-size: 12px; color: var(--text-tertiary);">${timeStr}</span>` : ''} </div>`;
// Set up animated noise placeholder const noiseEl = card.querySelector('.preview-noise'); const { canvas, ctx, imageData } = createNoiseCanvas(56, 42); noiseEl.insertBefore(canvas, noiseEl.firstChild); const noiseId = startNoiseAnimation(canvas, ctx, imageData);
// Load image with oven-style fallback const img = card.querySelector('.keeps-card-preview img'); img.onload = () => { img.classList.add('loaded'); clearInterval(noiseId); noiseEl.remove(); }; img.onerror = () => { // Keep noise placeholder visible — image not baked yet }; img.dataset.src = thumbUrl; observeImage(img);
// Whole card + keep button all open the mint/keep modal card.style.cursor = 'pointer'; card.addEventListener('click', () => openMintModal(entry));
return card; }
function renderGrid() { grid.innerHTML = ''; grid.classList.toggle('list-view', activeTab === 'kept');
const paginationEl = document.getElementById('keeps-pagination');
if (filteredCodes.length === 0) { const emptyMessage = allCodes.length === 0 ? 'No visible pieces yet. Publish or keep a piece to populate your index.' : 'No matches for your search.'; grid.innerHTML = ` <div class="keeps-error" style="grid-column: 1 / -1;"> <div class="error-icon">🔍</div> <div class="error-msg">${emptyMessage}</div> </div>`; if (paginationEl) paginationEl.style.display = 'none'; return; }
totalPages = Math.ceil(filteredCodes.length / PAGE_SIZE); if (currentPage >= totalPages) currentPage = totalPages - 1; if (currentPage < 0) currentPage = 0;
renderPage(); renderPaginationControls();
if (activeTab === 'kept') { ensureTokenOwners().then(updateRowOwners); } }
function renderPage() { grid.innerHTML = ''; const start = currentPage * PAGE_SIZE; const end = Math.min(start + PAGE_SIZE, filteredCodes.length); const frag = document.createDocumentFragment();
for (let i = start; i < end; i++) { frag.appendChild(activeTab === 'kept' ? createRow(filteredCodes[i]) : createCard(filteredCodes[i])); }
grid.appendChild(frag);
// Deep-link: if the URL points at /kept/$code or /unkept/$code, open // the mint modal for that piece once we have the entry in allCodes. if (pendingMintModalCode) { const match = allCodes.find(e => e.code === pendingMintModalCode); if (match) { const code = pendingMintModalCode; pendingMintModalCode = null; // Defer to next tick so the current render pass completes first. setTimeout(() => openMintModal(match), 0); } } }
function renderPaginationControls() { const paginationEl = document.getElementById('keeps-pagination'); if (!paginationEl) return;
if (totalPages <= 1) { paginationEl.style.display = 'none'; return; }
paginationEl.style.display = 'flex'; paginationEl.innerHTML = '';
const prev = document.createElement('button'); prev.className = 'page-btn'; prev.textContent = '\u2190 Prev'; prev.disabled = currentPage === 0; prev.addEventListener('click', () => goToPage(currentPage - 1));
const info = document.createElement('span'); info.className = 'page-info'; info.textContent = `Page ${currentPage + 1} of ${totalPages}`;
const next = document.createElement('button'); next.className = 'page-btn'; next.textContent = 'Next \u2192'; next.disabled = currentPage === totalPages - 1; next.addEventListener('click', () => goToPage(currentPage + 1));
const goForm = document.createElement('span'); goForm.className = 'page-go'; const goInput = document.createElement('input'); goInput.type = 'number'; goInput.className = 'page-go-input'; goInput.min = 1; goInput.max = totalPages; goInput.placeholder = '#'; goInput.value = ''; goInput.title = 'Go to page'; goInput.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { const p = parseInt(goInput.value, 10); if (p >= 1 && p <= totalPages) goToPage(p - 1); } }); const goBtn = document.createElement('button'); goBtn.className = 'page-btn page-go-btn'; goBtn.textContent = 'Go'; goBtn.addEventListener('click', () => { const p = parseInt(goInput.value, 10); if (p >= 1 && p <= totalPages) goToPage(p - 1); }); goForm.append(goInput, goBtn);
paginationEl.append(prev, info, next, goForm); }
function goToPage(page, { pushState = true } = {}) { if (page < 0 || page >= totalPages) return; currentPage = page; renderPage(); renderPaginationControls(); grid.scrollIntoView({ behavior: 'smooth', block: 'start' }); if (pushState) { const url = new URL(window.location); if (page > 0) { url.searchParams.set('page', page + 1); } else { url.searchParams.delete('page'); } history.pushState({ tab: activeTab, page: page }, '', url); } }
// ========================================= // FILTER & SEARCH // ========================================= function applyFilters() { const q = searchQuery.toLowerCase().trim(); const sigil = selectedSigil || '$'; filteredCodes = allCodes.filter(entry => { // Kept filter const keptOnActive = isKeptOnActiveContract(entry); if (activeTab === 'kept' && !keptOnActive) return false; if (activeTab === 'unkept' && keptOnActive) return false;
// Search filter if (q) { const prefixedCode = (sigil + entry.code).toLowerCase(); const bareCode = (entry.code || '').toLowerCase(); const source = (entry.source || '').toLowerCase(); const handle = (entry.handle || '').toLowerCase(); if (!prefixedCode.includes(q) && !bareCode.includes(q) && !source.includes(q) && !handle.includes(q)) return false; }
return true; });
// Sort unkept tab by hits if selected (default order from server is recent) if (activeTab === 'unkept' && unkeptSort === 'hits') { filteredCodes.sort((a, b) => (b.hits || 0) - (a.hits || 0)); }
currentPage = 0; renderGrid(); }
// ========================================= // EVENT LISTENERS // =========================================
// Sigil custom dropdown function toggleSigilMenu() { const menu = document.getElementById('sigil-menu'); const dropdown = document.getElementById('sigil-dropdown'); if (!menu || !dropdown) return; const opening = !menu.classList.contains('open'); menu.classList.toggle('open', opening); if (opening) { const rect = menu.getBoundingClientRect(); dropdown.style.top = (rect.bottom + 2) + 'px'; dropdown.style.left = rect.left + 'px'; const close = (e) => { if (!menu.contains(e.target)) { menu.classList.remove('open'); document.removeEventListener('click', close, true); } }; setTimeout(() => document.addEventListener('click', close, true), 0); } }
document.getElementById('sigil-dropdown')?.addEventListener('click', (e) => { const opt = e.target.closest('.keeps-sigil-option'); if (!opt || opt.classList.contains('disabled')) return; const sigil = opt.dataset.sigil; selectedSigil = sigil; document.getElementById('sigil-label').textContent = sigil; document.querySelectorAll('.keeps-sigil-option').forEach(o => o.classList.toggle('active', o.dataset.sigil === sigil)); document.getElementById('sigil-menu').classList.remove('open'); applyFilters(); });
// Search let searchDebounce;
searchInput.addEventListener('input', () => { clearTimeout(searchDebounce); searchDebounce = setTimeout(() => { searchQuery = searchInput.value; applyFilters(); }, 0); });
// Tab + About routing const isKeepsDomain = window.location.hostname === 'keep.kidlisp.com';
function normalizeTabName(tabName) { if (tabName === 'kept') return 'kept'; if (tabName === 'market') return 'market'; return 'unkept'; }
function pathForTab(tabName) { if (isKeepsDomain) { if (tabName === 'kept') return '/kept'; if (tabName === 'market') return '/buy'; return '/'; } else { if (tabName === 'kept') return '#kept'; if (tabName === 'market') return '#buy'; return location.pathname; } }
function switchTab(tabName, { pushState = false } = {}) { const normalizedTab = normalizeTabName(tabName); activeTab = normalizedTab; const tabBtn = document.querySelector(`.keeps-tab[data-tab="${normalizedTab}"]`); if (tabBtn) { document.querySelectorAll('.keeps-tab').forEach(t => t.classList.remove('active')); tabBtn.classList.add('active'); } document.querySelectorAll('.keeps-tab-content').forEach(content => content.classList.remove('active')); if (normalizedTab === 'market') { hideInlineAbout(); const marketTab = document.getElementById('tab-market'); if (marketTab) { marketTab.classList.add('active'); startMarketLiveUpdates(); loadMarket({ force: !marketLoaded, silent: marketLoaded }); } } else if (normalizedTab === 'kept') { stopMarketLiveUpdates(); hideInlineAbout(); const indexTab = document.getElementById('tab-index'); if (indexTab) indexTab.classList.add('active'); updateSortVisibility(); if (indexReady) applyFilters(); } else { stopMarketLiveUpdates(); const indexTab = document.getElementById('tab-index'); if (indexTab) indexTab.classList.add('active'); updateSortVisibility(); if (indexReady) applyFilters(); } if (pushState) { if (isKeepsDomain) { history.pushState({ tab: normalizedTab }, '', pathForTab(normalizedTab)); } else { const hash = pathForTab(normalizedTab); history.replaceState({ tab: normalizedTab }, '', hash === location.pathname ? location.pathname : hash); } } }
// ========================================= // MARKET TAB // ========================================= let marketSort = 'price'; let marketLoaded = false; let marketListings = []; let marketSales = []; let marketFeedEvents = []; let marketIsFetching = false; let marketRefreshTimer = null; let marketLastUpdatedAt = null; const MARKET_FETCH_LIMIT = 120; const MARKET_REFRESH_MS = 15000;
// ========================================= // KEEP DETAIL MODAL (permalink) // ========================================= let detailSortedEntries = []; // populated by renderMarket for index-based lookup let pendingDeepLinkCode = null; // set on load if URL is /$code (detail modal) let pendingMintModalCode = null; // set on load if URL is /kept/$code or /unkept/$code const detailOverlay = document.getElementById('keep-detail-overlay'); const detailContent = document.getElementById('keep-detail-content'); const detailTitle = document.getElementById('keep-detail-title');
function openKeepDetailModal(entry) { if (!entry) return; const token = entry.token || {}; const tokenId = token?.token_id ?? ''; const name = token?.name || `Token #${tokenId}`; const code = name.startsWith('$') ? name.slice(1) : name; const codeDisplay = name.startsWith('$') ? name : `#${tokenId}`;
detailTitle.textContent = codeDisplay;
const xtz = (entry.mutez / 1_000_000).toFixed(2); const thumb = token?.thumbnail_uri ? token.thumbnail_uri.replace('ipfs://', 'https://ipfs.io/ipfs/') : ''; const seller = shortAddress(entry.sellerAddress); const buyer = shortAddress(entry.buyerAddress); const contract = getKeepsContractAddress(); const network = getTezosNetwork(); const objktBase = getObjktBase(network); const objktUrl = `${objktBase}/tokens/${contract}/${tokenId}`; const tzktBase = network === 'mainnet' ? 'https://tzkt.io' : `https://${network}.tzkt.io`; const tzktUrl = `${tzktBase}/${contract}/tokens/${tokenId}`; const acUrl = `https://aesthetic.computer/${code}`; const permalink = `https://keep.kidlisp.com/$${code}`;
const statusClass = entry.status === 'sold' ? 'sold' : entry.status === 'active' ? 'active' : 'unlisted'; const statusText = entry.status === 'sold' ? 'Sold' : entry.status === 'active' ? 'For Sale' : 'Not Listed';
let sideInfo = ''; if (entry.status === 'sold') { sideInfo = `<div class="keep-detail-row"><span class="keep-detail-label">Seller</span><span class="keep-detail-value">${escapeHtml(seller)}</span></div>`; if (buyer) sideInfo += `<div class="keep-detail-row"><span class="keep-detail-label">Buyer</span><span class="keep-detail-value">${escapeHtml(buyer)}</span></div>`; } else if (entry.status === 'active' && seller) { sideInfo = `<div class="keep-detail-row"><span class="keep-detail-label">Listed by</span><span class="keep-detail-value">${escapeHtml(seller)}</span></div>`; }
let timeRow = ''; if (entry.timestamp) { const d = new Date(entry.timestamp); timeRow = `<div class="keep-detail-row"><span class="keep-detail-label">Date</span><span class="keep-detail-value">${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>`; }
const buyBtnLabel = entry.status === 'active' ? 'Buy on objkt.com' : 'View on objkt.com';
detailContent.innerHTML = ` <div class="keep-detail-thumb">${thumb ? `<img src="${escapeHtml(thumb)}" alt="${escapeHtml(codeDisplay)}" />` : ''}</div> <div class="keep-detail-body"> <div class="keep-detail-row"> <span class="keep-detail-status ${statusClass}">${statusText}</span> <span class="keep-detail-price ${statusClass}">ꜩ${xtz}</span> </div> <div class="keep-detail-row"><span class="keep-detail-label">Token</span><span class="keep-detail-value">#${escapeHtml(String(tokenId))}</span></div> ${sideInfo} ${timeRow} </div> <div class="keep-detail-actions"> <a class="keep-detail-btn primary" href="${escapeHtml(objktUrl)}" target="_blank" rel="noopener">${buyBtnLabel} ↗</a> <div class="keep-detail-btn-row"> <a class="keep-detail-btn secondary" href="${escapeHtml(acUrl)}" target="_blank" rel="noopener">Run Piece ↗</a> <a class="keep-detail-btn secondary" href="${escapeHtml(tzktUrl)}" target="_blank" rel="noopener">TzKT ↗</a> </div> <div class="keep-detail-btn-row"> <button class="keep-detail-btn secondary" onclick="copyPermalink('${escapeHtml(permalink)}')">Copy Link</button> <a class="keep-detail-btn secondary" href="https://twitter.com/intent/tweet?url=${encodeURIComponent(permalink)}&text=${encodeURIComponent(codeDisplay + ' — KidLisp Keep on Tezos')}" target="_blank" rel="noopener">Share on X</a> </div> </div> `;
detailOverlay.classList.add('open'); document.body.style.overflow = 'hidden';
// Push permalink URL if (isKeepsDomain) { history.pushState({ tab: 'market', code }, '', `/$${code}`); } }
function closeKeepDetailModal() { detailOverlay.classList.remove('open'); document.body.style.overflow = ''; // Restore market URL if (isKeepsDomain && location.pathname.match(/^\/\$/)) { history.pushState({ tab: 'market' }, '', '/buy'); } }
function copyPermalink(url) { navigator.clipboard.writeText(url).then(() => { const btn = detailOverlay.querySelector('[onclick^="copyPermalink"]'); if (btn) { const orig = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = orig; }, 1500); } }).catch(() => {}); }
async function openKeepDetailByCode(code) { // First check if we have it in the current market data const match = detailSortedEntries.find(e => { const name = e.token?.name || ''; return name === `$${code}` || name === code; }); if (match) { openKeepDetailModal(match); return; }
// Show loading state while fetching detailTitle.textContent = `$${code}`; detailContent.innerHTML = '<div class="keep-detail-loading">Loading token data...</div>'; detailOverlay.classList.add('open'); document.body.style.overflow = 'hidden';
try { await ensureKeepsConfig(); const contract = getKeepsContractAddress(); if (!contract) throw new Error('No contract');
// Query objkt for this token by name const query = `query { token(where: { fa_contract: { _eq: "${contract}" } name: { _eq: "$${code}" } }) { token_id name thumbnail_uri } listing_active(where: { fa_contract: { _eq: "${contract}" } token: { name: { _eq: "$${code}" } } } order_by: { price_xtz: asc } limit: 1) { price_xtz seller_address timestamp } }`;
const data = await fetchObjktGraphQL(query); const tokens = data?.token || []; const listings = data?.listing_active || [];
if (tokens.length === 0) { const unkeptMatch = allCodes.find(e => e.code === code); if (unkeptMatch) { const acUrl = `https://aesthetic.computer/$${code}`; const klUrl = `https://kidlisp.com/$${code}`; const previewUrl = ovenPreviewUrlForPiece(code); detailContent.innerHTML = ` <div style="text-align:center;padding:16px 0;"> <img src="${previewUrl}" alt="$${code}" style="max-width:280px;border-radius:8px;image-rendering:pixelated;" /> </div> <div style="text-align:center;padding:8px 0;color:var(--text-tertiary);font-size:13px;"> <span>by ${escapeHtml(unkeptMatch.handle || 'anon')}</span> <span style="margin-left:12px;">${(unkeptMatch.hits || 0).toLocaleString()} hits</span> </div> <div style="display:flex;gap:10px;justify-content:center;padding:16px 0;"> <a href="${acUrl}" target="_blank" rel="noopener" style="padding:8px 16px;background:var(--ac-purple);color:white;border-radius:6px;text-decoration:none;font-family:var(--font-mono);font-size:13px;">Run Piece</a> <a href="${klUrl}" target="_blank" rel="noopener" style="padding:8px 16px;background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:6px;text-decoration:none;font-family:var(--font-mono);font-size:13px;">Edit in KidLisp</a> </div>`; } else { detailContent.innerHTML = '<div class="keep-detail-loading">Piece not found.</div>'; } return; }
const token = tokens[0]; const listing = listings[0]; const entry = { status: listing ? 'active' : 'unlisted', timestamp: listing?.timestamp || null, mutez: listing ? toMutez(listing.price_xtz) : 0, sellerAddress: listing?.seller_address || null, buyerAddress: null, token, }; openKeepDetailModal(entry); } catch (err) { console.error('[keep-detail] fetch error:', err); detailContent.innerHTML = `<div class="keep-detail-loading">Failed to load: ${escapeHtml(err.message)}</div>`; } }
// ========================================= // TOKEN OWNERS (for Kept tab) // ========================================= let tokenOwners = {}; // tokenId -> { address, alias } let tokenOwnersPromise = null;
async function loadTokenOwners() { await ensureKeepsConfig(); const contract = getKeepsContractAddress(); if (!contract) return; try { const res = await fetch(`https://api.tzkt.io/v1/tokens/balances?token.contract=${contract}&balance.gt=0&limit=1000`); if (!res.ok) return; const balances = await res.json(); tokenOwners = {}; for (const b of balances) { const tid = b.token?.tokenId; if (tid != null) { tokenOwners[String(tid)] = { address: b.account?.address, alias: b.account?.alias || null, }; } } } catch (e) { console.warn('Failed to load token owners:', e.message); } }
function ensureTokenOwners() { if (!tokenOwnersPromise) tokenOwnersPromise = loadTokenOwners(); return tokenOwnersPromise; }
function updateRowOwners() { for (const [tid, owner] of Object.entries(tokenOwners)) { const span = document.getElementById(`row-owner-${tid}`); if (!span || !owner.address) continue; const label = owner.alias || (owner.address.slice(0, 8) + '…' + owner.address.slice(-4)); const objktUrl = `https://objkt.com/profile/${owner.address}`; span.innerHTML = `<a class="keeps-row-owner-link" href="${objktUrl}" target="_blank" rel="noopener" onclick="event.stopPropagation()">${escapeHtml(label)}</a>`; } }
function setMarketSort(sort) { marketSort = sort; document.getElementById('market-sort-price').classList.toggle('active', sort === 'price'); document.getElementById('market-sort-recent').classList.toggle('active', sort === 'recent'); renderMarket(); }
function setMarketLiveBadge(state = 'live', text = null) { const badge = document.getElementById('market-live-status'); if (!badge) return; badge.classList.remove('stale', 'error'); if (state === 'stale') badge.classList.add('stale'); if (state === 'error') badge.classList.add('error'); if (text) badge.textContent = text; }
function updateMarketLastUpdated(ts = null) { const label = document.getElementById('market-last-updated'); if (!label) return; if (!ts) { label.textContent = 'waiting...'; return; } const date = new Date(ts); if (Number.isNaN(date.getTime())) { label.textContent = 'updated'; return; } label.textContent = `updated ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`; }
function refreshMarketNow() { loadMarket({ force: true, silent: false }); }
function stopMarketLiveUpdates() { if (marketRefreshTimer) { clearInterval(marketRefreshTimer); marketRefreshTimer = null; } }
function startMarketLiveUpdates() { if (marketRefreshTimer) return; marketRefreshTimer = setInterval(() => { if (activeTab !== 'market') return; if (document.visibilityState === 'hidden') return; loadMarket({ force: true, silent: true }); }, MARKET_REFRESH_MS); }
function shortAddress(address) { if (!address || typeof address !== 'string') return ''; if (address.length <= 12) return address; return `${address.slice(0, 8)}...${address.slice(-4)}`; }
function toMutez(value) { const n = Number(value); return Number.isFinite(n) ? n : 0; }
async function fetchObjktGraphQL(query, retries = 2) { for (let attempt = 0; attempt <= retries; attempt++) { try { const res = await fetch('https://data.objkt.com/v3/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }), }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } const json = await res.json(); if (Array.isArray(json?.errors) && json.errors.length > 0) { const message = json.errors.map(err => err?.message || 'GraphQL error').join('; '); throw new Error(message || 'GraphQL error'); } return json?.data || {}; } catch (error) { if (attempt >= retries) throw error; await new Promise(resolve => setTimeout(resolve, 250 * (attempt + 1))); } } return {}; }
async function loadMarket({ force = false, silent = false } = {}) { if (marketLoaded && !force) { renderMarket(); return; } if (marketIsFetching) return;
await ensureKeepsConfig(); const contract = getKeepsContractAddress(); const grid = document.getElementById('market-grid'); const refreshBtn = document.getElementById('market-refresh-btn');
if (!contract) { if (grid) grid.innerHTML = '<div class="market-empty">Contract address not available.</div>'; setMarketLiveBadge('error', 'NO CONTRACT'); updateMarketLastUpdated(null); return; }
marketIsFetching = true; if (refreshBtn) refreshBtn.disabled = true; setMarketLiveBadge(marketLoaded ? 'stale' : 'live', marketLoaded ? 'SYNC' : 'LIVE'); if (!silent && grid && !marketLoaded) { grid.innerHTML = '<div class="market-empty">Loading Objkt feed…</div>'; }
try { const activeQuery = `query { listing_active( where: { fa_contract: { _eq: "${contract}" } } order_by: { timestamp: desc } limit: ${MARKET_FETCH_LIMIT} ) { id price_xtz timestamp seller_address token { token_id name thumbnail_uri } } }`;
// Objkt feed stream for this collection (how Objkt builds live market activity). const feedQuery = `query { event( where: { fa_contract: { _eq: "${contract}" } marketplace_event_type: { _in: ["list_create", "list_buy", "list_cancel"] } } order_by: { timestamp: desc } limit: ${MARKET_FETCH_LIMIT} ) { timestamp marketplace_event_type price_xtz creator_address recipient_address token { token_id name thumbnail_uri } } }`;
let activeData = {}; let feedData = {}; let activeError = null; let feedError = null;
try { activeData = await fetchObjktGraphQL(activeQuery); } catch (error) { activeError = error; console.warn('[market] listing_active query failed:', error?.message || error); }
try { feedData = await fetchObjktGraphQL(feedQuery); } catch (error) { feedError = error; console.warn('[market] event feed query failed:', error?.message || error); }
marketListings = Array.isArray(activeData?.listing_active) ? activeData.listing_active : []; marketFeedEvents = Array.isArray(feedData?.event) ? feedData.event : [];
const latestSales = []; const seenSaleToken = new Set(); for (const ev of marketFeedEvents) { if (ev?.marketplace_event_type !== 'list_buy') continue; const tokenId = String(ev?.token?.token_id ?? ''); if (!tokenId || seenSaleToken.has(tokenId)) continue; seenSaleToken.add(tokenId); latestSales.push({ timestamp: ev?.timestamp, price_xtz: ev?.price_xtz, buyer_address: ev?.recipient_address, seller_address: ev?.creator_address, token: ev?.token || {}, }); } marketSales = latestSales;
if (activeError && feedError) { throw activeError; }
marketLoaded = true; marketLastUpdatedAt = Date.now(); updateMarketLastUpdated(marketLastUpdatedAt); setMarketLiveBadge(activeError || feedError ? 'stale' : 'live', activeError || feedError ? 'LIVE*' : 'LIVE'); renderMarket(); } catch (e) { console.error('[market] error:', e); setMarketLiveBadge('error', 'ERROR'); if (!marketLoaded && grid) { grid.innerHTML = `<div class="market-empty">Failed to load market data: ${escapeHtml(e.message)}</div>`; } } finally { marketIsFetching = false; if (refreshBtn) refreshBtn.disabled = false; } }
function renderMarket() { const grid = document.getElementById('market-grid'); if (!grid) return; const contract = getKeepsContractAddress();
const activeTokenIds = new Set( marketListings .map(listing => String(listing?.token?.token_id ?? '')) .filter(Boolean) );
const soldByToken = []; const soldSeen = new Set(); for (const sale of marketSales) { const tokenId = String(sale?.token?.token_id ?? ''); if (!tokenId) continue; if (soldSeen.has(tokenId)) continue; if (activeTokenIds.has(tokenId)) continue; soldSeen.add(tokenId); soldByToken.push(sale); }
let sorted = [ ...marketListings.map(listing => ({ status: 'active', timestamp: listing?.timestamp, mutez: toMutez(listing?.price_xtz ?? listing?.amount), sellerAddress: listing?.seller_address, buyerAddress: null, token: listing?.token || {}, })), ...soldByToken.map(sale => ({ status: 'sold', timestamp: sale?.timestamp, mutez: toMutez(sale?.price_xtz ?? sale?.price ?? sale?.amount), sellerAddress: sale?.seller_address, buyerAddress: sale?.buyer_address, token: sale?.token || {}, })), ];
if (marketSort === 'price') { sorted.sort((a, b) => b.mutez - a.mutez); } else { sorted.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); }
if (sorted.length === 0) { detailSortedEntries = []; grid.innerHTML = '<div class="market-empty">No listings or sales found on objkt.com yet.</div>'; return; }
// Store for detail modal index-based lookup detailSortedEntries = sorted;
grid.innerHTML = sorted.map((entry, idx) => { const xtz = (entry.mutez / 1_000_000).toFixed(2); const token = entry.token || {}; const tokenId = token?.token_id ?? ''; const name = token?.name || `Token #${tokenId}`; const code = name.startsWith('$') ? name : `#${tokenId}`; const thumb = token?.thumbnail_uri ? token.thumbnail_uri.replace('ipfs://', 'https://ipfs.io/ipfs/') : ''; const seller = shortAddress(entry.sellerAddress); const buyer = shortAddress(entry.buyerAddress); const statusText = entry.status === 'sold' ? 'Sold' : 'For Sale'; const statusClass = entry.status === 'sold' ? 'sold' : 'active'; const sideText = entry.status === 'sold' ? `sold by ${seller}${buyer ? ` to ${buyer}` : ''}` : `listed by ${seller}`;
return `<div class="market-card ${entry.status === 'sold' ? 'sold' : ''}" onclick="openKeepDetailModal(detailSortedEntries[${idx}])" role="button" tabindex="0"> <div class="market-card-thumb">${thumb ? `<img src="${escapeHtml(thumb)}" loading="lazy" />` : ''}</div> <div class="market-card-body"> <span class="market-card-code">${escapeHtml(code)}</span> <span class="market-card-status ${statusClass}">${statusText}</span> <span class="market-card-price ${entry.status === 'sold' ? 'sold' : ''}">ꜩ${xtz}</span> ${seller ? `<span class="market-card-seller">${escapeHtml(sideText)}</span>` : ''} <span class="market-card-objkt">${entry.status === 'sold' ? 'view on objkt ↗' : 'buy on objkt ↗'}</span> </div> </div>`; }).join('');
// Handle pending deep-link after market renders if (pendingDeepLinkCode) { const code = pendingDeepLinkCode; pendingDeepLinkCode = null; openKeepDetailByCode(code); } }
document.querySelectorAll('.keeps-tab').forEach(tab => { tab.addEventListener('click', () => { switchTab(tab.dataset.tab, { pushState: true }); }); });
// Route from URL path (production) or hash (dev) on load function tabFromLocation() { const pageParam = new URLSearchParams(location.search).get('page'); const page = pageParam ? Math.max(0, parseInt(pageParam, 10) - 1) : 0; if (isKeepsDomain) { const parts = location.pathname.replace(/^\/+/, '').split('/'); const seg1 = parts[0] || ''; const seg2 = parts[1] || ''; // Redirect /buy and /market to buy.kidlisp.com if (seg1 === 'buy' || seg1 === 'market') { window.location.href = 'https://buy.kidlisp.com'; return { tab: 'unkept', code: null, page: 0 }; } // /kept/$code and /unkept/$code → gallery tab with mint modal open // for that piece. Distinct from the bare /$code market-detail permalink. if ((seg1 === 'kept' || seg1 === 'unkept') && seg2.startsWith('$') && seg2.length > 1) { return { tab: seg1, code: seg2.slice(1), page, mintOpen: true }; } if (seg1.startsWith('$') && seg1.length > 1) { return { tab: 'unkept', code: seg1.slice(1), page }; } const tab = seg1 === 'kept' ? 'kept' : 'unkept'; return { tab, code: null, page }; } else { const hash = location.hash.replace(/^#/, ''); if (hash === 'buy' || hash === 'market') { window.location.href = 'https://buy.kidlisp.com'; return { tab: 'unkept', code: null, page: 0 }; } const tab = hash === 'kept' ? 'kept' : 'unkept'; return { tab, code: null, page }; } } const initialRoute = tabFromLocation(); if (initialRoute.mintOpen && initialRoute.code) { pendingMintModalCode = initialRoute.code; } else if (initialRoute.code) { pendingDeepLinkCode = initialRoute.code; } switchTab(initialRoute.tab);
// Show wallet button immediately; check if already connected updateWalletBtn(); initTezosWallet().then(updateWalletBtn).catch(() => {});
// Handle browser back/forward window.addEventListener('popstate', () => { const route = tabFromLocation(); const mintOverlayEl = document.getElementById('mint-overlay'); const mintOpenNow = mintOverlayEl?.classList.contains('open'); // Close modals whose permalink context no longer matches the route. if (!route.code && detailOverlay.classList.contains('open')) { detailOverlay.classList.remove('open'); document.body.style.overflow = ''; } if (!route.mintOpen && mintOpenNow) { closeMintModal(); } switchTab(route.tab); // Open the correct modal for the new URL. if (route.mintOpen && route.code) { const match = allCodes.find(e => e.code === route.code); if (match) openMintModal(match); else pendingMintModalCode = route.code; } else if (route.code) { openKeepDetailByCode(route.code); } if (route.page > 0 && route.page < totalPages) { goToPage(route.page, { pushState: false }); } }); if (!isKeepsDomain) window.addEventListener('hashchange', () => { switchTab(tabFromLocation().tab); }); document.addEventListener('visibilitychange', () => { if (activeTab !== 'market') return; if (document.visibilityState === 'visible') { loadMarket({ force: true, silent: true }); } }); window.addEventListener('beforeunload', stopMarketLiveUpdates);
// Legacy version dropdown switching (if present in About content) const versionSelect = document.getElementById('version-select'); if (versionSelect) { versionSelect.addEventListener('change', () => { document.querySelectorAll('.keeps-version-docs').forEach(d => d.classList.remove('active')); const target = document.getElementById(`docs-${versionSelect.value}`); if (target) target.classList.add('active'); }); }
// Modal close (unified: mint overlay handles Escape + backdrop click) mintOverlay.addEventListener('click', e => { if (e.target === mintOverlay) closeMintModal(); }); detailOverlay.addEventListener('click', e => { if (e.target === detailOverlay) closeKeepDetailModal(); }); document.addEventListener('keydown', e => { if (e.key !== 'Escape') return; if (detailOverlay.classList.contains('open')) { closeKeepDetailModal(); return; } if (mintOverlay.classList.contains('open')) closeMintModal(); });
// ========================================= // UTILITIES // ========================================= function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
// Animated noise placeholder (oven-style) function createNoiseCanvas(width = 56, height = 42) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(width, height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const gray = Math.floor(Math.random() * 80) + 20; data[i] = gray; data[i + 1] = gray; data[i + 2] = gray + Math.floor(Math.random() * 20); data[i + 3] = 255; } ctx.putImageData(imageData, 0, 0); return { canvas, ctx, imageData }; }
function startNoiseAnimation(canvas, ctx, imageData, interval = 180) { const data = imageData.data; const id = setInterval(() => { for (let i = 0; i < data.length; i += 4) { const gray = Math.floor(Math.random() * 80) + 20; data[i] = gray; data[i + 1] = gray; data[i + 2] = gray + Math.floor(Math.random() * 20); data[i + 3] = 255; } ctx.putImageData(imageData, 0, 0); }, interval); return id; }
// Lightweight KidLisp syntax highlighter const KL_SPECIAL = new Set(['def', 'if', 'cond', 'later', 'once', 'lambda', 'let', 'do', 'repeat']); const KL_OPS = new Set(['+', '-', '*', '/', '%', 'mod', '=', '>', '<', '>=', '<=', 'abs', 'sqrt', 'min', 'max']); const KL_FNS = new Set([ 'line', 'box', 'circle', 'write', 'paste', 'stamp', 'ink', 'wipe', 'flood', 'scroll', 'zoom', 'blur', 'contrast', 'spin', 'draw', 'wiggle', 'pan', 'jump', 'grid', 'noise', 'sin', 'cos', 'rand', 'floor', 'ceil', 'round', 'log', 'list', 'nth', 'len', 'push', 'pop', 'map', 'filter', 'reduce', 'apply', 'print', 'type', 'str', 'num', 'not', 'and', 'or', 'true', 'false', 'pixel', 'plot', 'poly', 'ellipse', 'rect', 'tri', 'quad', 'arc', 'color', 'opacity', 'scale', 'rotate', 'translate', 'move', 'goto', 'sound', 'synth', 'wave', 'freq', 'vol', 'note', 'beat', 'bpm', 'load', 'save', 'fetch', 'delay', 'every', 'after', ]);
function highlightKidLisp(source) { // Tokenize using a regex that matches comments, strings, parens, whitespace, and words const tokenRegex = /(;[^\n]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[()[\],]|\s+|[^\s()[\],"';]+)/g; let result = ''; let match; while ((match = tokenRegex.exec(source)) !== null) { const tok = match[1]; if (tok.startsWith(';')) { result += `<span class="kl-comment">${escapeHtml(tok)}</span>`; } else if (tok.startsWith('"') || tok.startsWith("'")) { result += `<span class="kl-string">${escapeHtml(tok)}</span>`; } else if (/^\s+$/.test(tok)) { result += tok; } else if (tok === '(' || tok === ')' || tok === '[' || tok === ']' || tok === ',') { result += `<span class="kl-paren">${tok}</span>`; } else if (/^-?\d+(\.\d+)?$/.test(tok) || /^\d*\.?\d+[sf]!?$/.test(tok) || /^\d*\.?\d+[sf]\.\.\.?$/.test(tok)) { result += `<span class="kl-number">${escapeHtml(tok)}</span>`; } else if (/^[$#!][0-9A-Za-z]+$/.test(tok)) { result += `<span class="kl-code-ref">${escapeHtml(tok)}</span>`; } else if (KL_SPECIAL.has(tok)) { result += `<span class="kl-special">${escapeHtml(tok)}</span>`; } else if (KL_OPS.has(tok)) { result += `<span class="kl-op">${escapeHtml(tok)}</span>`; } else if (KL_FNS.has(tok)) { result += `<span class="kl-fn">${escapeHtml(tok)}</span>`; } else { result += `<span class="kl-var">${escapeHtml(tok)}</span>`; } } return result; }
function formatRelativeTime(date) { const now = Date.now(); const diff = now - date.getTime(); const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const months = Math.floor(days / 30); const years = Math.floor(days / 365);
if (seconds < 60) return 'just now'; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; if (days < 30) return `${days}d ago`; if (months < 12) return `${months}mo ago`; return `${years}y ago`; }
function formatDateForInput(date) { return new Date(date).toISOString().slice(0, 10); }
function ipfsToHttp(uri) { if (!uri || typeof uri !== 'string') return null; const value = uri.trim(); if (!value) return null; if (value.startsWith('ipfs://')) { return `https://ipfs.aesthetic.computer/ipfs/${value.slice(7)}`; } return value; }
function ovenPreviewUrlForPiece(piece, size = 320) { const cleanPiece = String(piece || '').replace(/^\$+/, '').trim(); if (!cleanPiece) return ''; return `https://oven.aesthetic.computer/grab/png/${size}/${size}/$${cleanPiece}?density=2&nowait=true&source=keep`; }
function formatPackedDateLabel(value) { if (!value) return null; const asDate = new Date(value); if (!Number.isNaN(asDate.getTime())) { return asDate.toLocaleDateString(); } if (typeof value === 'string' && value.trim()) { return value.trim(); } return null; }
function classifyBundleRuntimeSource(sourceText) { const source = String(sourceText || ''); if (!source) return 'unknown';
const hasFixedSignature = source.includes('acUseWebGLComposite=!se') || source.includes('acUseWebGLComposite=!isOpaqueOrigin') || source.includes('acUseWebGLComposite = !isOpaqueOrigin');
const hasLegacySignature = source.includes('acUseWebGLComposite=!0') || source.includes('acUseWebGLComposite=true') || source.includes('acUseWebGLComposite = true');
if (hasFixedSignature) return 'modern'; if (hasLegacySignature) return 'legacy'; return 'unknown'; }
async function decodeGzipBase64ToText(gzipBase64) { if (typeof gzipBase64 !== 'string' || !gzipBase64) return null; if (typeof DecompressionStream !== 'function') return null;
const binary = atob(gzipBase64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); }
const decompressed = new Blob([bytes], { type: 'application/gzip' }) .stream() .pipeThrough(new DecompressionStream('gzip')); return await new Response(decompressed).text(); }
async function inspectLegacyRuntime(artifactUri) { const key = (artifactUri || '').trim(); if (!key) { return { status: 'unknown', reason: 'missing-artifact-uri', checkedAt: Date.now() }; }
if (legacyRuntimeCache.has(key)) return legacyRuntimeCache.get(key); if (legacyRuntimeInFlight.has(key)) return legacyRuntimeInFlight.get(key);
const inspectPromise = (async () => { try { const artifactUrl = ipfsToHttp(key); if (!artifactUrl) { return { status: 'unknown', reason: 'invalid-artifact-uri', checkedAt: Date.now() }; }
const response = await fetch(artifactUrl, { cache: 'no-store' }); if (!response.ok) { return { status: 'error', reason: `http-${response.status}`, checkedAt: Date.now() }; }
const outerHtml = await response.text(); const outerStatus = classifyBundleRuntimeSource(outerHtml); if (outerStatus !== 'unknown') { return { status: outerStatus, reason: 'outer-signature', checkedAt: Date.now() }; }
const b64Match = outerHtml.match(/const\s+b64\s*=\s*['"]([^'"]+)['"]/); if (!b64Match) { return { status: 'unknown', reason: 'no-gzip-payload', checkedAt: Date.now() }; }
const innerHtml = await decodeGzipBase64ToText(b64Match[1]); if (!innerHtml) { return { status: 'unknown', reason: 'gzip-inspection-unavailable', checkedAt: Date.now() }; }
const innerStatus = classifyBundleRuntimeSource(innerHtml); return { status: innerStatus, reason: 'inner-signature', checkedAt: Date.now() }; } catch (error) { return { status: 'error', reason: error?.message || 'inspection-failed', checkedAt: Date.now() }; } })();
legacyRuntimeInFlight.set(key, inspectPromise); try { const result = await inspectPromise; legacyRuntimeCache.set(key, result); return result; } finally { legacyRuntimeInFlight.delete(key); } }
function renderLegacyRuntimeNote(noteEl, result) { if (!noteEl) return; noteEl.classList.remove('legacy', 'modern');
if (!result || result.status === 'unknown') { noteEl.textContent = 'Bundle runtime check unavailable.'; return; } if (result.status === 'error') { noteEl.textContent = 'Bundle runtime check failed. You can still run upgrade.'; return; } if (result.status === 'legacy') { noteEl.classList.add('legacy'); noteEl.textContent = 'Legacy bundle runtime detected (built before March 10, 2026 sandbox WebGL fix). Upgrade recommended.'; return; } noteEl.classList.add('modern'); noteEl.textContent = 'Bundle runtime is current (sandbox-safe WebGL fallback present).'; }
async function ensureMintLegacyRuntimeStatus(entry, options = {}) { if (!entry) return; const activeKeep = getActiveKeepRecord(entry); if (!activeKeep?.artifactUri) return;
const cacheHit = legacyRuntimeCache.get((activeKeep.artifactUri || '').trim()); if (cacheHit) return;
const requestNonce = ++mintLegacyCheckNonce; await inspectLegacyRuntime(activeKeep.artifactUri); if (requestNonce !== mintLegacyCheckNonce) return; if (!document.getElementById('mint-overlay')?.classList.contains('open')) return; if (!mintCurrentEntry || mintCurrentEntry.code !== entry.code) return; renderMintPieceInfo(entry, options); }
function buildMintHeaderLinkGroup(label, uris = {}, accent = false) { if (!uris) return ''; const htmlUrl = ipfsToHttp(uris.artifactUri); const thumbUrl = ipfsToHttp(uris.thumbnailUri); const metaUrl = ipfsToHttp(uris.metadataUri); if (!htmlUrl && !thumbUrl && !metaUrl) return ''; const color = accent ? 'var(--ac-purple)' : 'var(--text-secondary)'; return ` <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:11px;"> <span style="color:${color};font-weight:700;">${escapeHtml(label)}:</span> ${htmlUrl ? `<a href="${htmlUrl}" target="_blank" rel="noopener" style="color:${color};">HTML ↗</a>` : ''} ${thumbUrl ? `<a href="${thumbUrl}" target="_blank" rel="noopener" style="color:${color};">THUMB ↗</a>` : ''} ${metaUrl ? `<a href="${metaUrl}" target="_blank" rel="noopener" style="color:${color};">META ↗</a>` : ''} </div> `; }
function renderMintPieceInfo(entry, { rebake = false, regenerate = false } = {}) { const infoEl = document.getElementById('mint-piece-info'); if (!infoEl) return; const activeKeep = getActiveKeepRecord(entry); const legacyStatus = activeKeep?.artifactUri ? legacyRuntimeCache.get((activeKeep.artifactUri || '').trim()) : null; let modeLabel = rebake ? 'Regenerate HTML + thumbnail for your existing keep. Connect your wallet to verify ownership.' : (regenerate ? 'Regenerate HTML + thumbnail artifacts before keeping on Tezos.' : 'Keep on Tezos (2.5 XTZ fee).'); if (rebake && mintAutoSyncAfterRebake) { modeLabel = 'Upgrade mode: regenerate media, then sync metadata on-chain in one flow.'; }
const onChainGroup = buildMintHeaderLinkGroup( activeKeep ? `On-chain${activeKeep.tokenId != null ? ` #${activeKeep.tokenId}` : ''}` : 'On-chain', mintOnChainUris ); const latestGroup = buildMintHeaderLinkGroup( rebake ? 'Latest Regenerated (not on-chain)' : 'Latest Prepared', mintLatestUris, true );
let legacyNotice = ''; if (legacyStatus?.status === 'legacy') { legacyNotice = `<div class="mint-legacy-warning">Legacy bundle runtime detected (before March 10, 2026 fix). This can black-screen in strict sandboxed embeds. Use Upgrade mode to rebake and sync on-chain metadata.</div>`; } else if (legacyStatus?.status === 'modern' && rebake) { legacyNotice = `<div class="mint-legacy-warning modern">Bundle runtime is current and includes sandbox-safe WebGL handling.</div>`; }
infoEl.innerHTML = ` <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;"> <strong>$${escapeHtml(entry.code)}</strong> <span style="color:var(--text-tertiary);font-size:11px;">by ${escapeHtml(entry.handle || 'anon')}</span> <a href="${KL_BASE}/$${escapeHtml(entry.code)}" target="_blank" rel="noopener" style="font-size:11px;color:var(--ac-purple);margin-left:auto;">Edit ↗</a> </div> <span style="font-size:11px; color:var(--text-tertiary);">${modeLabel}</span> ${onChainGroup} ${latestGroup} ${legacyNotice} `; }
function renderMintArtifactPanel({ title = 'Artifact Preview', piece = mintCurrentPiece, artifactUri = null, thumbnailUri = null, metadataUri = null, packDate = null, objktUrl = null, showRebakeButton = false, rebakeButtonLabel = 'Rekeep', showSyncButton = false, syncButtonData = null, append = false, } = {}) { const area = document.getElementById('mint-result-area'); if (!area) return;
const thumbUrl = ipfsToHttp(thumbnailUri) || ovenPreviewUrlForPiece(piece, 320); const htmlUrl = ipfsToHttp(artifactUri); const metaUrl = ipfsToHttp(metadataUri); const packedLabel = formatPackedDateLabel(packDate);
let links = ''; if (htmlUrl) links += `<a class="mint-link-btn" href="${htmlUrl}" target="_blank" rel="noopener">HTML</a>`; if (thumbUrl) links += `<a class="mint-link-btn" href="${thumbUrl}" target="_blank" rel="noopener">THUMB</a>`; if (metaUrl) links += `<a class="mint-link-btn" href="${metaUrl}" target="_blank" rel="noopener">META</a>`; if (objktUrl) links += `<a class="mint-link-btn" href="${objktUrl}" target="_blank" rel="noopener">OBJKT</a>`; if (showRebakeButton) { links += `<button class="mint-link-btn rebake" type="button" id="mint-inline-rebake">${escapeHtml(rebakeButtonLabel)}</button>`; } if (showSyncButton) { links += `<button class="mint-link-btn sync-onchain" type="button" id="mint-inline-sync">Sync On-chain</button>`; }
const panelHtml = ` <div class="mint-artifacts"> <div class="mint-artifact-head"> <span class="mint-artifact-title">${escapeHtml(title)}</span> ${packedLabel ? `<span>Packed ${escapeHtml(packedLabel)}</span>` : ''} </div> <div class="mint-artifact-media"> <div class="mint-artifact-preview"> <img src="${thumbUrl}" alt="Keep preview for $${escapeHtml(piece || '')}" loading="lazy" /> </div> ${htmlUrl ? `<div class="mint-artifact-iframe"><iframe src="${htmlUrl}" sandbox="allow-scripts allow-same-origin" loading="lazy" title="$${escapeHtml(piece || '')} artifact"></iframe></div>` : ''} </div> <div class="mint-artifact-links">${links}</div> </div> `; if (!append) { area.querySelectorAll('.mint-artifacts').forEach(panel => panel.remove()); } if (append || area.innerHTML.trim()) { area.insertAdjacentHTML('beforeend', panelHtml); } else { area.innerHTML = panelHtml; }
const inlineRebake = document.getElementById('mint-inline-rebake'); if (inlineRebake && mintCurrentEntry) { inlineRebake.addEventListener('click', () => { rerunRegenerate(); }); }
const inlineSync = document.getElementById('mint-inline-sync'); if (inlineSync && syncButtonData && mintCurrentEntry) { inlineSync.addEventListener('click', async () => { inlineSync.disabled = true; inlineSync.textContent = 'Syncing...'; try { const syncResult = await syncRebakeMetadataOnChain(syncButtonData); applySyncResultToCurrentEntry(syncResult, syncButtonData.tokenId);
const txShort = syncResult.txHash ? syncResult.txHash.slice(0, 8) + '...' : 'Signed'; setMintStep('sign', 'done', txShort); setMintStep('complete', 'done', 'Synced on-chain'); addTrackEntry('On-chain metadata synced', 'done');
// Re-render with synced artifacts const syncContract = syncResult.contractAddress || syncButtonData.contractAddress || getKeepsContractAddress(); const syncNetwork = syncResult.network || syncButtonData.network || getTezosNetwork(); const syncedObjktUrl = syncButtonData.tokenId != null && syncContract ? `${getObjktBase(syncNetwork)}/tokens/${syncContract}/${syncButtonData.tokenId}` : null;
const syncedArtifactUrl = ipfsToHttp(syncResult.artifactUri); const previewFrameEl = document.getElementById('mint-preview-frame'); if (previewFrameEl && syncedArtifactUrl) { previewFrameEl.innerHTML = `<iframe src="${syncedArtifactUrl}" sandbox="allow-scripts allow-same-origin" loading="lazy" title="$${escapeHtml(piece)} synced"></iframe>`; }
showMintResult('Metadata synced on-chain.', syncedObjktUrl, syncResult.txHash); renderMintArtifactPanel({ title: 'Synced Artifacts', piece, artifactUri: syncResult.artifactUri, thumbnailUri: syncResult.thumbnailUri, metadataUri: syncResult.metadataUri, packDate: syncButtonData.packDate, objktUrl: syncedObjktUrl, showRebakeButton: true, rebakeButtonLabel: 'Regenerate Again', }); } catch (syncErr) { inlineSync.disabled = false; inlineSync.textContent = 'Sync On-chain'; const msg = syncErr?.message || 'On-chain sync failed'; setMintStep('sign', 'error', msg); addTrackEntry(`On-chain sync failed: ${msg}`, 'error'); const area = document.getElementById('mint-result-area'); if (area) { area.insertAdjacentHTML('beforeend', `<div class="mint-error-msg">On-chain sync failed: ${escapeHtml(msg)}</div>` ); } } }); } }
let keepsStreamModulePromise = null;
async function getKeepsStreamModule() { if (!keepsStreamModulePromise) { keepsStreamModulePromise = (async () => { const candidates = [ '/aesthetic.computer/lib/keeps-stream.mjs', '../aesthetic.computer/lib/keeps-stream.mjs', `${AC_BASE}/aesthetic.computer/lib/keeps-stream.mjs`, ];
let lastError = null; for (const specifier of candidates) { try { return await import(specifier); } catch (err) { lastError = err; } }
throw lastError || new Error('Unable to load keeps stream module'); })(); }
try { return await keepsStreamModulePromise; } catch (err) { keepsStreamModulePromise = null; throw err; } }
function appendV6SimEvent(message) { const log = document.getElementById('v6-sim-events'); if (!log) return; const stamp = new Date().toLocaleTimeString('en-US', { hour12: false }); log.textContent += `[${stamp}] ${message}\n`; log.scrollTop = log.scrollHeight; }
function setV6SimStatus(message, tone = 'idle') { const status = document.getElementById('v6-sim-status'); if (!status) return; status.textContent = message; status.classList.remove('running', 'ok', 'error'); if (tone !== 'idle') status.classList.add(tone); }
let v6SimController = null;
async function runV6EndpointSimulation() { const runBtn = document.getElementById('v6-sim-run'); const output = document.getElementById('v6-sim-output'); const log = document.getElementById('v6-sim-events'); const endpoint = document.getElementById('v6-sim-endpoint'); if (!runBtn || !output || !log) return;
const pieceInput = document.getElementById('v6-sim-piece'); const walletInput = document.getElementById('v6-sim-wallet'); const handleInput = document.getElementById('v6-sim-handle'); const packedOnInput = document.getElementById('v6-sim-packed-on'); const sourceInput = document.getElementById('v6-sim-source');
const piece = (pieceInput?.value || '').replace(/^\$+/, '').trim() || 'sim-v9'; const walletAddress = (walletInput?.value || '').trim(); const handle = (handleInput?.value || '').trim(); const packedOn = (packedOnInput?.value || '').trim(); const source = sourceInput?.value || '';
if (v6SimController) { v6SimController.abort(); } v6SimController = new AbortController();
output.textContent = ''; log.textContent = ''; runBtn.disabled = true; setV6SimStatus('Running simulator...', 'running'); if (endpoint) endpoint.textContent = `Endpoint: ${(API_BASE || '') + '/api/keep-mint'} (mode=simulate)`;
const payload = { mode: 'simulate', piece, walletAddress, handle, packedOn, }; if (source.trim()) payload.source = source;
appendV6SimEvent(`POST /api/keep-mint mode=simulate piece=$${piece}`);
try { const { postKeepMintStream } = await getKeepsStreamModule(); let sawFinalPayload = false;
await postKeepMintStream({ apiBase: API_BASE, payload, signal: v6SimController.signal, onEvent: async ({ type, data }) => { if (type === 'progress') { const stage = data?.stage ? `[${data.stage}] ` : ''; const message = data?.message || '(no message)'; appendV6SimEvent(`${stage}${message}`); return true; }
if (type === 'simulated' || type === 'prepared' || type === 'complete') { sawFinalPayload = true; appendV6SimEvent(`${type}: payload received`); output.textContent = JSON.stringify(data, null, 2); setV6SimStatus('Simulation complete', 'ok'); return true; }
if (type === 'error') { const errMsg = data?.error || 'Unknown simulator error'; appendV6SimEvent(`ERROR: ${errMsg}`); output.textContent = JSON.stringify(data, null, 2); setV6SimStatus('Simulation failed', 'error'); return false; }
appendV6SimEvent(`${type}: ${typeof data === 'string' ? data : JSON.stringify(data)}`); return true; }, });
if (!sawFinalPayload) { setV6SimStatus('Simulation stream ended without final payload', 'error'); } } catch (err) { const message = err?.name === 'AbortError' ? 'Simulation cancelled' : (err?.message || 'Simulation failed'); appendV6SimEvent(`ERROR: ${message}`); setV6SimStatus(message, 'error'); } finally { runBtn.disabled = false; } }
function randomizeV6SimulationInput() { const examples = [ { piece: 'ac25namuc', handle: '@jeffrey', wallet: 'tz1Lc2DzTjDPyWFj1iuAVGGZWNjK67Wun2dC', source: 'black\nink rainbow 127\nrepeat 100 point\nzoom 1.05\nspin 1\nscroll 20', }, { piece: 'notepat', handle: '@bash', wallet: 'tz1dfoQDuxjwSgxdqJnisyKUxDHweade4Gzt', source: '(wipe)\n(color 36 204 167)\n(write "notepat")\n(line 0 0 127 127)', }, { piece: 'cow', handle: '@ac', wallet: 'tz1gkf8EexComFBJvjtT1zdsisdah791KwBE', source: '(wipe)\n(write "cow")\n(circle 64 64 22)\n(box 16 16 96 96)', }, ];
const sample = examples[Math.floor(Math.random() * examples.length)]; const pieceInput = document.getElementById('v6-sim-piece'); const walletInput = document.getElementById('v6-sim-wallet'); const handleInput = document.getElementById('v6-sim-handle'); const packedOnInput = document.getElementById('v6-sim-packed-on'); const sourceInput = document.getElementById('v6-sim-source');
if (pieceInput) pieceInput.value = sample.piece; if (walletInput) walletInput.value = sample.wallet; if (handleInput) handleInput.value = sample.handle; if (packedOnInput) packedOnInput.value = formatDateForInput(Date.now()); if (sourceInput) sourceInput.value = sample.source; setV6SimStatus('Example loaded', 'idle'); }
function initV6LaunchSimulation() { const runBtn = document.getElementById('v6-sim-run'); const randomizeBtn = document.getElementById('v6-sim-randomize'); if (!runBtn) return;
const packedOnInput = document.getElementById('v6-sim-packed-on'); if (packedOnInput && !packedOnInput.value) { packedOnInput.value = formatDateForInput(Date.now()); }
runBtn.addEventListener('click', runV6EndpointSimulation); if (randomizeBtn) randomizeBtn.addEventListener('click', randomizeV6SimulationInput); }
function updateV6RoadmapStatus() { const launchBadge = document.getElementById('v6-launch-badge'); if (launchBadge) { launchBadge.textContent = 'V7 PAUSED • V8 READY'; } }
// ========================================= // AUTH0 AUTHENTICATION // ========================================= async function initAuth0() { try { if (!window.auth0) return; const isKeepsDomain = window.location.hostname === 'keep.kidlisp.com'; const redirectUri = isKeepsDomain ? window.location.origin + '/' : window.location.origin + window.location.pathname; 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 }); // Handle redirect callback if (window.location.search.includes('code=') || window.location.search.includes('error=')) { try { await acAuth0Client.handleRedirectCallback(); window.history.replaceState({}, document.title, window.location.pathname); } catch (e) { /* 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(`${AC_BASE}/user?from=${encodeURIComponent(acUser.email)}&withHandle=true&withTezos=true`); const userData = await userRes.json(); if (userData.handle) acHandle = String(userData.handle).replace(/^@+/, '').trim(); if (userData.tezos?.address) linkedWalletAddress = userData.tezos.address; } catch (e) { console.warn('[keeps] Could not fetch handle:', e); } } updateAuthUI(); } catch (e) { console.error('Auth0 init error:', e); updateAuthUI(); } }
function updateAuthUI() { const loginBtn = document.getElementById('keeps-login-btn'); const signupBtn = document.getElementById('keeps-signup-btn'); const userMenu = document.getElementById('keeps-user-menu'); const handleEl = document.getElementById('keeps-auth-handle'); const tabsEl = document.querySelector('.keeps-tabs'); if (tabsEl) { tabsEl.classList.toggle('logged-out', !acUser); } if (acUser && hasRegisteredHandle()) { loginBtn.style.display = 'none'; if (signupBtn) signupBtn.style.display = 'none'; handleEl.textContent = `@${acHandle}`; userMenu.style.display = 'flex'; } else if (acUser) { loginBtn.style.display = 'none'; if (signupBtn) signupBtn.style.display = 'none'; handleEl.textContent = isDev ? (acUser.email || 'No handle') : 'Handle required'; userMenu.style.display = 'flex'; } else { loginBtn.style.display = 'inline-flex'; if (signupBtn) signupBtn.style.display = 'inline-flex'; userMenu.style.display = 'none'; } updateWalletBtn(); }
function toggleUserMenu() { const menu = document.getElementById('keeps-user-menu'); const dropdown = document.getElementById('keeps-user-dropdown'); if (!menu || !dropdown) return; const opening = !menu.classList.contains('open'); menu.classList.toggle('open', opening); if (opening) { // Position dropdown below the menu button const rect = menu.getBoundingClientRect(); dropdown.style.top = (rect.bottom + 2) + 'px'; dropdown.style.left = rect.left + 'px'; // Close on outside click const close = (e) => { if (!menu.contains(e.target)) { menu.classList.remove('open'); document.removeEventListener('click', close, true); } }; setTimeout(() => document.addEventListener('click', close, true), 0); } }
async function keepsLogin() { if (!acAuth0Client) await initAuth0(); if (!acAuth0Client) return; await acAuth0Client.loginWithRedirect({ authorizationParams: { prompt: 'login' } }); }
async function keepsImNew() { if (!acAuth0Client) await initAuth0(); if (!acAuth0Client) return; await acAuth0Client.loginWithRedirect({ authorizationParams: { prompt: 'login', screen_hint: 'signup' } }); }
async function keepsLogout() { if (!acAuth0Client) return; acUser = null; acToken = null; acHandle = null; const isKeepsDomain = window.location.hostname === 'keep.kidlisp.com'; const returnTo = isKeepsDomain ? window.location.origin + '/' : window.location.origin + window.location.pathname; await acAuth0Client.logout({ logoutParams: { returnTo } }); }
// ========================================= // TEZOS WALLET (Beacon SDK via CDN) // ========================================= let beaconClient = null; let beaconInitPromise = null;
async function loadBeaconSDK() { if (window.beacon) return; // Beacon SDK v4.x tries Object.defineProperty(window, 'ethereum', ...) // which throws if a browser extension (MetaMask etc.) already defined it // as non-configurable. Patch defineProperty to silently skip that case. // Beacon SDK v4.x calls Object.defineProperty(window, 'ethereum', ...) // both at load time AND later during init/connect. If a browser extension // (MetaMask etc.) already defined it as non-configurable, this throws. // Keep the patch active permanently since Beacon re-attempts it. const origDefProp = Object.defineProperty; Object.defineProperty = function(obj, prop, desc) { if (obj === window && prop === 'ethereum') { try { return origDefProp.call(this, obj, prop, desc); } catch { return obj; } } return origDefProp.call(this, obj, prop, desc); }; await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://unpkg.com/@airgap/beacon-sdk@4.8.1/dist/walletbeacon.min.js'; script.onload = () => resolve(); script.onerror = (e) => { Object.defineProperty = origDefProp; reject(e); }; document.head.appendChild(script); }); }
// Pre-initialize the Beacon client so it's ready before the user clicks. // This prevents popup blockers from killing the Kukai tab — the trusted // click context must survive all the way to requestPermissions(). async function initTezosWallet() { if (WALLET_VIA_PARENT) { // No local Beacon client when embedded — seed the address from the // parent's global wallet so the button reflects an existing connection. try { const r = await parentWalletRPC('getActiveAccount', { network: getTezosNetwork() }); if (r?.address) { mintWalletAddress = r.address; updateWalletBtn(); syncWalletToProfile(r.address); } } catch (e) { /* parent wallet not ready yet */ } return; } if (beaconClient) return; if (beaconInitPromise) return beaconInitPromise; beaconInitPromise = (async () => { // One-time clear of stale Beacon state from old app name if (!localStorage.getItem('beacon:kidlisp-v3')) { for (const key of Object.keys(localStorage)) { if (key.startsWith('beacon:')) localStorage.removeItem(key); } localStorage.setItem('beacon:kidlisp-v3', '1'); } await loadBeaconSDK(); const net = getTezosNetwork(); beaconClient = new window.beacon.DAppClient({ name: 'KidLisp', network: { type: net === 'ghostnet' ? window.beacon.NetworkType.GHOSTNET : window.beacon.NetworkType.MAINNET, }, walletConnectOptions: { projectId: '647c41004744244efab53b7c6ef80443', }, }); // Subscribe to active account changes before getActiveAccount fires it beaconClient.subscribeToEvent(window.beacon.BeaconEvent.ACTIVE_ACCOUNT_SET, (account) => { if (account) { mintWalletAddress = account.address; updateWalletBtn(); } }); // Check existing connection — sync to profile if logged in const activeAccount = await beaconClient.getActiveAccount(); if (activeAccount) { mintWalletAddress = activeAccount.address; updateWalletBtn(); syncWalletToProfile(activeAccount.address); } })(); return beaconInitPromise; }
async function connectWallet() { if (WALLET_VIA_PARENT) { // Delegate to the parent's global Beacon client (full-window modal). const r = await parentWalletRPC('connect', { network: getTezosNetwork() }); mintWalletAddress = r?.address || null; updateWalletBtn(); if (mintWalletAddress) await syncWalletToProfile(mintWalletAddress); return mintWalletAddress; } // initTezosWallet should already be resolved (pre-loaded at page init). // If not, this await is the fallback — but may cause popup blocking. await initTezosWallet(); console.log('🔑 KEEP-FLOW connectWallet()', { cachedMintWalletAddress: mintWalletAddress }); // Verify the session is still active (Kukai/WalletConnect sessions can expire) if (mintWalletAddress) { const active = await beaconClient.getActiveAccount(); console.log('🔑 KEEP-FLOW connectWallet: beacon active account', { active: active?.address }); if (active?.address === mintWalletAddress) return mintWalletAddress; // Session expired — clear stale address and re-request mintWalletAddress = null; updateWalletBtn(); } const perms = await beaconClient.requestPermissions(); mintWalletAddress = perms.address; console.log('🔑 KEEP-FLOW connectWallet: new perms', { address: mintWalletAddress }); updateWalletBtn(); // Sync wallet address to user profile so keep-prepare can validate it await syncWalletToProfile(mintWalletAddress); return mintWalletAddress; }
// Sign + broadcast an operation. Embedded → the parent's global Beacon // client (one connection, full-window modal); standalone → local client. async function requestWalletOperation(params) { if (WALLET_VIA_PARENT) { const r = await parentWalletRPC('requestOperation', { network: getTezosNetwork(), operation: params }); return { transactionHash: r?.transactionHash || null }; } if (!beaconClient) await initTezosWallet(); return beaconClient.requestOperation(params); }
// Save connected wallet address to user's AC profile (MongoDB) async function syncWalletToProfile(address) { console.log('🔑 KEEP-FLOW syncWalletToProfile entry', { address, hasAcToken: !!acToken, acTokenLen: acToken ? acToken.length : 0 }); if (!address || !acToken) { console.warn('🔑 KEEP-FLOW syncWalletToProfile SKIPPED', { reason: !address ? 'no address' : 'no acToken' }); return; } try { const net = getTezosNetwork(); const res = await fetch(`${API_BASE}/api/update-tezos-address`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${acToken}`, }, body: JSON.stringify({ address, network: net }), }); console.log('🔑 KEEP-FLOW syncWalletToProfile response', { status: res.status, ok: res.ok }); if (res.ok) linkedWalletAddress = address; } catch (e) { console.warn('🔑 KEEP-FLOW syncWalletToProfile failed:', e); } }
// ========================================= // MINT FLOW // ========================================= const MINT_STEPS = [ { id: 'wallet', label: 'Connect Wallet' }, { id: 'validate', label: 'Validate Piece' }, { id: 'analyze', label: 'Analyze Source' }, { id: 'thumbnail', label: 'Bake Thumbnail' }, { id: 'bundle', label: 'Pack HTML Bundle' }, { id: 'ipfs', label: 'Pin to IPFS' }, { id: 'metadata', label: 'Build Metadata' }, { id: 'review', label: 'Review & Pay' }, { id: 'sign', label: 'Sign Transaction' }, { id: 'complete', label: 'Keep Complete!' }, ];
let mintStepStates = {};
function renderTimeline() { // Create progress overlay inside the preview frame const pf = document.getElementById('mint-preview-frame'); if (pf && !pf.querySelector('.mint-progress-overlay')) { const overlay = document.createElement('div'); overlay.className = 'mint-progress-overlay'; overlay.id = 'mint-progress-overlay'; overlay.innerHTML = ` <div class="mint-progress-stage" id="mint-progress-stage"> <span class="step-label">Preparing...</span> <span class="step-detail"></span> </div> <div class="mint-progress-bar"> <div class="mint-progress-bar-fill" id="mint-progress-bar-fill"></div> </div>`; pf.appendChild(overlay); } }
function setMintStep(id, status, detail) { mintStepStates[id] = { status, detail: detail || '' };
// Update progress overlay const stageEl = document.getElementById('mint-progress-stage'); const barFill = document.getElementById('mint-progress-bar-fill'); const overlay = document.getElementById('mint-progress-overlay'); if (!stageEl || !barFill) return;
// Find step info const stepIndex = MINT_STEPS.findIndex(s => s.id === id); const stepInfo = MINT_STEPS[stepIndex]; if (!stepInfo) return;
// Calculate progress % const totalSteps = MINT_STEPS.length; let pct; if (status === 'done') { pct = Math.round(((stepIndex + 1) / totalSteps) * 100); } else if (status === 'active') { pct = Math.round(((stepIndex + 0.5) / totalSteps) * 100); } else if (status === 'error') { pct = Math.round(((stepIndex + 0.5) / totalSteps) * 100); } else { pct = Math.round((stepIndex / totalSteps) * 100); }
// Update label const label = stepInfo.label; const detailText = detail ? detail : ''; stageEl.className = `mint-progress-stage ${status}`; stageEl.innerHTML = `<span class="step-label">${label}</span>${detailText ? `<span class="step-detail">${detailText}</span>` : ''}`;
// Update bar barFill.className = `mint-progress-bar-fill ${status === 'error' ? 'error' : (status === 'done' && id === 'complete' ? 'done' : '')}`; barFill.style.width = `${pct}%`;
// Hide overlay when complete if (overlay && id === 'complete' && status === 'done') { setTimeout(() => overlay.classList.add('hidden'), 1500); } }
function openMintModal(entry, options = {}) { // Auto-detect rebake: if already kept on active contract and not explicitly keeping const isActiveKept = isKeptOnActiveContract(entry); let rebake = options.rebake === true; let regenerate = options.regenerate === true && !rebake;
// If already kept and no explicit mode, default to rebake if (isActiveKept && !rebake && !regenerate) { rebake = true; } const autoSync = options.autoSync === true && rebake; mintAutoSyncAfterRebake = autoSync;
if (mintAbortController) { mintAbortController.abort(); mintAbortController = null; } mintCurrentPiece = entry.code; mintCurrentEntry = entry; mintPreparedData = null; mintStepStates = {}; mintLatestUris = null;
const activeKeep = getActiveKeepRecord(entry); const tokenLabel = activeKeep?.tokenId != null ? ` #${activeKeep.tokenId}` : ''; mintOnChainUris = activeKeep ? { artifactUri: activeKeep.artifactUri || null, thumbnailUri: activeKeep.thumbnailUri || null, metadataUri: activeKeep.metadataUri || null, } : null;
const existingPending = entry?.pendingRebake && typeof entry.pendingRebake === 'object' ? entry.pendingRebake : null; if (existingPending) { mintLatestUris = { artifactUri: existingPending.artifactUri || null, thumbnailUri: existingPending.thumbnailUri || null, metadataUri: existingPending.metadataUri || null, }; }
document.getElementById('mint-title').textContent = rebake ? `Regenerate $${entry.code}${tokenLabel}` : (regenerate ? `Regenerate $${entry.code}` : `Keep $${entry.code}`); renderMintPieceInfo(entry, { rebake, regenerate }); ensureMintLegacyRuntimeStatus(entry, { rebake, regenerate }); // Live preview iframe const previewFrame = document.getElementById('mint-preview-frame'); const pieceUrl = `${AC_BASE}/$${encodeURIComponent(entry.code)}?nolabel=true&nogap=true`; previewFrame.innerHTML = `<iframe src="${pieceUrl}" sandbox="allow-scripts allow-same-origin" loading="lazy"></iframe>`;
document.getElementById('mint-result-area').innerHTML = ''; document.getElementById('mint-actions').style.display = 'none'; document.getElementById('mint-confirm-btn').disabled = true; document.getElementById('mint-confirm-btn').textContent = 'Confirm & Sign'; document.getElementById('mint-timeline').innerHTML = '';
// Update start button label based on mode const startBtn = document.getElementById('mint-start-btn'); if (startBtn) { startBtn.innerHTML = autoSync ? 'Upgrade Legacy Bundle' : (rebake ? 'Regenerate Media' : 'Keep on Tezos ꜩ'); startBtn.onclick = () => { const startActions = document.getElementById('mint-actions-start'); if (startActions) startActions.style.display = 'none'; renderTimeline(); startMintFlow(mintCurrentPiece, { rebake, regenerate, autoSync }); }; }
// Show start button — user clicks to begin const startActions = document.getElementById('mint-actions-start'); if (startActions) { startActions.style.display = 'flex'; }
clearTrackLog(); document.getElementById('mint-overlay').classList.add('open'); document.body.style.overflow = 'hidden';
// Permalink: reflect the open mint modal in the URL so sharing /kept/$code // or /unkept/$code reopens this exact view. Only push when the URL isn't // already at this target (avoids duplicate history entries when the modal // was opened via the initial-route deep link). if (isKeepsDomain && entry?.code) { const tabPath = activeTab === 'kept' ? '/kept' : activeTab === 'unkept' ? '/unkept' : ''; const targetPath = `${tabPath}/$${entry.code}`; if (location.pathname !== targetPath) { history.pushState({ tab: activeTab, code: entry.code, mintOpen: true }, '', targetPath); } } }
function beginMintFlow() { // Fallback — the start button's onclick is now set dynamically in openMintModal const startActions = document.getElementById('mint-actions-start'); if (startActions) startActions.style.display = 'none'; renderTimeline(); startMintFlow(mintCurrentPiece, { autoSync: mintAutoSyncAfterRebake }); }
function closeMintModal() { mintLegacyCheckNonce++; mintAutoSyncAfterRebake = false; document.getElementById('mint-overlay').classList.remove('open'); document.body.style.overflow = '';
// Permalink: restore URL to tab root when the modal closes. Matches // /kept/$code or /unkept/$code (and bare /$code inside unkept context) // so direct-link exit → back button feels right. if (isKeepsDomain && /^\/(kept|unkept)?\/?\$[^/]+$/.test(location.pathname)) { const tabPath = activeTab === 'kept' ? '/kept' : '/'; history.pushState({ tab: activeTab }, '', tabPath); } if (mintPollTimer) { clearInterval(mintPollTimer); mintPollTimer = null; } if (mintAbortController) { mintAbortController.abort(); mintAbortController = null; } // Clean up iframe to stop the piece const previewFrame = document.getElementById('mint-preview-frame'); if (previewFrame) previewFrame.innerHTML = ''; const ovenPreview = document.getElementById('oven-live-preview'); if (ovenPreview) ovenPreview.remove(); mintOnChainUris = null; mintLatestUris = null; clearTrackLog(); }
function cancelMint() { closeMintModal(); }
// Job-based mint flow — uses keep-prepare + keep-status polling. // Survives disconnects, page refreshes, and proxy timeouts. let mintPollTimer = null; let mintJobId = null; let mintOvenPreviewTimer = null;
async function startMintFlow(piece, options = {}) { const rebake = options.rebake === true; const regenerate = options.regenerate === true && !rebake; const autoSync = options.autoSync === true && rebake; mintAbortController = new AbortController();
// TEMP instrumentation to debug first-open "validate ownership fails" — // remove once the root cause is nailed down. const logFlow = (...args) => console.log('🔑 KEEP-FLOW', ...args); logFlow('startMintFlow entry', { piece, rebake, regenerate, autoSync, hasAcAuth0Client: !!acAuth0Client, hasAcUser: !!acUser, acTokenLen: acToken ? acToken.length : 0, mintWalletAddress, linkedWalletAddress, acHandle, });
try { // Ensure Auth0 + acToken are ready BEFORE connectWallet runs, because // connectWallet → syncWalletToProfile early-returns if acToken is null, // leaving the user's profile without the linked wallet. keep-prepare // then rejects with "No wallet linked" on first open; second open // works because the first getTokenSilently populated acToken. if (!acAuth0Client) { logFlow('initAuth0 needed — client not yet ready'); try { await initAuth0(); logFlow('initAuth0 resolved', { hasClient: !!acAuth0Client }); } catch (e) { logFlow('initAuth0 threw', e?.message || e); } } if (acAuth0Client) { try { const t = await acAuth0Client.getTokenSilently(); acToken = t; logFlow('getTokenSilently OK', { tokenLen: t ? t.length : 0 }); } catch (e) { logFlow('getTokenSilently threw', e?.error || e?.message || e); } } else { logFlow('skipping getTokenSilently — no acAuth0Client'); }
// Step 1: Connect wallet addTrackEntry(rebake ? 'Starting regeneration...' : (regenerate ? 'Starting regeneration...' : 'Starting keep flow...'), 'active'); setMintStep('wallet', 'active', 'Connecting...'); logFlow('connectWallet() before', { acTokenReady: !!acToken }); const address = await connectWallet(); logFlow('connectWallet() after', { address, linkedWalletAddress }); if (!address) { setMintStep('wallet', 'error', 'Connection cancelled'); addTrackEntry('Wallet connection cancelled', 'error'); return; } setMintStep('wallet', 'done', address.slice(0, 8) + '...' + address.slice(-4)); addTrackEntry('Wallet connected: ' + address.slice(0, 8) + '...' + address.slice(-4));
// Step 2: Create job via keep-prepare setMintStep('validate', 'active', rebake ? 'Validating ownership...' : (regenerate ? 'Regenerating...' : 'Validating...'));
const prepBody = { piece: '$' + piece, walletAddress: address, network: getTezosNetwork(), ...(regenerate || rebake ? { regenerate: true, force: true } : {}), }; logFlow('keep-prepare POST', { url: `${API_BASE}/api/keep-prepare`, hasAuthHeader: !!acToken, body: prepBody, }); const prepRes = await fetch(`${API_BASE}/api/keep-prepare`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(acToken ? { Authorization: `Bearer ${acToken}` } : {}), }, body: JSON.stringify(prepBody), signal: mintAbortController.signal, });
const prepData = await prepRes.json(); logFlow('keep-prepare response', { status: prepRes.status, ok: prepRes.ok, data: prepData });
if (!prepRes.ok) { setMintStep('validate', 'error', prepData.error || 'Validation failed'); showMintError(prepData.error || 'Validation failed'); return; }
// Already minted? if (prepData.alreadyMinted) { setMintStep('validate', 'done', `Already kept as #${prepData.tokenId}`); setMintStep('review', 'done', 'Already kept'); setMintStep('sign', 'done', 'No signature needed'); setMintStep('complete', 'done', 'Rebake available'); document.getElementById('mint-actions').style.display = 'none'; showMintResult(`Already kept as Token #${prepData.tokenId}`, prepData.objktUrl); renderMintArtifactPanel({ title: 'Current Artifact Snapshot', piece, artifactUri: mintOnChainUris?.artifactUri || mintCurrentEntry?.kept?.artifactUri, thumbnailUri: mintOnChainUris?.thumbnailUri || mintCurrentEntry?.kept?.thumbnailUri, metadataUri: mintOnChainUris?.metadataUri || mintCurrentEntry?.kept?.metadataUri, objktUrl: prepData.objktUrl, showRebakeButton: true, }); return; }
mintJobId = prepData.jobId; if (prepData.resuming) { console.log('Resuming existing job:', mintJobId); }
setMintStep('validate', 'done', 'Validation passed');
// Step 3: Poll keep-status until ready or failed await pollJobStatus(piece, mintJobId, { rebake, regenerate, autoSync });
} catch (err) { if (err.name === 'AbortError') return; console.error('Mint flow error:', err); const message = err.message || 'Unexpected error'; const lastStage = Object.keys(mintStepStates).reverse().find(k => mintStepStates[k]?.status === 'active'); setMintStep(lastStage || 'validate', 'error', message); showMintError(message, true); } }
function pollJobStatus(piece, jobId, { rebake, regenerate, autoSync }) { return new Promise((resolve, reject) => { let pollCount = 0; const MAX_POLLS = 450; // 15 min at 2s intervals let lastStage = null; let thumbnailFallbackWarned = false; // only raise the fallback warning once per job const STAGE_STUCK_THRESHOLD_MS = 5 * 60 * 1000; // 5 min with no update = stale // Per-stage elapsed telemetry. Long oven bakes (up to 150s) look // identical to a hang without this — user was left staring at // "Baking thumbnail..." with no sense of motion. We track when each // stage first went active, append live elapsed seconds to the stage // detail, log it on stage transitions, and drop a heartbeat into the // track log every 30s of inactivity so things feel alive. const stageStartedAt = {}; const stageHeartbeatAt = {}; const STAGE_ELAPSED_SHOW_MS = 5 * 1000; // start appending "· 5s" after 5s const STAGE_HEARTBEAT_MS = 30 * 1000; // track-log "still working" every 30s const STAGE_FIRST_HEARTBEAT_MS = 20 * 1000; // first one at 20s since stage start const TRACK_LOG_ON_STAGE_ENTER = new Set(['thumbnail', 'bundle', 'ipfs', 'metadata']); const prettyStage = (id) => (MINT_STEPS.find(s => s.id === id)?.label || id); function formatElapsed(ms) { const s = Math.round(ms / 1000); if (s < 60) return `${s}s`; return `${Math.floor(s / 60)}m ${s % 60}s`; }
function stopPolling() { if (mintPollTimer) { clearInterval(mintPollTimer); mintPollTimer = null; } if (mintOvenPreviewTimer) { clearInterval(mintOvenPreviewTimer); mintOvenPreviewTimer = null; } }
// Listen for abort if (mintAbortController) { mintAbortController.signal.addEventListener('abort', () => { stopPolling(); reject(new DOMException('Aborted', 'AbortError')); }); }
mintPollTimer = setInterval(async () => { pollCount++; if (pollCount > MAX_POLLS) { stopPolling(); setMintStep(lastStage || 'thumbnail', 'error', 'Preparation timed out'); showMintError('Preparation timed out. Try again.', true); resolve(); return; }
try { const res = await fetch(`${API_BASE}/api/keep-status?jobId=${encodeURIComponent(jobId)}`); if (!res.ok) return; // Retry on next tick
const job = await res.json(); lastStage = job.stage;
if (job.artifactUri || job.thumbnailUri || job.metadataUri) { mintLatestUris = { ...(mintLatestUris || {}), ...(job.artifactUri ? { artifactUri: job.artifactUri } : {}), ...(job.thumbnailUri ? { thumbnailUri: job.thumbnailUri } : {}), ...(job.metadataUri ? { metadataUri: job.metadataUri } : {}), }; if (mintCurrentEntry && (rebake || regenerate)) { mintCurrentEntry.pendingRebake = { ...(mintCurrentEntry.pendingRebake || {}), ...(mintLatestUris || {}), }; } if (mintCurrentEntry) { renderMintPieceInfo(mintCurrentEntry, { rebake, regenerate }); } }
// Update timeline from job stage if (job.stage && job.stage !== 'ready') { const now = Date.now(); const isNewStage = !stageStartedAt[job.stage]; if (isNewStage) { stageStartedAt[job.stage] = now; stageHeartbeatAt[job.stage] = now; if (TRACK_LOG_ON_STAGE_ENTER.has(job.stage)) { addTrackEntry(`${prettyStage(job.stage)}: ${job.stageMessage || 'working...'}`, 'active'); } } const stageElapsed = now - stageStartedAt[job.stage]; const baseMsg = job.stageMessage || job.stage; // Append live elapsed seconds after 5s so long oven bakes // (thumbnail can take ~150s) visibly tick forward. const detail = stageElapsed >= STAGE_ELAPSED_SHOW_MS ? `${baseMsg} · ${formatElapsed(stageElapsed)}` : baseMsg; setMintStep(job.stage, 'active', detail); // Track-log heartbeat so the user sees motion even when the // stageMessage itself isn't changing. First at 20s from stage // start, then every 30s. const lastBeatAt = stageHeartbeatAt[job.stage]; const nextBeatAt = (lastBeatAt === stageStartedAt[job.stage]) ? stageStartedAt[job.stage] + STAGE_FIRST_HEARTBEAT_MS : lastBeatAt + STAGE_HEARTBEAT_MS; if (now >= nextBeatAt && !isNewStage) { stageHeartbeatAt[job.stage] = now; addTrackEntry(`${prettyStage(job.stage)}: still working — ${formatElapsed(stageElapsed)} elapsed`, 'active'); } // Mark previous stages done (log their total elapsed once). const stageIndex = MINT_STEPS.findIndex(s => s.id === job.stage); for (let i = 0; i < stageIndex; i++) { const prevId = MINT_STEPS[i].id; if (prevId !== 'wallet' && mintStepStates[prevId]?.status !== 'done') { const prevStart = stageStartedAt[prevId]; const prevElapsed = prevStart ? (now - prevStart) : 0; const prevDetail = mintStepStates[prevId]?.detail || ''; const detailWithTime = (prevElapsed && TRACK_LOG_ON_STAGE_ENTER.has(prevId)) ? `${prevDetail ? prevDetail + ' · ' : ''}took ${formatElapsed(prevElapsed)}`.trim() : prevDetail; setMintStep(prevId, 'done', detailWithTime); if (prevStart && TRACK_LOG_ON_STAGE_ENTER.has(prevId)) { addTrackEntry(`${prettyStage(prevId)}: done in ${formatElapsed(prevElapsed)}`, 'done'); } } } }
// Detect stale jobs stuck on the same stage too long // (lastStage was already set to job.stage above, so use updatedAt) const jobUpdatedAt = job.updatedAt ? new Date(job.updatedAt).getTime() : 0; const jobAge = jobUpdatedAt ? Date.now() - jobUpdatedAt : 0; if (job.status === 'preparing' && jobAge > STAGE_STUCK_THRESHOLD_MS) { stopPolling(); setMintStep(job.stage || 'ipfs', 'error', `Stuck at ${job.stageMessage || job.stage} — try again`); showMintError(`Pipeline appears stuck at "${job.stageMessage || job.stage}". The background process may have crashed. Try again.`, true); resolve(); return; }
// Live oven telemetry — poll oven /grab-status directly so stage // text + preview frame update without waiting for the MongoDB // → keep-status round-trip (which adds ~2-4s of perceived lag). if ((job.stage === 'thumbnail' || job.stage === 'bundle') && !mintOvenPreviewTimer) { mintOvenPreviewTimer = setInterval(async () => { try { const res = await fetch('https://oven.aesthetic.computer/grab-status'); if (!res.ok) return; const status = await res.json(); const progress = status.progress; if (!progress?.piece?.includes(piece)) return;
// Push stage detail into the timeline immediately. Mirrors // the formatting the background worker writes to MongoDB // (keep-prepare-background.mjs::startOvenProgressPoller). const detail = progress.stageDetail || progress.stage || ''; const frame = progress.currentFrame != null && progress.totalFrames ? ` (${progress.currentFrame}/${progress.totalFrames})` : ''; const pct = progress.percent != null ? ` ${Math.round(progress.percent)}%` : ''; const msg = `${detail}${frame}${pct}`.trim(); if (msg && (lastStage === 'thumbnail' || lastStage === 'bundle')) { setMintStep(lastStage, 'active', msg); }
if (!progress.previewFrame) return; const pf = document.getElementById('mint-preview-frame'); if (!pf || pf.querySelector('.job-thumbnail')) return; const overlay = pf.querySelector('.mint-progress-overlay'); let img = pf.querySelector('.oven-live-img'); if (!img) { const iframe = pf.querySelector('iframe'); if (iframe) iframe.remove(); img = document.createElement('img'); img.className = 'oven-live-img'; img.style.cssText = 'width:100%;height:100%;object-fit:contain;image-rendering:pixelated;position:absolute;inset:0;'; if (overlay) pf.insertBefore(img, overlay); else pf.appendChild(img); } img.src = 'data:image/jpeg;base64,' + progress.previewFrame; } catch {} }, 1000); } // Stop oven preview polling when bake stage is done if (job.stage !== 'thumbnail' && job.stage !== 'bundle' && mintOvenPreviewTimer) { clearInterval(mintOvenPreviewTimer); mintOvenPreviewTimer = null; }
// Surface silent thumbnail-bake fallback (oven failed, server // reused previous thumbnail or fell back to artifact). Without // this the user would see a "success" but their thumbnail stayed // stale — previously only logged server-side. if (job.thumbnailFallback && !thumbnailFallbackWarned) { thumbnailFallbackWarned = true; const fb = job.thumbnailFallback; const detail = fb.kind === 'preexisting' ? `⚠️ Thumbnail bake failed (${fb.reason || 'unknown'}) — reusing previous thumbnail. Sync will not update the image on objkt.` : fb.kind === 'artifact' ? `⚠️ Thumbnail bake failed (${fb.reason || 'unknown'}) — using artifact as thumbnail.` : `⚠️ Thumbnail bake fallback: ${JSON.stringify(fb)}`; addTrackEntry(detail, 'error'); if (mintStepStates['thumbnail']) { setMintStep('thumbnail', 'error', 'Bake failed — fallback used'); } }
// Show final IPFS thumbnail when available if (job.thumbnailUri) { const pf = document.getElementById('mint-preview-frame'); if (pf && !pf.querySelector('.job-thumbnail')) { const overlay = pf.querySelector('.mint-progress-overlay'); const ovenImg = pf.querySelector('.oven-live-img'); if (ovenImg) ovenImg.remove(); const iframe = pf.querySelector('iframe'); if (iframe) iframe.remove(); const img = document.createElement('img'); img.className = 'job-thumbnail'; img.src = job.thumbnailUri.replace('ipfs://', 'https://ipfs.aesthetic.computer/ipfs/'); img.style.cssText = 'width:100%;height:100%;object-fit:contain;image-rendering:pixelated;'; if (overlay) pf.insertBefore(img, overlay); else pf.appendChild(img); } }
// ── READY ──────────────────────────────────────────── if (job.status === 'ready' && job.preparedData) { stopPolling(); mintPreparedData = job.preparedData;
// Mark all prep steps done ['validate', 'analyze', 'thumbnail', 'bundle', 'ipfs', 'metadata'].forEach(id => { if (mintStepStates[id]?.status !== 'done') setMintStep(id, 'done', mintStepStates[id]?.detail || ''); });
const data = job.preparedData;
if (data.rebake === true) { const autoUpgrade = autoSync === true; if (autoUpgrade) { setMintStep('review', 'active', 'Regenerated, preparing sync...'); setMintStep('sign', 'active', 'Waiting for wallet...'); setMintStep('complete', 'active', 'Syncing metadata...'); addTrackEntry('Media regenerated. Preparing on-chain metadata sync...', 'active'); } else { setMintStep('review', 'done', 'Regenerated'); setMintStep('sign', 'done', 'No signature needed'); setMintStep('complete', 'done', 'Rebake complete'); addTrackEntry('Media regenerated successfully', 'done'); } document.getElementById('mint-actions').style.display = 'none'; mintLatestUris = { ...(mintLatestUris || {}), ...(data.artifactUri ? { artifactUri: data.artifactUri } : {}), ...(data.thumbnailUri ? { thumbnailUri: data.thumbnailUri } : {}), ...(data.metadataUri ? { metadataUri: data.metadataUri } : {}), }; if (mintCurrentEntry) { mintCurrentEntry.pendingRebake = { ...(mintCurrentEntry.pendingRebake || {}), ...(mintLatestUris || {}), createdAt: data.createdAt || new Date().toISOString(), packDate: data.packDate || null, network: data.network || getTezosNetwork(), contractAddress: data.contractAddress || getKeepsContractAddress(), contractProfile: data.contractProfile || getKeepsContractProfile(), contractVersion: data.contractVersion || getKeepsContractVersion(), }; renderMintPieceInfo(mintCurrentEntry, { rebake, regenerate }); }
// Swap preview frame to show the fresh IPFS bundle const freshArtifactUrl = ipfsToHttp(data.artifactUri); const pf = document.getElementById('mint-preview-frame'); if (pf && freshArtifactUrl) { pf.innerHTML = `<iframe src="${freshArtifactUrl}" sandbox="allow-scripts allow-same-origin" loading="lazy" title="$${escapeHtml(piece)} regenerated"></iframe>`; }
// Clear result area and show fresh artifact panel const resultArea = document.getElementById('mint-result-area'); if (resultArea) resultArea.innerHTML = '';
if (mintOnChainUris) { renderMintArtifactPanel({ title: 'On-chain Artifacts', piece, artifactUri: mintOnChainUris.artifactUri, thumbnailUri: mintOnChainUris.thumbnailUri, metadataUri: mintOnChainUris.metadataUri, objktUrl: data.objktUrl, }); }
const activeKeepForSync = mintCurrentEntry ? getActiveKeepRecord(mintCurrentEntry) : null; const syncTokenId = normalizeTokenId(data.tokenId ?? activeKeepForSync?.tokenId); const syncData = { piece, tokenId: syncTokenId, artifactUri: data.artifactUri, thumbnailUri: data.thumbnailUri, metadataUri: data.metadataUri || mintLatestUris?.metadataUri || null, contractAddress: data.contractAddress || activeKeepForSync?.contractAddress || getKeepsContractAddress(), contractProfile: data.contractProfile || activeKeepForSync?.contractProfile || getKeepsContractProfile(), contractVersion: data.contractVersion || activeKeepForSync?.contractVersion || getKeepsContractVersion(), network: data.network || activeKeepForSync?.network || getTezosNetwork(), packDate: data.packDate, };
renderMintArtifactPanel({ title: 'Regenerated Artifacts (Not on-chain yet)', piece, artifactUri: data.artifactUri, thumbnailUri: data.thumbnailUri, metadataUri: data.metadataUri || mintLatestUris?.metadataUri, packDate: data.packDate, objktUrl: data.objktUrl, showRebakeButton: true, rebakeButtonLabel: 'Regenerate Again', showSyncButton: syncTokenId != null, syncButtonData: syncData, append: !!mintOnChainUris, });
if (autoUpgrade) { try { const syncResult = await syncRebakeMetadataOnChain(syncData);
applySyncResultToCurrentEntry(syncResult, syncTokenId);
const txShort = syncResult.txHash ? syncResult.txHash.slice(0, 8) + '...' : 'Signed'; setMintStep('review', 'done', 'Regenerated'); setMintStep('sign', 'done', txShort); setMintStep('complete', 'done', 'Upgraded on-chain'); addTrackEntry('On-chain metadata synced', 'done');
const syncedArtifactUrl = ipfsToHttp(syncResult.artifactUri); const previewFrameEl = document.getElementById('mint-preview-frame'); if (previewFrameEl && syncedArtifactUrl) { previewFrameEl.innerHTML = `<iframe src="${syncedArtifactUrl}" sandbox="allow-scripts allow-same-origin" loading="lazy" title="$${escapeHtml(piece)} upgraded"></iframe>`; }
const syncContract = syncResult.contractAddress || data.contractAddress || getKeepsContractAddress(); const syncNetwork = syncResult.network || data.network || getTezosNetwork(); const syncedObjktUrl = syncTokenId != null && syncContract ? `${getObjktBase(syncNetwork)}/tokens/${syncContract}/${syncTokenId}` : data.objktUrl;
showMintResult('Legacy bundle upgraded and synced on-chain.', syncedObjktUrl, syncResult.txHash); renderMintArtifactPanel({ title: 'Synced Artifacts', piece, artifactUri: syncResult.artifactUri, thumbnailUri: syncResult.thumbnailUri, metadataUri: syncResult.metadataUri, packDate: data.packDate, objktUrl: syncedObjktUrl, }); } catch (syncErr) { const syncMessage = syncErr?.message || 'On-chain sync failed'; setMintStep('review', 'done', 'Regenerated'); setMintStep('sign', 'error', syncMessage); addTrackEntry(`On-chain sync failed: ${syncMessage}`, 'error'); const area = document.getElementById('mint-result-area'); if (area) { area.insertAdjacentHTML( 'beforeend', `<div class="mint-error-msg">Media regenerated, but on-chain sync failed: ${escapeHtml(syncMessage)}</div>` ); } } } } else { setMintStep('review', 'active', `${data.mintFee || 2.5} XTZ`); addTrackEntry('Artifacts prepared, ready for signing', 'done'); mintLatestUris = { ...(mintLatestUris || {}), ...(data.artifactUri ? { artifactUri: data.artifactUri } : {}), ...(data.thumbnailUri ? { thumbnailUri: data.thumbnailUri } : {}), ...(data.metadataUri ? { metadataUri: data.metadataUri } : {}), }; if (mintCurrentEntry) { renderMintPieceInfo(mintCurrentEntry, { rebake, regenerate }); } renderMintArtifactPanel({ title: 'Prepared Artifacts', piece, artifactUri: data.artifactUri, thumbnailUri: data.thumbnailUri, metadataUri: data.metadataUri, packDate: data.packDate, showRebakeButton: true, rebakeButtonLabel: 'Regenerate', }); document.getElementById('mint-actions').style.display = 'flex'; document.getElementById('mint-confirm-btn').disabled = false; }
resolve(); return; }
// ── FAILED ─────────────────────────────────────────── if (job.status === 'failed') { stopPolling(); const failStage = job.errorStage || job.stage || 'validate'; setMintStep(failStage, 'error', job.error || 'Preparation failed'); addTrackEntry(job.error || 'Preparation failed', 'error'); showMintError(job.error || 'Preparation failed', true); resolve(); return; }
} catch (pollErr) { // Network error during poll — just retry next tick console.warn('Poll error:', pollErr.message); } }, 2000); }); }
async function consumeSSEStream(response, handlers = {}) { const reader = response.body?.getReader(); if (!reader) { throw new Error('Streaming response unavailable'); }
const decoder = new TextDecoder(); let buffer = ''; let currentEventType = null;
const processLines = async (lines) => { for (const line of lines) { if (line.startsWith('event: ')) { currentEventType = line.slice(7).trim(); continue; } if (!line.startsWith('data: ') || !currentEventType) continue;
let parsed = null; try { parsed = JSON.parse(line.slice(6)); } catch { parsed = { message: line.slice(6) }; }
const handler = handlers[currentEventType]; if (typeof handler === 'function') { await handler(parsed); } currentEventType = null; } };
while (true) { const { done, value } = await reader.read(); if (done) { buffer += decoder.decode(); break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; await processLines(lines); }
if (buffer.trim()) { await processLines(buffer.split('\n')); } }
function applySyncResultToCurrentEntry(syncResult, tokenId) { if (!mintCurrentEntry || !syncResult) return;
const normalizedTokenId = normalizeTokenId(tokenId ?? syncResult.tokenId); const contractAddress = (syncResult.contractAddress || getKeepsContractAddress() || '').trim(); const normalizedContract = normalizeAddress(contractAddress); const network = syncResult.network || getTezosNetwork(); const contractProfile = syncResult.contractProfile || getKeepsContractProfile(); const contractVersion = syncResult.contractVersion || getKeepsContractVersion();
const applyUris = (record) => { if (!record || typeof record !== 'object') return; record.artifactUri = syncResult.artifactUri || record.artifactUri || null; record.thumbnailUri = syncResult.thumbnailUri || record.thumbnailUri || null; record.metadataUri = syncResult.metadataUri || record.metadataUri || null; record.network = network || record.network || null; if (contractAddress) record.contractAddress = contractAddress; if (contractProfile) record.contractProfile = contractProfile; if (contractVersion) record.contractVersion = contractVersion; if (normalizedTokenId !== null) record.tokenId = normalizedTokenId; };
const recordMatches = (record) => { if (!record || typeof record !== 'object') return false; const recToken = normalizeTokenId(record.tokenId); const recContract = normalizeAddress(record.contractAddress || ''); const tokenMatches = normalizedTokenId === null || recToken === normalizedTokenId; const contractMatches = !normalizedContract || recContract === normalizedContract; return tokenMatches && contractMatches; };
let updated = false; if (recordMatches(mintCurrentEntry.kept)) { applyUris(mintCurrentEntry.kept); updated = true; }
if (Array.isArray(mintCurrentEntry.keptRecords)) { for (const record of mintCurrentEntry.keptRecords) { if (recordMatches(record)) { applyUris(record); updated = true; } } }
if (!updated) { const base = (mintCurrentEntry.kept && typeof mintCurrentEntry.kept === 'object') ? mintCurrentEntry.kept : {}; mintCurrentEntry.kept = { ...base, tokenId: normalizedTokenId ?? base.tokenId ?? null, network: network || base.network || null, contractAddress: contractAddress || base.contractAddress || null, contractProfile: contractProfile || base.contractProfile || null, contractVersion: contractVersion || base.contractVersion || null, artifactUri: syncResult.artifactUri || base.artifactUri || null, thumbnailUri: syncResult.thumbnailUri || base.thumbnailUri || null, metadataUri: syncResult.metadataUri || base.metadataUri || null, }; }
mintCurrentEntry.pendingRebake = null; mintOnChainUris = { artifactUri: syncResult.artifactUri || mintOnChainUris?.artifactUri || null, thumbnailUri: syncResult.thumbnailUri || mintOnChainUris?.thumbnailUri || null, metadataUri: syncResult.metadataUri || mintOnChainUris?.metadataUri || null, }; mintLatestUris = null; renderMintPieceInfo(mintCurrentEntry, { rebake: false, regenerate: false }); }
async function syncRebakeMetadataOnChain({ piece, tokenId, artifactUri, thumbnailUri, metadataUri, contractAddress, contractProfile, contractVersion, network, }) { const normalizedTokenId = normalizeTokenId(tokenId); if (normalizedTokenId === null) { throw new Error('Missing token ID for metadata sync'); } if (!artifactUri) { throw new Error('Missing artifact URI for metadata sync'); }
const walletAddress = mintWalletAddress || await connectWallet(); if (!walletAddress) { throw new Error('Wallet connection cancelled'); }
if (acAuth0Client) { try { acToken = await acAuth0Client.getTokenSilently(); } catch {} }
addTrackEntry('Preparing metadata update payload...', 'active'); const prepareResponse = await fetch(`${API_BASE}/api/keep-update`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(acToken ? { Authorization: `Bearer ${acToken}` } : {}), }, body: JSON.stringify({ piece: '$' + piece, tokenId: normalizedTokenId, artifactUri, thumbnailUri, walletAddress, mode: 'prepare', }), });
let prepared = null; let streamError = null; if (prepareResponse.body) { await consumeSSEStream(prepareResponse, { progress: async (eventData) => { if (eventData?.message) addTrackEntry(eventData.message, 'active'); }, prepared: async (eventData) => { prepared = eventData; }, error: async (eventData) => { streamError = eventData?.error || 'Metadata update preparation failed'; }, }); }
if (!prepareResponse.ok) { const fallback = await prepareResponse.json().catch(() => null); throw new Error(streamError || fallback?.error || `Metadata update prepare failed (${prepareResponse.status})`); }
if (streamError) { throw new Error(streamError); }
if (!prepared?.michelsonParams) { throw new Error('Metadata update payload was not prepared'); }
setMintStep('sign', 'active', 'Sign metadata update...'); addTrackEntry('Sign metadata update in your wallet...', 'active');
const operationResult = await requestWalletOperation({ sourceAddress: walletAddress, operationDetails: [{ kind: 'transaction', destination: prepared.contractAddress || contractAddress || getKeepsContractAddress(), amount: '0', parameters: prepared.michelsonParams, }], });
const txHash = operationResult?.transactionHash; if (!txHash) { throw new Error('Wallet did not return a transaction hash'); }
addTrackEntry(`Metadata update submitted: ${txHash.slice(0, 8)}...`, 'done');
const confirmPayload = { piece: '$' + piece, tokenId: normalizedTokenId, txHash, artifactUri: prepared.artifactUri || artifactUri, thumbnailUri: prepared.thumbnailUri || thumbnailUri || artifactUri, metadataUri: prepared.metadataUri || metadataUri || null, contractAddress: prepared.contractAddress || contractAddress || getKeepsContractAddress(), contractProfile: contractProfile || getKeepsContractProfile(), contractVersion: contractVersion || getKeepsContractVersion(), };
const confirmResponse = await fetch(`${API_BASE}/api/keep-update-confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(acToken ? { Authorization: `Bearer ${acToken}` } : {}), }, body: JSON.stringify(confirmPayload), });
const confirmData = await confirmResponse.json().catch(() => null); if (!confirmResponse.ok) { throw new Error(confirmData?.error || `Metadata update confirm failed (${confirmResponse.status})`); }
// Warm public IPFS gateways for the final URIs so objkt's indexer has // well-peered sources the moment it re-reads the on-chain pointer. // Fire-and-forget but surfaced in the track log so the user sees it. const finalArtifact = confirmData?.artifactUri || confirmPayload.artifactUri; const finalThumb = confirmData?.thumbnailUri || confirmPayload.thumbnailUri; const finalMeta = confirmData?.metadataUri || confirmPayload.metadataUri; const cidsToWarm = [finalArtifact, finalThumb, finalMeta] .filter(Boolean) .map((u) => (u.startsWith('ipfs://') ? u.slice(7) : u)) .filter((cid) => /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|ba[a-z0-9]{56,})$/.test(cid)); if (cidsToWarm.length) { addTrackEntry(`Warming public IPFS gateways for ${cidsToWarm.length} CID${cidsToWarm.length === 1 ? '' : 's'}...`, 'active'); try { const warmRes = await fetch(`${API_BASE}/api/warm-gateways`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cids: cidsToWarm }), }); const warmData = await warmRes.json().catch(() => null); if (warmRes.ok && warmData?.ok) { const gws = (warmData.gateways || []).join(', '); addTrackEntry(`Gateways warmed: ${gws}`, 'done'); } else { addTrackEntry('Gateway warming skipped (non-critical)', 'done'); } } catch (warmErr) { addTrackEntry('Gateway warming failed (non-critical)', 'done'); console.warn('warm-gateways failed:', warmErr); } }
return { txHash, tokenId: normalizedTokenId, network: network || getTezosNetwork(), contractAddress: confirmPayload.contractAddress, contractProfile: confirmData?.contractProfile || confirmPayload.contractProfile || null, contractVersion: confirmData?.contractVersion || confirmPayload.contractVersion || null, artifactUri: finalArtifact, thumbnailUri: finalThumb, metadataUri: finalMeta, }; }
async function confirmMint() { if (!mintPreparedData || !mintWalletAddress) return;
const confirmBtn = document.getElementById('mint-confirm-btn'); confirmBtn.disabled = true; confirmBtn.textContent = 'Signing...';
try { setMintStep('review', 'done', 'Approved'); setMintStep('sign', 'active', 'Check your wallet...');
// Build contract call via Beacon SDK requestOperation const mintFeeMutez = String(Math.floor((mintPreparedData.mintFee || 2.5) * 1_000_000)); const contractAddress = mintPreparedData.contractAddress || getKeepsContractAddress(); if (!contractAddress) { throw new Error('Missing active keeps contract address'); }
const response = await requestWalletOperation({ sourceAddress: mintWalletAddress, operationDetails: [{ kind: 'transaction', destination: contractAddress, amount: mintFeeMutez, parameters: mintPreparedData.michelsonParams, }] });
const txHash = response.transactionHash; setMintStep('sign', 'done', txHash.slice(0, 8) + '...'); setMintStep('complete', 'done', 'Kept!');
let confirmedTokenId = normalizeTokenId(mintPreparedData.tokenId);
// Confirm to backend try { if (acAuth0Client) { acToken = await acAuth0Client.getTokenSilently(); } const confirmRes = await fetch(`${API_BASE}/api/keep-confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(acToken ? { 'Authorization': `Bearer ${acToken}` } : {}), }, body: JSON.stringify({ piece: mintCurrentPiece, txHash, walletAddress: mintWalletAddress, network: mintPreparedData.network || getTezosNetwork(), contractAddress, contractProfile: mintPreparedData.contractProfile || getKeepsContractProfile(), contractVersion: mintPreparedData.contractVersion || getKeepsContractVersion(), artifactUri: mintPreparedData.artifactUri, thumbnailUri: mintPreparedData.thumbnailUri, metadataUri: mintPreparedData.metadataUri, tokenId: mintPreparedData.tokenId, }), }); const confirmData = await confirmRes.json().catch(() => null); const tokenFromConfirm = normalizeTokenId(confirmData?.tokenId); if (tokenFromConfirm !== null) { confirmedTokenId = tokenFromConfirm; } } catch (e) { console.warn('keep-confirm failed (non-critical):', e); }
const mintNetwork = mintPreparedData.network || getTezosNetwork(); const objktUrl = `${getObjktBase(mintNetwork)}/tokens/${contractAddress}/${confirmedTokenId ?? ''}`;
if (mintCurrentEntry && confirmedTokenId !== null) { const activeKeep = { tokenId: confirmedTokenId, txHash, walletAddress: mintWalletAddress || null, network: mintNetwork, contractAddress, contractProfile: mintPreparedData.contractProfile || getKeepsContractProfile(), contractVersion: mintPreparedData.contractVersion || getKeepsContractVersion(), artifactUri: mintPreparedData.artifactUri || null, thumbnailUri: mintPreparedData.thumbnailUri || null, metadataUri: mintPreparedData.metadataUri || null, keptAt: new Date().toISOString(), }; mintCurrentEntry.kept = activeKeep; mintCurrentEntry.keptRecords = [activeKeep]; mintCurrentEntry.pendingRebake = null; mintOnChainUris = { artifactUri: activeKeep.artifactUri, thumbnailUri: activeKeep.thumbnailUri, metadataUri: activeKeep.metadataUri, }; mintLatestUris = null; renderMintPieceInfo(mintCurrentEntry, { rebake: false, regenerate: false }); }
addTrackEntry(`Kept $${mintCurrentPiece} on Tezos`, 'done'); showMintResult(`Successfully kept $${mintCurrentPiece}!`, objktUrl, txHash); renderMintArtifactPanel({ title: 'Kept Artifacts', piece: mintCurrentPiece, artifactUri: mintPreparedData.artifactUri, thumbnailUri: mintPreparedData.thumbnailUri, metadataUri: mintPreparedData.metadataUri, packDate: mintPreparedData.packDate, objktUrl, });
// Hide action buttons document.getElementById('mint-actions').style.display = 'none';
} catch (err) { if (err.message?.includes('aborted') || err.message?.includes('denied')) { setMintStep('sign', 'error', 'Transaction rejected'); confirmBtn.disabled = false; confirmBtn.textContent = 'Confirm & Sign'; } else { setMintStep('sign', 'error', err.message || 'Signing failed'); showMintError(err.message || 'Transaction failed'); } } }
function showMintResult(message, objktUrl, txHash) { const area = document.getElementById('mint-result-area'); let html = `<div class="mint-result">${escapeHtml(message)}`; if (objktUrl) html += `<br><a href="${objktUrl}" target="_blank">View on objkt.com</a>`; if (txHash) html += ` · <a href="https://tzkt.io/${txHash}" target="_blank">View TX</a>`; html += '</div>'; area.innerHTML = html; }
function showMintError(message, showRetry = false) { const area = document.getElementById('mint-result-area'); const retryHtml = showRetry && mintCurrentEntry ? `<button class="mint-action-btn primary" style="margin-top:10px;width:100%;" onclick="retryMint()">Retry</button>` : ''; area.innerHTML = `<div class="mint-error-msg">${escapeHtml(message)}</div>${retryHtml}`; }
function retryMint() { if (!mintCurrentEntry) return; // Reset state and restart if (mintAbortController) { mintAbortController.abort(); mintAbortController = null; } mintPreparedData = null; document.getElementById('mint-result-area').innerHTML = ''; document.getElementById('mint-timeline').innerHTML = ''; mintStepStates = {}; renderTimeline(); startMintFlow(mintCurrentEntry.code, {}); }
// ── Track Record Log ─────────────────────────────────── function addTrackEntry(msg, status = '') { const log = document.getElementById('mint-track-log'); if (!log) return; const now = new Date(); const ts = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const el = document.createElement('div'); el.className = `mint-track-entry${status ? ` track-${status}` : ''}`; el.innerHTML = `<span class="track-time">${ts}</span><span class="track-msg">${escapeHtml(msg)}</span>`; log.appendChild(el); log.scrollTop = log.scrollHeight; }
function clearTrackLog() { const log = document.getElementById('mint-track-log'); if (log) log.innerHTML = ''; }
// ── In-place Regenerate (no modal reset) ─────────────── async function rerunRegenerate() { if (!mintCurrentEntry) return; const entry = mintCurrentEntry; const piece = entry.code; const rebake = isKeptOnActiveContract(entry);
// Abort any in-flight job if (mintAbortController) { mintAbortController.abort(); mintAbortController = null; } if (mintPollTimer) { clearInterval(mintPollTimer); mintPollTimer = null; }
// Reset step states but keep the modal open with existing content mintStepStates = {}; mintPreparedData = null;
// Hide action buttons during regeneration document.getElementById('mint-actions').style.display = 'none'; document.getElementById('mint-actions-start').style.display = 'none';
// Show progress overlay on preview frame renderTimeline();
addTrackEntry('Regenerating media...', 'active');
// Run the mint flow (which handles wallet, validation, polling) await startMintFlow(piece, { rebake, regenerate: !rebake }); }
// ========================================= // EMBEDDED FOCUSED-KEEP MODE // When this page runs inside the kidlisp.com editor's "Keeps" tab iframe it // skips the browse UI / preamble and opens straight onto a single piece — // the one currently open in the editor — for keeping on Tezos. The editor // and this page are same-origin, so the current piece arrives either via // postMessage or directly from the shared localStorage seed. // ========================================= const IS_EMBEDDED = window.self !== window.top; let pendingEmbedPayload = null; let embedHandledKey = null;
async function openEmbeddedKeep(payload = {}) { let code = String(payload.code || '').replace(/^\$/, '').trim(); const source = typeof payload.source === 'string' ? payload.source : ''; // Dedupe so repeated posts of the same piece don't reopen the modal. const key = source && source.trim() ? 'src:' + source.length + ':' + source.trim().slice(0, 48) : (code ? 'code:' + code : ''); if (!key || key === embedHandledKey) return;
// Auto-store the current source first: store-kidlisp dedupes by hash and // returns the canonical $code, so we always keep exactly what's in the // editor (even if the cached code was for an older edit). if (source && source.trim()) { try { const headers = { 'Content-Type': 'application/json' }; if (acToken) headers['Authorization'] = `Bearer ${acToken}`; const res = await fetch(`${AC_BASE}/api/store-kidlisp`, { method: 'POST', headers, body: JSON.stringify({ source }), }); if (res.ok) { const d = await res.json(); if (d.code) code = d.code; } } catch (e) { console.warn('[KEEPS embed] store-kidlisp failed:', e?.message || e); } } if (!code) return;
embedHandledKey = key; document.documentElement.classList.add('has-piece'); openMintModal({ code }); }
if (IS_EMBEDDED) { window.addEventListener('message', (e) => { if (e.origin !== window.location.origin) return; const d = e.data; if (!d || d.type !== 'keeps:piece') return; pendingEmbedPayload = d; openEmbeddedKeep(d); }); }
// ========================================= // INIT // ========================================= async function init() { // Embedded: reveal immediately, skip the preamble + the 14k-item grid, // and focus on the editor's current piece. if (IS_EMBEDDED) { await ensureKeepsConfig(); loading.style.display = 'none'; revealUI(); hideInlineAbout(); const tabsEl = document.querySelector('.keeps-tabs'); if (tabsEl) tabsEl.classList.remove('locked'); initTezosWallet().catch(() => {}); // Tell the parent editor we're ready for the current piece. try { window.parent.postMessage({ type: 'keeps:ready' }, window.location.origin); } catch (e) {} if (pendingEmbedPayload) { openEmbeddedKeep(pendingEmbedPayload); } else { // Fallback: same-origin localStorage seed the editor keeps updated. const seeded = (localStorage.getItem('kidlisp-current-code') || '').trim(); if (seeded) openEmbeddedKeep({ code: seeded }); } return; }
setIndexSearchVisible(false); loading.style.display = 'flex'; grid.innerHTML = ''; grid.appendChild(loading);
await ensureKeepsConfig();
// Skip preamble when navigating directly to a tab route or permalink const route = tabFromLocation(); const isDirectRoute = route.code || route.tab !== 'unkept';
if (isDirectRoute) { // Go straight to the content — no preamble loading.style.display = 'none'; revealUI(); hideInlineAbout(); // Castle rises immediately for direct routes const castle = document.getElementById('keeps-castle'); if (castle) { castle.classList.remove('grounded'); castle.classList.add('risen'); } const tabs = document.querySelector('.keeps-tabs'); if (tabs) tabs.classList.remove('locked'); setIndexSearchVisible(true);
// Pre-load Beacon SDK initTezosWallet().catch(e => console.warn('Beacon pre-init failed:', e));
const codes = await fetchAllCodes(); loading.style.display = 'none'; if (codes === null) { grid.innerHTML = ` <div class="keeps-error" style="grid-column: 1 / -1;"> <div class="error-icon">⚠</div> <div class="error-msg">Failed to load $codes. Check your connection and try again.</div> <button class="retry-btn" onclick="init()">Retry</button> </div>`; return; }
const scopedCodes = scopeIndexEntries(codes); allCodes = scopedCodes; filteredCodes = scopedCodes; indexReady = true; startLiveUpdates(5000); applyFilters(); // Restore page from URL ?page= param if (initialRoute.page > 0 && initialRoute.page < totalPages) { goToPage(initialRoute.page, { pushState: false }); } return; }
// Show the preamble — lock tabs until Enter loading.style.display = 'none'; revealUI(); showInlineAbout(); const tabs = document.querySelector('.keeps-tabs'); if (tabs) tabs.classList.add('locked');
const enterBtn = document.getElementById('keeps-enter-btn'); if (acUser && hasRegisteredHandle()) { // Logged in — show green Enter button if (enterBtn) enterBtn.style.display = ''; } else { // Not logged in — Enter bumps the login button if (enterBtn) enterBtn.style.display = ''; } return; // Wait for Enter click to proceed }
async function enterKeeps() { const enterBtn = document.getElementById('keeps-enter-btn');
if (!acUser) { // Flash the login button (keep Enter visible for retry) const loginBtn = document.getElementById('keeps-login-btn'); if (loginBtn) { loginBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { loginBtn.classList.add('blinking'); setTimeout(() => loginBtn.classList.remove('blinking'), 2500); }, 300); } return; }
if (enterBtn) enterBtn.style.display = 'none';
// Rise the castle and start floating codes const castle = document.getElementById('keeps-castle'); if (castle) { castle.classList.remove('grounded'); castle.classList.add('risen'); }
if (!hasRegisteredHandle()) { renderIndexGate('handle'); return; }
const tabs = document.querySelector('.keeps-tabs'); if (tabs) tabs.classList.remove('locked'); hideInlineAbout(); setIndexSearchVisible(true); loading.style.display = 'flex'; grid.innerHTML = ''; grid.appendChild(loading);
// Pre-load Beacon SDK + DAppClient so wallet connect is instant on click // (avoids popup blocker killing Kukai's tab) initTezosWallet().catch(e => console.warn('Beacon pre-init failed:', e));
const codes = await fetchAllCodes(); loading.style.display = 'none';
if (codes === null) { grid.innerHTML = ` <div class="keeps-error" style="grid-column: 1 / -1;"> <div class="error-icon">⚠</div> <div class="error-msg">Failed to load $codes. Check your connection and try again.</div> <button class="retry-btn" onclick="init()">Retry</button> </div>`; return; }
const scopedCodes = scopeIndexEntries(codes); allCodes = scopedCodes; filteredCodes = scopedCodes; indexReady = true;
// Start polling for new pieces every 5 seconds startLiveUpdates(5000);
// Populate floating castle $codes with visible index entries const floaters = document.querySelectorAll('.keeps-code-float'); if (scopedCodes.length >= floaters.length) { const shuffled = [...scopedCodes].sort(() => Math.random() - 0.5); floaters.forEach((el, i) => { el.textContent = '$' + shuffled[i].code; }); }
// Handle ?q= search param const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('q')) { const queryParam = urlParams.get('q') || ''; const leading = queryParam.charAt(0); if (leading === '$') selectedSigil = '$'; searchInput.value = queryParam.replace(/^[$!*#]/, ''); searchQuery = searchInput.value; } else { searchInput.value = ''; searchQuery = ''; } if (urlParams.has('kept')) { switchTab('kept'); } else { applyFilters(); } // Restore page from URL ?page= param if (initialRoute.page > 0 && initialRoute.page < totalPages) { goToPage(initialRoute.page, { pushState: false }); } }
initV6LaunchSimulation(); updateV6RoadmapStatus();
// Auth gates the index: do not load entries until user is authenticated + has handle. async function bootstrapKeepsPage() { ensureKeepsConfig(); // start loading immediately, don't block auth await initAuth0(); await init(); }
bootstrapKeepsPage();
// Pause/resume live updates on tab visibility document.addEventListener('visibilitychange', () => { if (document.hidden) { stopLiveUpdates(); } else if (indexReady) { fetchNewCodes(); startLiveUpdates(5000); } });
// Live XTZ price for lede (function() { const el = document.getElementById('keeps-xtz-usd'); if (!el) return; fetch('https://api.coingecko.com/api/v3/simple/price?ids=tezos&vs_currencies=usd') .then(r => r.json()) .then(data => { const price = data?.tezos?.usd; if (price) el.textContent = '~$' + (price * 2.5).toFixed(2) + ' USD'; }) .catch(() => { el.textContent = '~$0.91 USD'; }); })();
// $code slot animation (function() { const el = document.getElementById('code-slot'); if (!el) return; const codes = ['cow', 'zen', 'dot', 'orb', 'hex', 'ray', 'arc', 'pod', 'hum', 'web']; const maxLen = Math.max(...codes.map(c => c.length)); // Build DOM: $ + character slots el.innerHTML = '<span class="slot-dollar">$</span>' + Array.from({length: maxLen}, () => '<span class="slot-char"></span>').join(''); const chars = el.querySelectorAll('.slot-char'); // Each character cycles independently on its own timer chars.forEach((ch, j) => { let ci = 0; ch.textContent = codes[0][j] || ''; function flip() { ch.classList.add('flip'); setTimeout(() => { ci = (ci + 1) % codes.length; ch.textContent = codes[ci][j] || ''; ch.classList.remove('flip'); }, 120); } setInterval(flip, 400 + j * 150); }); })(); </script>
<footer class="keeps-footer"> <div class="footer-buy"> <span class="footer-buy-label">See also:</span> <a href="https://buy.kidlisp.com" class="footer-buy-link"><span class="buy-b">b</span><span class="buy-u">u</span><span class="buy-y">y</span><span class="dot">.</span><span class="baby-k">K</span><span class="baby-i">i</span><span class="baby-d">d</span><span class="baby-l">L</span><span class="baby-i2">i</span><span class="baby-s">s</span><span class="baby-p">p</span><span class="com-part">.com</span></a> </div> </footer>
<!-- Development live reload via session-server --><script>(function() { if (window.location.hostname !== 'localhost' && !window.location.hostname.includes('local.')) return;
let ws = null; let reconnect = null;
function connect() { if (ws && ws.readyState === WebSocket.OPEN) return; if (reconnect) { clearInterval(reconnect); reconnect = null; }
const url = window.location.hostname.includes('local.') ? 'wss://session.local.aesthetic.computer' : 'wss://localhost:8889';
try { ws = new WebSocket(url); } catch { scheduleReconnect(); return; }
ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.type === 'reload') { const piece = msg.content?.piece; if (piece === '*refresh*' || piece === 'keeps' || piece?.includes('keeps')) { setTimeout(() => window.location.reload(), 150); } } } catch {} };
ws.onclose = () => { ws = null; scheduleReconnect(); }; ws.onerror = () => {}; }
function scheduleReconnect() { if (!reconnect) reconnect = setInterval(connect, 3000); }
connect();})();</script></body></html>