From 967febf13112741f1ba3155a4ee3bebf086ab0b9 Mon Sep 17 00:00:00 2001 From: =AhmedAshraf <=ahmedashrafelgendy25@gmail.com> Date: Sun, 22 Mar 2026 11:34:10 +0200 Subject: [PATCH] [Feature] Color-Picker: Added Color Picker, For Memos and it's Comments as it's depending on the color pref. of specific memo --- memos_prod.db-shm | Bin 32768 -> 32768 bytes memos_prod.db-wal | Bin 189552 -> 453232 bytes .../MemoActionMenu/MemoActionMenu.tsx | 2 +- web/src/components/MemoContent/index.tsx | 2 +- .../MemoReactionListView/ReactionSelector.tsx | 2 +- .../MemoReactionListView/ReactionView.tsx | 2 +- web/src/components/MemoView/MemoView.tsx | 82 +++++++++++++++++- .../components/MemoCustomizeColor.tsx | 55 +++++++----- .../MemoView/components/MemoHeader.tsx | 18 ++-- web/src/components/MemoView/types.ts | 4 + .../PagedMemoList/PagedMemoList.tsx | 1 + web/src/components/VisibilityIcon.tsx | 2 +- web/src/hooks/useMemoQueries.ts | 7 ++ web/src/pages/MemoDetail.tsx | 3 +- 14 files changed, 141 insertions(+), 39 deletions(-) diff --git a/memos_prod.db-shm b/memos_prod.db-shm index 99ea35d8089af232d6bca72d4facc314cb389ed1..ad56e3d1518a97455c9c74ddb8980f0fdb0e32c7 100644 GIT binary patch delta 587 zcmb`9yGuf07{-4d4@D{^nPgkgB0<{yYMR|myIYoBwX&-=H8ptF#&GB#Xl^-E&?4C=$C|U=e)(mk7-u z_`K2lKWgFsF8u0Ky}A>RdhNBFD_d~kp_d^7Oft&?t8B0%5*aD=9IPmFQcnYoG|@~a zUBbAP)NDxBTUzO3m{Fz(u}Fj%yTW~tstnXnOB?<87-O0_mWZ;+Uf#AEX=i{D#tAad yGHYxR7uJ(xHPOK!ekPb9%nIvllgL}@V=Pyy@1{q+J9w4!ZoEQ%k<^<*yZjC~NMY#! delta 223 zcmZo@U}|V!s+V}A%K!rWK+MR%AfN}NC4l(#l)&c?4zE~cx5x8-uD;C=+0AS3wVYVK zhg9`Iqrd=UE(rgJ0*ENX#QKAqU6>}=Zw_E$VcdMhQA1#}6Vo@Q$)7?*fQ(m6n?HoJ wFaudXm{_?Olo_-(J2L%bV&n!=f0-D0z|`hvX+QXYO8zi03PS{LRBNyR095=-8vpP*Q4w8HZp*&!^PI`tl9`lV zXfFMo<|Jp%{rR5foSFIeIsfClvhZMz>G^YVa?Z$+-G)b6o*7+x@oV>)k8JVxe9I?B z=~BnP_v*j&|G$U+>(%ZpZGot291Mj=m8j7smd?%1dB4#p=FJtoSYM(m6+`)nS5xat zd7Wu6H)qYq-zX4MbcQ$O^m7bvF8O)crKJ;Pl|_Fn?bQFK=z)SO_4nti1^s#Ly5Hr! zTlb*u!AvV+T$+IG?81tsmAafj(68=|>>3fra6lQ4hP2lMqv~jAATq3k)d5R#c-cYI znfi(qD|ClUQN=r=hE>HE4TOU6zh}9-JoP;uV^4iUo5vU*Yg}DkVl)Q)#+HsAPm`z1 z*xA+6Uf;FN*y`D4tncmF+|nVIYxi{Y)W{iqV!5d5ABaYb4J}P#8Dqz0(f78s88>wDXJjH@Hj@L)7Bs;;hD6{)H*Rs}&!G-ZPclCji{ocWM>VlJ~gm=mKh>ZB|09-U^=QsBFfM- z?Zlz3+PA+-Kpq%dqOVxGRCiTV%*8(z@Q)eNP5fiWC^A014j5y{SE~v^Wpvs;X`{1L zjDTO%slZ@B01O63qG~w3pvmG?#Vg@pU?|%>u_CWB6jr#FW~)1*_yZR4>x@ITGJVC$ zmAb1fnv4>SD!$=Su{X(~@-(O9_>;+%JS7sRdHQL0VrTXEnEymB3gVckGY{zWqv9P2 zd5sNin;Q}nVNv}vTyYW$Pi-Nr z4vH&o&^N7GPM>W|nHUKv)2?pGnPQd8RzBO+KUqZvEXm;|2U<$>6;)NbgIlz!rj;hF zjwtdiAniqIT7^rS6svR7AQD%2O+;IzcmtSqrYz^-)CGEKQF%ocyO&&7jZrQqugtE} z!ix5)Y-d;6E>FD!#!Fgu=&~Yx#j<6(YnCS%8qr2JTZZ)n> zncmW2TwT?%xvRatO}=y2ce$Hewt6;-x9?;fjZfA-4#eJHvt27=C*STsB;XB*&oC3y z=9}7n%5-h}oxKfhEpEB}&aTZp9(Rw}0qsT0R`K~IZC}KP6>XeWHOB(Mpql(36Tia6 ziJOAI-saaKASh0TDtTj{nNLF&-!a{w;s#Us!2CW{BN5q>?M2$uR z!J*V)eOkRtof4~`+L+`;Dg9jBsZJBs7N9FY-8C*Rg413Wrj>Hq#MScJUn7>RKC~uJJ9n?X zBw;4En4h+^9CM84?g@TqJ;ArPl+!EiGi|(Hrc6K9(@$Dtc;`&{pcD>`OsfmY6S=qB z(-k{I)lnrdGVSDyf1ICse+veDJ2Px*+FWBwB(f(I_74mzk>T_eYIRWAtwfdZzqI1~j>gZ&NZPC1 zwAz+FEmrSlGR%73y`8e8o}hB|^2A)9TUfEVI@^hvzGDNH^w)W@Zu~32>qC)@y1Q0*~0R#|0009ILKmdWoDWJs&;_V0?e5?9_x6b>@ z<0Z1J#03`T5vNoLAbo%;xq zKnx*(00IagfB*srAb8AzkmM^K7Zul zA=(Qhffzyn0R#|0009ILKmY**5I|rN3usLP;&FlZeX272+aLbNb+i{)#7CT>A%Fk^ z2q1s}0tg_000IagkQ7)V2jg*pQs*tdH$A+wj`jjcAchb?009ILKmY**5I_I{1Q1xn z0tQ*OcwFH63qEl7weE+8XfLpck2pm`009ILKmY**5I_I{1Q0+VDR71yjK>8W6%{qV zc;Mi3v=>MMF@yjD2q1s}0tg_000IagfWRUaI8&A_9v8Uy;`ZMMF@yjD2q1s}0tg_000IagfWRUaSSHIBj|(Vw9NBU316%%q_5zFe zh*LBK5I_I{1Q0*~0R#|0009J&0`HQ8@wmW!+s6u@{I~9(l()zQ#EWLOEO44v)n^3?Zuj6L-YZ60HC4&&FBAE3yde1=<)RT$b~yL_ZT~R+uDr19W7gWJ=$#Ac4D)} z*BhTro9+v%N;DK65VK^TuKEX}5o1G3lb9uCGaEgP^}TI9#?_H%crY3mRaaN7id5AY ztAe3DRn^tn`qjN-fv_4$T|T~;+*NI!#?4)xmZlD^psS~LxZ2p|Y4mh?I^3Ra4QqA4 zUv1pnVch6x^9V(EeYd-Qqo=&2T7Q4u<4r4dIf0;G-5c385{Rk;%6K%Sy&gz94_K1J z%MO~()K{!np*v*Kj+a32MFXK={O?(5$0|NHairo>vmYUiHjOSWFA8yVxuYJLP%x?n zqw>gU6(GKHjWoW?1JQ{wHN*5+4OyMsMvc*zSYM4XZSiUiIoIc2kqtR9yCpuf^x(2H z^c58qxA5RaBJh6+W!`)WGgpW{58KisC_~e< z6NkEL-~K8Ad0=dbzGCT8-BnF77ynqmKW0cb@sAy&h`bi0)q&J2Ku{T-woh6O$x<-_ zeo?0ag8>0B7#NAF;b|+5*W9WqZ7rhG3%@Zr~Dnns~YiYK+BZ@y@5x>qjWGmAy zp;ucp86_H3e8Zz+Z<0ghX->)UCzC6AO1#|*V*JEQ9O~JwhvQ>@C3^f91#wK&nFlnn z<*B3M9SM1j4Q-nn5)%^F+L?zvG0)V(k-(%nqv(m5%-Ug(zpG4L2(@t$@iro=e}*eg zV&SPRgw;VctOkA4s^#?A#*~SXkTShyCn{j9av5IBab&;xC#%SSB{{t0Kud|fqN+-F zaEn&ew98@FxV36|{&nWcibAE+Gdj>LH z;EnEPkGs{lI%RrGhjDdP$L6l~`ZoEQr@qVG+_KfPQGD=8R^<3(`GIZf{WaUQGIl-P z9f$O&x8K>dxyR$~5j&v0XxS=0Yo_gs_^_gl)2i24 zAQ)6YX`l)6U)mef~8odtyz7hR;=rj>8%GipTLH7+lL(_R*)m2%p|)$-b3BbKZ_v?fnG zcdx!AVJ5ejpSH9dbByQi34Ulj!MC@R(<}Y+%!!|V(jvnf_olW* zoNCvh{n6TqR!sd@T1&@9PrqmlnrUv?{4u#iqNSwm?81udO<q&I-1!RlN!SY3No zVa2hvrKGW{XwLlh8>`axbgCVAqQ${6HN-p(o2c5#XS;+ZTMnkRA{1|Fx$|qDWUCsEh+8iz*HxO=Sel+#Z_=h zg8%{uAbr>2wN}ai~#yDGhfqdx^zmMQYPwLdzo%F6>sZdxPz*n?go<3mbV@ydrO zSZnJywzqU#U{x-#m@jB`weGPgo1CNL6P`7Vb(3Rz$A-6#Z&MnZ+%0=u?p^(D+nTrg zb~%UoJ6dXMJDaxjjSRVkQ-QRK#|5ryHhJ$d=YBnQAHmBxhL;z#Bs-4)0tg_000Iag zfB*srAbm*H+Oj&yFA^^13jBtJssWqD#PkvSd9$#gm$XINaeaq_fgBB%lVP zEy2N1<+^>9@~d+FkQgDh)E=4)jEpF29c#?S)qUbYt)V>;V@HqCVqRljZxkbH?dy$u zYwgv>`mwPQwNLf72BK>nHs=~!t#Ng0b5DC)jd3KfQ#CfJzMY|JqkA|U8dcX??6qse zD#Tj5l|dyOh%e&t4<)C`f&|9u{eDqy!B%Mzj}MRwS>y!2x;x-gdnU$IF~zS&c1A;E za(qb68Q)Wjd96j8+Zz@;u7<@Vu?5wb*nH*w{g=z<5fq6&(O$s1^4U8txozwpWA_of zlw)}5=!c#;5I_I{1Q0*~0R#|0009ILKwtp~oRwc(U7Q}c5qmA~BS^Ft`1NJ49KQ5> zKiwzW3*;JpFP>8F6ihuwD2q1s}0tg_000IagfWV><=+u?wxQfN23eI0% zbdjsA!M$TsbA!9zI%#uRqur6ocAqL9T(DmDv4YU5zm{?_izRTx%P&4w`IsuhnEXdli${tT;>#zr&(B zymdai!z%OXWzRckbFQ=5*4Uj^v$@XUv>YYxU~+DotJY~I@1T} zbiEcA;CTe|^xDU6A%Fk^2q1s}0tg_0KumzRz`RO;e>~44ATBT;o=1=t7dV_heB)1U z{`f~|FTiyc0R#|0009ILKmY**=D)!4#0B^wI>%o`)7uNEiej&|dmSd#?{JvxgAS|7 zTkCL{oMyjW9aJ1Xo6A0XdjW@io!Pv`VXL**xvaHyM`nKmY**5I_I{1P~xDu;AMZ5Eq!^FpYK}LE3W)mFuqB zko!RGQW+P>HM}IBQ}`0s8UzqP009ILKmY**5I_I{1Q1vR0!896g*y3|!rZLSDYVa*|)p9w=ZzyutoNgDS6h7xXXYEs4g1?r}DaoeIcb$*k} zTxT(PEe^lcWp~;K&5qgf4%V$R*R8X<*3_CU4ztr~uAMdSVCvk?I;+dda|&lZr|`!; zANaxg(p&SixB$;7JcZXYHiZBJ2q1s}0tg_000PG@KwMzK=N%+2Fvnq8@_M~ylh@@P^ws&jR>fzXy}f|dvd(5-&)g_yN&h& zGvflk+Vbh&-uaJ@JxhCmQ++*Sdk7$a00IagfB*srAaLvg#03_7djaACa~!6b85dak z?SuQa+fC!N7dZAS5=$U}00IagfB*srAb`NB7C4@`0AEDs_={+IdjX5;Fk9-(s%cQQ znoV}qZ!;-%s?FqZsSc;l@AnNlEwi^5usPS+ENg5|r`>9;wOEdlUm!VmU7gcvr@g?; zxWFBM{hnLj{N%e=(q7HyEiIZdwGI-g0j9#E|!V}RFZJ4$;2hk2d7c8$g9vf3@qx})V6NY3qYxtu(YVE@dx zz@t}w?QatPxWF8TX=cU+Ui^t= z?ehP4d@bz-j{SahfbHi=)m_YqL6hv$hv-tuxok_5!s|r`2II+Z?mz7f6}gW_7ykF7gY^ zj0^l~UqR2Me;9s~_5!E+ddBt;KmY**5I_I{1Q0;r*ae6SEco^U#0BO!OrymG3gx-C z;gObSM%P~a+Gkh(_{Q&!-)59?fn38&IfhroFFFJeKmY**5I_I{1Q0*~0R#|O_yR?_ zdCPR8>S!oWughCjJRVWQqEN-MVDY%XU+(^!+{U5}Ka+8RJj2;JhPw=xi9d7*Ab)Q zFPDAC=C1boHl?$xWovzpXJ6%5I5eh)qZ5_u_U+%_nLD+f^Tm3s{e!_Kf8$nnWRtto z-xr+h+uhyU7dUd*BKz2ShLuE@WsSO&HM-gw+&eZkH@N$)lQx$%+8vo}_o;y+H?5a_ zY>nHLkrBn{Rf9@N<~53gS|a*$;&FjHzWJ96{Hxw7mm~8Gcjp+MFuWlC&>?^T0tg_0 z00IagfB*srAb`L;5I8T-rOOs| z@(URLm?M7Z5I_I{1Q0*~0R#|0009ILKwz;5lW8;?9G+Y9K33oMqSPB{=j009ILKmY**5I_I{1Q3V`EGZCSf#TG-fL=^U#0Bn~ zyzS;|KQ{0wt-V0m!#U!I4gmxZKmY**5I_I{1Q3{~0>|?_0utcQaRU7G=M*{>#iH73 zZKgrHx7K7A&l)r-KC|6a>#Fq+TFk1?R$EtDW2{v7js?PMq&`}?uF`6@)|$jTR!fiB zwa#i@XRljhx4RsU+B)meo>Q2Z+v%vQv)Xx1;mqe0e&-`ME&coEzpv2Z0!vzQ#19<; z2q1s}0tg_000IagFi!=D3oQ8O6cQJh<1o$4=M=uDu`p6UQK!WP@(j;v&nXmt=ny~v z0R#|0009ILKmY**5I|r73Y6yMttbwwiZ2=n1v5XV@GM!Zc;3PF-#hY(?wVGAEbrix zIff?}V4>M%1Q0*~0R#|0009ILKmY**PFsN*U13gsvFF^tk^6o=s*Z*tYcoH;;Q3o_ zfBCDQ5KK!&pU5w8{&Sxm{KAX-|0QH?1xEk@1Q0*~0R#|0009ILKw!ZN zR27KOK=Ip(3lz#i#@h>+dM~yqefR!Wwin1VJQKT*;F$%>z>Xt;00IagfB*srAb&WciW>x$>5dj0$UWZs2-;s<`Ky0<&ixqo5zLRn&0ZjY z00IagfB*srAbxUnfae@3L z-_9}IWw^|6wiuv8009ILKmY**5I_I{1Q0-A{tC3_m*%*N^YhO=cX`o88+#Og>(=qz z!+TuKt!nqKUbTK=R0$k;;J;!i(Pjdn*S+kI-_$W7~IA6w%#Wn@G#dexv( zl6j5dkjh&I63-*3zxbLTU8n!f&2nVE;f5T;6NVQIHyG}ozlWYZLjVB;5I_I{1Q0*~ z0R#|0V37%&m*>)D4sPVjNtr_xd2&+b=tJ(gd9LEjVFV#pD)r*+1#bOb+v=;X`^mmo z-ofW{41Y8{zsPhc4gv@ufB*srAb&@By}7;J)6r9|lP~k) zF9k%I=o9S)4(v>WJkXd``v%o;!`OC@KO7AbTcexy_H~3jeM7r9I{F)%$M-aJhzx;$mVIiLz&&9#sAf)|N8Z7cly~s;H=e%n zmv4S@t-Oz*z%UulJ2*+^!IM+?^T0tg_000IagfB*srAb`L^5Qya&Ohg6pjxq0G z@w;Dq^ETVp4v}|oAslS>5dj1cKmY**5I_I{1Q0*~fmsEzdKmY**5I_I{1Q0*~ z0R#|0VEzfj@(Lz`0{JJHcW`Lg(}gR(_QFc?4$i+r%^o3u00IagfB*srAba*q@eDRB4{L-%uzxqBI7tojgS6bdd@sbV!1Q0*~0R#|0009ILKwu#XH0o3G z1@`yu9p32ai0<9f<=xV?v%Y(KPkpFI-B=2DNcwnf(vnLcda_{A`k8vqk zvIOR)WC^tP4+fk3ja%K3P3}(ryv;lKi4Rrwe(2UqN9BD4dc(%lyn`FbJh%`~e)bsw z1Q0*~0R#|0009ILK;WGgkQoFsM_cp@F7M!-q1#`}fBw2r*009IL zKmY**5I_I{1Q0*~f%za%ULX?<>Jy0uPh#G|4{d0@;=A|Vy-v0lC@|cU$U7(o=@38w z0R#|0009ILKmY**5I|tA1!8#x6ET5;lbm<(t^Q?O?s@uyP2?S%>jTa95kLR|1Q0*~ z0R#|0009ILIAMV-c?XNK<{d0sQm(t^&sTY6T%f4@mT7qh#RxhC5I_I{1Q0*~0R#|0 z0D)5{&{&j`FVHbGUaJO&Mu$R;jrLugf$)kyn~~PQ2KA#Po)YR zR>Z%|`2rjBQrNd|>D(J?>e%J8dYrcLMW1)@fA4fu4Lv^kRe2vlk)c}V9jrFIV7S3B zX&5k&dGOSo`)mmT1Q0*~0R#|0009ILKwy3e$P9v+Lo-E-DevHC{`~oGKDzamOJsY2 zLc<*?c?ZP{Is_0v009ILKmY**5I_I{1Q3`5f$~C`Xs{@eXz-oOJNV)^{&4dKV_17m zVWHu0O5Q>7f(`)$5I_I{1Q0*~0R#|0009I}tw1cVU?LI)3-78&sVSN5~# z3ncGgYL>t~VKt~`P8HaiKP7(d@}i42_9*_=t>e3g_qdu{)$U!rYW)Ox2Q$Nosw`6s zp0nYRmS;xSUi{kgckYN>diERpuG_bNf2S@5&ehi7-m$5S=ig#RxhC5I_I{ z1Q0*~0R#|0009ILIOziAMKaM~Ng~nUT;&~n^ueb-{*{LxyGFJbC^8(FmUmE$phExw z1Q0*~0R#|0009ILKmdW0ClJdkmueCI0(G*VN{2U;ISCti2Vb8x zUtnWlN?yk9@%}FN#!c47=E)&tW7BBJwWDF%PLV6{RoTzF9`X+M1twHLou{KosE@4oAjAItUv#fFI)c?ZQPIs_0v z009ILKmY**5I_I{1Q0k;f%0ORXs|SqXmFn79W4EF`#mpT^}!cpdx2uZt{Hg;#V9%i z5I_I{1Q0*~0R#|0009ILI9`ERUcp2>pm?6;9lWZ&I(*yMP|J_WkJ zckl2$0DHJ>(r64-7SU_JjgQ?!8?0F)k%bmcZO8 z_VYzPOl$vOu*u)J)g9U7?)1;wyn{QQyyZ$=^b22?_YssWc{EGjK{0_20R#|0009IL zKmY**5I_Kdxfalp3ucTbm63NaCHjziZl0^y)z;wNv8lPi-EW;G@8Ii0j*b8Fcfb3- zY%fq^*pVUcpcqMq00IagfB*srAb>y&ULq3>mL(Dm&d { diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index b54bd8147..60906c8b9 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -35,7 +35,7 @@ const MemoContent = (props: MemoContentProps) => { const compactLabel = useCompactLabel(showCompactMode, t as (key: string) => string); return ( -
+
{ className, )} > - + diff --git a/web/src/components/MemoReactionListView/ReactionView.tsx b/web/src/components/MemoReactionListView/ReactionView.tsx index c0beae916..824eb291a 100644 --- a/web/src/components/MemoReactionListView/ReactionView.tsx +++ b/web/src/components/MemoReactionListView/ReactionView.tsx @@ -35,7 +35,7 @@ const ReactionView = (props: Props) => { type="button" className={cn( "h-7 border px-2 py-0.5 rounded-full flex flex-row justify-center items-center gap-1", - "text-sm text-muted-foreground", + "text-sm", isClickable && "cursor-pointer", !isClickable && "cursor-default", hasReaction && "bg-accent border-border", diff --git a/web/src/components/MemoView/MemoView.tsx b/web/src/components/MemoView/MemoView.tsx index f202ea7ab..359a40a70 100644 --- a/web/src/components/MemoView/MemoView.tsx +++ b/web/src/components/MemoView/MemoView.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useUser } from "@/hooks/useUserQueries"; @@ -14,7 +14,16 @@ import { computeCommentAmount, MemoViewContext } from "./MemoViewContext"; import type { MemoViewProps } from "./types"; const MemoView: React.FC = (props: MemoViewProps) => { - const { memo: memoData, className, parentPage: parentPageProp, compact, showCreator, showVisibility, showPinned } = props; + const { + memo: memoData, + className, + parentPage: parentPageProp, + compact, + showCreator, + showVisibility, + showPinned, + colorKey, + } = props; const cardRef = useRef(null); const [showEditor, setShowEditor] = useState(false); @@ -38,6 +47,61 @@ const MemoView: React.FC = (props: MemoViewProps) => { const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`); const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0; + const [customColors, setCustomColors] = useState<{ bgColor?: string; textColor?: string } | null>(null); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const storageKey = colorKey || memoData.name; + + try { + const stored = window.localStorage.getItem(storageKey); + if (!stored) { + return; + } + + const parsed = JSON.parse(stored) as { bgColor?: string; textColor?: string }; + setCustomColors({ + bgColor: parsed.bgColor, + textColor: parsed.textColor, + }); + } catch { + // Ignore malformed values + } + }, [colorKey, memoData.name]); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const storageKey = colorKey || memoData.name; + + const handleColorChange = (event: Event) => { + const customEvent = event as CustomEvent<{ + key: string; + colors: { bgColor?: string; textColor?: string }; + }>; + + if (!customEvent.detail || customEvent.detail.key !== storageKey) { + return; + } + + setCustomColors({ + bgColor: customEvent.detail.colors.bgColor, + textColor: customEvent.detail.colors.textColor, + }); + }; + + window.addEventListener("memo-colors-changed", handleColorChange as EventListener); + + return () => { + window.removeEventListener("memo-colors-changed", handleColorChange as EventListener); + }; + }, [colorKey, memoData.name]); + const contextValue = useMemo( () => ({ memo: memoData, @@ -85,8 +149,20 @@ const MemoView: React.FC = (props: MemoViewProps) => { className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? "mb-0 rounded-b-none" : "mb-2", className)} ref={cardRef} tabIndex={readonly ? -1 : 0} + style={ + customColors?.bgColor || customColors?.textColor + ? { backgroundColor: customColors?.bgColor, color: customColors?.textColor } + : undefined + } > - + setCustomColors(colors)} + /> diff --git a/web/src/components/MemoView/components/MemoCustomizeColor.tsx b/web/src/components/MemoView/components/MemoCustomizeColor.tsx index 4e53f75b9..6cd568155 100644 --- a/web/src/components/MemoView/components/MemoCustomizeColor.tsx +++ b/web/src/components/MemoView/components/MemoCustomizeColor.tsx @@ -5,12 +5,11 @@ import { useEffect, useState } from "react"; import { HexColorPicker } from "react-colorful"; interface Props { + name:string; className?: string; onOpenChange?: (open: boolean) => void; onSavePreferences?: (colors: { bgColor: string; textColor: string }) => Promise | void; } - -const STORAGE_KEY = "memo-customize-color"; const MIN_CONTRAST_RATIO = 4.5; const parseHexColor = (hex: string) => { @@ -63,7 +62,7 @@ const getContrastRatio = (foreground: string, background: string) => { }; function MemoCustomizeColor(props: Props) { - const { className, onOpenChange, onSavePreferences } = props; + const { className, onOpenChange, onSavePreferences,name } = props; const [open, setOpen] = useState(false); const [bgColor, setBgColor] = useState("#121212"); const [textColor, setTextColor] = useState("#FFFFFF"); @@ -76,7 +75,7 @@ function MemoCustomizeColor(props: Props) { } try { - const stored = window.localStorage.getItem(STORAGE_KEY); + const stored = window.localStorage.getItem(name); if (!stored) { return; } @@ -96,25 +95,7 @@ function MemoCustomizeColor(props: Props) { // eslint-disable-next-line no-console console.error("Failed to load memo color preferences", error); } - }, []); - - useEffect(() => { - if (typeof window === "undefined") { - return; - } - - try { - window.localStorage.setItem( - STORAGE_KEY, - JSON.stringify({ - bgColor, - textColor, - }), - ); - } catch { - // Ignore write errors (e.g., private mode) - } - }, [bgColor, textColor]); + }, [name]); const bgPresets = ["#121212", "#2c2f33", "#1d3557", "#2d6a4f", "#601010", "#000000"]; const textPresets = ["#FFFFFF", "#E1E8ED", "#89CFF0", "#C7F9CC", "#FEFAE0", "#FAD2E1"]; @@ -130,6 +111,32 @@ function MemoCustomizeColor(props: Props) { const handleSave = async () => { try { + if (typeof window !== "undefined") { + try { + window.localStorage.setItem( + name, + JSON.stringify({ + bgColor, + textColor, + }), + ); + + window.dispatchEvent( + new CustomEvent("memo-colors-changed", { + detail: { + key: name, + colors: { + bgColor, + textColor, + }, + }, + }), + ); + } catch { + // Ignore write errors + } + } + if (onSavePreferences) { await onSavePreferences({ bgColor, @@ -152,7 +159,7 @@ function MemoCustomizeColor(props: Props) { className, )} > - + diff --git a/web/src/components/MemoView/components/MemoHeader.tsx b/web/src/components/MemoView/components/MemoHeader.tsx index e069812be..36419cf4e 100644 --- a/web/src/components/MemoView/components/MemoHeader.tsx +++ b/web/src/components/MemoView/components/MemoHeader.tsx @@ -19,7 +19,7 @@ import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext"; import type { MemoHeaderProps } from "../types"; import MemoCustomizeColor from "./MemoCustomizeColor"; -const MemoHeader: React.FC = ({ showCreator, showVisibility, showPinned }) => { +const MemoHeader: React.FC = ({ name, showCreator, showVisibility, showPinned, onColorPreferencesChange, showColorCustomizer = true }) => { const t = useTranslate(); const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false); const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext(); @@ -60,7 +60,13 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh onOpenChange={setReactionSelectorOpen} /> )} - + {showColorCustomizer && ( + + )} {showVisibility && memo.visibility !== Visibility.PRIVATE && ( @@ -79,7 +85,7 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh - + @@ -108,7 +114,7 @@ const CreatorDisplay: React.FC = ({ creator, displayTime, o
@@ -116,7 +122,7 @@ const CreatorDisplay: React.FC = ({ creator, displayTime, o