From 87aaf3fa1966743052204bfe01a240f173766c51 Mon Sep 17 00:00:00 2001 From: devpotatoes Date: Mon, 11 Aug 2025 09:26:06 +0200 Subject: [PATCH] Add privatri wco --- wco/crypto/crypto.js | 49 + wco/privatri/alias.mustache | 6 + wco/privatri/apx.js | 194 ++ wco/privatri/assets/icon.png | Bin 0 -> 12177 bytes wco/privatri/createThread.js | 44 + wco/privatri/createThread.mustache | 34 + wco/privatri/editMessage.mustache | 16 + wco/privatri/inviteAlias.js | 52 + wco/privatri/inviteAlias.mustache | 12 + wco/privatri/main_fr.html | 108 + wco/privatri/main_fr.mustache | 101 +- wco/privatri/message.mustache | 23 + wco/privatri/output.css | 2733 +++++++++++++++++++++++++ wco/privatri/privatri.js | 658 ++++-- wco/privatri/thread.mustache | 7 + wco/privatri/threadAliasList.js | 88 + wco/privatri/threadAliasList.mustache | 6 + wco/privatri/threadSettings.js | 72 + wco/privatri/threadSettings.mustache | 33 + wco/privatri/toastAlert.mustache | 5 + wco/privatri/utils.js | 46 + 21 files changed, 4106 insertions(+), 181 deletions(-) create mode 100644 wco/privatri/alias.mustache create mode 100644 wco/privatri/apx.js create mode 100644 wco/privatri/assets/icon.png create mode 100644 wco/privatri/createThread.js create mode 100644 wco/privatri/createThread.mustache create mode 100644 wco/privatri/editMessage.mustache create mode 100644 wco/privatri/inviteAlias.js create mode 100644 wco/privatri/inviteAlias.mustache create mode 100644 wco/privatri/main_fr.html create mode 100644 wco/privatri/message.mustache create mode 100644 wco/privatri/output.css create mode 100644 wco/privatri/thread.mustache create mode 100644 wco/privatri/threadAliasList.js create mode 100644 wco/privatri/threadAliasList.mustache create mode 100644 wco/privatri/threadSettings.js create mode 100644 wco/privatri/threadSettings.mustache create mode 100644 wco/privatri/toastAlert.mustache create mode 100644 wco/privatri/utils.js diff --git a/wco/crypto/crypto.js b/wco/crypto/crypto.js index 7f52ffb..baecb49 100644 --- a/wco/crypto/crypto.js +++ b/wco/crypto/crypto.js @@ -58,4 +58,53 @@ apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => { ); }; +apx.crypto.sign = async (message, privateKey) => { + privateKey = await openpgp.readPrivateKey( + { + armoredKey: privateKey + } + ); + + return await openpgp.sign( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signingKeys: privateKey + } + ); +}; + +apx.crypto.verifySignature = async (message, signature, publicKey) => { + publicKey = await openpgp.readKey( + { + armoredKey: publicKey + } + ); + + const verified = await openpgp.verify( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signature: await openpgp.readSignature( + { + armoredSignature: signature + } + ), + verificationKeys: publicKey + } + ); + + if (await verified.signatures[0].verified) { + return true; + } else { + return false; + }; +}; + export default apx; \ No newline at end of file diff --git a/wco/privatri/alias.mustache b/wco/privatri/alias.mustache new file mode 100644 index 0000000..bfabfaa --- /dev/null +++ b/wco/privatri/alias.mustache @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/wco/privatri/apx.js b/wco/privatri/apx.js new file mode 100644 index 0000000..a64f712 --- /dev/null +++ b/wco/privatri/apx.js @@ -0,0 +1,194 @@ +var apx = apx || {}; + +apx.crypto = apx.crypto || {}; +apx.indexedDB = apx.indexedDB || {}; + +apx.crypto.genKey = async (uuid) => { + return await openpgp.generateKey( + { + type: "ecc", + curve: "curve25519", + userIDs: [ + { + alias: uuid + } + ], + passphrase: "", + format: "armored", + } + ); +}; + +apx.crypto.encryptMessage = async (message, publicKey) => { + publicKey = await openpgp.readKey( + { + armoredKey: publicKey + } + ); + + return await openpgp.encrypt( + { + message: await openpgp.createMessage( + { + text: message + } + ), + encryptionKeys: publicKey + } + ); +}; + +apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => { + privateKey = await openpgp.readPrivateKey( + { + armoredKey: privateKey + } + ); + + const message = await openpgp.readMessage( + { + armoredMessage: encryptedMessage + } + ); + + return await openpgp.decrypt( + { + message, + decryptionKeys: privateKey + } + ); +}; + +apx.crypto.sign = async (message, privateKey) => { + privateKey = await openpgp.readPrivateKey( + { + armoredKey: privateKey + } + ); + + return await openpgp.sign( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signingKeys: privateKey + } + ); +}; + +apx.crypto.verifySignature = async (message, signature, publicKey) => { + publicKey = await openpgp.readKey( + { + armoredKey: publicKey + } + ); + + const verified = await openpgp.verify( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signature: await openpgp.readSignature( + { + armoredSignature: signature + } + ), + verificationKeys: publicKey + } + ); + + if (await verified.signatures[0].verified) { + return true; + } else { + return false; + }; +}; + +apx.indexedDB.set = async (db, storeName, value) => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(db, 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains("threads")) { + db.createObjectStore("threads", { keyPath: "uuid" }); + }; + + if (!db.objectStoreNames.contains("messages")) { + db.createObjectStore("messages", { keyPath: "privatriid" }); + }; + }; + + request.onsuccess = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains(storeName)) { + return resolve(); + }; + + const transaction = db.transaction(storeName, "readwrite"); + const store = transaction.objectStore(storeName); + + const putRequest = store.put(value); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = (error) => reject(error); + }; + + request.onerror = (error) => reject(error); + }); +}; + +apx.indexedDB.get = async (db, storeName, key) => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(db, 1); + + request.onsuccess = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(storeName)) { + return resolve(null); + } + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + + const getRequest = store.get(key); + + getRequest.onsuccess = () => { + resolve(getRequest.result || null); + }; + + getRequest.onerror = () => resolve(null); + }; + + request.onerror = (error) => reject(error); + }); +}; + +apx.indexedDB.del = async (db, storeName, key) => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(db, 1); + + request.onsuccess = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains(storeName)) { + return resolve(); + }; + + const transaction = db.transaction(storeName, "readwrite"); + const store = transaction.objectStore(storeName); + + const deleteRequest = store.delete(key); + deleteRequest.onsuccess = () => resolve(); + deleteRequest.onerror = (error) => reject(error); + }; + + request.onerror = (error) => reject(error); + }); +}; + +export default apx; \ No newline at end of file diff --git a/wco/privatri/assets/icon.png b/wco/privatri/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c64489cecf2bc78557b5a33780daa0a76f9f2755 GIT binary patch literal 12177 zcmeIYiC>KC8!&!9Pmh|>HkA}f(=JLmrP5-ih4vysk~&C83GLfVQdvThqCG^m3ORL3 z%@9IbNJ>Q$B2rBW(>C*7lk+|2_b>cD@9Xm+-Osh(*L~mDc65!ao#gEKvk^j)4)(V0 z2;uNw9Fauf$5_Ox3HX5>bhoocB~1&yAVftDwpQySwhq3vyy0h;nK{y_s?uw(QLx*e z^oOiQ+AX=%)jQ}%zDYf?Iz#C-CaWo8D;}J%6(!4B)8EI5kdBVjDCc;}~HeIg1C9V$th2QC_ zC<|c(g^uFMWD1jP6YeMc1tUnzg~A^cy00}(_&#Xyo;L%9;RRwd-zn!(5q8cXWaH`? zC?@Lv{hiR#pd&4bw`XN$(5Wcu|B?T{#5jF_lWz=6TuUGQA(Gf7;Agu(oAmR1*XZ(i zQ0ClmaF!+eDss=U>o|hi^wB;;#dnO<_|go&zo)OzNK^w;3^#~imn5@Ku&YXX(%pmV z7z53LJeGGxU-ivIel$Vp&qi3kIjv2-%z?w^GVZl2$(9{`jT2Q`bX0ZHZG?2sne&k= zsMI01_>v@?nMh(m$@NATPE)TN{wYYMBA*E|PXHP80p8E`_LG|m1XVCrX1+WHJ5e}F(PmT z`w!DcfvnjVbw4$Z60&5{?Fh4ZK=xvu`{LhZHMCpa0NJOL4v{3DC|#P`g2lUn{QU(L z=|_H(bs&eV1hOw{-RJ&;qh;^}aE?Yh@BWRGn!@%4V`&WN_fdcIvBndm7_zBl?dgaA zAwrT65=mRRPV^rlY6e1mjz+oa{KNPhdj}9P8ZxO@|4jtP&j^VW{7Ps3CUS!0EF^NV z_Mq->B1Rg%JO(akysg`_Jf-dh$=Ly{x9;)KOFgn8-|eL~rkDNl%{s8TaP1~Zk*}Yo zjoqBz`uG3V^kA*J_zctgoUSz|4ktKqNnv!~Gt6Gr(f$!!Hkg$8h?JvFM@{>`&%vvK zX!Y9D%yq>UhuJFtc!7zVYT0oB7H>UxJVq{9VWqS>_Yc@n8%nV)o690}$(6)K7YZ*2 zVJ-5c^;BP;&+N0rmSe3uNmJl^{RFX83)UpYJ`dW{10o(Jz+5gX3RVfHdsysTGB zV_u60T&V;O==FZc>jbmcM^cYWvKmNzL!vO>$cFbUCL{!lH|2M|GQkQfm5mnzMPsngNuELm=o}QVs~3 z&)hJ4y1n7{y_*#^xiX4p5T^=5QY_6M9d#O z!PT=z?k~n>utMjqrn#$8Q`-0LbO21hRbAdo+aXP1s%{Uxx*4pZcl)QyZ*^)wooI4Y z++w&^u2p)!=BiWyvRxyU@+e)aa;O1Q!E6M9Op{_=y;JKj|-{<=CnzN^sO|(%s z`+I*3$Qg6~3hP#H{>lqCW?hzHhI;#)bq^AJkMqp<pcjxQO>%W@&xb!Sa$=`2cVl-8Kba6moBjM}a-o;@_j%V__YsnEc zx((GgP}_0^*qDv)cJB*-19ADvgDeuesaJ4r`x&$7+3q)&f!9g+U%5vIJ1$g^A*tLL zsEYG+9PhQA?!B8={6alJj*1e*a;Bq_7(WkJUDJ;#E*Y5kP^Ukci^*>tKCfnJxD@CI z{q<$vPTrMM#TB@X*w;G!>9+(Yk5;(6XI-&>JzSuV8_JI>&EtDNeC2hzux{+ZLaCe8 z6>HkOP9dzg{&iC`rIMQEe+3atQ=~#r*@ts5O+0}APdWYdcKVI{>s5|DTG>}Gtr&8lV1MHmk z05^L+zxRnhE!ap%IXt9qQjYnI=?}Y-zkK}iuZ3%Ya%;x?$za?dtkxySpJ#G^sD;B? zz46OF%p!ApardsvE5K7$9FVFm0x3JK@37hx1apluSI|U@0 zdg;Y#odlGUw&O#zBL9R)a11YaFISRXpKi0tF5m<>a!Ph!N27$m;FVO=d3wL^-bJYQ z_QjFM;G&P8FaNe7iJ`bhEKu&81F~m_(Kg+(VxnEATj3^a?YG#J%-#z+e;s>aR4vQg zeWCW}g(SgUi@`8*{JdFAV#`ajgF)8l)AQ*PFC~6>@AqI!{LXAdE^c3EGwh6#PX3x` zn8$B7y!*kA*f|%;27{IE$Ox@ey>Ab%=R}KWDEawPuwD>DvceowT>8>X;g6|-;JX7w z;O)((HY>Jd6*OAI8ClaEyKpD(*ZHo)9?4R*j|B^vHzRC3_sV}@H9NpjeXi-^k;KSa zH0QPpHKbot0re1je>IFPA+g8wxE83+=U~WcCW=|OY0;Uc3BQR0WZ}9ItZxsor-pQd z3hhO*`8%Y;q&34CT|V)SHhw(L6faCDQOCF+n7x~)v&3bX0j5%Sjx$D8w4Tk=w?r1XJAYTgkLatExuij8DIA{`fjKKeA)xk+f&yF^4YX33m{>%H2JVpc@qocy0G7s%#TpU z6ivG=H%@X@{~Gf2m&BFf#`|nNyDzN7)~$^S56qgu9d5MAVU0z8Lz)z`C!*t`Y!fWH zI)EKY__Ejh+WK@Ke^}GcTN^eb)+%wdx^%sp4+y@+>Q<{|j7ZDw;Ae3s*#i)YukM}q zO(?hRa@Pw7h41tQC(rgDKeGiDEQ}+Kt5bN};F4OaFmF8y;>E~O+H6lWtD+MkyJDxR zN-9_a#^f)=OQ5$C8Ghtx3#CnPM%wp1;J>%=IXCU+Id2tv7_&Vc=IV#{3SOC2qEzNH z$?QL)@l@5sq~#V>BLriIKyxJ>pPc|D{1incG2aB8_ zr{NLypg+IDhaasF@=m|rSq*7Wt8U@V>L|X&SWH`rC?U;2ZJSNP1H%DodEAr*g&2ogLLWY2|v2FzWwz>T-9tLV%^upa`L=1g zT5zl>T|pkM>d2$4a}GwV)~2I4$I34~4;5?b_w<}XueA0u05=U`Jk1TVExVKDn63%y zjB!#qyoDz3lAkFfoD&*O=YyPCZ{ zC^MlfWYa4FJ8zIA+w!!*8kvzD&y%RBkY7bCla@`r?wx-l358J7WM$}7$Uw@@lgS;n zufi~FS3}Pi617DK)|QycmWi*+UPTm0hF?)*(m#Xe3?T=0(E3~AWV-IYE2>O371j!= z4s6ve%Z`(U&j^*t_Jy@xx&!;q9ib{z6gu9ij1i(BjO#2X-1X1v-ziveU0ccTWG>ju zS2lS5+q=@T%z(&FNsQ2fCwORf7SWwJoRv&wFJCZt2;paBs3=^_6(3q1(IZN5R>tM6 zM)3|{2&Y5ZSf;N<1sOA~Z(Ii?g?Fl_NGh{o&BCrDXu$`+I*`Waha0KovjH{aOCf zqN!WYL#5VqsldbCa9dYU_`{n-|3TQ({T>npY8-gg19keckJUfF{bMruc-&pxhI&EG z_}li-=^FQEJ#eKw{q*R$2V1bHXzHS$XB?~TE}2e>lwBiZh!qpz@Z{451#5Osp0B8p z6jU_aj%k;DP*uVxPakb|(qVPomh#VJcsq^;KfN z+1Un?T}mHiIqFoXV7xQQ>_I!k0*wX77av8or}-Zj$Ofy1;lVP_bpo~2`igP2(M%jH zab21=N!xl=dyib@^inpfIQB{XG?_lT2y3wFGHI>sUkg11yximrtY#P zmsHfX5&YiUx{QIZF8N)jK&ZH%IW6I=@xUc>z&V-l*kQv}b~ZHd^U(Vq9Y@i6W>!o? z%P-oB<8^&9%(^FWacnM)JTy^nO+zI14{U3tI#x;9Y3G#~CmQn5@6LL>>}db@SxEx& zt{)4@vEyWV4OPG6JlcY8wdz^Z#LWE0Jl_x;-?SuwmG6(t3b?P#{9I=;GuW>|G+x1-D#SI`QkNyXdJ)=-Tx-jt3fko6h_Yq zap?bStDiz!QTq_tG;&)En*^zLpylG{pVtwtbO+2sioLL z4_oA3f93+ncYKaXTp=EiQm1> ztG~e+{j`0#R}$NmR9l^3Lo>Pk^88jbb%yc!b+`A%{^KCo=a<`5v0YX*J%%&$mp`0e2_f|N?D)OG8 zHO=Oa-fVaH>Z}(Lu2-n9k5!tNW+_paSt0fB%*PIDh(G&6K#KD+Dzi;y;=L@h;lTA< zGFY)yXr84F&FK%{Tt`5b&j895h?QZylzaqDb!zr73Y1n6r(Vs`y+oYi*IQ@yP-c> z0gycc5_6R%F32*=wlpuWMNMnIKR-xV)4Vo6G$YyaX4)Nu*C1ZUWsPTK; zRfMOS<&_*4pUyd|wmb)Duf2KZQ%;M*8FcCC^i^vT{cA*({?f5KT8Cb*!F_pl<<3=x z);^AS>QX`dEE(pj5s9^1`ain_ZmT0f0i)XE<&esRwJftdHh4)2F!nzOj1TT{SF#3+ zW$C5G4Yy>TMwf=fglr3siBET5T~!i|8B7J-U2QU%RC@$+G#`qP>_+f1H_0e(aUgq9 zv#QH*Oj85=KS4G66Av2GI);Th7j@o`dQH&*t{Wm9}J_ z0*(qZ96`|qRmXOOgDO7q(*~1=Ya;-$X9m#+5MRhcNDhd{o+Njvj}@r{$7CVLW5Ds$ zkY?uz1SL~HoxbE~{cq#Fq{FqrfG!ZCJFf%umzL|RDS)1LB$;76<|jlyDn!5bzIIba zU+hn%Uuvj=G0KV+f%uID)rhf&b~r)T-=P|;#P{*bWRBsS&wS5`6@khGHPEORDi6UQ zrDEdQF6f<~)~f^*>9drHbFgTwx`R6iM|yY z)z8Nb?3!x2N8jGo_3>N!ml($VE_kl|s@529S(%_KPhsjh6n}vDy}L_nplU*fT|=pl z%R?!-!>7AsnCj&>Zo05fp&`w0rRbegFz+*&&6OFhrD7c?o;^T#VZMw1P;1>OwDqie zoW;Qv&CXWTG0pG8nEl&zU=fRT^1mQ~*1z3jT7$aL#C7JIuHxOXM1c-}Jw#RO_d9z# z_kVur`O=%jP9S0?ducs6HN~HkG$|7o#zm>2Dnic!i%#TEGn_AIW0lj3I03Q-jZWpi zE)c%@S4Y}qm?MTwadl&?ae<<*3C`u}A@Y-V{E;6r%!p5(ZW_q*%=wEYB}oFkv=V|z zJRg~>d`}ZICGF+(5x$(@Gd?$~dwYr?3jJ7W6%$EN^CDl%k?F5@JTAH3(A#qpKHTcL zoKT^T#Ns9IL^VlZ03RMlRZG1~LJJ6E?g88&xYD7- z_&Ha_5hC@pcJ*2boYHJDaFn11{IUJj@>g6HSI8z6_r1CAMPVkeMYp;lQK^cNwj?FK z4djQrrm`NY!25FE-|{rt;c|JnDro_~4nFix2AXfO{7-o$@4${;S~=*ry3}dv)_=-R z+z9CJrQL%MD|@OEJmmgmV#3=~CoRwcLlP|Gk^28^{N;LyPLjX~G?|E+l-l5gq;RE( z6~YXu;JS<9)D>BXxyL3Rs@VJ|)~Tw?^ZBi05bJ}U#iswyo-FYRtm>r+_vBgW+Nq!a ziFKfTc^@}_0~lqfV#GJ}KLg(kxw|`A@Cvvuu5lgdlKt1fPd(dT)iI(VD_GI zIa7wIoNCXOvLDQ#FvGsx$#q7zHD6v{SjW%~hcfXTatFjD^ROfSc|UCN)aB zH^@-9)8L>Rl|=;gWa!T`B>K<8Utjz4R6YuKR~-|JZ}V|QUk!))2)xC~y}f-mFW=V@ zxzg1!?Si)G!|2uauC6AcpyFZ4)pjoJ>1mWh*x{~+s`<54b<0U5!xj%Gc(aKmuF; zS%voVV^xVyhI^dp&-6A0C__S^+I0wRA<~E~#3;}B12UF34pl2y|$v1G5yqrldsn>+J3PC^7Jlg z|JtJ~!(V=v5FAf{uvkPBQxJLkt|Spn9d&=kSX?yqIxKPnLE+Jg(rMKYcT1QK+)37`g~RM~!>{O!gP(hNh+hS$T;3S>C3^S2X0HUblsxcW~u_Zbr1=i3O5 z#9fi*RHz>}=>;xDpP&7l1G!&I2Es@jH~gBl7V;SBTmV@)yljWkG_f|i9COdEz!Ze5 zgDCM|(wK|O$imLdDir?$GEUB62X-Hu%mV`8=HT+?t7u z9J7cmqIqab(#zG5lS1~{Vh|`BOpd{W$*Z#=L*5Ig{=-8D_FIg?td{A7WPK+T@MJGR zhC4eRbtg900ooD_Pt}N&4K^d;!Si#)7MHh9AtX%`&H~8LeRo1xdF_M-XT2i9nPV&s zSt>Lh=(nY)Sh9#JNk|qiM41BKGVno*3AI*(v^x;C#a(8wVt2@X-XA1 zRVvxDOM*%4rOtCznF!B^;?z^~L&RQDTVyQb2`i)+S0)N+k=NinN+r?7EN|EnlqYsj zu50-C^q4*Sj+G44HEPp8MTRG5>yNjkGPOr2%#%@@u0rJ@8DUS(JSuUtXZxQcu}hk6 zkw88kbPD>*)a}{Yg)&U3=uJ;yXYvrXEC2r0+4gMx4ifvS)F=gvDrDV_X>hJ^N%VTl z8zBT`7qBkh&UpG%1CGT@dt)ouD=4C2YWZ967!d=ybTy8X9>NxmJine}dW)bi7c2X% zg9JYp+7*j>9W<$l$fArD+L|eHH~o5w6=xbw-Q&t*UR$69xy8Zo{#H3h`Z^4TJi4u#YH+RaCNdjs#uT!9f_|z7|_|hrU zg3r96(p=QtZ16)$&Ymug@kZ*}i%Z?bN==Es$oC-6zCP%;A;l5u!CBufMoTWm3UN+8 zUpwMYt*ZCQJ+ll%w4P12ZE70izCn`0{^Spk+3w%zOI`i-k5#Z_!^5_|fl zZ}JKsccJ3Y~nu>6PNaDhA+r^L!52!0CTHV&b3+45xk`teb|a{Pc~>J`U*ezW8aB<_HC4 zmBO>z;Zdn?`ZWsCaKq`7HGf(Ine7!hU4!bryx|W!+SDO%H(#KY>I|0bB`ggTu{uT> z{bGp?blIfCT-5+P;YY^60ySTr3bZ4)12`>5D^+OtFN9^s3apOx9eHKCq0QPYh0#IOR8F4+J0WGS??OAobOjuWFxAC@f!v8hHb$+O^v6``76nA3y$4^eP=AbeP( zKa~m{Jj$+m3bLfw7o@>%vL%!~&O)9iU)soU5l&sGrPF7D{R+uuH)KI`*-c$5-JnT- zG8k8I`du3L-sas7o$nso+|M~WAn3Y>Im2lL)kkaH`jA2*=9X^5|;m6kXA&G=;gKTFV;hXg{rLwqJ#p=zxW?9lSH(dl0vfwMk;kaqW^<327dmN}L z#FsZlLU+`{+-<8Z=MrHwjx>=%;>usfE>Yn@AABqvp{C#9+ zO_!n3-F8(v`U*W42V7VHtK&m3lIga0|D+$QLM3iU@b>B8a9j%NJ`wy$QbKUP)h!|% zPc@jMIX!mg=SFE+ZdXyd;Ad~?yBeR2KJNfyVSD{;-acNq(%Z^JIp+R5p?7A(wKc2w z!@k|TQ~CQdp#b6i_;UXSSX?stHZMR3$pgp{!>~GWxc|ieJjpuSON)E#-IIz!l=I(T zgvDL&-{$!XAuRwh@^)COINaRg)RU}4A*4-rDw2@7=wvHHMQblrTh?=jjjP6IYoqUe zLyyDZ!3}3C3D(vzrc#otRA*YxS6!G;MDHHtCow8sGy?oX?N_d}TrzZfz<13@Z0D%#d7AkPyHOfP-)l z&y>zY3z%)6-RoG*-Romv#KRP8RR0c7yp8`po;2t?4Koida{t)kYrxgso{d|t)quX0 z=z^ZGE1vYcldJN_hRjYiI{J$=J}eVq3lCtsRFWY0bXCh%Xjbd%3{Vyo!DvAzo2vrz z04v~eTA;ae`V~lJFrvh-v;Csx51W7IqlJkk^owo3B?&0Ysvg$B_Zq^ICY;ZR?M`C+ zlBr^Qz?z4~py4c73w@r%fT;{V^wBwcF8oviN&Ahr*0H+!=WLzX&DXUypo#b6J-qOF z*%t!f$-hhPVl09okZMy&fp8!MP77sK=L#F}9q&iGc;WJy+kU%Y(?FJRj4ZB_on4~8 z%x*+@u~+vA?WoMPFjI;D-On@VX{@fg)!XB-qc4TO3s;=XkyUOVea=2J0M!>M(TFBCtAQ}Ze0 zRYnHWN7PyVL~s6LsYhcFKaQoPo2`=(M&AR{tHzbEz%$FHx<H)sKSEoh~lWoah zl}=}W-zaR#&_82~S;Dmx7Grc@(!2W?LD7pEUlul!QklVHNet5iVkaS9NjZ1TJW-B{ z{rN;v*gu{xjD#%z{3WhV$FE&9p1Q6c#&!nPop)CUgyI?n^S9Np=5JPNhJAdVbuW4& zfGxEP;Q5?!K$EmJz*%ThX0Tl%e?F`MN7ih_P*DA&>}5m_>RB=3bVQdx)(S zPu5@qYh0XiM4gV_Z!#wXK@lOr^R)m6ZhGrpZW6#@ngHkg7AM}riHP{*Y76w}LVA}2 zF+31%L*wq;V?sC~J?A$#(Fp<$c9;tVRGy(%GHa$i*f|<*28uvzQ^zp$5U1>d_o}>^ zJ`05Iw-*X+q(`U1AWN#cm7wz z6Y`9JjW{t1jK}|2D+5eq9fXQx%p!e+WaYr83yTS3vPP0%@P)dp^9LAgvpLB#$V*h_tk1Y@* X?w@WwYpz&;;E%&9SKAV6-$VZgB=Pf! literal 0 HcmV?d00001 diff --git a/wco/privatri/createThread.js b/wco/privatri/createThread.js new file mode 100644 index 0000000..b29119f --- /dev/null +++ b/wco/privatri/createThread.js @@ -0,0 +1,44 @@ +import apx from "./apx.js"; + +const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn"); + +autoDeletionBtnElArray.forEach(btn => { + btn.addEventListener("click", () => { + autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200")); + btn.classList.add("bg-base-200"); + }); +}); + +document.querySelector("#createThreadBtn").addEventListener("click", async () => { + const { publicKey, privateKey } = await apx.crypto.genKey(); + + const messageObj = await (async (publicKey) => { + const uuid = crypto.randomUUID(); + const timestamp = Date.now(); + const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey); + + return { + privatriid: `${uuid}_${timestamp}`, + thread: uuid, + timestamp: timestamp, + owner: alias, + title: await apx.crypto.encryptMessage(document.querySelector("#threadNameInput").value, publicKey), + sender_alias: alias, + publicKey: publicKey, + aliases: [ + alias + ], + message: await apx.crypto.encryptMessage(document.querySelector("#threadDescriptionInput").value, publicKey), + dt_autodestruction: (() => { + const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200")); + return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10); + })(), + urgencydeletion: document.querySelector('input[type="checkbox"].toggle').checked + }; + })(publicKey); + + // Faire un post sur l'endpoint /privatri + + await apx.indexedDB.set("privatri", "threads", { uuid: messageObj.thread, privateKey: privateKey }); + await apx.indexedDB.set("privatri", "messages", messageObj); +}); \ No newline at end of file diff --git a/wco/privatri/createThread.mustache b/wco/privatri/createThread.mustache new file mode 100644 index 0000000..cfbf198 --- /dev/null +++ b/wco/privatri/createThread.mustache @@ -0,0 +1,34 @@ +
+

New thread

+
+ + + + +
+ + +
+ +

Urgency deletion

+
+ +
\ No newline at end of file diff --git a/wco/privatri/editMessage.mustache b/wco/privatri/editMessage.mustache new file mode 100644 index 0000000..6358d23 --- /dev/null +++ b/wco/privatri/editMessage.mustache @@ -0,0 +1,16 @@ + +
+ {{date}} +
+ + +
+
\ No newline at end of file diff --git a/wco/privatri/inviteAlias.js b/wco/privatri/inviteAlias.js new file mode 100644 index 0000000..29c3247 --- /dev/null +++ b/wco/privatri/inviteAlias.js @@ -0,0 +1,52 @@ +const url = "https://www.facebook.com/"; + +const bodyEl = document.querySelector("body"); +const qrCodeCanvasEl = document.querySelector("#qrCodeCanvas"); +const invitationLinkInputEl = document.querySelector("#invitationLinkInput"); + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +(async () => { + const qrCode = new QRCodeStyling({ + width: 425, + height: 425, + type: "svg", + data: url, + image: "./assets/icon.png", + dotsOptions: { + color: "#ffffff", + type: "rounded" + }, + backgroundOptions: { + color: getComputedStyle(bodyEl).backgroundColor || "#000000", + }, + imageOptions: { + crossOrigin: "anonymous", + margin: 20 + } + }); + + qrCode.append(qrCodeCanvasEl); + + invitationLinkInputEl.value = url; + + copyBtn.addEventListener("click", async () => { + navigator.clipboard.writeText(invitationLinkInputEl.value); + + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("Invitation link copied to clipboard.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }); +})(); \ No newline at end of file diff --git a/wco/privatri/inviteAlias.mustache b/wco/privatri/inviteAlias.mustache new file mode 100644 index 0000000..ce01b6d --- /dev/null +++ b/wco/privatri/inviteAlias.mustache @@ -0,0 +1,12 @@ +
+

Invite an alias into this thread

+
+
+ + +
+
\ No newline at end of file diff --git a/wco/privatri/main_fr.html b/wco/privatri/main_fr.html new file mode 100644 index 0000000..d663c77 --- /dev/null +++ b/wco/privatri/main_fr.html @@ -0,0 +1,108 @@ + + + + + + + + + + + Document + + +
+
+ + + +
+
+ + + + + +
+
+
+
+ {threadName} +
+ +
+
+
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/wco/privatri/main_fr.mustache b/wco/privatri/main_fr.mustache index 1a6a152..fc6c1a7 100644 --- a/wco/privatri/main_fr.mustache +++ b/wco/privatri/main_fr.mustache @@ -1,8 +1,93 @@ -
- -

Messages Privés (privatri)

-

Ce composant gère le stockage sécurisé des messages dans le navigateur.

-
- -
-
+
+
+ + + +
+
+ + + + + +
+
+
+
+ {threadName} +
+ +
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/wco/privatri/message.mustache b/wco/privatri/message.mustache new file mode 100644 index 0000000..9aa563f --- /dev/null +++ b/wco/privatri/message.mustache @@ -0,0 +1,23 @@ +
+
+
+ {{message}} +
+
+
+
+ {{date}} + +
+
\ No newline at end of file diff --git a/wco/privatri/output.css b/wco/privatri/output.css new file mode 100644 index 0000000..e25d2bc --- /dev/null +++ b/wco/privatri/output.css @@ -0,0 +1,2733 @@ +/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --spacing: 0.25rem; + --container-xs: 20rem; + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --radius-lg: 0.5rem; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .drawer-side { + pointer-events: none; + visibility: hidden; + position: fixed; + inset-inline-start: calc(0.25rem * 0); + top: calc(0.25rem * 0); + z-index: 1; + grid-column-start: 1; + grid-row-start: 1; + display: grid; + width: 100%; + grid-template-columns: repeat(1, minmax(0, 1fr)); + grid-template-rows: repeat(1, minmax(0, 1fr)); + align-items: flex-start; + justify-items: start; + overflow-x: hidden; + overflow-y: hidden; + overscroll-behavior: contain; + opacity: 0%; + transition: opacity 0.2s ease-out 0.1s allow-discrete, visibility 0.3s ease-out 0.1s allow-discrete; + height: 100vh; + height: 100dvh; + > .drawer-overlay { + position: sticky; + top: calc(0.25rem * 0); + cursor: pointer; + place-self: stretch; + background-color: oklch(0% 0 0 / 40%); + } + > * { + grid-column-start: 1; + grid-row-start: 1; + } + > *:not(.drawer-overlay) { + will-change: transform; + transition: translate 0.3s ease-out; + translate: -100%; + [dir="rtl"] & { + translate: 100%; + } + } + } + .drawer-open { + > .drawer-side { + overflow-y: auto; + } + > .drawer-toggle { + display: none; + & ~ .drawer-side { + pointer-events: auto; + visibility: visible; + position: sticky; + display: block; + width: auto; + overscroll-behavior: auto; + opacity: 100%; + & > .drawer-overlay { + cursor: default; + background-color: transparent; + } + & > *:not(.drawer-overlay) { + translate: 0%; + [dir="rtl"] & { + translate: 0%; + } + } + } + &:checked ~ .drawer-side { + pointer-events: auto; + visibility: visible; + } + } + } + .drawer-toggle { + position: fixed; + height: calc(0.25rem * 0); + width: calc(0.25rem * 0); + appearance: none; + opacity: 0%; + &:checked { + & ~ .drawer-side { + pointer-events: auto; + visibility: visible; + overflow-y: auto; + opacity: 100%; + & > *:not(.drawer-overlay) { + translate: 0%; + } + } + } + &:focus-visible ~ .drawer-content label.drawer-button { + outline: 2px solid; + outline-offset: 2px; + } + } + .menu { + display: flex; + width: fit-content; + flex-direction: column; + flex-wrap: wrap; + padding: calc(0.25rem * 2); + --menu-active-fg: var(--color-neutral-content); + --menu-active-bg: var(--color-neutral); + font-size: 0.875rem; + :where(li ul) { + position: relative; + margin-inline-start: calc(0.25rem * 4); + padding-inline-start: calc(0.25rem * 2); + white-space: nowrap; + &:before { + position: absolute; + inset-inline-start: calc(0.25rem * 0); + top: calc(0.25rem * 3); + bottom: calc(0.25rem * 3); + background-color: var(--color-base-content); + opacity: 10%; + width: var(--border); + content: ""; + } + } + :where(li > .menu-dropdown:not(.menu-dropdown-show)) { + display: none; + } + :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + display: grid; + grid-auto-flow: column; + align-content: flex-start; + align-items: center; + gap: calc(0.25rem * 2); + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1.5); + text-align: start; + transition-property: color, background-color, box-shadow; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + grid-auto-columns: minmax(auto, max-content) auto max-content; + text-wrap: balance; + user-select: none; + } + :where(li > details > summary) { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + &::-webkit-details-marker { + display: none; + } + } + :where(li > details > summary), :where(li > .menu-dropdown-toggle) { + &:after { + justify-self: flex-end; + display: block; + height: 0.375rem; + width: 0.375rem; + rotate: -135deg; + translate: 0 -1px; + transition-property: rotate, translate; + transition-duration: 0.2s; + content: ""; + transform-origin: 50% 50%; + box-shadow: 2px 2px inset; + pointer-events: none; + } + } + :where(li > details[open] > summary):after, :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { + rotate: 45deg; + translate: 0 1px; + } + :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title), li:not(.menu-title, .disabled) > details > summary:not(.menu-title) ):not(.menu-active, :active, .btn) { + &.menu-focus, &:focus-visible { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + color: var(--color-base-content); + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title):not(.menu-active, :active, .btn):hover, li:not(.menu-title, .disabled) > details > summary:not(.menu-title):not(.menu-active, :active, .btn):hover ) { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + box-shadow: 0 1px oklch(0% 0 0 / 0.01) inset, 0 -1px oklch(100% 0 0 / 0.01) inset; + } + :where(li:empty) { + background-color: var(--color-base-content); + opacity: 10%; + margin: 0.5rem 1rem; + height: 1px; + } + :where(li) { + position: relative; + display: flex; + flex-shrink: 0; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + .badge { + justify-self: flex-end; + } + & > *:not(ul, .menu-title, details, .btn):active, & > *:not(ul, .menu-title, details, .btn).menu-active, & > details > summary:active { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + color: var(--menu-active-fg); + background-color: var(--menu-active-bg); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + &:not(&:active) { + box-shadow: 0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg); + } + } + &.menu-disabled { + pointer-events: none; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + } + .dropdown:focus-within { + .menu-dropdown-toggle:after { + rotate: 45deg; + translate: 0 1px; + } + } + .dropdown-content { + margin-top: calc(0.25rem * 2); + padding: calc(0.25rem * 2); + &:before { + display: none; + } + } + } + .dropdown { + position: relative; + display: inline-block; + position-area: var(--anchor-v, bottom) var(--anchor-h, span-right); + & > *:not(summary):focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + .dropdown-content { + position: absolute; + } + &:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within) { + .dropdown-content { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + } + &[popover], .dropdown-content { + z-index: 999; + animation: dropdown 0.2s; + transition-property: opacity, scale, display; + transition-behavior: allow-discrete; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + @starting-style { + &[popover], .dropdown-content { + scale: 95%; + opacity: 0; + } + } + &.dropdown-open, &:not(.dropdown-hover):focus, &:focus-within { + > [tabindex]:first-child { + pointer-events: none; + } + .dropdown-content { + opacity: 100%; + } + } + &.dropdown-hover:hover { + .dropdown-content { + opacity: 100%; + scale: 100%; + } + } + &:is(details) { + summary { + &::-webkit-details-marker { + display: none; + } + } + } + &.dropdown-open, &:focus, &:focus-within { + .dropdown-content { + scale: 100%; + } + } + &:where([popover]) { + background: #0000; + } + &[popover] { + position: fixed; + color: inherit; + @supports not (position-area: bottom) { + margin: auto; + &.dropdown-open:not(:popover-open) { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + &::backdrop { + background-color: color-mix(in oklab, #000 30%, #0000); + } + } + &:not(.dropdown-open, :popover-open) { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + } + } + .btn { + :where(&) { + width: unset; + } + display: inline-flex; + flex-shrink: 0; + cursor: pointer; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: calc(0.25rem * 1.5); + text-align: center; + vertical-align: middle; + outline-offset: 2px; + webkit-user-select: none; + user-select: none; + padding-inline: var(--btn-p); + color: var(--btn-fg); + --tw-prose-links: var(--btn-fg); + height: var(--size); + font-size: var(--fontsize, 0.875rem); + font-weight: 600; + outline-color: var(--btn-color, var(--color-base-content)); + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 0.2s; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + background-color: var(--btn-bg); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--btn-noise); + border-width: var(--border); + border-style: solid; + border-color: var(--btn-border); + text-shadow: 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 0.15)); + touch-action: manipulation; + box-shadow: 0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow); + --size: calc(var(--size-field, 0.25rem) * 10); + --btn-bg: var(--btn-color, var(--color-base-200)); + --btn-fg: var(--color-base-content); + --btn-p: 1rem; + --btn-border: var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%)); + } + --btn-shadow: 0 3px 2px -2px var(--btn-bg), + 0 4px 3px -2px var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), + 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000); + } + --btn-noise: var(--fx-noise); + .prose & { + text-decoration-line: none; + } + @media (hover: hover) { + &:hover { + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + } + } + &:focus-visible { + outline-width: 2px; + outline-style: solid; + isolation: isolate; + } + &:active:not(.btn-active) { + translate: 0 0.5px; + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 5%); + } + --btn-border: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + --btn-shadow: 0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0); + } + &:is(:disabled, [disabled], .btn-disabled) { + &:not(.btn-link, .btn-ghost) { + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + box-shadow: none; + } + pointer-events: none; + --btn-border: #0000; + --btn-noise: none; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + @media (hover: hover) { + &:hover { + pointer-events: none; + background-color: var(--color-neutral); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-neutral) 20%, transparent); + } + --btn-border: #0000; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + } + } + } + &:is(input[type="checkbox"], input[type="radio"]) { + appearance: none; + &::after { + content: attr(aria-label); + } + } + &:where(input:checked:not(.filter .btn)) { + --btn-color: var(--color-primary); + --btn-fg: var(--color-primary-content); + isolation: isolate; + } + } + .list { + display: flex; + flex-direction: column; + font-size: 0.875rem; + :where(.list-row) { + --list-grid-cols: minmax(0, auto) 1fr; + position: relative; + display: grid; + grid-auto-flow: column; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding: calc(0.25rem * 4); + word-break: break-word; + grid-template-columns: var(--list-grid-cols); + &:has(.list-col-grow:nth-child(1)) { + --list-grid-cols: 1fr; + } + &:has(.list-col-grow:nth-child(2)) { + --list-grid-cols: minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(3)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(4)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(5)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(6)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) + minmax(0, auto) 1fr; + } + :not(.list-col-wrap) { + grid-row-start: 1; + } + } + & > :not(:last-child) { + &.list-row, .list-row { + &:after { + content: ""; + border-bottom: var(--border) solid; + inset-inline: var(--radius-box); + position: absolute; + bottom: calc(0.25rem * 0); + border-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent); + } + } + } + } + } + .toast { + position: fixed; + inset-inline-start: auto; + inset-inline-end: calc(0.25rem * 4); + top: auto; + bottom: calc(0.25rem * 4); + display: flex; + flex-direction: column; + gap: calc(0.25rem * 2); + background-color: transparent; + translate: var(--toast-x, 0) var(--toast-y, 0); + width: max-content; + max-width: calc(100vw - 2rem); + & > * { + animation: toast 0.25s ease-out; + } + &:where(.toast-start) { + inset-inline-start: calc(0.25rem * 4); + inset-inline-end: auto; + --toast-x: 0; + } + &:where(.toast-center) { + inset-inline-start: calc(1/2 * 100%); + inset-inline-end: calc(1/2 * 100%); + --toast-x: -50%; + } + &:where(.toast-end) { + inset-inline-start: auto; + inset-inline-end: calc(0.25rem * 4); + --toast-x: 0; + } + &:where(.toast-bottom) { + top: auto; + bottom: calc(0.25rem * 4); + --toast-y: 0; + } + &:where(.toast-middle) { + top: calc(1/2 * 100%); + bottom: auto; + --toast-y: -50%; + } + &:where(.toast-top) { + top: calc(0.25rem * 4); + bottom: auto; + --toast-y: 0; + } + } + .toggle { + border: var(--border) solid currentColor; + color: var(--input-color); + position: relative; + display: inline-grid; + flex-shrink: 0; + cursor: pointer; + appearance: none; + place-content: center; + vertical-align: middle; + webkit-user-select: none; + user-select: none; + grid-template-columns: 0fr 1fr 1fr; + --radius-selector-max: calc( + var(--radius-selector) + var(--radius-selector) + var(--radius-selector) + ); + border-radius: calc( var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)) ); + padding: var(--toggle-p); + box-shadow: 0 1px currentColor inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset; + } + transition: color 0.3s, grid-template-columns 0.2s; + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --toggle-p: calc(var(--size) * 0.125); + --size: calc(var(--size-selector, 0.25rem) * 6); + width: calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2); + height: var(--size); + > * { + z-index: 1; + grid-column: span 1 / span 1; + grid-column-start: 2; + grid-row-start: 1; + height: 100%; + cursor: pointer; + appearance: none; + background-color: transparent; + padding: calc(0.25rem * 0.5); + transition: opacity 0.2s, rotate 0.4s; + border: none; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:nth-child(2) { + color: var(--color-base-100); + rotate: 0deg; + } + &:nth-child(3) { + color: var(--color-base-100); + opacity: 0%; + rotate: -15deg; + } + } + &:has(:checked) { + > :nth-child(2) { + opacity: 0%; + rotate: 15deg; + } + > :nth-child(3) { + opacity: 100%; + rotate: 0deg; + } + } + &:before { + position: relative; + inset-inline-start: calc(0.25rem * 0); + grid-column-start: 2; + grid-row-start: 1; + aspect-ratio: 1 / 1; + height: 100%; + border-radius: var(--radius-selector); + background-color: currentColor; + translate: 0; + --tw-content: ""; + content: var(--tw-content); + transition: background-color 0.1s, translate 0.2s, inset-inline-start 0.2s; + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000); + } + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + } + @media (forced-colors: active) { + &:before { + outline-style: var(--tw-outline-style); + outline-width: 1px; + outline-offset: calc(1px * -1); + } + } + @media print { + &:before { + outline: 0.25rem solid; + outline-offset: -1rem; + } + } + &:focus-visible, &:has(:focus-visible) { + outline: 2px solid currentColor; + outline-offset: 2px; + } + &:checked, &[aria-checked="true"], &:has(> input:checked) { + grid-template-columns: 1fr 1fr 0fr; + background-color: var(--color-base-100); + --input-color: var(--color-base-content); + &:before { + background-color: currentColor; + } + @starting-style { + &:before { + opacity: 0; + } + } + } + &:indeterminate { + grid-template-columns: 0.5fr 1fr 0.5fr; + } + &:disabled { + cursor: not-allowed; + opacity: 30%; + &:before { + background-color: transparent; + border: var(--border) solid currentColor; + } + } + } + .input { + cursor: text; + border: var(--border) solid #0000; + position: relative; + display: inline-flex; + flex-shrink: 1; + appearance: none; + align-items: center; + gap: calc(0.25rem * 2); + background-color: var(--color-base-100); + padding-inline: calc(0.25rem * 3); + vertical-align: middle; + white-space: nowrap; + width: clamp(3rem, 20rem, 100%); + height: var(--size); + font-size: 0.875rem; + touch-action: manipulation; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --size: calc(var(--size-field, 0.25rem) * 10); + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + &:where(input) { + display: inline-flex; + } + :where(input) { + display: inline-flex; + height: 100%; + width: 100%; + appearance: none; + background-color: transparent; + border: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + :where(input[type="url"]), :where(input[type="email"]) { + direction: ltr; + } + :where(input[type="date"]) { + display: inline-block; + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + z-index: 1; + } + &:has(> input[disabled]), &:is(:disabled, [disabled]) { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + } + &:has(> input[disabled]) > input[disabled] { + cursor: not-allowed; + } + &::-webkit-date-and-time-value { + text-align: inherit; + } + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * -3); + } + } + &::-webkit-calendar-picker-indicator { + position: absolute; + inset-inline-end: 0.75em; + } + } + .table { + font-size: 0.875rem; + position: relative; + width: 100%; + border-radius: var(--radius-box); + text-align: left; + &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { + text-align: right; + } + tr.row-hover { + &, &:nth-child(even) { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + } + } + } + } + :where(th, td) { + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + vertical-align: middle; + } + :where(thead, tfoot) { + white-space: nowrap; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 60%, transparent); + } + font-size: 0.875rem; + font-weight: 600; + } + :where(tfoot) { + border-top: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-top: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000); + } + } + :where(.table-pin-rows thead tr) { + position: sticky; + top: calc(0.25rem * 0); + z-index: 1; + background-color: var(--color-base-100); + } + :where(.table-pin-rows tfoot tr) { + position: sticky; + bottom: calc(0.25rem * 0); + z-index: 1; + background-color: var(--color-base-100); + } + :where(.table-pin-cols tr th) { + position: sticky; + right: calc(0.25rem * 0); + left: calc(0.25rem * 0); + background-color: var(--color-base-100); + } + :where(thead tr, tbody tr:not(:last-child)) { + border-bottom: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-bottom: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000); + } + } + } + .chat-bubble { + position: relative; + display: block; + width: fit-content; + border-radius: var(--radius-field); + background-color: var(--color-base-300); + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 2); + color: var(--color-base-content); + grid-row-end: 3; + min-height: 2rem; + min-width: 2.5rem; + max-width: 90%; + &:before { + position: absolute; + bottom: calc(0.25rem * 0); + height: calc(0.25rem * 3); + width: calc(0.25rem * 3); + background-color: inherit; + content: ""; + mask-repeat: no-repeat; + mask-image: var(--mask-chat); + mask-position: 0px -1px; + mask-size: 13px; + } + } + .checkbox { + border: var(--border) solid var(--input-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000)); + } + position: relative; + display: inline-block; + flex-shrink: 0; + cursor: pointer; + appearance: none; + border-radius: var(--radius-selector); + padding: calc(0.25rem * 1); + vertical-align: middle; + color: var(--color-base-content); + box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0 #0000 inset, 0 0 #0000; + transition: background-color 0.2s, box-shadow 0.2s; + --size: calc(var(--size-selector, 0.25rem) * 6); + width: var(--size); + height: var(--size); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + &:before { + --tw-content: ""; + content: var(--tw-content); + display: block; + width: 100%; + height: 100%; + rotate: 45deg; + background-color: currentColor; + opacity: 0%; + transition: clip-path 0.3s, opacity 0.1s, rotate 0.3s, translate 0.3s; + transition-delay: 0.1s; + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 70% 80%, 70% 100%); + box-shadow: 0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + font-size: 1rem; + line-height: 0.75; + } + &:focus-visible { + outline: 2px solid var(--input-color, currentColor); + outline-offset: 2px; + } + &:checked, &[aria-checked="true"] { + background-color: var(--input-color, #0000); + box-shadow: 0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)); + &:before { + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 0%, 70% 0%, 70% 100%); + opacity: 100%; + } + @media (forced-colors: active) { + &:before { + rotate: 0deg; + background-color: transparent; + --tw-content: "✔︎"; + clip-path: none; + } + } + @media print { + &:before { + rotate: 0deg; + background-color: transparent; + --tw-content: "✔︎"; + clip-path: none; + } + } + } + &:indeterminate { + &:before { + rotate: 0deg; + opacity: 100%; + translate: 0 -35%; + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 80% 80%, 80% 100%); + } + } + &:disabled { + cursor: not-allowed; + opacity: 20%; + } + } + .radio { + position: relative; + display: inline-block; + flex-shrink: 0; + cursor: pointer; + appearance: none; + border-radius: calc(infinity * 1px); + padding: calc(0.25rem * 1); + vertical-align: middle; + border: var(--border) solid var(--input-color, currentColor); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid var(--input-color, color-mix(in srgb, currentColor 20%, #0000)); + } + box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset; + --size: calc(var(--size-selector, 0.25rem) * 6); + width: var(--size); + height: var(--size); + color: var(--input-color, currentColor); + &:before { + display: block; + width: 100%; + height: 100%; + border-radius: calc(infinity * 1px); + --tw-content: ""; + content: var(--tw-content); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + } + &:focus-visible { + outline: 2px solid currentColor; + } + &:checked, &[aria-checked="true"] { + animation: radio 0.2s ease-out; + border-color: currentColor; + background-color: var(--color-base-100); + &:before { + background-color: currentColor; + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)); + } + @media (forced-colors: active) { + &:before { + outline-style: var(--tw-outline-style); + outline-width: 1px; + outline-offset: calc(1px * -1); + } + } + @media print { + &:before { + outline: 0.25rem solid; + outline-offset: -1rem; + } + } + } + &:disabled { + cursor: not-allowed; + opacity: 20%; + } + } + .rating { + position: relative; + display: inline-flex; + vertical-align: middle; + & input { + border: none; + appearance: none; + } + :where(*) { + animation: rating 0.25s ease-out; + height: calc(0.25rem * 6); + width: calc(0.25rem * 6); + border-radius: 0; + background-color: var(--color-base-content); + opacity: 20%; + &:is(input) { + cursor: pointer; + } + } + & .rating-hidden { + width: calc(0.25rem * 2); + background-color: transparent; + } + input[type="radio"]:checked { + background-image: none; + } + * { + &:checked, &[aria-checked="true"], &[aria-current="true"], &:has(~ *:checked, ~ *[aria-checked="true"], ~ *[aria-current="true"]) { + opacity: 100%; + } + &:focus-visible { + transition: scale 0.2s ease-out; + scale: 1.1; + } + } + & *:active:focus { + animation: none; + scale: 1.1; + } + &.rating-xs :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 4); + height: calc(0.25rem * 4); + } + &.rating-sm :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 5); + height: calc(0.25rem * 5); + } + &.rating-md :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 6); + height: calc(0.25rem * 6); + } + &.rating-lg :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 7); + height: calc(0.25rem * 7); + } + &.rating-xl :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 8); + height: calc(0.25rem * 8); + } + } + .progress { + position: relative; + height: calc(0.25rem * 2); + width: 100%; + appearance: none; + overflow: hidden; + border-radius: var(--radius-box); + background-color: currentColor; + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, currentColor 20%, transparent); + } + color: var(--color-base-content); + &:indeterminate { + background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% ); + background-size: 200%; + background-position-x: 15%; + animation: progress 5s ease-in-out infinite; + @supports (-moz-appearance: none) { + &::-moz-progress-bar { + background-color: transparent; + background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% ); + background-size: 200%; + background-position-x: 15%; + animation: progress 5s ease-in-out infinite; + } + } + } + @supports (-moz-appearance: none) { + &::-moz-progress-bar { + border-radius: var(--radius-box); + background-color: currentColor; + } + } + @supports (-webkit-appearance: none) { + &::-webkit-progress-bar { + border-radius: var(--radius-box); + background-color: transparent; + } + &::-webkit-progress-value { + border-radius: var(--radius-box); + background-color: currentColor; + } + } + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .dropdown-right { + --anchor-h: right; + --anchor-v: span-bottom; + .dropdown-content { + inset-inline-start: 100%; + top: calc(0.25rem * 0); + bottom: auto; + transform-origin: left; + } + } + .chat-start { + place-items: start; + grid-template-columns: auto 1fr; + .chat-header { + grid-column-start: 2; + } + .chat-footer { + grid-column-start: 2; + } + .chat-image { + grid-column-start: 1; + } + .chat-bubble { + grid-column-start: 2; + border-end-start-radius: 0; + &:before { + transform: rotateY(0deg); + inset-inline-start: -0.75rem; + } + [dir="rtl"] &:before { + transform: rotateY(180deg); + } + } + } + .dropdown-left { + --anchor-h: left; + --anchor-v: span-bottom; + .dropdown-content { + inset-inline-end: 100%; + top: calc(0.25rem * 0); + bottom: auto; + transform-origin: right; + } + } + .dropdown-center { + --anchor-h: center; + :where(.dropdown-content) { + inset-inline-end: calc(1/2 * 100%); + translate: 50% 0; + [dir="rtl"] & { + translate: -50% 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: center; + .dropdown-content { + top: auto; + bottom: calc(1/2 * 100%); + translate: 0 50%; + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: center; + .dropdown-content { + top: auto; + bottom: calc(1/2 * 100%); + translate: 0 50%; + } + } + } + .dropdown-end { + --anchor-h: span-left; + :where(.dropdown-content) { + inset-inline-end: calc(0.25rem * 0); + translate: 0 0; + [dir="rtl"] & { + translate: 0 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + } + .right-8 { + right: calc(var(--spacing) * 8); + } + .bottom-8 { + bottom: calc(var(--spacing) * 8); + } + .textarea { + border: var(--border) solid #0000; + min-height: calc(0.25rem * 20); + flex-shrink: 1; + appearance: none; + border-radius: var(--radius-field); + background-color: var(--color-base-100); + padding-block: calc(0.25rem * 2); + vertical-align: middle; + width: clamp(3rem, 20rem, 100%); + padding-inline-start: 0.75rem; + padding-inline-end: 0.75rem; + font-size: 0.875rem; + touch-action: manipulation; + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + textarea { + appearance: none; + background-color: transparent; + border: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + } + &:has(> textarea[disabled]), &:is(:disabled, [disabled]) { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + } + &:has(> textarea[disabled]) > textarea[disabled] { + cursor: not-allowed; + } + } + .z-1 { + z-index: 1; + } + .drawer-content { + grid-column-start: 2; + grid-row-start: 1; + min-width: calc(0.25rem * 0); + } + .chat-image { + grid-row: span 2 / span 2; + align-self: flex-end; + } + .chat-footer { + grid-row-start: 3; + display: flex; + gap: calc(0.25rem * 1); + font-size: 0.6875rem; + } + .chat-header { + grid-row-start: 1; + display: flex; + gap: calc(0.25rem * 1); + font-size: 0.6875rem; + } + .m-1 { + margin: calc(var(--spacing) * 1); + } + .filter { + display: flex; + flex-wrap: wrap; + input[type="radio"] { + width: auto; + } + input { + overflow: hidden; + opacity: 100%; + scale: 1; + transition: margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s; + &:not(:last-child) { + margin-inline-end: calc(0.25rem * 1); + } + &.filter-reset { + aspect-ratio: 1 / 1; + &::after { + content: "×"; + } + } + } + &:not(:has(input:checked:not(.filter-reset))) { + .filter-reset, input[type="reset"] { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + &:has(input:checked:not(.filter-reset)) { + input:not(:checked, .filter-reset, input[type="reset"]) { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-8 { + margin-top: calc(var(--spacing) * 8); + } + .mt-16 { + margin-top: calc(var(--spacing) * 16); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .ml-4 { + margin-left: calc(var(--spacing) * 4); + } + .ml-auto { + margin-left: auto; + } + .badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: calc(0.25rem * 2); + border-radius: var(--radius-selector); + vertical-align: middle; + color: var(--badge-fg); + border: var(--border) solid var(--badge-color, var(--color-base-200)); + font-size: 0.875rem; + width: fit-content; + padding-inline: calc(0.25rem * 3 - var(--border)); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + background-color: var(--badge-bg); + --badge-bg: var(--badge-color, var(--color-base-100)); + --badge-fg: var(--color-base-content); + --size: calc(var(--size-selector, 0.25rem) * 6); + height: var(--size); + } + .alert { + display: grid; + align-items: center; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + color: var(--color-base-content); + background-color: var(--alert-color, var(--color-base-200)); + justify-content: start; + justify-items: start; + grid-auto-flow: column; + grid-template-columns: auto; + text-align: start; + border: var(--border) solid var(--color-base-200); + font-size: 0.875rem; + line-height: 1.25rem; + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08)); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px color-mix( in oklab, color-mix(in oklab, #000 20%, var(--alert-color, var(--color-base-200))) calc(var(--depth) * 20%), #0000 ), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08)); + } + &:has(:nth-child(2)) { + grid-template-columns: auto minmax(auto, 1fr); + } + &.alert-outline { + background-color: transparent; + color: var(--alert-color); + box-shadow: none; + background-image: none; + } + &.alert-dash { + background-color: transparent; + color: var(--alert-color); + border-style: dashed; + box-shadow: none; + background-image: none; + } + &.alert-soft { + color: var(--alert-color, var(--color-base-content)); + background: var(--alert-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + background: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 8%, var(--color-base-100) ); + } + border-color: var(--alert-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 10%, var(--color-base-100) ); + } + box-shadow: none; + background-image: none; + } + } + .join { + display: inline-flex; + align-items: stretch; + --join-ss: 0; + --join-se: 0; + --join-es: 0; + --join-ee: 0; + :where(.join-item) { + border-start-start-radius: var(--join-ss, 0); + border-start-end-radius: var(--join-se, 0); + border-end-start-radius: var(--join-es, 0); + border-end-end-radius: var(--join-ee, 0); + * { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + > .join-item:where(:first-child) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + :first-child:not(:last-child) { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + } + > .join-item:where(:last-child) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + :last-child:not(:first-child) { + :where(.join-item) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + } + > .join-item:where(:only-child) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + :only-child { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + } + .chat { + display: grid; + column-gap: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1); + --mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e"); + } + .prose { + :root & { + --tw-prose-body: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, #0000); + } + --tw-prose-headings: var(--color-base-content); + --tw-prose-lead: var(--color-base-content); + --tw-prose-links: var(--color-base-content); + --tw-prose-bold: var(--color-base-content); + --tw-prose-counters: var(--color-base-content); + --tw-prose-bullets: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-bullets: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --tw-prose-hr: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-hr: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --tw-prose-quotes: var(--color-base-content); + --tw-prose-quote-borders: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-quote-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --tw-prose-captions: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-captions: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --tw-prose-code: var(--color-base-content); + --tw-prose-pre-code: var(--color-neutral-content); + --tw-prose-pre-bg: var(--color-neutral); + --tw-prose-th-borders: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-th-borders: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --tw-prose-td-borders: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --tw-prose-kbd: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-kbd: color-mix(in oklab, var(--color-base-content) 80%, #0000); + } + :where(code):not(pre > code) { + background-color: var(--color-base-200); + border-radius: var(--radius-selector); + border: var(--border) solid var(--color-base-300); + padding-inline: 0.5em; + font-weight: inherit; + &:before, &:after { + display: none; + } + } + } + } + .block { + display: block; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .table { + display: table; + } + .size-12 { + width: calc(var(--spacing) * 12); + height: calc(var(--spacing) * 12); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-16 { + height: calc(var(--spacing) * 16); + } + .h-20 { + height: calc(var(--spacing) * 20); + } + .h-fit { + height: fit-content; + } + .h-full { + height: 100%; + } + .h-screen { + height: 100vh; + } + .max-h-12 { + max-height: calc(var(--spacing) * 12); + } + .max-h-screen { + max-height: 100vh; + } + .min-h-0 { + min-height: calc(var(--spacing) * 0); + } + .btn-wide { + width: 100%; + max-width: calc(0.25rem * 64); + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-16 { + width: calc(var(--spacing) * 16); + } + .w-20 { + width: calc(var(--spacing) * 20); + } + .w-52 { + width: calc(var(--spacing) * 52); + } + .w-100 { + width: calc(var(--spacing) * 100); + } + .w-fit { + width: fit-content; + } + .w-full { + width: 100%; + } + .w-screen { + width: 100vw; + } + .max-w-20 { + max-width: calc(var(--spacing) * 20); + } + .max-w-\[20rem\] { + max-width: 20rem; + } + .min-w-110 { + min-width: calc(var(--spacing) * 110); + } + .min-w-xs { + min-width: var(--container-xs); + } + .flex-1 { + flex: 1; + } + .flex-shrink { + flex-shrink: 1; + } + .flex-shrink-0 { + flex-shrink: 0; + } + .border-collapse { + border-collapse: collapse; + } + .scale-50 { + --tw-scale-x: 50%; + --tw-scale-y: 50%; + --tw-scale-z: 50%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .skeleton { + border-radius: var(--radius-box); + background-color: var(--color-base-300); + @media (prefers-reduced-motion: reduce) { + transition-duration: 15s; + } + will-change: background-position; + animation: skeleton 1.8s ease-in-out infinite; + background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% ); + background-size: 200% auto; + background-repeat: no-repeat; + background-position-x: -50%; + } + .link { + cursor: pointer; + text-decoration-line: underline; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } + } + .resize { + resize: both; + } + .grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .grid-cols-\[repeat\(auto-fit\,minmax\(5rem\,1fr\)\)\] { + grid-template-columns: repeat(auto-fit,minmax(5rem,1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .justify-evenly { + justify-content: space-evenly; + } + .gap-0 { + gap: calc(var(--spacing) * 0); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-x-2 { + column-gap: calc(var(--spacing) * 2); + } + .gap-x-4 { + column-gap: calc(var(--spacing) * 4); + } + .gap-x-8 { + column-gap: calc(var(--spacing) * 8); + } + .gap-y-6 { + row-gap: calc(var(--spacing) * 6); + } + .gap-y-8 { + row-gap: calc(var(--spacing) * 8); + } + .overflow-y-auto { + overflow-y: auto; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-box { + border-radius: var(--radius-box); + } + .rounded-box { + border-radius: var(--radius-box); + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-tl-lg { + border-top-left-radius: var(--radius-lg); + } + .rounded-tr-lg { + border-top-right-radius: var(--radius-lg); + } + .rounded-br-lg { + border-bottom-right-radius: var(--radius-lg); + } + .rounded-bl-lg { + border-bottom-left-radius: var(--radius-lg); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-none { + --tw-border-style: none; + border-style: none; + } + .border-solid { + --tw-border-style: solid; + border-style: solid; + } + .alert-info { + border-color: var(--color-info); + color: var(--color-info-content); + --alert-color: var(--color-info); + } + .border-transparent { + border-color: transparent; + } + .border-r-base-300 { + border-right-color: var(--color-base-300); + } + .bg-base-100 { + background-color: var(--color-base-100); + } + .bg-base-200 { + background-color: var(--color-base-200); + } + .bg-base-300 { + background-color: var(--color-base-300); + } + .bg-base-content { + background-color: var(--color-base-content); + } + .bg-info { + background-color: var(--color-info); + } + .bg-transparent { + background-color: transparent; + } + .mask-repeat { + mask-repeat: repeat; + } + .fill-current { + fill: currentcolor; + } + .object-cover { + object-fit: cover; + } + .p-0 { + padding: calc(var(--spacing) * 0); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } + .pt-4 { + padding-top: calc(var(--spacing) * 4); + } + .pr-0 { + padding-right: calc(var(--spacing) * 0); + } + .pr-8 { + padding-right: calc(var(--spacing) * 8); + } + .pb-1 { + padding-bottom: calc(var(--spacing) * 1); + } + .pb-4 { + padding-bottom: calc(var(--spacing) * 4); + } + .pl-8 { + padding-left: calc(var(--spacing) * 8); + } + .text-center { + text-align: center; + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .text-wrap { + text-wrap: wrap; + } + .text-base-100 { + color: var(--color-base-100); + } + .text-base-content { + color: var(--color-base-content); + } + .text-info-content { + color: var(--color-info-content); + } + .underline { + text-decoration-line: underline; + } + .opacity-50 { + opacity: 50%; + } + .opacity-75 { + opacity: 75%; + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-none { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } + .btn-neutral { + --btn-color: var(--color-neutral); + --btn-fg: var(--color-neutral-content); + } + .outline-none { + --tw-outline-style: none; + outline-style: none; + } + .group-hover\:flex { + &:is(:where(.group):hover *) { + @media (hover: hover) { + display: flex; + } + } + } + .placeholder\:text-base-content { + &::placeholder { + color: var(--color-base-content); + } + } + .placeholder\:opacity-75 { + &::placeholder { + opacity: 75%; + } + } + .focus-within\:outline { + &:focus-within { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + } + .focus-within\:outline-2 { + &:focus-within { + outline-style: var(--tw-outline-style); + outline-width: 2px; + } + } + .focus-within\:outline-primary { + &:focus-within { + outline-color: var(--color-primary); + } + } + .hover\:bg-base-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + } + } + } + .hover\:bg-base-300 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-300); + } + } + } +} +@layer base { + :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); + --color-primary: oklch(45% 0.24 277.023); + --color-primary-content: oklch(93% 0.034 272.788); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: oklch(58% 0.233 277.117); + --color-primary-content: oklch(96% 0.018 272.314); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } + } +} +@layer base { + :root:has(input.theme-controller[value=light]:checked),[data-theme=light] { + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); + --color-primary: oklch(45% 0.24 277.023); + --color-primary-content: oklch(93% 0.034 272.788); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + :root:has(input.theme-controller[value=dark]:checked),[data-theme=dark] { + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: oklch(58% 0.233 277.117); + --color-primary-content: oklch(96% 0.018 272.314); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + :root { + --fx-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E"); + } +} +@layer base { + :root, [data-theme] { + background-color: var(--root-bg, var(--color-base-100)); + color: var(--color-base-content); + } +} +@layer base { + :root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not([class*="drawer-open"]) > .drawer-toggle:checked ) { + overflow: hidden; + } +} +@layer base { + @property --radialprogress { + syntax: ""; + inherits: true; + initial-value: 0%; + } +} +@layer base { + :where( :root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not(.drawer-open) > .drawer-toggle:checked ) ) { + scrollbar-gutter: stable; + background-image: linear-gradient(var(--color-base-100), var(--color-base-100)); + --root-bg: var(--color-base-100); + @supports (color: color-mix(in lab, red, red)) { + --root-bg: color-mix(in srgb, var(--color-base-100), oklch(0% 0 0) 40%); + } + } + :where(.modal[open], .modal-open, .modal-toggle:checked + .modal):not(.modal-start, .modal-end) { + scrollbar-gutter: stable; + } +} +@layer base { + :root { + scrollbar-color: currentColor #0000; + @supports (color: color-mix(in lab, red, red)) { + scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) #0000; + } + } +} +@keyframes progress { + 50% { + background-position-x: -115%; + } +} +@keyframes rating { + 0%, 40% { + scale: 1.1; + filter: brightness(1.05) contrast(1.05); + } +} +@keyframes skeleton { + 0% { + background-position: 150%; + } + 100% { + background-position: -50%; + } +} +@keyframes dropdown { + 0% { + opacity: 0; + } +} +@keyframes radio { + 0% { + padding: 5px; + } + 50% { + padding: 3px; + } +} +@keyframes toast { + 0% { + scale: 0.9; + opacity: 0; + } + 100% { + scale: 1; + opacity: 1; + } +} +@layer base { + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --color-base-100: oklch(14% 0 0); + --color-base-200: oklch(20% 0 0); + --color-base-300: oklch(26% 0 0); + --color-base-content: oklch(97% 0 0); + --color-primary: oklch(55% 0.013 58.071); + --color-primary-content: oklch(98% 0.001 106.423); + --color-secondary: oklch(60% 0.25 292.717); + --color-secondary-content: oklch(96% 0.016 293.756); + --color-accent: oklch(58% 0.233 277.117); + --color-accent-content: oklch(96% 0.018 272.314); + --color-neutral: oklch(20% 0 0); + --color-neutral-content: oklch(98% 0 0); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(26% 0.051 172.552); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(27% 0.077 45.635); + --color-error: oklch(70% 0.191 22.216); + --color-error-content: oklch(25% 0.092 26.042); + --radius-selector: 1rem; + --radius-field: 2rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } + } +} +@layer base { + :where(:root),:root:has(input.theme-controller[value=synthwave]:checked),[data-theme="synthwave"] { + color-scheme: dark; + --color-base-100: oklch(14% 0 0); + --color-base-200: oklch(20% 0 0); + --color-base-300: oklch(26% 0 0); + --color-base-content: oklch(97% 0 0); + --color-primary: oklch(55% 0.013 58.071); + --color-primary-content: oklch(98% 0.001 106.423); + --color-secondary: oklch(60% 0.25 292.717); + --color-secondary-content: oklch(96% 0.016 293.756); + --color-accent: oklch(58% 0.233 277.117); + --color-accent-content: oklch(96% 0.018 272.314); + --color-neutral: oklch(20% 0 0); + --color-neutral-content: oklch(98% 0 0); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(26% 0.051 172.552); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(27% 0.077 45.635); + --color-error: oklch(70% 0.191 22.216); + --color-error-content: oklch(25% 0.092 26.042); + --radius-selector: 1rem; + --radius-field: 2rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@property --tw-scale-x { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-y { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-z { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-scale-z: 1; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-ease: initial; + } + } +} diff --git a/wco/privatri/privatri.js b/wco/privatri/privatri.js index f5fadd3..f1d28b5 100644 --- a/wco/privatri/privatri.js +++ b/wco/privatri/privatri.js @@ -1,194 +1,506 @@ -/** - * @file privatri.js - * @description WCO component for managing private, thread-based messages in IndexedDB. - * @version 1.0 - */ +import apx from "./apx.js"; +import { getOldestPrivatriids, syncronizeBackend } from "./utils.js"; -((window) => { - 'use strict'; +const bodyEl = document.querySelector("body"); +const searchInputEl = document.querySelector("#threadSearchBar"); +const threadFilterOptionsElArray = document.querySelector("#threadFilterOptions").querySelectorAll("li"); +const threadsContainerEl = document.querySelector("#threadsContainer"); +const threadPageEl = document.querySelector("#threadPage"); +const messageInputEl = document.getElementById("messageInput"); +const messagesContainerEl = document.querySelector("#messagesContainer"); +const attachmentsInputEl = document.querySelector("#attachmentsInput"); - // --- Component Definition --- - const privatri = {}; +function formatDate(timestamp) { + const date = new Date(timestamp); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); - // --- Private State --- - let _db = null; // Holds the single, persistent IndexedDB connection - const DB_NAME = 'privatriDB'; - const DB_VERSION = 1; + return `${day}/${month}/${year} ${hours}:${minutes}`; +}; - /** - * Logs messages to the console with a consistent prefix. - * @param {string} level - The log level (e.g., 'log', 'error', 'warn'). - * @param {...any} args - The messages to log. - */ - const _log = (level, ...args) => { - console[level]('[privatri]', ...args); - }; +searchInputEl.addEventListener("input", () => { + const value = searchInputEl.value.toLowerCase(); - /** - * Opens and initializes the IndexedDB database. - * This function is called by init() and handles the connection and schema upgrades. - * @param {string[]} initialStoreNames - An array of store names (thread IDs) to ensure exist. - * @returns {Promise} A promise that resolves with the database connection. - */ - const _openDatabase = (initialStoreNames = []) => { - return new Promise((resolve, reject) => { - _log('log', `Opening database "${DB_NAME}" with version ${DB_VERSION}.`); - - const request = indexedDB.open(DB_NAME, DB_VERSION); - - // This event is only triggered for new databases or version changes. - request.onupgradeneeded = (event) => { - const db = event.target.result; - _log('log', 'Database upgrade needed. Current stores:', [...db.objectStoreNames]); - - initialStoreNames.forEach(storeName => { - if (!db.objectStoreNames.contains(storeName)) { - _log('log', `Creating new object store: "${storeName}"`); - db.createObjectStore(storeName, { keyPath: 'key' }); - } - }); - }; - - request.onsuccess = (event) => { - _log('log', 'Database opened successfully.'); - _db = event.target.result; - - // Generic error handler for the connection - _db.onerror = (event) => { - _log('error', 'Database error:', event.target.error); + Array.from(threadsContainerEl.children).forEach(child => { + if (child.querySelector("li > div").textContent.toLowerCase().includes(value)) { + child.style.display = "block"; + } else { + child.style.display = "none"; }; + }); +}); + +threadFilterOptionsElArray.forEach(option => { + option.addEventListener("click", async () => { + const filter = option.getAttribute("data-filter"); + + if (filter === "date") { + Array.from(threadsContainerEl.children).sort((a, b) => { + return a.querySelector("li > span").getAttribute("data-timestamp") - b.querySelector("li > span").getAttribute("data-timestamp"); + }).forEach(child => { + threadsContainerEl.appendChild(child); + }); + } else if (filter === "name") { + Array.from(threadsContainerEl.children).sort((a, b) => { + return a.querySelector("li > div").textContent.localeCompare(b.querySelector("li > div").textContent); + }).forEach(child => { + threadsContainerEl.appendChild(child); + }); + }; + }); +}); + +const sendNewMessage = async (timestamp, message, attachmentsArray) => { + let messageTemplate = ""; + + await fetch("./message.mustache") + .then(res => res.text()) + .then(template => { + messageTemplate = template; + }); + + messageTemplate = Mustache.render(messageTemplate, { timestamp, message, date: formatDate(timestamp) }); + + const tempNode = document.createElement("div"); + tempNode.innerHTML = messageTemplate; + const messageNode = tempNode.firstElementChild; + + attachmentsArray.forEach(attachment => { + messageNode.querySelector("div.attachmentsContainer").insertAdjacentHTML("beforeend", `${attachment.filename}`); + }); + + return messageNode.outerHTML; +}; + +const editMessage = async (timestamp, message) => { + let editMessageTemplate = ""; + + await fetch("./editMessage.mustache") + .then(res => res.text()) + .then(template => { + editMessageTemplate = template; + }); + + return Mustache.render(editMessageTemplate, { message, date: formatDate(timestamp) }); +}; + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +sendMessageBtn.addEventListener("click", async () => { + const message = messageInputEl.value.trim(); + + if (message !== "") { + const messageObj = await (async (publicKey) => { + const timestamp = Date.now(); + const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey); + + return { + privatriid: `${threadPageEl.getAttribute("data-uuid")}_${timestamp}`, + timestamp: timestamp, + sender_alias: alias, + message: await apx.crypto.encryptMessage(message, publicKey), + attachments: await (async () => { + const attachmentsArray = []; + const localAttachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []; + + if (localAttachmentsArray.length > 0) { + for (const attachment of localAttachmentsArray) { + attachmentsArray.push( + { + fileType: attachment.fileType, + filename: attachment.filename, + content: await apx.crypto.encryptMessage(attachment.content, publicKey) + } + ); + }; + }; + + return attachmentsArray; + })() + }; + })((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messagesContainerEl.firstElementChild.getAttribute("data-timestamp")}`)).publicKey); - resolve(_db); - }; + const newMessageHTML = await sendNewMessage(messageObj.timestamp, message, JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []); - request.onerror = (event) => { - _log('error', 'Failed to open database:', event.target.error); - reject(event.target.error); - }; + sessionStorage.removeItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`); + + messagesContainerEl.insertAdjacentHTML("beforeend", newMessageHTML); + + messageInputEl.value = ""; + + // Faire un post sur l'endpoint /privatri + + await apx.indexedDB.set("privatri", "messages", messageObj); + }; +}); + +document.addEventListener("DOMContentLoaded", async () => { + const lastConnection = JSON.parse(localStorage.getItem("lastConnection")) || Date.now(); + + await syncronizeBackend(lastConnection); + + const privatriidArray = await getOldestPrivatriids("privatri", "messages"); + + const thread = async (name, uuid) => { + let threadTemplate = ""; + + await fetch("./thread.mustache") + .then(res => res.text()) + .then(template => { + threadTemplate = template; + }); + + return Mustache.render(threadTemplate, { uuid, name }); + }; + + for (const privatriid of privatriidArray) { + const obj = await apx.indexedDB.get("privatri", "messages", privatriid) + + const privateKey = (await apx.indexedDB.get("privatri", "threads", obj.thread)).privateKey; + const name = (await apx.crypto.decryptMessage(obj.title, privateKey)).data; + + threadsContainerEl.insertAdjacentHTML("beforeend", await thread(name, obj.thread)); + }; + + Array.from(threadsContainerEl.children).forEach(child => { + child.addEventListener("click", async () => { + const uuid = child.getAttribute("data-uuid"); + + const messagesObj = await new Promise((resolve, reject) => { + const request = indexedDB.open("privatri", 1); + + request.onsuccess = (event) => { + const db = event.target.result; + + const transaction = db.transaction("messages", "readonly"); + const store = transaction.objectStore("messages"); + const cursorRequest = store.openCursor(); + + const result = []; + cursorRequest.onsuccess = (event) => { + const cursor = event.target.result; + + if (cursor) { + const obj = cursor.value; + const keyUuid = obj.privatriid.split("_")[0]; + + if (keyUuid === uuid) { + result.push(obj); + }; + cursor.continue(); + } else { + resolve(result); + }; + }; + + cursorRequest.onerror = (event) => reject(event); + }; + + request.onerror = (event) => reject(event); + }); + + threadPageEl.setAttribute("data-uuid", uuid); + threadPageEl.querySelector("#threadName").innerText = (await apx.crypto.decryptMessage(messagesObj[0].title, (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey)).data; + + messagesContainerEl.innerHTML = ""; + + let privateKey = ""; + + for (const message of messagesObj) { + if (privateKey === "") { + privateKey = (await apx.indexedDB.get("privatri", "threads", message.thread)).privateKey; + }; + + const decryptedMessage = (await apx.crypto.decryptMessage(message.message, privateKey)).data; + + let decryptedAttachments = []; + + if (message.attachments !== undefined) { + decryptedAttachments = await (async () => { + const attachmentsArray = []; + + if (message.attachments.length > 0) { + for (const attachment of message.attachments) { + attachmentsArray.push({ + fileType: attachment.fileType, + filename: attachment.filename, + content: (await apx.crypto.decryptMessage(attachment.content, privateKey)).data + }); + }; + }; + + return attachmentsArray; + })(); + }; + + messagesContainerEl.insertAdjacentHTML("beforeend", await sendNewMessage(message.timestamp, decryptedMessage, decryptedAttachments)); + }; + }); }); - }; - // --- Public API --- + document.querySelectorAll("a").forEach(link => { + link.addEventListener("click", async (event) => { + event.preventDefault(); - /** - * Initializes the privatri component. - * Opens the database connection and ensures initial object stores are created. - * @param {object} config - Configuration object. - * @param {string[]} [config.threads=[]] - An array of initial thread IDs (store names) to create. - * @returns {Promise} - */ - privatri.init = async (config = {}) => { - if (_db) { - _log('warn', 'privatri component already initialized.'); - return; - } - const threads = config.threads || []; - await _openDatabase(threads); - }; + window.history.replaceState({}, document.title, window.location.pathname); - /** - * Retrieves a value from a specific store (thread). - * @param {string} storeName - The name of the store (thread ID). - * @param {string} key - The key of the item to retrieve. - * @returns {Promise} A promise that resolves with the value or null if not found. - */ - privatri.getValue = (storeName, key) => { - return new Promise((resolve, reject) => { - if (!_db || !_db.objectStoreNames.contains(storeName)) { - _log('warn', `Store "${storeName}" does not exist.`); - return resolve(null); - } - const transaction = _db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - const request = store.get(key); + const templateName = link.getAttribute("data-template"); - request.onsuccess = () => resolve(request.result ? request.result.value : null); - request.onerror = (e) => { - _log('error', `Error getting value for key "${key}" from store "${storeName}":`, e.target.error); - reject(e.target.error); - }; + if (templateName === "threadAliasList" || templateName === "threadSettings") { + window.history.pushState({}, "", `${window.location.pathname}?uuid=${threadPageEl.getAttribute("data-uuid")}`); + }; + + await fetch(`./${templateName}.mustache`) + .then(res => res.text()) + .then(template => { + template = Mustache.render(template, {}); + + bodyEl.innerHTML = template; + }); + + const script = document.createElement("script"); + + script.type = "module"; + script.src = `./${templateName}.js`; + + bodyEl.appendChild(script); + }); }); - }; +}); - /** - * Adds or updates a key-value pair in a specific store (thread). - * @param {string} storeName - The name of the store (thread ID). - * @param {string} key - The key of the item to set. - * @param {any} value - The value to store. - * @returns {Promise} - */ - privatri.setValue = (storeName, key, value) => { - return new Promise((resolve, reject) => { - if (!_db || !_db.objectStoreNames.contains(storeName)) { - _log('error', `Cannot set value. Store "${storeName}" does not exist.`); - return reject(new Error(`Store "${storeName}" not found.`)); - } - const transaction = _db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.put({ key: key, value: value }); +window.addEventListener("beforeunload", () => { + if (apx) + localStorage.setItem("lastConnection", JSON.stringify(Date.now())); +}); - request.onsuccess = () => resolve(); - request.onerror = (e) => { - _log('error', `Error setting value for key "${key}" in store "${storeName}":`, e.target.error); - reject(e.target.error); - }; - }); - }; +window.attachDeleteMessageEvent = async function attachDeleteMessageEvent(btn) { + const messageEl = btn.parentElement.parentElement.parentElement; - /** - * Removes a key-value pair from a specific store (thread). - * @param {string} storeName - The name of the store (thread ID). - * @param {string} key - The key of the item to remove. - * @returns {Promise} - */ - privatri.removeKey = (storeName, key) => { - return new Promise((resolve, reject) => { - if (!_db || !_db.objectStoreNames.contains(storeName)) { - _log('warn', `Cannot remove key. Store "${storeName}" does not exist.`); - return resolve(); // Resolve peacefully if store doesn't exist - } - const transaction = _db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.delete(key); + const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey; - request.onsuccess = () => resolve(); - request.onerror = (e) => { - _log('error', `Error removing key "${key}" from store "${storeName}":`, e.target.error); - reject(e.target.error); - }; - }); - }; + const signatureMessage = `${JSON.parse(localStorage.getItem("apx")).data.headers.xalias}_${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`; + const signature = await apx.crypto.sign(signatureMessage, privateKey); + + let verified = false; - /** - * A utility function to save a batch of messages, demonstrating how to use the component. - * @param {object} threadsObj - An object where keys are thread IDs and values are message objects. - * @example - * const messages = { - * "thread-123": { "1678886400": { text: "Hello" } }, - * "thread-456": { "1678886401": { text: "Hi there" } } - * }; - * await privatri.storeMessages(messages); - */ - privatri.storeMessages = async (threadsObj = {}) => { - if (!_db) { - _log('error', 'Database not initialized. Please call privatri.init() first.'); - return; - } - _log('log', 'Storing messages in IndexedDB...'); - for (const [uuid, threadObj] of Object.entries(threadsObj)) { - // Ensure the object store exists before trying to write to it. - if (!_db.objectStoreNames.contains(uuid)) { - _log('warn', `Store "${uuid}" not found during message storage. You may need to re-init with this thread.`); - continue; - } - for (const [timestamp, messageObj] of Object.entries(threadObj)) { - await privatri.setValue(uuid, timestamp, messageObj); - } - } - _log('log', 'Finished storing messages.'); - }; + try { + verified = await fetch("", { + method: "GET", + body: JSON.stringify({ + message: signatureMessage, + signature: signature, + uuid: threadPageEl.getAttribute("data-uuid"), + timestamp: messageEl.getAttribute("data-timestamp"), + }) + }); + } catch (error) { + console.error("Error while verifying signature:", error); + }; - // Expose the component to the global window object - window.privatri = privatri; + const authorAlias = await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, privateKey).data; -})(window); + if ((JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) && (verified === true)) { + await apx.indexedDB.del("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`); + + messageEl.remove(); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to delete this message.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; +}; + +window.attachEditMessageEvent = async function attachEditMessageEvent(btn) { + const messageEl = btn.parentElement.parentElement.parentElement; + + const authorAlias = (await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey)).data; + + if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) { + const messageValue = messageEl.querySelector("div.message").innerText; + + const attachmentsArray = (() => { + const attachmentsArray = []; + + messageEl.querySelector("div.attachmentsContainer").querySelectorAll("img").forEach(img => { + attachmentsArray.push( + { + fileType: img.getAttribute("src").match(/^data:(.*);base64,/)[1], + filename: img.getAttribute("alt"), + content: img.getAttribute("src").split(",")[1] + } + ); + }); + + return attachmentsArray; + })(); + + messageEl.innerHTML = await editMessage(parseInt(messageEl.getAttribute("data-timestamp"), 10), messageEl.querySelector("div.message").innerText); + + messageEl.querySelector("button.cancelEditBtn").addEventListener("click", async () => { + const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`); + + messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, messageValue, attachmentsArray); + }); + + messageEl.querySelector("button.saveEditBtn").addEventListener("click", async () => { + const newMessageValue = messageEl.querySelector("textarea").value.trim(); + + if (newMessageValue !== "") { + const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey; + + const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`); + + messageObj.message = await apx.crypto.encryptMessage(newMessageValue, privateKey), + + await apx.indexedDB.set("privatri", "messages", messageObj); + + messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, newMessageValue, attachmentsArray); + }; + }); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this message.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; +}; + +window.attachEventDisplayAttachment = function(img) { + let overlay = document.getElementById("imageOverlay"); + + if (overlay === null) { + overlay = document.createElement('div'); + + overlay.id = "imageOverlay"; + overlay.style.position = "fixed"; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.background = "rgba(0, 0, 0, 0.75)"; + overlay.style.display = "flex"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + overlay.style.zIndex = 999; + + overlay.onclick = () => { + overlay.remove(); + }; + + document.body.appendChild(overlay); + } else { + overlay.innerHTML = ""; + overlay.style.display = "flex"; + }; + + const fullScreenImage = document.createElement("img"); + + fullScreenImage.src = img.src; + fullScreenImage.alt = img.alt; + fullScreenImage.style.maxWidth = "90vw"; + fullScreenImage.style.maxHeight = "90vh"; + overlay.appendChild(fullScreenImage); +}; + +document.querySelector("#attachmentsBtn").addEventListener("click", () => { + attachmentsInputEl.click(); +}); + +attachmentsInputEl.addEventListener("change", async () => { + const filesArray = Array.from(attachmentsInputEl.files); + const maxFileSize = 5 * 1024 * 1024; + const maxSize = 512; + + for (const file of filesArray) { + if (file.size <= maxFileSize) { + const attachmentObj = await new Promise((resolve, reject) => { + const attachmentObj = { + fileType: file.type, + filename: file.name, + content: "" + }; + + const img = new Image(); + + img.onload = () => { + if (img.width > maxSize || img.height > maxSize) { + let width = img.width; + let height = img.height; + + if (width > maxSize) { + height *= maxSize / width; + width = maxSize; + }; + + if (height > maxSize) { + width *= maxSize / height; + height = maxSize; + }; + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob(blob => { + const reader = new FileReader(); + + reader.onload = (event) => { + attachmentObj.content = event.target.result.split(",")[1]; + + resolve(attachmentObj); + }; + + reader.onerror = reject; + reader.readAsDataURL(blob); + }, "image/jpeg", 0.8); + } else { + const reader = new FileReader(); + + reader.onload = (event) => { + attachmentObj.content = event.target.result.split(",")[1]; + + resolve(attachmentObj); + }; + + reader.onerror = reject; + reader.readAsDataURL(file); + }; + }; + + img.onerror = reject; + img.src = URL.createObjectURL(file); + + return attachmentObj; + }); + + if (sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`) === null) { + sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify([attachmentObj])); + } else { + const attachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)); + attachmentsArray.push(attachmentObj); + + sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify(attachmentsArray)); + }; + }; + }; +}); \ No newline at end of file diff --git a/wco/privatri/thread.mustache b/wco/privatri/thread.mustache new file mode 100644 index 0000000..2103725 --- /dev/null +++ b/wco/privatri/thread.mustache @@ -0,0 +1,7 @@ +
    +
  • + +
    {{name}}
    + 17 +
  • +
\ No newline at end of file diff --git a/wco/privatri/threadAliasList.js b/wco/privatri/threadAliasList.js new file mode 100644 index 0000000..9456e0a --- /dev/null +++ b/wco/privatri/threadAliasList.js @@ -0,0 +1,88 @@ +import apx from "./apx.js"; +import { getOldestPrivatriids } from "./utils.js"; + +const bodyEl = document.querySelector("body"); +const aliasListContainerEl = document.querySelector("#aliasListContainer"); + +let aliasesArray = []; + +const newAlias = async (alias) => { + let aliasTemplate = ""; + + await fetch("./alias.mustache") + .then(res => res.text()) + .then(template => { + aliasTemplate = template; + }); + + return Mustache.render(aliasTemplate, { alias }); +}; + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +(async () => { + const params = new URLSearchParams(window.location.search); + const uuid = params.get("uuid"); + + const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey; + + const privatriidArray = await getOldestPrivatriids("privatri", "messages"); + + privatriidArray.forEach(async privatriid => { + if (privatriid.split("_")[0] === uuid) { + aliasesArray = (await apx.indexedDB.get("privatri", "messages", privatriid)).aliases; + const ownerAlias = (await apx.indexedDB.get("privatri", "messages", privatriid)).owner; + + for (const alias of aliasesArray) { + aliasListContainerEl.insertAdjacentHTML("beforeend", await newAlias({ + decrypted: (await apx.crypto.decryptMessage(alias, privateKey)).data, + crypted: alias + })); + }; + + document.querySelectorAll("button.removeAliasBtn").forEach(btn => { + btn.addEventListener("click", async () => { + if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) { + if (btn.getAttribute("data-alias") !== ownerAlias) { + const alias = btn.getAttribute("data-alias"); + aliasesArray = aliasesArray.filter(a => a !== alias); + btn.parentElement.parentElement.remove(); + + privatriidArray.forEach(async privatriid => { + if (privatriid.split("_")[0] === uuid) { + const messageObj = await apx.indexedDB.get("privatri", "messages", privatriid); + messageObj.aliases = aliasesArray; + + await apx.indexedDB.set("privatri", "messages", messageObj); + }; + }); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You cannot remove the owner of the thread.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to remove this alias.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; + }); + }); + }; + }); +})(); + diff --git a/wco/privatri/threadAliasList.mustache b/wco/privatri/threadAliasList.mustache new file mode 100644 index 0000000..a42e90a --- /dev/null +++ b/wco/privatri/threadAliasList.mustache @@ -0,0 +1,6 @@ +
+

Alias list

+
+ +
+
\ No newline at end of file diff --git a/wco/privatri/threadSettings.js b/wco/privatri/threadSettings.js new file mode 100644 index 0000000..22ac9c9 --- /dev/null +++ b/wco/privatri/threadSettings.js @@ -0,0 +1,72 @@ +import apx from "./apx.js"; +import { getOldestPrivatriids } from "./utils.js"; + +const bodyEl = document.querySelector("body"); +const threadNameInputEl = document.querySelector("#threadNameInput"); +const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn"); +const applyModificationsBtnEl = document.querySelector("#applyModificationsBtn"); + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +let messageObj = {}; + +(async () => { + const params = new URLSearchParams(window.location.search); + const uuid = params.get("uuid"); + + const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey; + + const privatriidArray = await getOldestPrivatriids("privatri", "messages"); + let ownerAlias = ""; + + privatriidArray.forEach(async privatriid => { + if (privatriid.split("_")[0] === uuid) { + messageObj = await apx.indexedDB.get("privatri", "messages", privatriid); + ownerAlias = messageObj.owner; + + threadNameInputEl.value = (await apx.crypto.decryptMessage(messageObj.title, privateKey)).data; + + (Array.from(autoDeletionBtnElArray).find(el => el.getAttribute("data-auto-deletion") === String(messageObj.dt_autodestruction))).classList.add("bg-base-200"); + + if (messageObj.urgencydeletion === true) { + document.querySelector('input[type="checkbox"].toggle').checked = true; + }; + }; + }); + + applyModificationsBtnEl.addEventListener("click", async () => { + if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) { + messageObj.title = await apx.crypto.encryptMessage(threadNameInputEl.value, privateKey); + messageObj.dt_autodestruction = (() => { + const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200")); + return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10); + })(); + messageObj.urgencydeletion = document.querySelector('input[type="checkbox"].toggle').checked; + + await apx.indexedDB.set("privatri", "messages", messageObj); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this thread.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; + }); +})(); + +autoDeletionBtnElArray.forEach(btn => { + btn.addEventListener("click", () => { + autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200")); + btn.classList.add("bg-base-200"); + }); +}); diff --git a/wco/privatri/threadSettings.mustache b/wco/privatri/threadSettings.mustache new file mode 100644 index 0000000..3ea3a91 --- /dev/null +++ b/wco/privatri/threadSettings.mustache @@ -0,0 +1,33 @@ +
+

Thread settings

+
+ + + + +
+ +
+ +

Urgency deletion

+
+ +
\ No newline at end of file diff --git a/wco/privatri/toastAlert.mustache b/wco/privatri/toastAlert.mustache new file mode 100644 index 0000000..00a4393 --- /dev/null +++ b/wco/privatri/toastAlert.mustache @@ -0,0 +1,5 @@ +
+
+ {{message}} +
+
\ No newline at end of file diff --git a/wco/privatri/utils.js b/wco/privatri/utils.js new file mode 100644 index 0000000..3eb0d02 --- /dev/null +++ b/wco/privatri/utils.js @@ -0,0 +1,46 @@ +async function getOldestPrivatriids(dbName, storeName) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(dbName, 1); + + request.onsuccess = (event) => { + const db = event.target.result; + + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + const cursorRequest = store.openCursor(); + const uuidMap = {}; + + cursorRequest.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor) { + const obj = cursor.value; + + const [uuid, timestamp] = obj.privatriid.split("_"); + + if (!uuidMap[uuid] || Number(timestamp) < uuidMap[uuid].timestamp) { + uuidMap[uuid] = { privatriid: obj.privatriid, timestamp: Number(timestamp) }; + }; + + cursor.continue(); + } else { + const result = Object.values(uuidMap).map(event => event.privatriid); + + resolve(result); + }; + }; + + cursorRequest.onerror = (event) => reject(event); + }; + + request.onerror = (event) => reject(event); + }); +}; + +async function syncronizeBackend(lastConnection) { + +}; + +export { + getOldestPrivatriids, + syncronizeBackend +}; \ No newline at end of file