From 3a0a1800cade4b5c7f3fc7dc2b1addb0987d9570 Mon Sep 17 00:00:00 2001 From: mrsibe Date: Fri, 2 Oct 2026 12:48:20 +0800 Subject: [PATCH] feat: move notes tools into studio-style content cards --- .github/images/notes-studio-dark.png | Bin 0 -> 15041 bytes .github/images/notes-studio-light.png | Bin 0 -> 14672 bytes DESIGN.md | 88 ++++++++++++++- src/renderer/src/assets/theme.css | 28 +++++ .../src/components/notebook/NotePanel.tsx | 104 ++++++++++-------- .../src/components/notebook/noteTools.ts | 49 +++++++++ src/renderer/src/locales/en-US/notebook.json | 2 +- src/renderer/src/locales/zh-CN/notebook.json | 2 +- test/noteTools.test.ts | 86 +++++++++++++++ 9 files changed, 309 insertions(+), 50 deletions(-) create mode 100644 .github/images/notes-studio-dark.png create mode 100644 .github/images/notes-studio-light.png create mode 100644 src/renderer/src/components/notebook/noteTools.ts create mode 100644 test/noteTools.test.ts diff --git a/.github/images/notes-studio-dark.png b/.github/images/notes-studio-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ebf1d2b852ef984a0dde040c4bc4b7da75fafa59 GIT binary patch literal 15041 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 Cc)6

