S9A5E>PXxE9b3M=K3^F1pP|^~>Xgk_
zt}YJoj7sU#SdymB!~e`^-J=_V)6{KBw>@a`DOR4}Du?TO2RHuwAKq
zf7R>vS-D#cUPrUu+qQ1iG_wuY1Mey?TfChuKc&cV!wy&Lft1><5Qu;}i-B~D??As<&t@5w?*co|{C!m~6}qpVGGBGpy+kA|GBzc&Jecd9
zdQbmza82;itp&oD4X4-z1uJq*nBW;Z~t7c5_HYH(&kQi<-)6otI_IV(;V8MsyT_OR>{nER?p&O1j`
zC;UZuSxH#KmKT@fgO_VeS~f)`Z>{?c6`0p}TE_=QE@Pfy5$)&U=hx0ADXNuTun%Hgr?!SLNy3lN|
zXzPy^+fK&|nF#G(t-+8iY03WlfxP^&y1y!qJOx7~V&Y$hn@h)(?}~WVl-k=py+=OZ
z@?-P&$B*rgJLcYdGi8?biuVTs)?GLsXuCN#CjZs4q}gcCNRA{L;so|MI?$D^giEiA77A4-$%s
zJb%xBysS}a@sTSTL8j*wwpIM@e|z00ylnk?iQvf+e0F~xlpZXM_Yi&BHH9POv2E?r
zT4Bf96McQsxkh;t(}e%|U4E#j6T;JnV2{WQ}z5_uR2m^}s{%>!)^z^u)<}dF?F?yp#P^SL@Q6Lv3BlEHkqVESMylk9ad5mprfVr_@_)nVjl{XL+3e8Okm#
zNMKV`)hTz7{Cmvtv(nd}bH6g*!^x9$gAy$&H(s^6<8z+VkN0J=cnsCY%#3Tjze_LGz#He^pBs2Hy%x7Bv&-
zU|KBn=C&g{`;U*GFPxcNmCD3!rL{R=|H@tJMHPDHtu_Vj^O+XA2=?2ro>z6v!DO9~_S{gHl8L87wO1XN
zYH-p{
z0^R(y*`d7axnLE{X#g1#&Ji~PlrnN5V;#{$)CAlu(QP|G|yyyHGc&6Mj;CTM&
zOTn@9<~^+cGDX)^m)^d2J*4c=y6+`_WWEMmw!Z8uk)^gI$y|G7$BM9Xw)a`&cb+u3
zk)p{}czW)#kHLoPcTB%@JBj6<`5Nh!*1zxm$o(#N-*ugys8p5ZvgKXV%7t2bJUni_
zF|Rx)@%g-CaAv@%g^4f2q`iXHeN|vB?7p-jc(2FARG;bRJS9)q)$g~>bMjE?V9VBO
zaH;EgFL`^awN2p!fB%W5CKVxBx0(diPM*8Qy1{TBf8*mnKV=z@GG^6IVXauP>MEzU
z(rQ+n8F##e3XDHK_~2IgyJXVUovS=NPgPuWl?_?HA!O2uA7QcWlEHH(xU)ZIZhZ1(
zrSQ@~$0^OGcAGSKelP31KKbKm0SonGj`DSuAF2*{_GUXY`CMpyB)K>;w{L;lk(0s(
z-$g#T70%;V{&wQ!7DmaH;yEGBO9jdnZ@m>Bf9cNCy=#lEIU4=T)eSo&vYscLMP8O!
zwzrxiXo`#5*L&v=ZuyqFVnTXyTwdv?%UfjxtlVvLLqaZ!O9t-vdi=G&v{U(}tyUM{~)=sUm5<=bl6#aFm{O}#^QHi{`~nRxX7T)3#9;AVb+fYFk~
zpq?dac3WJNCSB?X+;;8SM7HhQ#aql{UF+9t^mJ`Hn7JY)_GSvB_X36ok1am_E~$HY
zWX=kO@Y9tnX$te_n=R&&yAY`_YUbTG>zbOi*=oC9mhG#A{dX;Je)YCLrLXoz#-935
znlX=AKKhAxDLuZmscCA(7qM--&Ocf!zTJDNZfSVsT5;>1l2G%AXRY_F3{E=A8t++A
z=2d36Dq!t@2LHao3o;Rw#q|@rt9}RT9{S88%ewu_0kPHpu70I42&*Um}Gw^v`k^pEVDfHhiY
z6B@F#HemMwmK388#eJ5~&sF^WmCU6YkeQPtJlWvdv?;rGeNEn>8#Zn4wc9}}ZU43G
zkYqp1WZ?0n_^Qz%v5r!OqaKHLgnc-%NhR0q){MiRE7zKQz5GumDEQ8*_`(OG%TihX
z9=LnX@>t>dh{LL`OQ(G`^nN_$R$PYOqm+y8<-Y^7FPUt6{X8?}y(p7&j`fK@fY$6rayP4;+9ke6?#^8B?q_ZRJsF_SxS
z|3%j71zn5!xN9z*i&SgzH};vKB4ZhlaEHC8@p*%)qW-4vUG-h(G_})3g!DL{Z`-Tq
z9~J$2-lej0H`kO-?GJc$OPZ(rw)vxLcfM}9a%>iBqEYMDM+u^@gc8;m9V`-Qp5yCX
zuw}-}Pgy%EQx^qST)X`GX}qqK`9{IA`!3O6=N4|Bq15GTUi!yNYF7)(RfRjtwKaaE!%SJrD~RYAgiyzS)|XudOYcpJ)eUi#RP75^EQOk$eE#uqYG>y)!V)vD=1
zk8)RBTl!sf>5q4O?OD4vsU_c9+tYlQuSUM7q)#br&BF%GMNwKAeO-;4#dw=QX
zPvytV>IWY$6rPvMJeWp+HAqtPQK!NvKsL-K&+x#)v3C88ge2`HQuiB?^e
z8qynmW#T6W3%vqPhL0cI~2#Rv6eEYIhBU3PWa
zzCy|Ur5fkD`&LI>x}_~wl~uJd
zdh4yrR=-|9W%}-vOIwU?JzKewf9{k=>Kwl-9>{*|vzdHt1J|img|{BxCcEsJ+RL$i
z?)t0lP9fY|k2P(3kYD!F$5DJjlUrC`^c~06A0H=9o>RIe$of8SI@f|`wXeT|Z|Hq<
zzSDQ^)m&ZYTesKh>@u2}nJRSkvUS+BX(?)^DhW#_%vo(`B&?AYpe?(N3w5f5fP&bhQz-{dmy$I~xPhU?l1HpZ-sd3XH8
z=e`LHdv1yF#Oa=Cd>SSdWN`4vL~|B@iQleb-@XTVuCSI~{5mgemvDSaW$(@<3x%}K
z@~jiQEbYW7oiO#d;KXB9hgY2cyP)*?%+HI`d8gXB|ClV!^w_(~HN$G&+w57kh5nU$
z&$~wkXU)wJTclyj7|40cf7R^UD(|K{qd6~A4WX??G6*arO;?k|4LOXhQ?JxNFMe>6u$Ij`i(WDepHH2bo;
zwt7kGw#>b%mAfyz>|7hJT`GOr+UN1Ll^Kr}71iagxfM?fSfX7$4#&>br$fF9?eW@F
z7pb7Z!S={zbIRo1bNJbRywFosKI*gO(OgSc`P&KZOH5p)CH`DX{c+21z9sYD`d{-~
zVnVudB(|2WU%GtpDv!Wv4qXv*64rAc3g9UZk{=Y{!AlXfDHd2aYo@^UmT3ik@uzHfWyZ
z-CX~pCzx)F7rzRd$Mdjs
zVUu7(V}Omu@^B^=`u4V|)B|RR>jiC`?$blR%
zSmhOY{wuP!md#kYe%G0lic8DpE}FJfMCHuVJBBBvyPBUoc`!#%o8$JWDXW(ryHc7I
zmn!MRQ(`53W&O(hrPco#DuV9o&HFc9DlpKQ`;z-B*H=@2GN-gM%0+sp2!ArZ{7y0Q
z(w2@!=_`NC47Xiac73N>uEVa$Zg05l+J!V5z
zzr(|ZJ4Sw1z7FiW52zjgq<(3I*;c2ChANygeEXW}u1vc+Iru_w$V<&qspxg$XZ`&{
zI)4=ZXV|fLxyk#Ddt~;y`rMvwJv$+$U~j6XPR~o9$;Q{rmh&WuOjtZcg@5V!&*4`7
zFT=QVFGnfAveNFGx0_MdB&6fbmbV(20g@-AWy?3M)O^BlkI(bo)F)0YD-BiTXT7(Q
z3t>BT^UQ_>tpvf^THVumr8BiMvvs7pcKe53zjWSKdJDfv%~3^}e4iieCr-%J{!;Gb
z^69EpxNQZ
zoXw$ChbGPPDqU&UQ+Ud7#tCa_mEsRI={=wOI!1)LnsS*V!ep2hRC9
z9CK~!5$)Gb^{_}ty~)0?WK+~R;nZ5q{-t`mCT;e9d0VGMr0j?5q+MS$b3dpU
zx}@&n2vg}P>gZb6#@gW6y=Y&qI0-Oe*W_(pv+f$^
z?TE>}uB~+1J9W3eud8a`$3P{Q=$am$B6o$}wlj18@OeBaZJIJkNPy#d5Z`{2n5k{H2De{ek%#iFtV6o4cdYFb^S+mGXVp19S-JFod_?AMZsX9HeJYdRU5%*ka~&+_Bg?jxHozbRWk>3+q{
z>y4jG%^35q%xa3hUsOFiGH=PoD+hDMqDw`tzPTDV`B$;~i;0}ea%3B4D?H_kEGnO<
zxSzA}b9Uu*uKo%eqxJi*pVBP7`#N>e@jIq#FMq!Cc-gMYorhvHg_sufwDl%Way6XT
zWP7A}f_vWvhu{>okG@7BGOS^rUTQmMP^Mx!ER!uEE>#M&c#Yi*viK6su`Izf^YD{$8x|N?o(vtuouWE*7=~oRqj3
zCKVqtWx~S^jt7|Y9OTti6J0M(>AW@hh(O!5kR110Gycw8d*ScFxBK>7-WBbiEsr&NeIg<63$A_WMI2A2&Wc^7BgF
zuX5*YzaCv&P@Q_~t!;G8tXWe__PQSWoW5$&_TwH(U4mh$TAK`ej>y(_H@|xs?RB7b
z1OK0Uv)q2yF|WT)Rx|I=15z>s>SD{PAt@2E4JZs-{(&ny^kV;j+Z@Y;$6$8TxY}f)o5vTv+EMw
zD^-ibef`tRw%=L%dHb#^tH=H-UsnE{_a>+4T!Uc$<##fR{J%W#3kq7KQte|{|-yQ3)?fOyf
zPM3{MA5N);JUNo;aa(z+j!KN+`G_FzF8N0bl{WoLS!lSzAouJ29}9Z}lXhk6U5Q$i
z8xmU1^;R}em9sbTYkHM&zi-M7{%1;4oeJ9p-Cj?4ysW+PvXu6{22st{n5LSeOgC4z
zY`T1^Zut;~c|*AJ2c+JYPCDKQJt!*n%ZEZQetlZI_kL9j&!t2rWD;lhEA9d5)1m
zUj6aPET_$(C2L%+@;~C4t(M_fA9CW%!N!9M6?sJkl`8)k66efOGhZm08D^{-(ygqZ
zIwj9>`yKJDAIy?}JLKy8BiIw8IxQzh1-oTb*e?pX_xfqr%BH(PO5Ac!W-JnOogl=n
z>Tu!%=jRL7NA|5*c4*>}#CxfWHs~L|*Rv%t=a#dA#jq!JjX_+qcN=)
zMg6YEdpTJ=mbA1~UoOYa=jNC+Q~BBwcHz#ae$)o9TSt*Jqd3U-t!DmM&Xlsi~n*
zwd>Ta#Q}*cIp(bp+`v4Ig~9Xqov+WWEjeqoqmINWgfA~VKKq0xn>)wLYcr&hro9ba
zrL%SGe}*mz+XYwdwGAvM@RZTnc7Wzm>@l^k}Nm
zTe?|Wa+r;g+R4)p6`!{v%@@1vd{144+TBz3(
zDxo#W_K3`Ug)jHyw-#3VCGoi3@t>+;t-kqAq^Bp#nV7e^3V*gn9sU}8KT5Yc>(W-)
zz1zbt#0subUbDO_HK0w?UjGXJ%hjuoNqjz0FoU-#vUux8-+x#7oG;Ihik0ndED#D?
z;I?numXd{WZ)%fHXTN>B>-(L1S0f*aYwgU4zBqT;B*`OruL2~NoZrZADedQ0cjT}4
zjBiUh`+Ny?lFQ)|aC#tkW!7oFX+8r!q)0
z__*36ta`#M_;C(n@&z{;)%ZQZduME!9=7G*;m@Dlt=zu5mR(bVo%5cmTW3
z79IF+`jTjNqWH4U$26z12BzS5x)O%`r
z_~%viY;al|w@|yKNy}7W;_|ewA*{+g;q15j_!wF0mA~8Pb(D0S`Oh$Q)erZOj~*Wx
zgCzomtMGW{QK*}_A4|e
zIEix!+orecR$efdGFez|CYfJ!^X-(vn7u61Btug;ZaRs^SOo9vJYmzf-r!duXOkX_
z|D!!=suK=Bd;FgvD)W=o#c6%-``X{SO=*vQu~#y@aQ*gsa+lVMt>n1mX)0W`)o;Du
zPyLso_B@9R66Z+qT5Y?sWQB|C&wKa1uatjSc{DX3K%ptgM9A`wNaMSuuWAeLNY|~}
z9&p~(d*!o5!WXZc(NbGpsrE(XFb~^l?WqdM^32CQ6DPGz+ji{o*M0j+1B;@(c0@nh
zy5P%Q-D|SRS(C2_MroHU5t%aS?3t-kS4Rf794e4Zo*={#ICImUifc;M)9(1&@6|bU
zyy9_|lt5kcM>jL=w6&YRTTRoRJL}Z7-D^x1T$}N@YFDSyqJ*%ZPgNaHHnL9P&rVp!
z_ik$aF_rB6?^mw%1>I^&IcPmGQ${zd|7)_xc3tJ%_08MLf8VT%b@Dpn#KjY@Zt>ZVLq*zzfK_J4-(8<|`8#oSw*Vexj$tm|Rd
zW4HW$CHW;(^x4O>HG7V}3W`dKGA#%axuvBfd}Z;koVA@YQxAsOX6R^s@k$W6SINBY
z!Q0%eYaa;(w_AqkEl|ix`Ny{ZX!`vX$;N-xzHO^IGxO=Lu#?J`C(|ObHXZph$y4Re
z3|CZ}3kKEj{;-ymBu8ez!vR~Of
z&E~Cibxpr4rS&pW_SyaNyd0i;SxAiv!E03yvYdYC+Wj5#i
zE7<`;LYKM?LL9fmx!va#ezop$t$DfB**lZ#-uW(S4vS3ux9Dm}eTT@lRTGa))fPTB
z$!D{H$E(ngR>jl+iD{Z9+YR2SiUp@Tsh?hPdxEi==62>&8mDI1Jn|PzO73{PbZunc
zq)FeCJ>Hn_5RzKbTeBgh@PN>f!_w0&4zhng_(Ql+TQyVc=h`Z>shrHwp=(uDv!~9=
zxOrvcjg*|mNqn6D8SKx0KJOINRI==lr^2ePm20*ZZ*t_GG4o=Rgy-*xP3-e`KJCBr
zr(|P+^ehwCR;%3bh_#wqd?k|4tIzq;;u=vXd0
zbNwu&cvqcN&iAjHe8+6L$Sao>a@PHie=ZZL|4@59G{SW2&6TU4tlxgv-B)L-(E7*9
z5hr;rte2a2yvVzi%RA+VTJ@{UfTsFRT{L{)jWzf^N&snTs7?K4w46=b^mTU=Nqmw1*A3cACQ35l0mw->O=0
zYfR-;v-m0+?(F=fHh0+-6SH?eV>4csiUddJ8TuAYeVp-`bCQVA<%tsCj{Y)y^7-Ve
zNy0Zz|LvRIaXrsm`Y7l5SC@=8sj8Joz1*}yeShutU#Zt+7j4d{7B6^PQno5sHFer!
z-lC?~#;wQk+A8R7-^2R!@*xhl5!T$6^kSblX$nTpK)k1QW>+}$(El;n!{<&yQ
zoaNMKnT#F}S%fdlzMm{m5te<;is?VYZQlIYMfvAcRxOiT6YTcnXrSMNmU%Y(7eby}
z`zx%UQ04hVtn+;GXRj|eY@92W9bWsQ)?J_~FmBSeAnEEY*FEIaRg8C}#-w^AeyC7+
zpvPwC-x3?B=CE8?<+j)Q)l=SoE&m}i{|{f2fM?g9@T~6+Z-ZET%uhY%@;uM}p!Ckf
zwWXTpD;KY_s(2%IY~DjAH}~7ezrTLcq?cK$VzFoCg1^>lcymQ{Q)Pp<^u38Kp0jD^
z=lL7|Gi;C7G$>j%Crpyh%R|tu?5yO1JcWB(b(?NUy7*k~IH|GG>BG@UO9X^0Q(QkW
zB>(yH+hVyWXV8}IQ!D;mep7I$yQlNc+vn%{O5V+V8)Q<>9U8cBX{zBW-C(Y=QVo|?
zJ(5wO8Z9y(Di!8+Eo(Y&XUgb$gW+DMds0r+JgN8>cMq8Co4PBDW74M6uJO`Gj%aOv
zwje1$fBAEXI~+Pi9QQ;STs&vaioH|3A}pypk5Q@C;zE9*UDMWSYi7-Q?tD*Lb(co=
zYdxbGYk1v0t+_YvYT2?0d0I+xk9HDP7>)
zzjj6betDPJ>-);gxBuOH@$!xJ(|MJHO{RKf^O`^EJ}_}XhtC4B6BFh>EwC#-?x&cz
z<)BiCgZP!X?8<8yl4jNmgso&?UNK|umo=vs@9X=wROfpYZ(!i<6EBu8*wN^pwjVBzpzO&80wuL{Zt~LA16wy!R
z!ZDXFT?h$vR+R~vVC$M=a&dFZa@IRXCA3~iw5FZ<*psrj^T@(<6PN1C5;4xbJu8K(
zZu_sFE_LnhstZ;2?#FG*IxlXJYTC5c_f*oRJ6ZE9Cr*5pxFc_pThao?>FP3bzO&E#
zTH*5kxZl31)-oO*xmOR|sPB&utt`8AciXiyYgera%bL5PW74!oD|u^~Wu}KtNOECU
zm=N8>$<8iox9RY5?M@*xH4&SaCF@1o|1+%gdNq4#TvWZ(rT4*q&2E(35qPC{^_8xp
zW9hQCsH*7FLwoMzeixanuVb`k_vRhi3Gdhy7%xSvVYcL&ts=bS;JlXV;2VC|E03}y
zF(|}LOfs9l{-D#|D{o!PD}U>IuFx&B)G&>k+pu?&8T*`FDcD3KCiGZ
z;E?v$9i^gK-Tr#Dc8iw=W{X(sOHY{T^|kkhM)BOTYn%1+euv-l_FMF)yrd-c+-Xtm
z4$YNM{X>lO8E?6q68v`XnEiRLZ_T+&p1A(}&mj87T6{}jXRcQy!(5imzS^wXMVIof
zU0IR4%*5V~{qv>kY`$?(Ogg12zjVvW=BTmWS$yljzyA#O+g+5heP^&pOmud#TQ04&jdfT#QK3+Mo6;pA=ej>%BOVBM
zw$6?SF}Kc*2-tS?%#5%1W{dp|UH#_U=E8khvp6$eFUqPq@yPC1>XckPJ3iw%{1Pt>
zZZj6#FuAoqU>nywWiP>H@BAioe>5?@b#BXyC)TNLl82^ANVgqs@CZEr^JTll(PrHR
zA!X;&Cvz9{g>TnVxbXMphR8f_)$Q?dFKRO*XZ~lfXwfM;|1RvL`uTrV!Yhxl{JUuD
znv%G|PyTmZRoK~!{~3a2spUO>S@GJs_eXchw~WWjGq27}oqqAl`Q1tD9=&=n?I4dk
zyREOEvBdQZb+O0kTEfyoHilB?_RV?86P~y*cHZOMR6Wt$B)3fVYPWk=m%h0*>F~cA3t7RzqSA9T8}fcV@m8_+?#Q5f6F0;
z$0xKV)y;pmV&!hfm9-X2_GDMAQuo+vo_~1Ll9^9_mouK1e}BL|*?x7O`fFdy+d7io
zxe~9`Z*{!X`4(&F_T_8Tx+YCNDNnVD0!@|@(tj5SKK}OdN@DaOm8KW+R<|ZbzfTGg
zy{kQK_s;;|$GdD-tX)5=ZE?WK;#G?#nND-tyfj3?@FC0Xg2ggzIvk&Wmo)cpsEO=)
z+~rc2`E5hQwj(Vp%?DlNHik_*HQROfj@Rzq<;$~UPo2_w
zMx1gV`1shYpRga2R@S=wgm0T|^4!8ByWR<`%(T3;;?k4M?+R}{X1iZ4DUbEZHJ$F6
zQzLI
zh&4?YVQKIbZ?xHWr{eOe{#WAeVWBhA*KR+VGS8)N*4Cwrv&*h(s^l0;y5&qbGPlM2
s4j0$kC1%Dux%?LQ@UCBeIIgn(
Automata
+
diff --git a/web/root/src/theme.ts b/web/root/src/theme.ts
index 88f7625..8dd0244 100644
--- a/web/root/src/theme.ts
+++ b/web/root/src/theme.ts
@@ -1,10 +1,4 @@
-import { getGraphTheme, invalidateGraphThemeCache, network } from "./visualizer.ts";
-
-function cssVar(name: string, fallback = ""): string {
- return getComputedStyle(document.documentElement)
- .getPropertyValue(name)
- .trim() || fallback;
-}
+import { updateGraphTheme } from "./visualizer.ts";
const themeBtn = document.getElementById("themeToggle") as HTMLButtonElement;
@@ -28,66 +22,25 @@ function setTheme(theme: Theme) {
// update button label
themeBtn.textContent = theme === "dark" ? "🌙 Dark" : "☀️ Light";
- applyGraphTheme();
+ updateGraphTheme();
}
+
+setTheme(getPreferredTheme());
+
+themeBtn.addEventListener("click", toggleTheme);
function toggleTheme() {
const current = (document.documentElement.dataset.theme as Theme) || "dark";
setTheme(current === "dark" ? "light" : "dark");
}
-// init
-setTheme(getPreferredTheme());
-// click handler
-themeBtn.addEventListener("click", toggleTheme);
-// optional: respond to OS theme changes (only if user hasn't chosen a theme)
globalThis.window.matchMedia?.("(prefers-color-scheme: light)")
?.addEventListener("change", () => {
- if (localStorage.getItem("theme")) return; // user has chosen, don't override
+ if (localStorage.getItem("theme")) return;
setTheme(getPreferredTheme());
});
-function applyGraphTheme() {
- invalidateGraphThemeCache();
-
- network.setOptions({
- nodes: {
- font: {
- color: cssVar("--graph-node-text"),
- bold: {
- color: cssVar("--fg-1"),
- mod: ''
- },
- },
- },
- edges: {
- labelHighlightBold: true,
- font: {
- align: "top",
- size: getGraphTheme().edge_font_size,
- color: cssVar("--fg-0"),
- strokeColor: cssVar("--bg-0"),
- bold: {
- color: cssVar("--fg-1"),
- size: getGraphTheme().edge_font_size,
- mod: ''
- },
- },
- color: {
- color: cssVar("--graph-edge"),
- highlight: cssVar("--graph-edge-active"),
- hover: cssVar("--graph-edge-hover"),
- },
- shadow: {
- enabled: true,
- color: cssVar("--bg-2")
- }
- },
- });
-}
-
-
diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts
index 0e9712b..526d123 100644
--- a/web/root/src/visualizer.ts
+++ b/web/root/src/visualizer.ts
@@ -6,6 +6,106 @@ import * as vis from "npm:vis-network/standalone";
export const nodes = new vis.DataSet();
export const edges = new vis.DataSet();
+
+type Color = string;
+type GraphTheme = {
+ bg_0: Color;
+ bg_1: Color;
+ bg_2: Color;
+ fg_0: Color;
+ fg_1: Color;
+ fg_2: Color;
+
+ node_anchor: Color;
+ node_border: Color;
+ current_node_border: Color;
+
+ edge: Color;
+ edge_hover: Color;
+ edge_active: Color;
+
+ edge_font_size: number,
+ node_font_size: number,
+};
+
+let _graphTheme: GraphTheme | null = null;
+
+function invalidateGraphThemeCache() {
+ _graphTheme = null;
+}
+
+function getGraphTheme(): GraphTheme {
+ function cssVar(name: string, fallback = ""): string {
+ return getComputedStyle(document.documentElement)
+ .getPropertyValue(name)
+ .trim() || fallback;
+ }
+
+ if (_graphTheme) return _graphTheme;
+
+ _graphTheme = {
+ bg_0: cssVar("--graph-bg-0"),
+ bg_1: cssVar("--graph-bg-1"),
+ bg_2: cssVar("--graph-bg-2"),
+ fg_0: cssVar("--graph-fg-0"),
+ fg_1: cssVar("--graph-fg-1"),
+ fg_2: cssVar("--graph-fg-2"),
+
+ node_anchor: cssVar("--graph-node-anchor"),
+ node_border: cssVar("--graph-node-border"),
+ current_node_border: cssVar("--graph-current-node-border"),
+
+ edge: cssVar("--graph-edge"),
+ edge_hover: cssVar("--graph-edge-hover"),
+ edge_active: cssVar("--graph-edge-active"),
+
+ edge_font_size: Number(cssVar("--graph-edge-font-size")),
+ node_font_size: Number(cssVar("--graph-node-font-size")),
+ };
+
+ return _graphTheme;
+}
+
+export function updateGraphTheme() {
+ invalidateGraphThemeCache();
+ const gt = getGraphTheme();
+
+ network.setOptions({
+ nodes: {
+ font: {
+ color: gt.fg_0,
+ bold: {
+ color: gt.fg_1,
+ mod: ''
+ },
+ },
+ },
+ edges: {
+ labelHighlightBold: true,
+ font: {
+ align: "top",
+ size: gt.edge_font_size,
+ color: gt.fg_0,
+ strokeColor: gt.bg_0,
+ bold: {
+ color: gt.fg_1,
+ size: gt.edge_font_size,
+ mod: ''
+ },
+ },
+ color: {
+ color: gt.edge,
+ hover: gt.edge_hover,
+ highlight: gt.edge_active,
+ },
+ shadow: {
+ enabled: false,
+ }
+ },
+ });
+}
+
+
type StateId = string;
type GraphDef = {
initial: StateId;
@@ -163,58 +263,6 @@ function createGraph(): vis.Network {
return network;
}
-export type GraphTheme = {
- bg_0: string;
- bg_1: string;
- bg_2: string;
- fg_0: string;
-
- anchor: string;
- selected: string;
- node: string;
- current: string;
- edge: string;
- glow: string;
- edge_font_size: number,
-};
-
-let _graphTheme: GraphTheme | null = null;
-
-export function invalidateGraphThemeCache() {
- _graphTheme = null;
-}
-
-export function getGraphTheme(): GraphTheme {
- function cssVar(name: string, fallback = ""): string {
- return getComputedStyle(document.documentElement)
- .getPropertyValue(name)
- .trim() || fallback;
- }
-
- if (_graphTheme) return _graphTheme;
-
- _graphTheme = {
- bg_0: cssVar("--bg-0"),
- bg_1: cssVar("--bg-1"),
- bg_2: cssVar("--bg-2"),
- fg_0: cssVar("--fg-0"),
-
- selected: cssVar("--bg-2"),
-
- node: cssVar("--focus"),
- current: cssVar("--success"),
-
- anchor: cssVar("--warning"),
-
- edge: cssVar("--graph-edge", "rgba(201,209,217,0.55)"),
-
- glow: cssVar("--accent", "#79c0ff"),
- edge_font_size: 10,
- };
-
- return _graphTheme;
-}
-
function renderNode({
ctx,
id,
@@ -236,8 +284,8 @@ function renderNode({
const isFinal = id === "q1"; // <-- change if your schema differs
const isActive = id === "q0"; // <-- change if your schema differs
- const fill = selected ? t.glow : hover ? t.bg_1 : t.bg_0;
- const stroke = isActive ? t.current : t.node;
+ const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0;
+ const stroke = isActive ? t.current_node_border : t.node_border;
const emphasis = (selected ? 1 : 0) + (hover ? 0.6 : 0);
@@ -306,7 +354,7 @@ function renderNode({
const physicsOff = node.physics === false;
if (physicsOff) {
- drawPinIndicator(ctx, x, y, r, t.anchor);
+ drawPinIndicator(ctx, x, y, r, t.node_anchor);
}
ctx.restore();
diff --git a/web/root/src/wasm.ts b/web/root/src/wasm.ts
index 74f9084..80bf4a2 100644
--- a/web/root/src/wasm.ts
+++ b/web/root/src/wasm.ts
@@ -9,7 +9,7 @@ try{
wasm.init();
}catch(e){
console.error("Failed to start: " + e);
- document.getElementById("the_canvas_id")!.remove();
+ document.getElementById("app")!.remove();
document.getElementById("center_text")!.innerHTML = `
An error occurred during loading:
@@ -20,6 +20,7 @@ try{
Make sure you use a modern browser with WebGL and WASM enabled.
`;
+ throw e;
}
export default wasm
\ No newline at end of file
diff --git a/web/root/style/themes.scss b/web/root/style/themes.scss
index 58710a4..ac86de9 100644
--- a/web/root/style/themes.scss
+++ b/web/root/style/themes.scss
@@ -29,6 +29,25 @@
--dur-med: 160ms;
--dur-slow: 240ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
+
+
+ --graph-bg-0: var(--bg-0);
+ --graph-bg-1: var(--bg-1);
+ --graph-bg-2: var(--bg-2);
+ --graph-fg-0: var(--fg-0);
+ --graph-fg-1: var(--fg-1);
+ --graph-fg-2: var(--fg-2);
+
+ --graph-node-font-size: 14;
+ --graph-edge-font-size: 10;
+
+ --graph-node-border: var(--focus);
+ --graph-current-node-border: var(--success);
+ --graph-node-anchor: var(--warning);
+
+ --graph-edge: var(--fg-muted);
+ --graph-edge-hover: var(--accent);
+ --graph-edge-active: var(--focus);
}
:root[data-theme="dark"] {
@@ -52,20 +71,6 @@
--warning: #f2cc60;
--error: #f85149;
- --graph-bg: var(--bg-0);
-
- --graph-node-bg: #1f6feb;
- --graph-node-border: #388bfd;
- --graph-node-text: var(--fg-0);
-
- --graph-node-active-bg: #79c0ff;
- --graph-node-active-border: #ff0000;
-
- --graph-edge: rgba(201, 209, 217, 0.55);
- --graph-edge-hover: rgba(201, 209, 217, 0.864);
- --graph-edge-active: var(--accent);
-
-
--ansi-fg-30: #0b0f14; /* black */
--ansi-fg-31: #ff7b72; /* red */
--ansi-fg-32: #7ee787; /* green */
@@ -115,21 +120,6 @@
--warning: #9a6700;
--error: #cf222e;
- --graph-bg: var(--bg-0);
-
- --graph-node-bg: #1f6feb;
- --graph-node-border: #0969da;
- --graph-node-text: #ffffff;
-
- --graph-node-active-bg: #54aeff;
- --graph-node-active-border: #0969da;
-
- --graph-edge: rgba(31, 41, 55, 0.45);
- --graph-edge-hover: #0969da;
- --graph-edge-active: #54aeff;
-
-
-
--ansi-fg-30: #111827; /* black */
--ansi-fg-31: #b42318; /* red */
--ansi-fg-32: #1a7f37; /* green */
diff --git a/web/tools/build.ts b/web/tools/build.ts
index bc88fe0..fa4834d 100644
--- a/web/tools/build.ts
+++ b/web/tools/build.ts
@@ -14,11 +14,14 @@ async function run(cmd: string[], cwd?: string) {
}
}
-// Clean dist
+console.log("clean dist...");
await Deno.remove(DIST, { recursive: true }).catch(() => {});
await Deno.mkdir(DIST, { recursive: true });
+console.log("copy assets");
+await copyFolder(new URL("assets/", ROOT), DIST);
+
console.log("compiling scss...");
const result = sass.compile(String(new URL("style/style.scss", ROOT).pathname), {
style: "compressed",
@@ -40,3 +43,23 @@ if (!bundleRes.success) {
}
console.log("Build complete: dist/");
+
+
+
+export async function copyFolder(
+ srcDir: URL,
+ destDir: URL,
+): Promise {
+ await Deno.mkdir(destDir, { recursive: true });
+
+ for await (const entry of Deno.readDir(srcDir)) {
+ const srcPath = new URL(entry.name, srcDir.href);
+ const destPath = new URL(entry.name, destDir.href);
+
+ if (entry.isDirectory) {
+ await copyFolder(srcPath, destPath);
+ } else if (entry.isFile) {
+ await Deno.copyFile(srcPath, destPath);
+ }
+ }
+}
\ No newline at end of file