j zoS@6T!_nvN1^Y_MCwwOrULJ-P1w}dcsI1!N`Erp`MPf-xh(8k6_ zQx@D0u@Ko2A9il9``xnMHew@p+UBn(IvO#mI11z$Sw%%!3p&URsCV_>n!yfqV_48i@*0p)kj9bJj--cm`0q7ApwVkRCAw$|oi zq*2gMRSrH5Fd6)V3iuc;*)t|}7+)Vo0C|NVy&9{kQ)MPF*P{0fTG;AGH>ki4A(Aqqv@3vLikb=7|Hgqbmg1 zC9dSz;8^80m=BDK!Z5y4XEGD_Y0)7J5S7;TI7znGCE@6{*LQ8N<=EBK<=L=$sNo#T z+tUo`FOCA9E{_*%$L?IWn7~@hQ%MY{MdZ_yA`@95P zW!{I$!O^=1RLTpCO+88(!>Nf*I}uxxTZXn70!V1ReKCYwxtTRhBSBn`7gVeaQlF4u zFBB7NA6F`z*l(K#MywmWOeP7>P4)CIQJ8GXY0?__igq61vY&x|LXb=Q$3fAyb!?2y z&Im9B*OZ(Nq+-6}u)6*e+FBjIY&nLP=(|Zg@nw)fuG819R`hB+USu9+Qd_*#+&@o+ zH|5h(b-x?0yK!hN$PPK&o(K+ZVur^pR^+6FeS>?BuS78DWLv zkhs=FIp#EYg^Z*3pnw91Jh)zmkbUT-pRVZjUT5*en$!?&%c;YJnbAvQH{-=fM za`E=~A#zvKZOQQ@$Jzk+*dNi}Z*wBjkVozO8B5 zwL=88792#;_;6+`k%}_yRx!hJ0}JxUaZkePL)1xRq3@jjeJBooiaMBwA z6s=^Rj@{NN{k3=`;J7s&Epd}!$`x>oT~fHRykK3 zhIT9lYRVJCX57N0eTVAz?a#7A(IupYoUiO3?Rmz@7gq&GlRy!WP77)ecrR7@!}LZW zkhhL7h?qy^hd!v~X3+m>?fic%FPTkv3RE6Z5D<)XIHV1DXnwQ~PiHnLI~#RWpkvwC%@&oU9b;SulZZf+tXB1R%XVBuY4UmI0ZQ_|s- zrCS~Qjp}Y3z=u95eB3Fr-1I_HJ6}=?&yQVHN!eU2j)jv7jYqHU5vRS~=2<}N;!v?J zx?dcx&Ivg$D0f8@#Kr<0!!Rfl9blne*AwmTI+g=S`E#HTpn;(5TTiF90u)wJkib*t z0Q9LEQ{5-|oKM;w+Ai9()If)1QMQ&$i2>=!gakM|DZGl5jgF6$yq95nj4}2(0SQ8> z#JPw@@XV(Us43?23A?bi_Arp>9qv2vi4qwM&MKvW&}yxAR#ouv1*&6j;$!Kl{BEvP z7((_75&Vb4IK;Y%Y+wyXe zOPVGvK^T>l^O^;$|H@O*K(j4VPzMN?ur%n@7LnPp=FOXQitc@re^91rym z@YnYeA6-6(1%dtDHao_)rybwycV0KfgR7rtc+l}OpE>oVSa<`(C5ud z4=-R2^4rtryUqAM1rmxpS`^VePeM3CYt+Lho8bg|aPg6tvoH?^k03~A$d*6=XjJjr z90k&K+XGSFxM9{CGZbi49&vhO=fzPL&9jOQ{nDI!#d;6FSJmuq-=whYOh6;v=d8_O ziZKCK!$TCJjjk@UWF9}i!-b^>zU&&tlL_fZhO9~C8!0v@t69;_!c0CDyN7giAC|*_ zdGG@M=l5JjKzqk77VlbL6bq{uFTb`170-*7QqOGziYZ-TCQo8xg|wF9Jus{mrp4}~ z&P01slSByZQx6T|%=bS+}qVK1_+dWtN1z2fO1_-V*o-|2EP+NH>BIRRYCe z$jAvJKPS2Crl{;{tLYlx2dYaa%D+j7ewZYY9sSo*)U2$uv-4ttFXTJ~n!?2Q7^awT zU4Gke`a>6vXv41dJPd^FtBy+F#$yko8q1TCdIyddDkT+yJ}=kWpg-6tp7zesQgJmM zTU@HLTFc=;%UK8s0nOfJ;zoj!kVCFu5WODa`@43wkL|WUKzo#BcDQW6=J)*63YZEz z>_RtdT`AwvU|`F&k0$WF23XamE(TFLhYE|gO~*r%QtnP(CVtZzXco1#a^6DP*=x0| zlQJZl8?E5xjW$#rQaOu9N#%B@Oxy1>(`b5h$;E`t;E|X#nvKQRt*g|ukDHEr;(*d? z!EIi@l2$1y)-=qBlwjbi3)6^6LZb9$?ud>UuQEapQwqKz0rzhf)c;~?{L6Ov)zo?p z5;KvRN(CY>&f4{qZk=tpoVxP`1S&hZV7!MFQ8w)epEqswEqmx^36M%ZPk8E&$U2AhRY z9K0)eH&<)T!qtlMA{S@t%qAez>ZsE}ro!eLvjphp%W8y2ezkMN!%wkmd_F2_ufqH9 zQcy3o;&;x~TD3s1Ge%q6lauPpw$e(D>#?MaTRXEUso>RK1DQpUgk9qMDN*ZI0oRbX z_O0_FI4%}ji1MxWh-gu1b(&AdaDlU_2Rsq-f_>HLTC76R%dKqJ{c8THvju~L%rG>l zw7r~kPy55)3uQ`W?AGW>4YXVyvRN!7GvQHxxHZ+LKlpeBeIl>_LJLmBELZ8UP9%D| z9!?|N`ef765~mq=Z3-nrxv#UhvqNF@3k zycmi5V!TSMtugv&A%A~ZR13Kw%?cJ9f^?cs*^&s2iInN_uE9_H1;k;umaPSU$|{bV zkP2V5^}7PtT)yNHoF^Ch^p-qQP_@NBjL6xYsu4-R7E})mhtOi1)pX{l#kbj>1&3_q ztZx6Mx$Or}Qz#KI5XAIcD>%A>QrFW^UhqL<*jD=B>f%ddevJ?~8q=k?o+cW(V22~{ zLwzbal(fXO)V`It4J#F*Doe4!l(0K(lglDNH62gJAVh%O%%%}*BlfF5wV8mZM~>0>Ya zjGiz)KER&oS)M6NE5>$3%(xp8ul9*;X-7fLbVN%iyCSQNe#<#s`ZP$8;+)e>7?2Q< zt+nx!mm1b>GAv@n$5SV?;X64gIaQ3Mc^o$~US}Jb*X9!8DpAC4 zbe9iNs06c02VYk>-JLcxV`Y8Na4?y!q~@B}tB{LRbKqFy#;Rl@S|&PDn?3Yh?VuBZ zeaoyV(9?0(k$$!AQ+Ya?nu&IFrJa;icFp80eBb9PI^$BIneQTl@80~IpQk%96#+t; z*Ut2|!EmbfDlUeT`rjTB8XiU$XT#vXwTlWJ2~U-&`q^Ec-Ukv2mnMcWiuw$V-w=q?6Z^0EP)s72DO~=ITBr3itT5uqZ-n z)-F=39P$dQN!4mtw1}9sWlJM~a5$YlJD=EoGZ}zja7Hhm3Qt^{2 zqED4GDV8=BHB~#;_PgbK9>Kk&gyHN;|I8}lJGrO5{q2$v&Xyko`S&4zLB{!|`DS2g zq~2Xz30YkQzj81xk3=Xs76vbEPrdV2Y}4asjeDs09m2;oYf&E-9%qbty;rF6%{0-s zgv4v3eCQ`az@#>7*?Rel5)Q0+N}7@Wa5>#C|HbSmMWWsOWXw%3(ryoji5b50=`|hF zBw`Yy8qW=9;1NTQnrbFakAbeI`4zxy^O`JPS?+Imaq>Y|Zr@F9E3jDjEkR!kv$%YT zluN+|zV8$tx8-C$SgdjGoKK`uya`6V)-<}ycc!G_-&sId5!KkQR@0zz3KqtbN?D=) z{b47MdH050F(olC$FrErj*StvUq7L`wk(rAJ3kUo$3|1TQmPS=eu`K@uEW%f+zuh> zpm%f^c@arH)N;i6nX4M%hdX~ru~nYV+ggb(X1twsjhw?o4=iwaCeGcIRMKPmzu%F> zZy-qZE7DQWhQc(lUz>*C*eR!$Z%ZAyx)7E&5FtxCYAlVddSqLaG&2BmMbLiN; z`6;fc$F&O{*6F+4kTebkN*nXx!YU1pn4IX8HkXn0+PMXu-ac<%j+@s&@}EHN zAtO?C)xNB(p z>xh6cM%Lp$5!bNh4n;P9%}kLT&7al~%$A8H<1S9K5-6T7I)4aWqt0$Q=$qP@>Hk`h zlQ1)3(4B1ce6&29tqQRYM~MiypLR-T8rI1~Z}XH}LqI9BbmLBHef({m5ONwA^@R0t zv4NoXY&c7DSfiAhR%^07I$R$}G8;zIra=%^-+z$P5A1yI!nSMx3QOqdNn9=^jKw#S zr30o1>JdJ8o9D|?L&0B{jjz-Y8BZ=YZ7fZZJUMRHaE(`vLqsapMr>l>h#E>Wrx+Qp z4QEZtJM0e;->D4;t}VnuC}SLW#H61KJ>{&PZUWnmk*BrsQXcp;mzAx!^~QhPhdu+g z>B5osRgJUVJVNB4XEW5L=Lro;fa2QTP1Y*kEdzwq@-`V!Dn@9_t;=<|7z}?o4N3Rh z&-|`%8^?`nrR?R0&Yu5AFqi|0_7eB{o zY@9`_UEfmWq9+$#nnroA z_hu+b^;qQ=l|HWj|6P~gxB=1{d#uI-tI67L44inkNy~3&NGPRHyMJ$ZI5Cb6FPE(x zS7zN)_>;7wA3W@&kOU!G3}UR=&e~0sR9zk4kbA7R&{DLkvBVVCHfCLSj+te<_Pfzo zAp7t0evqo6F%vFo_v+a1UQdKPq+tC`1Hy)B?Oj(6Q)r-u=B5;WWNEvEvHbnHKL`o? zF?>`_>*>=pS8yOeW8j3l08ItHuf4f1cDooj9$3=y9<;D_+q%YP95Q=WM2~dA3ONl=GuN)+<<6i)*W?>rP5Cnm(;gb#GVZAiLdD&KKUpRZa_+ zYbEcJuN)J|Lqyqp-}{jjE|*D5n&jnZnEA)31JU7k*~y}BB0w^tXWhwpF!>;LM&Kaq zP=TVH+nbUTr|zJGH0=0IH{OL7N33up+j~QJSqmSULC#rc@9>q$NQ54cw{nr-$CjC* zD>xRJ>7QcB@o{|3EvP+PhVTvPXmL`8zBS%{al>akdNV}r<=jNr8^d`|Z@ni6{i~bx zQ#)QXli!@UY9c*j-Cz7hkr_Oa*6qRSv`VBOuT9L1&DZ%{1JMe8YOcnCZ0Q<_b4S6a z?OQ3$+w|oYUq2hJKO?b>4|H%V#=pAM_d4Hvzjsd|akvEMC1CrrG?8d=bn!mqKWqFX zYtns@QB<((Ulu?x5oP)EW^(FEv3iJBmd}<$SlaAW8|s`bbWz}(Dem-}4ATYXV&2a| z?648OS>d(a6h#YDAUQEkoZL+-Ff!A_rP4}mXASAh*tNb)CEaTO)X3pPc+ZGLAb7#8 zJOMUN-KO$jxXeqzjtc9F7jqf@X9LQzb0$AKV@r)DNdMr{aG><`z%HZBy4OTs_Hlp+ zaxoFYRlppZ@OzL8Sa$M7F@^W7$$L40HRN{~_ksl0aJ|zr8_js^x_46hyK7@r8`Xn> z!qA$0>$S+V9xu7KA|=?65^lS*?bz+#!`C9yk;vIdI4u=EM9#A$?Z>f%mkJS#t(R+P zWNj{ISLw+-?e-d9TJ@alP@p*Y&~S5tn4cQDs#{Hl zGT?lZ$f#W)+l4K~ehP4nHQY|ELm?$#SDAh$TPV16JSswc(3@#~!+ZT%$~!GIvC?y} z2?_P`6C_vV|Kj>+x84q87Ry7Qkh~}43%CCV;Q!0@Q<#cJV~ypDot+ias?b@f7&J9D zV!(uJzuT6Xm(c^s)pnmC)ld+!qZZo`{Jsq0)F|6GqoUbt>)6hFXm(t zqPqBY*pyq;SYhbcKBlV>QmtL)tFP8WO*&7kLBm6f8A0XK5`*~mlkreaow`~;Ioha> zy77xU)qnwv%Ks_I;^&Nh-6Lnc7))tQ_XSGCyrY=;m_9|*R~m`xMa#Mh;ku@%yo&K9 zGLHI@uj@3uL@{Aj-;9+zlhy@1z+aiat^zuY5N+ZpLL1`@E12mKdrbq5K;nbBU5_kE zL4u^5l2!f9Vz-%fL(n%vC-n}s^VVY^M?Pir3(XY9d$#w-77xt`24ec_+kkE~2W!IE z&PU*Xm5%qQs-{B29@yTlSSGALp9|+L}+)IWr9SI}^qo#&RSI@9gV9H}nL*u;oq^Vm^)dW;wN)dQpCS z+4@`1ZM})n-La7<7tslLF)Nadg(*wCtdKiQO&>-$wNNl&(klw`;SJ>zr+rzfx}Pss zV%-5;nX;Hv$0-8w*743ktywJo<8O-y(&<=CnT)lM5Xk`0YTJEta8j|ZLE1K9t))Q- z)*-}E1qLJUblQJrSSOpl|lJW3ZyVRH&bN7 z$BNutHmRhMSPc6#Hb;@u*|G`tV;66=Chmp9$T*0t) zhKFMt36E@w;RfmXRuhGR1p4@e{$L_<-rCGO0{jFVO0ZM0iE|n4+js!X>|}rvX~pR6 zGlx_i>*%X$pd%N7UrX`r&4JN&r!CLpDOwyDRriP4R>W=(-Lu>E%P%1%YnyFvwrFRV z(#3}3gSKzWO3e0SIit23P(Y_)aJifg`)A?dQwKG`tKhi>$Y;sXKWiJidg(EcaC>%zJ6+2Q=BR+XZ%TPxSY^?Ja-Tm=r{&FDx# zmsb3=HXzCU@qup@obPJ)@-GUQvKbGLYtH=o$oz2Gxq}2J-{)?`ZkN{!=c7~CxZY-4 zdW7%G&Ok=bv?1_x)#;>Hht_x?BFHW___fu|gDXdTv_C=J>F2p60<<7{lg`(0XA|eT z4U4`=1Agq!$q?XP8nj%IZvo3pL21@?lL(tbjjjSkzH#BhG)=&}IK7Ppir^|cc8hrI9oIWPFxr-Z;JwMdHzVlOFL(?{gu;2_EmPCF6V zg9!Yp*ZVej2_lh+`@_}Il|CQa@9&Duy5qJMPHREuMSFKw&dKv3@W)O2KloKF*sxuB zyQfD+Pgl1mo#b^PScc_u2;SO|std_VOmf8A5*^R-O>(YORKgvwYR*qgpPZZT?OhIV zPg8h`>|aS;^B(&Pgi4SI_{tCBdNn{3YFgGy>N_4($Nd>fr@lvV8%cz54p1Anu3Y5Lln zUfVeL)Fyw`J*!4FTZ9UqE{w!{M24bIjlQE@IKMwKws}G zk(NV6+fVe*h;jya0^U|6E$ni7TqHT0oS2Rd?i3yrR<5O$3E`9>VSZ*?@tak`H-Al3 zmOR#Iw^V#KZ{nLPeoGe6D^ke+!qxpRC*^o)^}CC z&YGJw%KipjLXUgl>%*@p#eB8t6rP}m0{=c(;4EAa= z0?AV@l3ehO@FL>9j*(Lj}b`Qt(?5yLv z@FFfPj~1%=VJHsD>vB}xPy`){8`#E9C(_3^FV5v*qm9Nofhkt6=&}1xiH%m8M@1c+ zQ6?9>kJUFjdY=5FbJbzIztFi0&kj#yStA#}_EkM;8F!_fRjcLunXa~$ZMGWyoGslx zb|3fIw<|r)8fBkq)P$q=m(8 zPn#NiL`L54G&_d2JAOgXYSX(lt&vY{6}d}nG1TvEps`a|2EV$Mi6h1zh%#(sTJqYy zq?^$jA99YM7A#vP)4j5lHrQ8@Nm&zensABd5jSWv8H*% zWjAuym6|HJ$eQIgj*wq?q90phr(%%hjVI*b)lW&lz+;Tf1-XTi17UV4iea90Fj=6!N0OFB9V_t?c zELd(Yqul+Xm`$f-ZauMNp0IP*a|4AyQ`y8WCfodwn!Cl%j(Ja1*qTgP`nsHtM-yH| z@?ovga z$*4g#)GMArM;u?(T|CcZloD)oDXX3!Xwj`1JA z$So7KY2!%3c7O#YE&FN+p*4Kx(Sl{2?JK8R%u-w9*GcybnbA>vS@uGI70BGx;tJiz z)L5EGuOShFLyR^?6U9-{6J!6Bw*OwmjF!bbiQD;vQP$*fEN**>Oii1rY3fw5xjTiJ z{pA(&d(|5+Iz|yUIzh!J_`bgS$K-08o>}tHvJ$4x_UPak%Fk8&`4IXj881+<3+=?=w=E;{%6#ePyoacj@good(U$k5hT&NJs9kEmaev$iYG zJsTc49usPFk z=SM#_+Ntfoh2IfS#i;JcCidOJS+V{B7@%0apW8>Tswzq=3!7Q9U0RrEvfKI{JHEeT)+!bWFT)dUYi&JZEMHBTJjAvw0Oq*ms~|c6{J0YTu<$}SWCxN?vv;-0X89;- zmJGv^#6d@F-1jIu`S*s$Ma{Uh_hu3Yw`)dVwTbmq!`b2S{7DPlYU-5L1EsM%vK`KSEf3UxNU<3TdzWX(JVr4zg4LDE`>`rmxUrIW$O3S*hK z%xXqmG3=0?C4*S{;9$!{RNhw4NdN&qnkUTWB{Mq-=aqxBhG4+&rSj||6J4Q>VkL7=MH2XsnI4>&9oIMAoG29`=V25`DRs&b5XV)O4U9sRXCjH}==^Yza6jWi2PZcLGB zjpkRqcbt1(BwgkPng^n}#JvrYHt8oDuMVR1&dZfeJ4D&0rwsN*DFHxyhsUzz7P*~f z+eO3G)02$0!}8}bqyWOzIr(3){3q=n?(|#AH4PBketo6w{$k0+MBZH#;5>ra4OWL2 z-?Av4=w)~^wS6ztgr}WSvOihfw`VH^4Hss|moJB8zJ;QbUaz&BYXO^Hr*F9LyAArg z@2$82*Z=hAy8ZaH7_hWuoWj+ben~@Wa)t` zXrE4ctSd9A*A{s`!A$sD?jt#!%>GPQZH?NU%Jr{0Y5VJW{Rv5+%^)tH79aAq%^SDj z(CXL}s%Y1iy5Quxy}AJaM|s?WPW5rB?^uy_R1Z|Vc@ynJ$A(rNwDEQcTL#!eD~aOs{>~~I{K=v_l=$C z_(hEN-lE-DCK)C%i)#oKk>Q4WfBWJjX=Dp?vz083j&CM6YG`gNkj!RZ<%LF-JP z9xEj=-cm{ong@-%rXeAxCc;S*PL2kscLT@O6;#B3H~rE5B44)Nc$m;G$SgQN=%N&} z#g4;P^^$DXTol9XFq(Gp1`2Mm+$rioRl21eOXiy6ka7axHP17-uP|dqW0_3=&9X9H z4bp6Yx3{YUvE%M=sRU3nJKNf^jFq>QWG@d?jW=gClwi0&o^5RQZFSWoIBA|ui3jZF zsXgh*J6_f;jF)bmRbN;YWSW{7ZHTXqOJMMYJsZ!>ok#;i-L{dKf@ELQ6}(@imoDS? zWab6gy0As%WZJIHHrtH7RA#08n51~bOcI^;Jzuo3&X3lx&AFvqAMzUl)mY{eK>I4y zKnwP}^xj{vKRS-4eg_}+)|!qdI;g9&!=V+#Sg$QAt>R>H5;P&;RyLz(i7j6V?}i1a zoo`l2-AxlocZywKUG2fNJ?JD!)eCT^;HJ<;(=E3bKV*+~{MuD3R z7SFz!&{uIXd_*2tdZW78wYn$fVzZo)sJFFhvS4zLHMxlbSdHq;h+hx3o?Egcf+9>1 zT@P0zp`d*M5LbkL?zs2YR8Uk(rEo;2ACztQ{e-V-Z$vAkP$`j8wAJ^~ATind<3Bjr zlVgOS!7@1xmkygg(&Nsht&4X$4S>U9xM7it)MC4AGg2>XuWq;0d@>K& zWZ{?jIUSc3(r{0<=yPn0C7#Mt&|JSRfO|dAYxj_Ud5L7XmfUFlcQjofd5d{T_P#U< zv|~JFz8ihy3inl=bhQ2v>CV0*ZON8!E???tOu}q#16IN*Ah%3|OflA*Hs#{t`a^Ov z*lck*%!Pw>H~h1cothOqGiS*3z1(Qlbd`l~asKPT5*5C`k(jfBwWER^Bi72u_7-?v zM;XyFl7m91yQE;iXNZu^nq=;JQ>)@GJW;JuI+&EZgHKJC;wdXn9rjIDK~ptYgVhH z>lB3|2H}OUl>++@it&HBM*jnxHCQ0Q%uBv_GhNabZ!Z6BPy*VjY*rreB~SE*w`z}e zhD>4rmnl%0F?IZhNIRJ(sZ@C?zUt-)3XB*5y z3}OR2Q&kt4FIH-kij;IsmBzg1d<#D-dD6-lRd&m$B#cnefqY^v+isxtB2O42C|@|6 z(V&gyE$KwqRKEq?5kIB%dMBrt)pvj=Rk=P+C|IYKNMh_r(^m`I1Embe`?7m2sg(N` z!Ba>R`t*_8W#mMEI-a(_?7@M5X%iLogqP4H@qRY#(cy=F$E$85rgg0>Ay596f?+6c8`AC4BM{bS`(L+ z=O?M1?t38~9^|%kl7U|P+(>EASI3UijMKz{;nr$`Exp!SfPld4{u3k6@;q-hP8MJ^ zSAXSu^AR#W<3+ycp(CKGs;naL#iM1uwP>1KoHMmfv|NUXAw1G!qDAX(g90%Bq0tnZ z|DAK-KO>KS(Wd>^O8NgjiCKBWX?=Y?g&g4`(T{|<&2}d-5Kl@L)NBKvPp7-V?Z*(x zSix^*IM~>pjMn){NvvQy?lMRDh`Mp>V)sFL^W`zvICSMX1DcUOkM zwbFbN<`9TV4hm5a7NG#S5>!~o*Qe`w!El8FzLxqk!{5KL?=_VtOWEBigMYsMiN3rY znYWG*_qSM{Uu^Q}eFjlOdk{c2sZMKe{r=P4!PjX#JD5s(z1!|MuQC%%i`rW1V}oM@ z%+E!*A;x)NbT*%g$gQZRrpNZSKHZOXgm2PQ(b?g8&nEw`Ea<)u_6Rc@d2H?X1O`$g z7zMYxvk)QC@Swm&U%RXRf;tmq*AfEiK?Y8K$lZ--}{8=UfSp#9^g=1J^z6+L756LVv4(RRcS35_9!~3lp#DSQs(q zlEeMw4QGtX=&y%~Oelq?_MeHwOfcBJNBC4m21jv{R}giDm#sg#PuYDQd~DNBHxER` zuNv&0w6c>MkwRcIyUz!jKm3>HuMam2MUI)6boG`ghXh_B`<(enq8J`XMjFw9RQA1{;CcPSVHx>7jVns*=T zc@71|>v5di{R7@>rkkwrBRa11o-Zw8VY?A6R?cE+bu`Hgsj7|o>$pT6unIkxU5Zv{ zCf1pp>}l-oFU73%Ja$vNsV76*qy5W}t9@vdYxD@gBMS@YEs{OF@}#J2pLzx{qj}xm z`Z1%pJ`&oJnv$|AbZ4*`#1hUhq706?m#UrXLHinp)Dv2W+F1K$o7@2;t^p=B+1GwZ zQJx)WkoyA=(LL5VMfiv7BF;1q;%vG+{$k26|C$nmG1GWH=( z%^H-0#iXPv%FE+L=S7uI&(6#&EYwt0yWg0s2zYKLc_(WLyDe*`wY&wKRoBuI_d|R# z@t4VLwFfi1T^})zb0k7HvtxrTI9Smdx&og~#oN+R0)q%@jJ*c984TD`PVV;Tkgg|4 z^-<*lw^6h#xcArKf4Vv^X))!*m?$#v6#nesALQe+*qMD9H%emoF>BF5=x=4I@DwQ3 zpDGk9_y%BN%wb@3zSM1b+WGb9v^!L^jT&Dn;A+_a_NG5uMFj5t{>z4P8Cwo^iwqwW zAL1*tC?rI*qO|lo%-OEn&5nTM!VwgBPugDscL&#?mhPU_MAWRA{(ZkSA+6VV$Ls!9@$Da);C%u` zx4O8(C-J)N*O^=lAl4A^LFv-R<~gYcgy!>ARF%K`yYH)pAQJR@Z#}=*=H0-WGj>pkYQMWa+eRrioD zRYB!9ApJK!TY949lR**nkd|v6&-=;g99$3SoBho-43q#R;t4)1A{2+o74|;^{&^eN zJBFB>^rcy8b>o3{(R&4Ye3#PQVLg)M6TICn9)hw4x@1$+qZ86!!837%loZSWW@ZYa zsT1?DdlSWfMmB@%C=V^Neh$?OoMb`JYvYBS01&BLPJVMsu=ao-TbGq$U@X}GqCM8& z_nbRCHEwgdblbg%QQAGP7PZ~#yjxnOFZ|0R3dwFAB}5}!AoVrUZbFyO7@Met@d5r< z`;UdkCVmI}63gg4k&kwV-RGaM))mcr>JRX^GoFvvhwz!?AvHoMak~jF3oI%%FgXfJ zj0V44As%Bg_-syQ+l$X~Z|E|s;yRXOBZnvVv}m%OmO(~Djj1NW)A+7aOOIsw?^a<$ z&l{|d%R%KgmAE_a_F-7;CJM+<*__gFs_&`L;k>|w7UmedFOvhYCMI2J+3>oA7EO#% zJ1V!iMljau`3nY%iggh{Z$5ghEL^Q> zDoZ_8>HA(YixUFUH}4mExpTtd+VUm+j;mb@&gVc}KX zgr75-@{7h5yO-@<9Xw^7%PURcZef7YX%`u>%g0*h#G=Qa?`k^#w#iL{_+3 I@VEc}0VyHciU0rr literal 0 HcmV?d00001 diff --git a/DESIGN.md b/DESIGN.md index c18ecfb..6d46f29 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -235,7 +235,9 @@ Rules: - **Never invent a background colour.** `bg-white`, `bg-black`, `bg-gray-*`, `bg-slate-*`, `bg-[…]` and raw `oklch()`/`hsl()` in a component are all violations. If the surface you need is not in the table, the table is wrong — - extend it here. + extend it here. The one narrow, deliberate exception is the four [tool + fills](#tool-fills): a coloured card background on the Notes creation tools and + nothing else. - `surface-hover` and `surface-selected` are translucent so that they adapt to whatever they are drawn on. Do not add alpha to them again (`bg-surface-hover/50` is a bug). @@ -272,6 +274,42 @@ blue-tinted neutral in the palette. They have been deleted; do not reintroduce them — the `secondary` **variant** name is unrelated to a `--secondary` colour and is spelled with `bg-muted`. +### Tool fills + +Implementation: `components/notebook/noteTools.ts` (the token on each tool), +`assets/theme.css` (values and Tailwind mappings), and +`components/notebook/NotePanel.tsx` (the only consumer). + +These four tokens exist for the Notes panel's creation tools — create note, mind +map, quiz, Anki — which are the one place a **card** carries colour. They are not +a surface tier, not an accent and not chart colours: a decorative pastel fill that +lets the four action cards be told apart at a glance. The grid is `2 x 2`, in the +order create note, mind map, quiz, Anki. + +| Token | Tailwind | Light | Dark | Tool | +| ---------------- | ----------------- | ------------------------ | ---------------------- | ----------- | +| `--tool-note` | `bg-tool-note` | `oklch(0.935 0.045 250)` | `oklch(0.32 0.04 250)` | Create note | +| `--tool-mindmap` | `bg-tool-mindmap` | `oklch(0.935 0.045 300)` | `oklch(0.32 0.04 300)` | Mind map | +| `--tool-quiz` | `bg-tool-quiz` | `oklch(0.935 0.045 150)` | `oklch(0.32 0.04 150)` | Quiz | +| `--tool-anki` | `bg-tool-anki` | `oklch(0.935 0.045 85)` | `oklch(0.32 0.045 85)` | Anki | + +Rules: + +- **Scope is exactly these four cards.** A fifth use — a badge, a tinted list row, + a status chip — is a rule change and belongs in this document before it belongs in + the code. `--chart-*` and `--primary` may not be borrowed for this either; the + chart guard stays as it is. +- **Text and icons stay neutral.** The icon stays `text-muted-foreground` (T2, + neutral) and the label stays `text-foreground` (T1, `text-sm font-medium`). Colour + carries the tool's identity; the glyph and the word carry the meaning, so colour is + never the only carrier of anything. +- **Low saturation on purpose.** Chroma stays at 0.04–0.05 so a pastel tile never + competes with the `--primary` action or a chart. They sit deliberately outside the + surface ladder: light values are a step _below_ `--surface-base` (a recessed tile) + and dark values a step _above_ it (a muted raise), so the card reads as a distinct + tile rather than as a panel in either scheme. Both stay above `--border`, so the + card's hairline is visible in both themes. + ## Window chrome The title bar is the one piece of the interface the OS and the renderer draw @@ -1039,6 +1077,54 @@ area, so a panel-level empty state opts out with `border-none` (`DocumentList`). The primitive keeps its dashed hairline — that opt-out is the exception, and it belongs to the panel that asked for it, not to `Empty`. +### Tool card + +Implementation: `components/notebook/NotePanel.tsx`, over the four +[Tool fills](#tool-fills). The Notes panel's creation grid is a `2 x 2` of these, +rendered as the first block **inside** the panel's content scroll area so a short +window scrolls the cards and the item list together. The panel header keeps the +`Notes` title only — it carries no tool buttons. + +```tsx +

+ +
+``` + +- **Density:** `p-3` card padding; `gap-2` between icon and label and between + cards. Height is content-driven (never `h-9`): a wrapped English label + ("Generate Anki Cards") sets the row height in a 260px panel. +- **Radius:** `rounded-lg` (8px), the container radius — a card is a container even + though it is a button. Border `border border-border`; **no shadow**. +- **Label never truncates:** `whitespace-normal` overrides `Button`'s default + `whitespace-nowrap`, which would clip the label at the panel minimum. The icon + sits above a left-aligned label (`flex-col items-start`), not beside it. +- **Hover is composited, not swapped.** The tool fill is opaque, so + `hover:bg-surface-hover` would replace it; the sanctioned overlay from + [Raised card](#raised-card) adds the translucent fill on top instead, and + each card's matching `hover:bg-tool-*` keeps its opaque fill instead of the + `ghost` variant's default hover fill. +- **Focus and disabled** come from `Button`: + `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background` + and `disabled:pointer-events-none disabled:opacity-50`. +- **No selected state.** These are actions, not a mode switch: pressing one runs and + returns (a blank note opens the editor; the other three generate an artifact), so + no card stays pressed. That state is unreachable and is recorded here rather than + invented. + ### Form field Implementation: `components/ui/field.tsx` — `Field`, `FieldSet`, `FieldLegend`, diff --git a/src/renderer/src/assets/theme.css b/src/renderer/src/assets/theme.css index 6ded888..1686d42 100644 --- a/src/renderer/src/assets/theme.css +++ b/src/renderer/src/assets/theme.css @@ -81,6 +81,22 @@ --chart-4: oklch(0.8249 0.097 82.2597); --chart-5: oklch(0.6936 0.1635 318.2073); + /* + * Tool fills. Four low-saturation card backgrounds used **only** by the Notes + * panel's creation-tool cards (create note / mind map / quiz / Anki), in that + * order. This is a sanctioned, narrow exception to the surface ladder: the tool + * cards are a decorative grid of action buttons, and a pastel tint is what lets + * the four be told apart at a glance. Nothing else may use these tokens, they + * carry neutral text and icons (the icon stays T2 and the label T1), and they + * are not chart colours — see DESIGN.md, "Tool fills". Light values sit a step + * below `--surface-base` (a recessed tile); the dark values below are the + * matching muted raise above it. + */ + --tool-note: oklch(0.935 0.045 250); + --tool-mindmap: oklch(0.935 0.045 300); + --tool-quiz: oklch(0.935 0.045 150); + --tool-anki: oklch(0.935 0.045 85); + /* Elevation: floating layers, plus a small lift for knobs and handles. */ --shadow-elevation: 0 6px 16px -4px hsl(0 0% 0% / 0.12), 0 2px 4px -1px hsl(0 0% 0% / 0.06); --shadow-control: 0 1px 2px 0 hsl(0 0% 0% / 0.16); @@ -202,6 +218,14 @@ --chart-4: oklch(0.8249 0.097 82.2597); --chart-5: oklch(0.6936 0.1635 318.2073); + /* Tool fills (Notes creation cards). A muted raise above `--surface-base`, and + above `--border` (0.3052), so the card's hairline stays visible in dark mode + the same way the light tile's does. */ + --tool-note: oklch(0.32 0.04 250); + --tool-mindmap: oklch(0.32 0.04 300); + --tool-quiz: oklch(0.32 0.04 150); + --tool-anki: oklch(0.32 0.045 85); + --shadow-elevation: 0 8px 22px -6px hsl(0 0% 0% / 0.55), 0 2px 4px -1px hsl(0 0% 0% / 0.35); --shadow-control: 0 1px 2px 0 hsl(0 0% 0% / 0.5); @@ -259,6 +283,10 @@ --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); + --color-tool-note: var(--tool-note); + --color-tool-mindmap: var(--tool-mindmap); + --color-tool-quiz: var(--tool-quiz); + --color-tool-anki: var(--tool-anki); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); diff --git a/src/renderer/src/components/notebook/NotePanel.tsx b/src/renderer/src/components/notebook/NotePanel.tsx index 780fee7..8207373 100644 --- a/src/renderer/src/components/notebook/NotePanel.tsx +++ b/src/renderer/src/components/notebook/NotePanel.tsx @@ -1,13 +1,15 @@ import { ReactElement, useEffect, useRef, useState } from 'react' -import { Save, Trash2, ArrowLeft, Network, FileText, ClipboardCheck, Layers } from 'lucide-react' +import { Save, Trash2, ArrowLeft } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { useParams } from 'react-router-dom' +import { cn } from '@/lib/utils' import { useItemStore } from '../../store/itemStore' import { useUIStore } from '../../store/uiStore' import { setupMindMapListeners } from '../../store/mindmapStore' import NoteEditor from './note/NoteEditor' import ItemList from './item/ItemList' +import { NOTE_TOOLS, type NoteTool, type NoteToolId } from './noteTools' import QuizStartDialog, { QuizStartParams } from './quiz/QuizStartDialog' import AnkiConfigDialog from './anki/AnkiConfigDialog' import { ScrollArea } from '../ui/scroll-area' @@ -137,6 +139,40 @@ function NoteEditorPanel({ ) } +// 创建工具卡片:图标在上、左对齐标签在下。悬停用合成填充(半透明 surface-hover +// 叠在彩色卡片上),不替换彩色底色。这些是动作按钮,没有持续选中态。 +function NoteToolCard({ + tool, + label, + onClick +}: { + tool: NoteTool + label: string + onClick: () => void +}): ReactElement { + const { Icon } = tool + return ( + + ) +} + export default function NotePanel(): ReactElement { const { t } = useTranslation('notebook') const { id: notebookId } = useParams() @@ -324,6 +360,14 @@ export default function NotePanel(): ReactElement { setShowQuizStartDialog(true) } + // 四张工具卡片的动作,按 noteTools 中的顺序引用 + const toolActions: Record void> = { + note: handleCreateNote, + mindmap: handleGenerateMindMap, + quiz: handleGenerateQuiz, + anki: handleGenerateAnki + } + // 开始生成答题 const handleQuizStart = async (params: QuizStartParams) => { if (!notebookId) return @@ -378,7 +422,7 @@ export default function NotePanel(): ReactElement { ) : ( // 列表页面 <> - {/* 顶部工具栏 */} + {/* 顶部标题栏:只保留标题,工具入口移到下方内容区 */} } - right={ - <> - - - - - - } /> - {/* Items 列表(笔记 + 思维导图等) */} + {/* 内容区:工具卡片与条目列表共用同一个滚动区,矮窗口下一起滚动 */} +
+ {NOTE_TOOLS.map((tool) => ( + + ))} +
readFileSync(path, 'utf8') + +const rendererSources = (dir: string, files: string[] = []): string[] => { + for (const entry of readdirSync(dir)) { + const path = join(dir, entry) + if (statSync(path).isDirectory()) rendererSources(path, files) + else if (/\.(ts|tsx)$/.test(path)) files.push(path.replace(/\\/g, '/')) + } + return files +} + +test('the tool grid is create note, mind map, quiz, Anki in that order', () => { + assert.deepEqual( + NOTE_TOOLS.map((tool) => tool.id), + ['note', 'mindmap', 'quiz', 'anki'] + ) +}) + +test('each tool carries its own approved fill and a locale label key', () => { + assert.deepEqual( + NOTE_TOOLS.map((tool) => tool.surfaceClassName.split(' ')[0]), + ['bg-tool-note', 'bg-tool-mindmap', 'bg-tool-quiz', 'bg-tool-anki'] + ) + assert.deepEqual( + NOTE_TOOLS.map((tool) => tool.labelKey), + ['createNote', 'generateMindMap', 'generateQuiz', 'generateAnki'] + ) + // The four are distinct; a duplicated token would make two cards indistinguishable. + assert.equal(new Set(NOTE_TOOLS.map((tool) => tool.surfaceClassName)).size, 4) +}) + +test('hover retains each opaque tool fill underneath the state overlay', () => { + for (const tool of NOTE_TOOLS) { + const [fill, hover] = tool.surfaceClassName.split(' ') + assert.equal(hover, `hover:${fill}`) + } + const panel = read('src/renderer/src/components/notebook/NotePanel.tsx') + assert.doesNotMatch(panel, /hover:bg-transparent/) + assert.match(panel, /bg-surface-hover opacity-0 transition-opacity group-hover:opacity-100/) +}) + +test('theme.css defines a light value, a dark value and a Tailwind mapping per fill', () => { + const css = read('src/renderer/src/assets/theme.css') + + for (const token of ['tool-note', 'tool-mindmap', 'tool-quiz', 'tool-anki']) { + const values = css.match(new RegExp(`--${token}: oklch\\(`, 'g')) || [] + assert.equal(values.length, 2, `${token} needs one light and one dark value`) + assert.ok(css.includes(`--color-${token}: var(--${token})`), `${token} needs a mapping`) + } +}) + +test('the tool fills are used only by the Notes tool cards', () => { + const users = rendererSources(join('src', 'renderer', 'src')).filter((file) => + read(file).includes('bg-tool-') + ) + assert.deepEqual(users, ['src/renderer/src/components/notebook/noteTools.ts']) +}) + +test('both locales carry the tool labels and point at the card, not a top-right plus', () => { + for (const locale of ['en-US', 'zh-CN']) { + const messages = JSON.parse(read(`src/renderer/src/locales/${locale}/notebook.json`)) + for (const key of ['createNote', 'generateMindMap', 'generateQuiz', 'generateAnki']) { + assert.ok(messages[key], `${locale} is missing ${key}`) + } + const description: string = messages.noNotesYetDesc + assert.ok(description, `${locale} has noNotesYetDesc`) + assert.doesNotMatch(description, /top right|右上角/) + // The empty-state hint names the card the user now clicks. + assert.ok(description.includes(messages.createNote), `${locale} should name the create card`) + } +})