From ca3205c9dd40b9268632b0c71e0eb2f269093d13 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Thu, 19 Mar 2026 16:07:41 -0400 Subject: [PATCH 01/29] feat: media plugin client --- e2e/fixtures/test-image.png | Bin 0 -> 549006 bytes e2e/playwright.config.ts | 2 + e2e/tests/smoke.cms.spec.ts | 147 +----- e2e/tests/smoke.media.spec.ts | 292 +++++++++++ examples/nextjs/.gitignore | 1 + examples/nextjs/app/cms-example/page.tsx | 81 +-- examples/nextjs/app/pages/layout.tsx | 171 +++--- examples/nextjs/lib/stack-client.tsx | 10 + examples/nextjs/lib/stack.ts | 7 + .../react-router/app/lib/stack-client.tsx | 9 + examples/react-router/app/lib/stack.ts | 5 + .../react-router/app/routes/pages/_layout.tsx | 128 ++--- examples/tanstack/src/lib/stack-client.tsx | 9 + examples/tanstack/src/lib/stack.ts | 5 + examples/tanstack/src/routes/pages/route.tsx | 128 ++--- packages/stack/build.config.ts | 4 + packages/stack/knip.json | 9 + packages/stack/package.json | 53 ++ .../client/components/forms/image-field.tsx | 39 +- .../forms/markdown-editor-with-overrides.tsx | 79 ++- .../components/forms/markdown-editor.tsx | 116 ++++- .../src/plugins/blog/client/overrides.ts | 56 ++ .../client/components/forms/content-form.tsx | 33 +- .../client/components/forms/file-upload.tsx | 85 ++- .../stack/src/plugins/cms/client/overrides.ts | 54 ++ .../client/components/forms/board-form.tsx | 2 +- .../client/components/forms/task-form.tsx | 8 +- .../src/plugins/kanban/client/overrides.ts | 24 + .../plugins/media/__tests__/plugin.test.ts | 19 +- .../stack/src/plugins/media/api/plugin.ts | 35 +- packages/stack/src/plugins/media/client.css | 1 + .../plugins/media/client/components/index.tsx | 7 + .../components/media-picker/asset-card.tsx | 96 ++++ .../components/media-picker/browse-tab.tsx | 109 ++++ .../components/media-picker/folder-tree.tsx | 188 +++++++ .../client/components/media-picker/index.tsx | 331 ++++++++++++ .../components/media-picker/upload-tab.tsx | 108 ++++ .../components/media-picker/url-tab.tsx | 67 +++ .../client/components/media-picker/utils.ts | 17 + .../pages/library-page.internal.tsx | 493 ++++++++++++++++++ .../client/components/pages/library-page.tsx | 40 ++ .../src/plugins/media/client/hooks/index.tsx | 9 + .../plugins/media/client/hooks/use-media.tsx | 411 +++++++++++++++ .../stack/src/plugins/media/client/index.ts | 2 + .../src/plugins/media/client/overrides.ts | 127 +++++ .../stack/src/plugins/media/client/plugin.tsx | 171 ++++++ .../media/client/utils/image-compression.ts | 131 +++++ .../stack/src/plugins/media/query-keys.ts | 96 ++++ packages/stack/src/plugins/media/schemas.ts | 3 +- packages/stack/src/plugins/media/style.css | 1 + .../components/image/image-edit-block.tsx | 21 + .../components/image/image-edit-dialog.tsx | 6 +- .../components/section/five.tsx | 5 +- .../minimal-tiptap/minimal-tiptap.tsx | 21 +- 54 files changed, 3601 insertions(+), 471 deletions(-) create mode 100644 e2e/fixtures/test-image.png create mode 100644 e2e/tests/smoke.media.spec.ts create mode 100644 packages/stack/src/plugins/media/client.css create mode 100644 packages/stack/src/plugins/media/client/components/index.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/index.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/utils.ts create mode 100644 packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx create mode 100644 packages/stack/src/plugins/media/client/components/pages/library-page.tsx create mode 100644 packages/stack/src/plugins/media/client/hooks/index.tsx create mode 100644 packages/stack/src/plugins/media/client/hooks/use-media.tsx create mode 100644 packages/stack/src/plugins/media/client/index.ts create mode 100644 packages/stack/src/plugins/media/client/overrides.ts create mode 100644 packages/stack/src/plugins/media/client/plugin.tsx create mode 100644 packages/stack/src/plugins/media/client/utils/image-compression.ts create mode 100644 packages/stack/src/plugins/media/query-keys.ts create mode 100644 packages/stack/src/plugins/media/style.css diff --git a/e2e/fixtures/test-image.png b/e2e/fixtures/test-image.png new file mode 100644 index 0000000000000000000000000000000000000000..e59b3490716d6080bb82c44b66c34ab7bd376c98 GIT binary patch literal 549006 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

eH`Va8s1gkf4ChraaqYx0G60{etwRLp@ zZg|6p-T^tSWriyObL*rg=iVV1fSj?I`irZ_i5Ng%bQTvbmD)q^GlVp~!PeR*6 zxIIW6rUZFpk&jusdEzUPi;J(87%v~D%vMaFv$cagKw1(&XSE%{6^?Q@F$FA;*dzsY zHIXLsXiU&qJ_uc#@8#I_{lEo7mX=!B0ch1?4M9f`O-ln(_PUTUC|aH4+S7=+2*8#X zZ>+#EgL`bBAG)@98L!?1?yM>TMKo*4MazkU5t7~cx9CK_W9(=NirWb47{qlh;Vq<4H+(pr*_ub~oP%Sqe|6mj|pp|84!uY&)S5_Q^;oNz_?^tx=Vd1@S5@6w6ULTtffe z$pLd>LAHZ=XuQNv#^NwUM(NltSNA0gM-KqUGB-(mIaQ6y4bkQQ6AV^B0Os@Es;yhz z7e^?8gi=dRi-G)nQ<&eev59lbS?>FBSS9W257 zUX?F7OuG8{F&4k{OGPRXu9f|&`ISEjYvQTo0ziQ?2Ty6x>dX*EQ{yMMDJ0=|RC7=yC@^YtbZf&VJaWG5sB4$c?RXPffVK%QA5^x5=$EVa+L^46Y8$sP^)=hR}f?mz^LO5zO5HFR*9 z3ZZz4yv!W*&PXaj5tlrvX-V`>^C>~$Y{y{U+};pPijNckNhT`NN&_g!d;;d}I=kCAQHMi=oa0pJAiY25^T;*O{kfO|Y^jn(8$ z)MUCntI1}jxqt=y)mM)fer!(Gm!U};+`WLi>+FOO-#6%|I2%A7YkYo;h(3$88o=!Zo?k+T*E}q0XUPd; z6)4;|_yZRH2Rt(h{Wm5gjbPu2k}2%NS-vz|5!YUC1Y~m5A z-9hSD=AV*jJf*xDlNtkr@y-;-*cK)f6I}!2oNIl!K-pO+MU06j zl`lSJ$5(#;&UKLq44G_Z0O>Okq_bj+1|oKpNzAkXqD1z8XV(3rs}7Lk($zXN)WPz~ z86hNFbR#uo6dZ_FTIKp%iCR#uc}(Dac--?H`_~LFMsUWMaw2^Jo!y{ zhAYwH_|KZfebX|8k9buL;Nj6|7Zi06O{_(>?|jRlvzL6DWHbG{w&vozFva-TNuZ#{ ztm+x1chCJQ4J6U)WxPg~=El_Kan=r|x+|XQkq-9Ji@DAAIJTvbZ0~TTnC4DOqw$&i z#DXy3_YT@%00i_ju7=#EZ#)zk`d!hw%&k<#zdt8c4`au^BUhC-%1>0S#S;NSEJmZH?JNd=9(bkj->D0y#vhaKYSEZ35tc&@zK^!6dM~s78@9Ns}T8z|11n>-Up4ve%hKXc%L~fA@n- zjD(s2-UZy=x=F95P{PdG-K%*2&~aCmwV+Be`AEvYRAW586#B2mWej}Tg-567@qL3A znitxr8`@zi@DQ*%zdr+DajiYXjj|Ry?!xUsJk-Qi7Q=w5><+Sp59aM<@7>-4f6}W< zlb(d0{P55dk9r=caTIWLLiinyzDpgLp{AR6cY6Hm5yg^;{X{+?Fq6BuyM~?`#^4#y z1#KP;qYc$ba;I8g=T=Odt3Zryr^=3O+oKiElE9MgDSJ;=l?6-cj0zopf{o#jxFPxnMY|BmT5;I|LI(0@fSp} zSP#~YWV1Ba<;~b<&}s}aHe&rq80;5q z@Z0$GnS5cBlklUq?lA4O-s&Z!ujiq@A}2A#rCtdo^%h;B=|wF3Yz74a^_M zW{RxeazgqEnD~3#G%lTbFcQigJ$Y^@mj;iP`>>-=^!agOH#eNTXFX5JN$Hb1c1Z93XW&T^u#mG7ad_@3^rk;YW@O_4>sjzSCUka5k)w=Uz7s1o{6k;0m z(Fxg(08yyDAUXL+M1;d|@&LYFKHPfhZc*@osrTM+5;(2%?WcYWXLNG!EIAB4oY%!L z%+We%AI*i>P}_dFO%GkPRtXGXrDiCHx|Mkh>UtZO5eo|syyJ@8B6s{?F-xENi(*nD z(e$|C!oRw{VaGQ#uc~hrO516NjD`pcd}1ORiIp4NPvKUD!u&HFbevxGCr03j0I`=x zmp?;|oW(7c|4z3Vvaf^VcEB6#!_wwKnI&@k|3H?_PfnY*T`H3o?Y@deGcfp2148`* zYNVf{s{@khv!yPRd;KMvbw0ujFyi&Qs_2dPi?oz{+QiGHU=8CbU^vr_qw?^Rx3nF9DX1A#n>o&#_1 zGo;zo_4!lRLF@z_{<3S#Uxfe4?H~~Ri%BYo$8QxM}N;yT;qGn z%KLCwVBq}vqjz{|qmwNvZfSJS`}Y%wIOTI<{6fZ_B{x9*1jvU8Bp{I;Ap>N%?*D-@ z?Xi&n!j_s7Js0#hP-9nnW|s?1f1##VTFW8)ellVII=whWQ-jw8fM49a`$h|tU{8|% z2114rrX7V&)pI_?T6&r0ar+s6%$bVmNCsp%asd_CpGOSQI*N+_-XY%A!Kh9#K{sC*5R|9{F5iBe*Ef-0{In7;etZ&(STrWzW?%P6>JUa3+Xc##Gv$%|zbjJ^{ zGW`OdFH+xo6N0!Sp0h0969Ky~(Eibg7PjnrHv>MdcdUfy5wo^cQW%jDhy8R?bW{-9 zx)mo$|^)>RF(JqacUUy==W1OBv$rxMkT~eCWWrBi`nvMckBn z)UMQ}49pubKAK{d@TH0?PQgfeg>jR@FG}raSuos51XsZ-Ppv0Rwp0 zNbsCIUq_|yJ7Cp^qIP~qhD8oa@blgIT=n7ajRf9XH18!F6f`#z8kF1|H(i~Wytf!S zb;H$u>4i^nVU|wi)Yh;A-83aULtT5mRqxvcS^5 z@?|VJxO~ChTJJqv8DgIz=je%Ss+)g|E=ntYWsV-_g!u%LOwyL%cgKeci*8w6Yq*}B zQ;nWkFE9NKn>6h`!Ed!2!1j;+Cqg|3{{>dZep5%8>$q&eRMr2y`Je~RmMbfl4?f@U zHVPrlW~z9)z?Ic(DBFEX%K;iM{=UkHiI}%-7a%}gxkVB*3ahw~_tJjT&32#Y=P(KB|NsAjQn8VG+l%-(BELZ|Nv2 z%tevm-lxZCs&8on;b9wy`oCvzn_YPw0HsU>NZO2@?=2_0SMNR~tZr9duLZ;JaL_L; z_|6M-Mj9?!@i@0o;4V3#M-0ZM7x2%AwrVqKH#|;Q#$7bUHI;fWZbL6HSN-nYJV%OB zOy;@odwH}Ft)9sP?*$*DPS)98of>Yl?%QNIb(_?Pp#8gi+_&Nj!J%w^YMoThcIeT? zLeJcQFtlieUmN>z31N^f7&Zp<6ml3Pb~fPwH)D z$bqHqKZfD}Hdz41iQnO$0hND+z>-cWQz^eH(>-CFNMH#PCPPe7#WUVI#2XZqoHiug zz7Z^zLREddEz!0IG?P;a@OuN(hKxQQT_|-+u>-L$i*Fg$p3$LFNugDTp%5!yT$-7g zSMzG0E_J$kJP>$*6r{H0#q8#U@ToG|OMSiH9}^^WK=vx1e(1#T;nJ z2b#r?U62S@6OuC1TWFWkA^knV$KDxYs9E$b+;8v}Grxxd5093|})cmqE%}^C2v^;S(HLt*A5PE((i0k}dINcwk93#EKm> z2vm7JoPc*d+id*{1<=9cn#2X2bII5Ly1;q!baNX#nGlwNl4xAu7)H=iMemS-RnsC% zF2(fm^;vt4_7oz~U5674cU@~${T<8{*UuLxy)+kInf&OKe!5#an(sEa1WX7W=?5F2b+ zM%LT1rOo`0>2hVj=P%?OW9gOQHZ7+=+Rxi&;xWNYCPJKQ;~siVKp!_OCY*ebWReiG z^A(;IYk%?j?fGKCHu<1u2V0p>ysDRK7D6z#ZF!AL31zpkIf8(|-)Z0Wto2lCwv2DQ43w z{nGGnA_zDD7hgExnbi}Sk)DTJdV&lxWQ;o;WCifLL+!J)Gn}@NFeL>0U{z#sNHS*W z<4U1qVV>vYgSnL~iMnPrU1WJGFXG?MDXqh#4E-fZGCi8gU>|y21(KwQbUb@pl3<|f zz|dy;RbOE=gn7G2En$<(dj%295uX$9ZVSH*A=`@Rwr;~>CYjodlLFM{1W-xyySo#a z^@Z@0jrGo)>aLZV7cak@I&)?KwN6hdn;*&&Xpr$c+yUy;YUr{cM+Cqw0}(bY7`NRSY>=NzJnz!5+--OXWsizZVZ!}ovQ-K!a^+#eAC+?#x037T`j10+q+!+icBg(GG1oE zJr(3~=ey^;RUZ#90&gxrbnO#CrQH-dks|>;Ei?w`bS7R~-t1CF!@QictJNsWf1fZCibP9qFkH)80dv503UxsQ;wOy1%Vg4Q z#M*WVt>=Eo$bWj-yX(?KX6!p*1g?qoS3Cv)WNLo5)m)4}!4at3QWO9Gv#$-_X0pq?pX(DyltB(rV4CH^JUChfsesj}MRUM0 zxxbDy>3CDOH%9(3`J0dpzpL^096AJcrHcZeJQ96yMRqWRw| z)dFx#Il1`R7IMuCA}kF0m2j>>O(u}?W!@(e_QkT%l_259JNQRa%z{ioB{kbou7g|A z4009LBK4rWYAglL2#8&e@mqHx(dkQFNbERbkPI|Usiw4t!&#WYM#boPD&(@ooS^2J z;oNj{c0n%Yuht|UDyTIa9O9I)P5b~cYT$1475kb-iBKl(I=z^nu`ayXCvbAWbKFtP za=MCrril}8iC{zb)fsL=|BFSdw+4h4w@j(j!pj8i4rHH0@cz7v&jZo2JgeP%6b5`I zHkDHVe5$mEZ7H+&aFADHL51I$`gXLG5x@AS#N0RxWw*O>T65#O(6IRHcj)*_6DgEJ ztVY5QvI9j;SVTb?(h;tdB6(JF!AyGb+@J7gFCxSqnKPXeJYm1g!Ln#cav%(9((UGq zqHCKwpFgfZJQoytuQmP0;VOqt4mklwCrF?zIi};*+9p^^cR<9`HlUOHvZyA&5OVbm z1vgat8yVp@H^PIr30)i&KdMX{*y^&ak@~+9Z$Q|e{4)D2hMO+8v~@0b%*&t3;1CCR z)&k#S_oDHQN-nU~B{M$s3C;^x{()#;ZNx!LM)Y~?mvQRFFO`{pX_afs*%IF&bW_0& z_fCh3DcIJZb%&FM5r0K*I$HWe(U9LaQqH2u+p#6&l;_ZNDRB#jNxAj8*#Gm!wtfD3Ro@$AE^Y&i|@b# zD%?2$36sQY@m#Z|C&q(taW}yVtjWBFP94Ex!v3$*G5)SQUY(RbZDX}_`gE}z0IhE?b7d1bpDTp{k|N5G$86M7H!J4xS&yEj6>?>$aV ztU@5Mgdm zJCv8xi&YTbcQ-xxE`~6BEap=*{tDK>^-gZpVgt13#55p^BqM|<%S^%*!<8dC4!1*s z;VixjS*`gPc+DHAfz3Cq`7bR(qgi2Q#{DzS4kRr}Ndu?t?4a9~pVG zm?9JL^%2%q#jI|s*@NsrfY^l3+9QP;0>m#CW-yALIf&ab>}D(TO965($XWU z)Epj+H4j>Xhto69-}!IyX3sSp0%;30Q-qM_&^4h*%#q z5_2MkJ0gtQu;+arNGEN!2nrfg5~34pG)c(;%z!y8C`&h1?m+;;K9|lvYK^EF3hH!# z)?roi@d^N%5ua}E(Pak z4hd#}?^XjO4`qorZNmbQu_Tbu#Dwegn&8gqas+6cz)WZ$QqnRR-tbV|&$$J#_^HxP zjC8ICe_TASi2U2 zP<)nLi*-q+7%anBfXG8+LO9iAN*?r8JEN`LUSTL*9rMKxs(4fDZs@m z%!>bMT7hi{g4C%4onLP$TuB(G42Uc$!W*{pV@1kf*79QohHHOVPFi(6yPh(cd>qJf z%F9ln7W`i=fP_r#>XHNir#1%PZ&Tn2>t&QDW5W1g=j$bc*F>`mc3Z?7TsKj}-QEp*;{6mJ<#9(+3Qv!yOd&%dkDpzPF7QHmf zGkCO<$Hf6r%52Dr&MtO#6X6B#YEZtB8uK3pBs+5aubYCYz$70Tm{p&XT_k)f&6Whf zEb(uO9gpU{d4n9HCYfxlHL17&$6(~>^vsXX_SrA>8R#QhJ0`bfST)G_P2oDGwL1qG z?}~35o$sv%zl;WNKKw_vK;Hi_3NWYPt#A7e%MC7%@;5BareiAM(JHQ4E%ESfLb|<+ z73xb)M2~IPnHPzI=S`C0BruYrZS(?0^ z7hVH`Zj>Jf0t`28ffrp}z?kq6?@NJEK@v-{1BLxBC@G%p*G9-RT=twnE$Yp4C?8B5 z7phF>zj0O!Xn8m;@Um`w!@N?Q$IhmH+u4XTB#aOU(L9v8f(%#}Wr!|?2mA}XOez<; zM&|*o=%;?$s!pE^aI55IN`-AW_G*LAKe9AVE=^W3Zr49S=jAm)*MtfQkoSTPq48*7 zV4RRbURA@V0r)i-HfGp{U!ZXd=gLnM=O_aK^SU1*Vg7yzPX{swsRrwRF32+_8rm&r zEZ5mCVGI*Mly|9~ecCGdivYT3+?DN}vqHxUpMrB+k_X@b*$)A(&_uz7#!fKslaVHW zzUEe#G~)XHp_{X*Cfj84#;O?V9(8-0rqjFm;jz@5OTKBxHSB^*#2FnqlbC>yXL0IB z%ig6L#Xoo^M!L}BL>N_m4(;vbVkbGu65H}eBAI8qC5|J63U`?kreJ4S|NIt$a@5-` zlRi@|{?q2rhL}&33s}*hT~xz~5pI}W_CzPv`HsIO=aBw_d=F?Y4IsVWZ!`?2Bx&d?S%C_+FK^l|d4HGFsn zf>Dj1O|W%iTG+ljQwgDvig#^3f1TE#*K2Ea^qHZ>Vmej8iVvE~{i*L0u-l*h5B9HK zp(X%XSQZH%o+luH$mJ=mT9V4#GA}F#pzQY1eLl@{5VG?|8(v{Z3u%OW!qd#DB#9_T zr>x9swh77MyoH`Bjdf~0ZJv}40qljVhkzm47GxVK?oUPDYv41|xs>?ShD&AgGlYF$=* zBe!3E2Z9?@2MI5B6?$)Wtu@-#Qih*fop+?YAqrIa^Jz&|imYI+^b5D4~t^0Cg<5|e^wc<_|;Jc5PC@5#P17@29S-+#B$wA(<)KG zYw6qVz;OyqSHKuDbyRN=e5OqaqGd3&LT)jCl z_rndL0>A=7LU%{-xRrlDuh-A-7n=b9sNr|(ke>5cNB`2c3xCu*)%d+6jB3gQry~eb zhx_qcGJp($MXJsTWz?VIDBcZm-g9j17P+7{V7~@UUlcgo;-#)lT7wB7grY=vBI4Tz z-~lM&q7his8?MpQa0$8_S`ekktV3o9EC+-Y%i%6H;^C87gG#wR{fzS>NBn_Mj`_nG zy3{h$J-H>G#XRNnq0J}yjpz0IOXf{NiB(d-(TC#lg0K3@Szg;nM`vf<*HR}ts)(AH za7}Ofyq$W>E0zc$^T+pNE?)Cc+#B?5Ufx30un;!0+et&mv_9^NmJbL!`(VA0FYv@5 zkj>=f<)Ees@C-{``E?TpH7?Qb5Ud?i`&nQDD&lu|C~Nnk??P1)`Rk!f%L7o#wW9no zwoq?Sf-i;}SOT}HdgA2fw~)T&@VFNEcA$-U00feg^^4D}H7LA2th!o78I%MKQv#Ny z2%Sv8P5ZyPg*z@f8sdo#=iT5qAJ?eZ1Sj`D9aO8OsAfOS7Mq_f`x>r2H9C%t)!>V>BdxedvJ?VE)*btWFUCJz z4HX`jS622a391V?KmKfF&+2Xa)v}e@#Wdtmq_OCOWKZWLuOcGki`iiKY-Xa<>S&fC ze;LzEg!oO~vI=O-`42;tP6B)iLYE$Ey;9r98(S$0 zPkLKS^d+Wfzu{$SdQ@eE(t=k5g@ z;13^aXMPn+(Z7`%7`m0qe5l9ivXf21? zK-h$eotvg<^pAA~w21f;OPb|5O1TB}Bgz?rVS&4 z=_c%w&}HHS8L__@K8t<<6J$2pqE4|fKpto~U%dfHnOVK2TlKh{M#xRQdqD|IWHI1r z4u!%c!1`;5p3(quPb`;Ik%N<<6PKo^-|?yr4w*P98cW}8-FS9L3hpz%uK;_G?X*V? zZUkJRfO)fDZBLQfkYKd_G%x#`?H!UG2JxA2R(Sx}WdYT7~bP?8MH2F9tW*gkj@l(S;wGE!mhmFqU zdUPu%YLUaEXaV}a)|A@dhd*rw&ALQC*h21<$}ru`ez`^t`t9sNCbAe(?5-1Xfza4) z7im_was$5Xo4doz;~V+c=ux#>U;HFMOFmE-xo~B9iG&s0ieINiVMc18fN8p!Y4*ST zTEu;}S{k8Xf*J0KXjSvCpB>#z$18QyB5WT3JhGb2k^+)ArOKAG#Nbj%&nBV$ zI@dB%P2p=IZff+R6w=Mz!$dg_?38?jgdk`gOnnps;|9O-ZgB6-R@2nk8kf#HC#Ehr ze+evK=)q6C!qxX=pXjbvjEj*%$AE3o8Uw8iwFAp=P{QBBhManU-mfc3uyQp zp;_FD4&W+1&IJa{V~%hy;Oe~PTF?boTW;gcvvH^0*}+VqkI8o(5kiNf{iScV?N1|G z(AVT>%^W2BJ%7-n%TjoHKEsn0Z0#%uN)h<{MG*1$YsO2I$s05#)1!+7RCtF|;>?^% ztG-PN;Dq%w|0&!upjwRGfzEO)Zpb$%w zcIU*{-C&Ozl8Uuk-=PDYZq;4hc?Im_;+9h+SB@4M84*q%dq2I7+p%g%I!unBy=Zdc zn1i-2%IT~3YrZw=epyv93qvT0Y;Q1Ys7QB86?;OGpYOi@l@T+OtiYX|{%@xPfkMHUJ{Vl8k5PDiIgAGb2wabY7V#I%nB-V=9Kgv#ZZ` zabeu>efqwm$S$zoYVD^VAzfo><->QR4p77qhG( z*a9Nr;yR4<2){Szw1on!gfBx#IB56ea-zhvh|cYZpv*zv5CX(ewR6D1@a{DC0Ms-2 zUgV?)gjHCWdT1IZB>nVmIA_ZZ;4Z*t63!9l=CLD!2I_k)U041zjuOBeZaxmc9-m_i z(anya{^e4rE(u8Gtg2MHRmTwEmI$MkI$Vv*;6Vniub~>5yn01 z`Psp?^V2|oMkwP#bvge;zD?>9*(aq%(f#=q2Q7t8RPpXwF`b6ahrS7Mq;hki^ojwv zL*UthsU`3ip#HpR2mNu+EEcNX4iW54Wfx@%am=&!ulP02bxmR=Z^%M#Bh?QFEwlE7&4ib2Ip3Z;d_)O z*%E1lK}n0!W%v7U36A0D*B<~#j-Nxm&zfSHD}M`yfSFrtFt{JB8%V&S9;eoPjA3N7 z_D=pTZnv;!-KU?le;x1ngTxFVM|@@c{@U0<+v&CYVRkGM@i-HwYExAJ#_ki{KlYJ& z28lDEMbP6sj?MpZ1f1D=o}8c}A~!DZtf*IG2X{w*rh4w&WTPzE3E6Z{xIN(=givMU zUY>^NIa0}I`$$($(oPZg!RbT3?XJ(-k1Hzp0+os2 zoYbf}G>X*E8}8SO0XD+sp!u>n&Zk|v(y-g`SB#8S)3X1W7X9z` z4?+^WmGxhd#uFVSqnU>6PwuBe#e`Ks;AbV3 zh3q`FVoy$Mip2qkM&ZW474aNO&Bf{+KJ0~=^%otdwQPDN?eNWWeaO_YFi!qbn@syW z0;3A!SM_lJrMm|X!Suv0;ooGAt~{tkukPF2gQZQa)+6}eufU$_8kR)=rL-9Gbvvl; zz;En~f_@Yfi`d7#EaEI7iX2r+qb<(rzvowa0n{=Oy(5F${Ogapl%@&5@sln0RRi!w zz;>yKGRs)gB$0Imgta2>cLo1{Y<**JWl`5=Y}-l4wr$(?jcs>q+qOEklTOFBJ2pE> zhdnp%TQgJN)YSQtTXlcrp0oEldp+v`e5U#2duWS0J9>8V>nO~V5oE&#&Myw$p@s%q zD|su+4kAVlGZqkp8Ls-J;R#x($@!Uz?$YN@Lu;r&<%H}UkZej2$rccC!g|N#3XeZB zaGDn0+SSX0!5mx!8Aa5>4Hg}rSCO)lJwDcFR;lGWupP-!V9H@rjV9V{lAr{> zv|DeK4H34%n)M3Kfh}{=Er$8uy(0Mk5$Az0D`PKl$gfWX+hlo5dlLFo*bi|sayyM& zhvT8^YC37w_Yg)xR0I|r*!s!NikN8CdIe<$iRn_|rbA2I(GSm1OU;#$L#SfsQytbc zD9(`5Or~WpOfwk95#i^h&`q8sG6r`DDPUb^$sHXJAee#)AAXShJ!0R7^6<-NsMAN| zx*cv-h>_6Eg0g4mQ<83CXNWz)yn!-+2Q+4{g%!ucy}w-Fl5*F)hRC;tFzO#@EAP>o zLz)<1afoWO#yzoailo*9Jo?9z;W3I64Gfk7<7abxzmxftzr(?p?(J^y%H@oDW`*eV z!JjYSTLw?q*aytS@}_2D}m`#yi8ex73Z*h>Y!Q7Uy%Ig=xbOsIR9c* zOA`S-ol;gv2P9DOww)kE{8|9T)c zG@y-li&bT2J$B-YIHiqPsHR&b^uvEy7Wcp&hPRj+?U!(1Ar#f$^+tfv+Hb2^6cpi# zw3LUjk{W$|5OthzxyjBZci#%~H&Atn9AQXl5Y2-65_s)2U&V!13b1F&X^;j$L>bab zQ7st<%!Nin9rzd?q&=Azg1h_V-f@7ns6Xu7(t&=0D_!lsK#{S+PEe>!P|3YvtO4FM zQwo!WE;{HMtw2I==(c%N1e|D}=c#W3=s~t7rDd;+a5l#6Vx=Ne)C0(Ym<-c#V0PG7 zAjm^mYiWiqqRvJYjc6_k0Uv+~un zC07b(NB*dDP->kA2I66g+~y>h0kd8#?sCy!5HZRKfy(Rw9f7S%)=u6^aewUSF0a$o z<)09mx)mg}KVbgE!F(H0zcKr*3FTfWjDWRBi?VSDbbK&vn`9gH`@DcfC^?>UEj(gR zgWL{HYSmc|wanduUE^idHWPDfsW+$Y+4ULZ!tNqv*a5x&cB^g21)td9!+fhKGV3Hl z)rCdQKSp`VMPfV_#Xy0a0qP#h)D0Fj#wDOpwR1?Z3o!{f2qj*NPA8cg&+~immpJ&J z5FZ`GpCaD&lp8Zo6`+i2f>1k}V3lrK4ncA%Eh2}=Vbw=8g*uAS;&*Ybn8hS2pCpy? zEM0yUDp=-D?pCpfB6!a|%h8PkBh-XSo@KMxE-+7BO%j6Hd*A;rCFAQaBvwR>x9_Jp z*1HVV@Nepf8ns&w*!(M{ zFHMYHaZKD?gMaJVw`lucrXBY{Ep3^#_D89HHi|y#u6?SwEB9LcB+%VrbrL-JCG^>- z4|I^-FpO``l$wjesxYZhUr^Izd_UY7aXYJl*S61<2#$g{jnfQ~@ya80s&kc>KEN4s z<3?>;G_YuS6A_3@7us&Z{dYIkzyzq*n7Nx6JINZvqRTO)bg9=Ejk66 zfVc7hqnfY1QOZ7mD(p>t5tm_}kd94h4xI}69)xy^IkSbICc~SjL`O}^2*JaS=-{FT zg4^(HMOZ7`etimw0~b?i6?1P~Wd-rZJ~X5j#DIm|Ck#Oog+F(mVA-lqCV*s$FV3~_ z!E@%@sdWwolKqV(y334vUz-pvMD;4sbC`h%l*J>TM(F5RCE`P~gh#sr@t7hcE!tZ~ zMs=Z!%SO!>j8z1su+3@z+kANBb#HdqW6kv{Z)kjf)?^U2$T!icAWdq0YnU2s0CE$9 z!D6mo(7xzVmUU!WzjS<1pH`-2H>1hMBlkkc884J*b=Q81{-Ih z9Qf)nFDo9;9Vqk|TtEk}NhQQB@8O848W#>hFl7TOHAwC0x?F^rjDSsL6q7Ql&Ft_< z4IMx?xxj8RqLc~F+Uk^qJzmG~Ziq!|B8j5z*L`dGKc+sw79kkwcq*$T<&XfKJApim zp7;(gvjGa`xV5#l3L5Xxg56N@bl}2NK3%M*s!;|>&p&g^|9KlIBGr=C(!vg>0vv?W zy5Fu-*3IK{tB3WJn}ZcXWQ?U~R(|DD5+K*OuSk%j3EwvXCe4S#5xd%J;yQIKuD3eB z`pxlqDdIZ!EPW0Q0@D4r>XLS21o6G}L97JUYpgofw(pROtDoeLpCk`n0CtcxtX0AP z_FAd7g;sDX`3FC~46rO3phnMy*>>Z{*@qS0Q!wlJ9vV0yW27n!jWM9QPCcyY^X57%X{wes2>l_9ZSCl8et75f9vQZ z2tA^G(!SbeCE3=4F8#p<3$NbsF@-eDfvBHp`e;ditVy-_SPAY9`pc;>PRx5>TsX}^ zK$5REHve1Hr3;g z-qyR=;ic0n(kCfD#?j%fOH?VcqaNQt6NFCikeIe6orpM-iovd-Eu;0XiB?}WrN0ub zOsd{Y8n>B*XRS-JWZ3UxfpSRSNg-8w7HH_&TJODpT4P20^ON+ti@K1GeLn+I7LFz? zK#si=EkxFu4Cf8ve=M~xFsQrB;|wRhUfn;_2T>k_JjX|-+BMe(w<6m}`~HhL_7$5B zNj7}OX=!?5+7qzlaq3tI3F)B+yifILf}$H|2u%^Y0Zqv zKwz_a^JPF!7mH}xt`9&zy6F9Vw0iU8#t5&lj19ct<_zI8D_8N#XU7nHu>#aFiPyjD zF}D}0Tps+ABp!y>cJ@ybs4?UMDZ5DvW)z$THwoBYQBn;emVLMJa@Dn0-lkD@%&}7) zRvAttW%u)d3r3(h+*{Uy1jU3a1%3E5omdtaJXV(6iQ#lQk>A)FV7&2>x3QD3L75gpnd%%E9I-)p&Iq)_o6|k@ zF|}!!5!mW`%FCsL?mL|PXkcBvG)i%b5R&7ta-~aDmU2(BZ*X*dr5dzIC-hQvS`Dm5 zBhi*G+VS-zS?rkPVQWMoK{@I%OW6on*MDm8GF;$v7->zFB31n6fC1AB&LwFUF1hcZ z^REDZol`4s>Ui!|-nvuA1-BRRJp6H`MpRp)90RN3vSL|Bia3cSBQ`JUQlul%v;g#^ z>3D~E#IANh^;PhC6Q$omH_3veQ3hmsD+??AZIlw0nK%OzX(1Fb8{0W|)6kgX?MKy2 z$0RZA(K5k2-3A#B^o}ZwpE-i)_hHpt2_mC+&1_BZ{XbyMS0N$T!;3TlWn@@W%}QnG zMEDnuKD)uaK7G`- zucIh&NMJtA- zhIi*9>|DD0%#MujHHeFVp9~Glfl0VqYVV&H<)WaaT}5`sUNP$MUtSny|LJu*2Jt>| zL9@V8o-5U02{r*4`*#Y8+pwsCE~27;AOIo-%w8d>aDv%B)2t|)c~CGu0#IBlVW5sh zFK?7p%VyMtIKM1kejZJH39VV{(Y+hm3WcA_PdU>DMLD^Np9Dh`tj3`XTMMZ=n5#>oNt7= zUAJ{lcx%WU%>Hb@A=Jn6eB-Nq_1qLeeMwnPcn2CV!iWt(n4CNA6~={?@bY4CgqJGo zQRqD;LKGPW@BpBrU8<;gRvxFWTg^)*HM-xPO(;5K?bz~d6PJiC<2HDxSuT6wv^CB_ z?KlTbTTls|b;~%7Tgeir;}P`VB;8QV_C#rP_jp&4Ff>MB?4`j#>hV#=FQ=r9v?QBk zbc-cTva&OZ>=dCvb#kNNA2uL|dg|k=EvXaqYIvo!IjShC=b>XC53O={qWhOGTs@=R zZyVZdF{bG!&Q&R&<<|A7N9m6Wg&qPfQ2Qp!F+CkGtN-xp<_m!Sd^4h-bSWCvFq~qH z3OgMFV2HbW`)Q8N=IiibUr?KH7r`$h)WC+xZomOnF#wCUNI)_u8*r9RsL`qmjGi|x zq7B*=`he0L_w+$xZ51iIkEO=KauP3B`k$)(TfEBBAo&%AMC_+Hgf8baEC1|OVFo*qF6alKrPzhUaUhW@D?IyVLPBq+3q#x@z|kqd~*0|9~+(bn}fZhC!&kGaF7 z-KBkYO1ZTB{@4#G%T7=!bm2`XHy1qThI09I;cEXCeI-3DD$;Y^{Xm2au|i;%1e$H( zVa5SiZTNFNHkLob{v;pPZ4@{(4B^(8&6+l1jPLUNh)9jHuM6O%&Byc#_CqC72KzyS zhzwl_QAi2V;MQdcF#Kz5TPd?lX&o{g$Z)O+P!>@8aVR&r$*Q9cbt~CKw8w)Pgw?-? zL!dV3%|Pz6oVe%jGoD45 zeVV$5JZ7^SsP3B!v$XYlmof1A6y1kZpy!uJYr(9inJQ7`m*>;mT0YuQvhBAuYb;$5 zHms@1RHMP7MBr#bvtDiqloFY3z5S-Z;HE+Mi2Jn%7psD9PrmbB-1U4NLBB4W6%Yn! zYnnpHXaZd*xVQ9N4z~hXepxl9p`O2O95IsIstQagq6`^n-43I(0B5H$J~nT+1;5&s{R?+D>8W z8}-mBWyyBWVm^o9%IWFp033GA=tq7@E0P2r;l2{!5DzEm1pUAN7=rU{_SeeJoX0bU zW_QDZW7%dkLrWxl`0NXaG6QJlfe1*P6!fl`bo3LRQk-&!GAN;=jpHXyWQlPN-qd~r z)jl;R9mparK830~3=zoK}{s$>g!2Kv7DC#+01KBorcN1F8ta+9>~l& z@XHaMbUF%W#`#JceYXUD8)ggIHV=Y;1M#eqkHAxyNt4Z#X~_!9*v3;6$~U~!Q%U#L zN7cq&&Q_-j{+K((E8kLD^_)IS!J!2PS} zD}+4juPF6&+LBH&lsKTpVkG&sD5a)zEx3zXf6cHdY|R;nVK_$kqB$9L2vrf8MaQNkt&%hT zOIg?xo&yP5jKk+T$H_QQ4%UZg_qvQ5;I$OpTXU`+mbcziZ?`Rehcgzq=5aTB6|pO( zPlLYMPLd*^*?q~l8DnVQDt=33wTId1EI;Urr=;H$=jy-n(tl3v`5lgbc-?)IUJ}n= z-JosKa^HG*w{6~i%9+Wlabr>0jwQU|W+*3+Z*gp8T>NZbd^E(c`jf+}kJE97Krz4R zulg7_V8~1IYiS;=fGAA;hu@ufhPPMR8-5%3Ye5q3=5kH4Jb^B354su$s)J_<9;6ky zkj##p4L7@362na%2?U3r{ulo@5zW9D>Ow3-o5ryXYBCYg3`JwrJvc9JB~RJ01B%UX zPzI7SUXCj&VL8A6TM%|TgRIf=769w8_0|#cT$b*5qqSF7DLjQHH6iM+6zw-v^NS6vw@oPbzSKY-Q=fwDp^bN2q` zV1Bf=zq&7i_7__Xhi<6?B3&yH0fBzn*$DvV@U1C%U*u^=AS?GsG_0(FpO0;JBJ9j2 zMvL}rVau^2eS&~atwW1yuapY<7~qE zWk*NA@7vHLUY#%3)kNO#du_aGv~?FPOtY44Uiqk3+t*{@uni<8e7kkh!1(;tl*uj5 z+7G4kBt#k)$r2p(x2rHA1Y>II;s1>GgK&#MbxUXR_}ZE4X>miYAaU*a+p()RE;I zzP|5O>pd`xqMT${eR;611jovM(q9X1$?qwhDCJt1$Z22lXR}W=`0PG``u}!+_}IooJB~SCrmLB1>*{JU_#?%>J7P z_4HsE;}fdFV4mZ)6M8u!VdMl}6x*DE1BA5E1AmN zp~>%REYC_?7^os&PGngKMt8k=9U?q=nP6d?R7OwhGgu^*fna7m4LLRPj%#*dC6FTA zC8;@Oif;lo)Urn)GF?;_)n$uAEx4`)FZl!;8R^@du`NeAWdKF=k(kN&3=cCV4Fqjv zVYwz9JaGJ@!-Ww2j7n7qm0a1NNhjWeBjyU3Gb2nhUew zH9`ICvb-P+8C)9W?Zk5B65HjkQur8vwaD9$4jz;JZsa8#Y?>_dbSJ;ZYgMh(gt9{X z0)dv4@GL$d#zB?p2P9Y;m%bFhhN6VM$Zi;Rxa?7fZQnUp&Z|(r67OipM~4_W4!Y+m zJA+awQNsmpG0j|l6H?E-uF18d=^iWm;Xi8cuR_yJw?&kn2ltHW+4K>Y;(~3QLAIPgFHQHoi1hX+q)lmJx{GaG-6Z^ECE+Os+LdvAo+M!d_*Mfs*kTj zz{BZn{{0J3rYOL@d82m)6xlOZL-8l_R{QvsuMWu159|~KE#l2bMM}c@NSb?|5JfHh zBF~!|EOUfA2+Z_g<~Fi&4VTbP_926wgh+%dbg`n2@mD_c$VeoaR`CuT{rI(IE>FsQ zb$WH4X6`AA$HKS1v_>)>BvKbbm<^L8Kc(fu!9vBv^Opi7N6S-wFQP+t0P2(mKo%ME z4^O-OTHn;Vv?B9w#Y&@ef^j)JB6dHsXqpvL&NSDk`SY=%_UI5LHY{Vv2CN9!i-H@Ve{>9cc2O~!dU-YKU}d@afoC? z{h>Z=x3BiQ(TYKg!xLxR^$9ErM~pWLIH_%t3BQlJ?jM;M!8bc^V4T$wD{ zu1k=nO``>=YWq%m(cTXz85iwK&0=)dNe==!2SR&GV2{NyjH>lWo zVp=h8Q}>RH?&BYHKs|S+BBE0xV)tKsPP@4_D$hXoC`UYW$KpXSOf)a=)3BtsppjviGP+ik9Cj@j&9=Dy6LNYHrP?T!Ts=cH@ zzMODwq{|f9XYXOBk4YZ66Ityd70fsm*|EZFKbaD2em%nh;;UkEclOMy8M#^G{y%c6 z#SKGoV9J!0@^rdskdf70FBCsWv;{Wy3)dYc;uNT2W$S)tgo^Yneq)lyTEuZb8mm0! z{~qj;U2hi*v7+?ZvlQ6GZHt*c3QyL+F69UEkZuL?dA90CoH;ACCOvRchkS2Ics?*?uig9UjiGu5b`2rOhyRL_}pzg~rWei{ASCK6eBl zkp9b-sO6Rhc{Jq8yio3^la-wv{g^z_nHz6zUCKb)uiP|1mN{#cd(*#(kRvY(|I|2M zK=*ixzW@{+aed&Q(yCIjdt>~%yx%T@*I&!K6;Hd(4RbfUiks>a@oRF%wo%O3`ihL)mYCAJm*XF};m(*g- z6^{8N;(s%$#JSMf@z1T4BK-3rC7^<~Uq0?Wb1my!OTh7Ep$B4zipABR`~~0#;{q$= z+QH1}C6$hGPs2J+FucR#4l_vObe49eOi&l77aEl!8*vOo6VbadvtTP~K5`zA`~exl z2AK5QH98h;KZFYAsPL>YZKB1L;nTwswd*GqNb1O_Dj2@G%t|*)gn;=h$~io;xg3dK zdCS2C!MT-LB7CC!Y)WcdS#Ai_)c~B?kjQZedG7(K2BH@JxDyiwcpg_>*hshHSQ2d9 zh8Ej|m`?Iwf$XqI*SB-GL2Df>rrL^9r!e@#Z$idZ&Idr_p}cy3LJDhG`DS%xCfQDX zKHKz#&djuynvlP>KDpUX-F$(*AV(Jm$ZdmWXzIooCCgepKT1@V)1?g25nIQ2Rd!P7 zu+w81O6x0bITaPTjW&QcW^Z5)M2PW{>V(E1t%h^Yo6&`ap39cK3+z0COQw*fG3#i&oc^$>mUu-j)~jVKS`Q50TWuz0yYg7ZS# zB3BA-%i(fN&oK0n*&Oz8HGOFqMpENF5b;s`izht^Yc(;%4i#KlqxC~mE}t1>n*aRz zYm^49T7>{?)ys9j1$qTozwh|@_g|dazzmM6F6y^heowR}5rRNL`FGqhJI{mCm0BO; zOk`%qoH9%P4=Roh;_%K&Kb>BeRIB%jw)1?Vru0X@(%{nW`!U!36x-|=kfK>yQzM5h zw!W*sV)=decsi%zJ6nR_s};|2rr9CzE2+n$7LcsN#x$Gd_TmLnC&t_ntP({?{5+l` zkjQYA3prUpZp>mWsGui;fPg?(!4tdI$@&)C%T6rGbc{KRQN4r|T>tgT26J{RX! z5_^HdkY&L(FvT{O1^21y_;XM%7-LP|D=whO(UXcoKx(X`TX+EKpV|;iSwbiOE8V%b>anTq0OfLcC+tu?p3ji^>@}k3R?s|C%%>=$$VhFkEq^D zTo4EB`@IZ&;L<960o9FxQ*18n6{{~b?YOuh-brmD_J-!fteV07BcjrH^Ghv)OR9v~ zG%ov%)a?zBSMShyI|RQ>3TP6x8I*+>APo{xoT@kKHze}^qv8$Z4IdRn5CwxtB7bpi zyT187ty_ZjS-=^{-cuS+IE_R(|pK^u3SL7B)BaKxpj2GL28`+IOn4xPND# z`P>4#KTUY>;Jz&L*KN@rw|Bf4>_#`~y@0e5Vxx2b!{IiWYoaT4Iv7O~CF4{CnHYxB;g1 zq#ynVL|m){^`#otz!;VOEl&K5mnn*Pdn2i2l;AdXM(uRe6aCpcn1z+$5I+P^I$|6D zke~Ja@VW^VNF`_5K@7#$o*IV;-+m^wX6-v$Xb)=aB6f@CgAg@TyN-pqvYtU3nu4 zsR)V!C!-suACOLGiRGp)mB~;f_!FFC+a+37jheZISNR!N!b5t3wBMvdWs?D|V3G8U ze;dLz9QE3ljd>da~ z=u}IrU=I3Ia&2kmM4S$*iIYC*>ep;-hbv(CmW= z-ELgr&J_e{lZZXJ@9{f9quz$pMn@yWn#9Z!sr+6BdxXr0L?qn_g)pd?)gD(KSpbZp z5q~tq4HeaPnrS@S6K0jN7N>wDM;1->f)Y2LSH55aZyL1QhClagJi2{-0QouN+B$KT zWmST(dCUg`yTc!8%@5MiHV}*Nu?aG@%fi{Dy?PxnElaVj25;c`vZ*i6ko*HC6Ips_sD|Z|27SjeFuM1>Ua7C zd#S{4#u?qwhSYcZ{uv}$IEjcx$iYaVY%*JQ zIq^@0f|)|}Vk>;mUZ+f}UZ{~K{)4YDj)PdXnM7p&Q!zN{z7v5Pl?!EXk>N*H#l4jD z9OBR02+YbBzu-!sp0=;n!1sr4fWtnHYw6(4>T3_eJ_LP&T3g%9*WTSCK4jbZ@t)VMT)XJl zmYH?x9uXkVx_HwXtm@u)nb)c-wYKPMJx!2sY`K&kDm}H?Wiu6`kM8s|lWhA&bGWz>n)mnZfH3_K!3h3h* z`XC$_XChW96go-JA4#9}>8GcE0W-Z1o8A5IZDmO=R4-Rsd`DARY_1i#Y$x&=P7Dmp zOjA8S^<%RPS3v)Kq_<%e{Ky0!jJQ4I8sl@G^0&&rrpG=VeviRo`qwo5infBCp|IL_ zct$h*6fULn5wu>=(s`a_x0`3+fmSdV&^dZ)-bMJhMM#Lc(`<;(Zl&BGQ?BS=rQ`{C z?jB$llMPA*9k3y{8+!XIy zPl`MLW=r*`Y_*$I#`XMIiZJVsx;Z>s_lZVC6LZTa4;ehZmhk~U$E16gXYC}Mw7;1j z3=83f#Vupivha0n4NJq>r7CktOlo$b!_#09WwBt|OQEk`J!T4CEql%i>Ppx{XqZuK z$xw*8ZKyMPj}xX%Qtm3$CRanV*soVG%Ja4MDeK1G)%xbiTwqw+N3ve(mPSiHNjh@O zL#Y%&afT)Bhek>mfxi!^t8FEbkyJj5y_AxrrSw!XO6A0>jI7zl z#p>8Vw=q#7q|U_Z4bh|k06c0>AYVz_txG(%??-G*$Q@dd9%rQ#rLj54r64jN+#8Go z@?4Pt^1OgbgIvy^R42=kD-iQU@GFo8z7RZXTpGwO)j&u7&lR~xKj|Y7KG%f+&T>QS zVjGY>V%P3x&kveZ$e4+AznsF|?g~QV*;&6W-Ihh0lv0j9j3T;sfxn1BB=W^T1@y)z zu()sh#5TILu;sE_+iMNi!N>7tmP7R@PJ?yj;$|0TNrSKBa@abJ-7$W}R>&Sk;+taZ zJVaZ)3?0WOkT+_PH41nEthRl-8VCZv0`8wyx&W^*la<@q@1M>7$7}_S-Pc`oWYQEu zUXY`lPLune!4c?5V57#FBsJio^&NnZXI}#gHcyS13lx0%U^(^1$qyBvYOJ zb&MV<;y_+^m^vln;Rk!TD0}GWXg>^Pf?uGz_fRXgX$*-kn$?`nsuH!}XFpRn3t9x0 zIdE68Go#pF0xJ1njVDjCr;{4Oc_eGWJZtGhiQTT22>d zhPL>=V3BzJyGZEvHKi?r)HABGgHc9pa|-?iI2O)MyP#?!^b_n`w)fZs9pOAk1e2yo zR4v@akcO=Kpw!9!RyL5Y1ZoKvZu4XooN!MAj;!+nvrkoqR09-@dqyKBX<8SBy1%S! zY%yABsaXUOU{N9=i&SH=N+j!JNguSdl&($4!PWM(tVL5u$6d=!uSQL<_G0VyhBkto zA(@m2*`V(Oe&wKI=LVO9iDi>yl9O4d+@Lc5l1xyS57r`&gQJ}jtrpJtC;9>B_Ix4o zrqH0ZV%L&(!Kqi`6hBfETs}k!8iIXJ@{)pj6^u}LWXj}oHoO~%%YzF0eAs06rl+aY z;2Dr>bAY)(90_!E)Q-H{Gdw*YNo9r?59|T|fa$J>4D|N!I`LiF$h#?G3VZbkOY5`O zf-4o@sXvqtD#KViq8`W{vL@|wpFSdFz3_cn3b}0Xo0_zLD&paF!a#)5Z(J=0K~m>f za%=;Sh--7E06dw>g2S0>NJ`I8B`F)I zmkV-R?zm`Eh}Mw}f`BwF*BcR%4ZSIpN<&}s8>}cLsdOGKz!AJ2kV*K&g8pd;1$72< zphYrjE+W#nEKS#1C%i$B?3ONV-7Kw}o783jk=R0k7K;`2652-vwkUdysKQR^KhJrz zgV3Hp*Wc1HHbJhxt(=gU!cH3l4ZRx0BQh}h3pK?X+2o!qWk4nS(1?>eEZWf>5gpcs zLYM(PTul5NEJO z!b2bJ;aQG7+JZ5&{7+FKLc^5UGfNivhnIBvdV-zxLMj*yqz5uwXjHKVbO-yfrVWu$z^ORC zI$cWr?}rTHxZdoj2Js?(?HZiw7BYbI!B|m={q_Az^kjM|SRiqgCnEN7pZ(zI$rg?< z3fjKeO8Tx=|Eq@Yc~!+0In&Ts;nC2R!prlH?~mOdcuph)u3b043?_Y)m`?rs-zf2> z`A*}>6n$8tI61lPA-E=?MOGyN&)vK3DdC=5&JjgL(NHMQonOKt57!<>dDM6qW0sTf zTQ2TB)%Hatq3z)xeXlmym1r{wtWJMR!Lg2@UHe8lN0Z*G)GBxdm$(Kfh@@~{QeKKP zdk>i04l=%0WDM9Png693cRY4f?3syUWAm-_Q3rCo{#l0PsPp}ox!rL6%P)P*k~hG-uwf2Oq{6I$l}bN_VUN`sdS+N@PaRN=^} z6Xn1PWdyaHNLN>cNH51gkdX%y1}%D@G;w^@NAwt_oCTxOCz_%n8d4F7=G@3-&HDp3OrcOS(BNVH1BxvpC5@ z?(0oN1$SFyW1v!8!LHNo5HSiSi~m zPP+{^!G-cEPzbx8z?661JffLnq|@C}iO!mf%i-(-Kpg*Imi#ptYS2ft8BFp(M5*p< zbwbMjv$lGbkTZT?Eko7VQWaze?Skw$LOrpcjiw+yP1X3hNSIc7+kb=hCwrMN6=X|! zthQ%fK;+iS5#9?wXzPDk2}lKRHO5LMG0%wQ1wG& z@+R^PZ$(=V?pLoZN~3Fj8jCP(ZCTmk;a|S2zj*u|G=prPLBi7l%*NHVRbV@igtcf; zQ<-)TxdAo_wo4|dH_$^M;FB7M!!o2b##Ni!7EQ*w$%*f~vjJiS#&FY=NIED_%u3sIALPWNJto!LOLDYDRP3O zubJs~`auB|`~2p(TOjSzJgp+p-U&3t9wf;^0k~0&f2o2DR$Mzi^G)#Tl8f6nLDk{w zwpa{oX(dfryIIv*FRv}>ub~64u-oat3Ghr@hw0~z+S5cE`R9QlA zxIy;R1wA6CTlfU~4t`T zN*OTSb+znrMBP&qQ51k?eC)K7f(gRVuC4-qFKtZfO zusJ?b>R~3MmoE+}M3;2b0HhHM^S+*zTp>Doh1dq8Df;o7`m{-ha0P>y zodbdYmKdhKwG)6SIC>fj(8`3&KJvLy{jRL6Op(}1TrfaJ>Sdn0{?2H2pr)qg!AQDY z1UkWKB&Fcb00qNE)Z_Gp{m0^k0OFKg=2mIcd0Dv@-p(*nCO%X}A&cBbB2h=Fz)7y_ zRtwXkxj^#OSd22~0GZ$vzD$9o(|HWM=uBcdFEq5`cBitX-#LCuY4RC3up)$GZm`S+ zvd>X1k-<{=E7CMH(*#280kbRZr!rXtks$2VZ%G_{yDx9de20~O+sg!L#T@IY)f0oP z=0hQBKycT+)Ma^B2y21=?Z9GS(5yhhpZV#~4_v1PNH;B= z!t$q)cwDmMs&oO*{CCA^N0{1;ft;&L>m+%iIfC}lh`W|O3fWx=8ZwC-634%3YoOP| zaoOz0hn2MZT)fM<&*;vL%uUDp-a&&gjc|GW~*@isy~ zND6=}S<0%~z**FV`HdPmlxk-`Z>`6|{;*CG^E>)9VVblM<;MXj!$zf|iPG8KUKR1s z!h_%kjb4{sRtB4~aHqld5BG)9ndSt)%aW(5MI}B2V`!;=71w*Pgxr;L$462$18l>} z=TnqjqGTjzMg-R;#&RD${n7Pc=t*Kxe*(+cKOU8v+?^u9xZlNzjd}QgQA`vj6F@hO zN}LUX(ugzf3sP~mb(pj~PeT=c0`ZqcxhU9dFDU07&_j5EIxq<9gLi4W-`eox0 z6WmbkmzC1(jKhZcK#9n`(}}+`cZw!I%EdAxZts$$h#EO;SL}jh=RgoqH8eDyfKc4d zbh*s7Jc%^C^=*kW$1X5)hJ97`DrO*)KQ)4w0D-Y(JQ;veATOqSoJA6g_@Ib# ziK_Hj*gC;C)JLKWiRH@v;^Q(fRxfp*_kyJ@Md+UD4?K2nf&BvEo5P69ozCHk*kcs@ z8~ELkn}UtPdjEn*0xCDzd7PhLjVWLbIUAyG)4Yqsyu7azF`oZ@ECh>@d$FL9X;vT& z-?$VA&Jikr%EwvuRm}XBAp5=#kRnSOQlcS&Gaq$x7dTc6je&V>%um!B3yAxmpi2@M ze$g+ja^HS+H*7Z<4hI|)R7jX^0ECuH)ZBP0{zwwbbp!L!@_fKO0iXgmm4m>DXdOCMGgaT*#D-!71Y#hPo6ymJV$u^|&-WbH-cbRiY4=ez5n?Q7UAcFYdT@wVt$ZS{z*@8uW!f z*4WP-sg)^R-(V4Qi{9%#pPywyi8OoPo`_-bi;#~4spK<_$=s-P&CF65 zbbH38|21XlbvPBJAjKA<`k!(v8k&VbX?A}#Rv9B8>fL8Vn)}UM%`p6WKTpFzc>xm` zbXX~#NybBe2($&}IBR!}ZCkz(aoTk#Zl>d83r>48hfj~j6IOJ!Y=p<*zl$?A{EExj zD@V|CTV*VV!Di3_v&kniG0G49IP^bW&+>SRm~G^!S^aC!aPUhB1H5g*o=$_x<4%ipO0UIvUooPap83V2J(dl)m|6HV3-e}~NN&Qd&K2S`l|n8#TeKKP zJ$ecL$|f)bM}R0r8LsKhY;>ccNNg!TjJ9O-xbMAbm=2|ug-nvy)ob~DSyN5#qIk#I zv^p&#U8nB6$>oUFciPtQK>lgjc15<^JJw!RZ!SJolAyA(dVx@|nrcG<9-HAl0LI^V zDvpp(m!04YAXWi1jn+#FeVDO}4;yMcvB!^d|IGZ9`A^{io|nzLN5ygPTc0R(N73u=Y#SyVTTL1?cACa$Y}-lG*tTsp zY;3DBHrB?Ct&MHl@AmnQ?;qG>_lLdj>zX-dhVb^=kBtLH81H4Mu>f2smi;wm0XD@o z_YGe+uz|hu52F#J?|)4_gQ7qxp}pA~j3MAhv}}NN1wcCE-Eat$ynLDp!rJ?tT8Dkp z(SfBvyqPzjaF!;DH;c}Ht3(nG%~*tdw;7MM?|BsrjBANcF&}}{-wY&h78tV|8T=h* zJ_8rH?J?f`uh}k}Y={tJ{C5VwSl{N~KK|+pa0#M_5^lG~&D_5kT?Plqh{(J|ryc^X zv;xuDSYCrqT=2F?BW7=bVXy+uxE^67uuwD^_7pK%$8O+Wj-UDaH4Ir(G-u$YVMXmC zFmj>F7<3ITDOql`?}1NiFdMI5F}d3TC;u~>l`%LH1*O9oMx>y@2R%|GW;+9gg2j*Zo~kD*_Gq-i)<8Oa?|ZXWz5&OWz72zEYUG`kKHe3J9fWr=r6}rx5?b)tFY42W+`&L!@Tt3GP>+D`+gSQ6O9u%?X~1+ z&*k}mn!XXs{O+9+gE{LG&knG*vbNc~F1~NQnZ!kt_d7Us@46u}*ZQHRCK05DSk!$v z5M?t!M|Y-s;a8Jslp`qEMq@wIFMS+A)MPd_ z<+)BHXn64fd>LS_uE%8gZOyR=8W()~v-W=TF~68*UfTKQ@-ic?@04%-C+&S)eq3Qg zHUtZU>FNBz_WAu~xvF{1>W8jxOeXrr?Ne?YxIKK}{P~UKtW>al-tKjHe1RU4FX;ZQ zRI!=Y{{bFcnA2^hAKyBP?*G`wAp|nBxN*vzX7YPNe)%&pBsWhG%RkX|S7oo29#Rht zuK^B2-nl*Ge%SnooPfogY5oP?>3ZK1-#9*hlRwv^kosjS6PQB_v6Jxq*JB!3EibwY zfyq{B5;b-ygvoawO?uw!Wqmo^4F(4zb&;?)G>hhyGv)ea0)C}f{l3PvuZ5j~Xpb^eQ6Y(wx+}Fh|Yp-A9 z;A+fJGpD3)^q@-epuv)%y!Mg28~ht7u1cUn3W*Sw!rg=?x#|121>QLcY z8^*&t3E6kxx&Pdc&iALUedEn;v7D&f6$VPrpv=vQzsT3e7l>1; z`1d(#9$|BQe(lVEfiGcHnDY>d5rIYUHXnT%Oz~cf32uv|Z%XihLp&IMqH(5LHKY#3 z6PuYf8`Fi7D~*eKC-AxTqiE~}a_6=On{!~();aef|L2T+2K#W0QW}eK*bW9LqFx0o zb!vYV${jzq@9fzjwnqERjB_Oqutfi74Xi{#36#1JLA{`4KsOSOY?IRMP&mUo3*}T( zZIi$oFIzu$RIq|fw*HRFCP747#AP7(Bojlge@1^O5cPIJqo;r{TQ7=z?d^E_mY

+qcL&;BA74zwt3mQl`Be$XtJP3xNv9+g9dA=iqv|e{V~ed2nN<7)@a5Q5h}purMty zG_Uj~&q$0enu%u~7Hqo_aEOf+-FqR{$wgB&KyKg7(`VJl-LZm8F%E8~+~&jDJBMXx zV(TG6tDmK@ilqYJktUCf;j9yRWV}3T6>*VFZ3|$M@tKzj@AoIj>sYi?-1|jB+PjRQIH?cX0w>fA)84 zHwn>wI?SS!Q5!}A;A@anT4Weqw2;1qY}w13ER(BzG^&=v22&5sLnlzJGLaOZ8Z7SK z69Fmv^V3el+Lzk73Uw3@0I?v2Fr~+bG|H^DeY-BfJ`S(EdlNZA*cYvDTRL?d^ELv@ zI*F<70IJ*++6^bfhbi!+(fa6pFESG-QIUVI_pRB6mjji>W zd{y;HzF=7(@u3b}j6c?>sqb?o&Uq%qugG&n`LgSI@H3HJR(9l@&6;$=eK4h-awb3 zo*ZKti?mSi6NS%tzuW9SmDPVvAk!3^3e3Y4Cb{9hAwCI+C$5_Mc=&kF<pR z>lXlS*SzlY3gv<+n`Wx?{n>YXsY#EAdg(Eg?RES|Kz{?wZ@Hb;4Yf@rG>Vn69sO~i zQsUnXfIfB3eaW)^{gKa8Ngj?F(BZ>9HA2b;|t@%lZ zEOwQ)Q7QV#Y-1dpD}V7O8YVh9f`Az-&1{fN#OE8`Xrp6$knI#Dn(dPmLbtYrlw2t3Tn_SpWNzo1;vMCc^ z!!r>h`kLc#w;EyT9U^%KaFn^B%UTHEesu*bSeTk7vmq>t&Con5H~S#|<^H3sMrl?7 z2>35K7C1j1IBvxu@nf011Rk4%yT^#f=-?`mWHrO$oz4zi4lG)f4$GNb_pjldoU+Aq=$+yU9lN3>Wg)*9Po+i()q=^=@uuI4k`elvJx`a~tB?HywAf|#kNi9ffG&}JTrX;O=&^2Hqm?)5dPqs3PPZneQw_JT z+gQJ{{?oTOo2`zob~-NZ+uf|zuGXQX@P)ixEvo3Ri)f$&DJ<%U^91)0tceBmwQhI) zF?Nh2`ce!h*hww^lU91wB)?H_hGd;$cD$N_+Elhr8!6I=7EiWemCRCB1dLttFZcE7 z7toj>L#@)kW}p0FIrh!Q4uF1~H^4g>TZc+ir9mSVgBo*xyFc%CFmqs=g62HWMoHQ@ z&lw_s=+4*9o?NRt2TT3+kZc#Fmuvsn27V`rolc|D0gf5PaIAL+<6ZYYc+}>&OO*)j zDH0vU==!-G6v#C~7V8<}i4;if9-rg)d{kd{35#1v?MQ0NOd`c)8;5-9i59Kf>(Emg z%eSMt?z*UJE=p-Z;|m;M>-UtBB#@wM#h7g%aiFpX1{3D%9A`!&hY!GwmJj}0_*h)u z`vT9O5Zlp8c&WMK^A9u;rg!l^Do*WCNFo@a)HPVA*?ROnM%v8#!aR7w6F_-B4B#^! z6a)w}dQ6c{k@4C;=cpKXt3u?X!LuDX-(LN@HmiRH=IcE|wM=oc*iUZh%c&Um=7{#5 zesJ|;&3JZsypuip0l2tpyV|A zx8QUCld8m{Bm@_1M^KG{t<5LRCu;I_Ja$f5WTmKUG(;89uWMTi@jo$m8m8*O@W<7x z4bt}RT$Z6rB5jmz;oxipDcRcX zM=Eov1*Hv)F0Pd2L)(bBrqA2!ncDMOC5*S8u=IQ}sVe6rh>2yq3m52}`E*v~eTzR$ zU4UbGyEAYjX*s+i-GiWE&KO?#X!5J2tn!*icKu!P?Nfo*JdnYSd5gnl$>KoPxk1?M zM$2WUQF!d|#+32l?ZUqsFCwiPOLtGTLBo<4Y~5+R;A&={qw6-gq+8oE>eK==TzHyl z4FXgm&1BB;doU*$blX-#3vQB2)ZYq~egm`1z4<1&b~^p)QvfXoF7PK=V$&6=Cd|xzw@d zRozdlZsc@7%_-x;MA{U6qU^g3XLK!qr%4)ZtP4Yw+31D2_GMLngiP-tD)5$$t9X8@d#oPx(0xpX(`3x>`DC2YK%dADf60X_><8(cBNZ&b2zbQ|9R5Unt5)zr6jR0 zz*iL{TV-W`_%^ElR?x3T6Kei_T|sc|gORu0LETzy|{A1$Cw!1izdf-~*0bJ)viIN-WT`6H6!03+At#&bs3}!CSfqMUsB*XNA!qa2R7jP`>DT zu_JMpPJI1M&79yF#yqx6h9Za{9w~5I+qVjH4qmL^GV=;X)Zcb$g$QrCBn##*$mJB>2<0qK%Nb=-qyIh70sQ_yju+K)Y^mr5I@3|-%%NLH~kqln4S1#F2e@C`6>LdLzpGT$jZ(xolD z@Z7{VXUXI~_oUZqH#Khl9tMMV5ah(e$WQM@BBe1mSj35yV~a^aHa#c7UjXM9h8CMB5(7;LRI5D0fmYxZ>LkQlv6T`ZN zvXnK0?n8_p7Xo^`!a98s`p`eT=qO?o|H+G!rT=)Nn-#7qLz%!IaZrIREyf_4dWY!FECWY#pUW?K0(v{swxNk4I4Q>->fIMEZ=)U^tqNN z=J-pj`;~!NdUJ{SvxQ+CGxSz=>Q_9Fk!Dm{ro53?ip6YmUG+dpWqDm)V)&>_+4%)N z>l8lcoOWl3gQ=0r$YljX=3RpPsyhkd0ws#E-qV%Ca2JC2K2P}`I7N@c@7ueFT)$zpUh4z=x8w+RI)fLXKjnF6ItATiiBn`q5?D5JK6dk@YI66K6NCE z@L620A3Jh&~^(ijvq!{XEUc<*$%OSD>9%15sKG|nXiOAq<% z`Whn7Ic2xNkR#`VvBvK+#MVDnP`7s*y5`NcxRkWaY%y8gBPOaa&BSgKH$_XlEeh*Z zVyMd5pf(OK!xDS0R6|`W+jV~x;bUGO6m}yjlSMh)=@;-d}=(}+K*P-g8?Yh(v~#{BH|Mmi^xr{htWo1S);oDzp_%A< z69vaB@ic-j2kCK-EMWY$p!C&pH8vkO@$Tm?p}(RbbB8UtAYQ4SM)RCuuI(s$Q~HlW ze^DZh!m52C%`wXO2T%1UMD0;^#zO~&>bM!PTZRHV3>KdQ1ggnm;O{T_7gBoEsN#q@mnDmyXmNF zNV(}Ubk8VSMu>XrsGeKaX8RXv3a1Jbby}_nrJ^Nrn7a0QJkkolDF)AGMvKm(PCA&* z*5hYaRZ5A8o@iGxP7|oN_&dkhRL_FWl--4ZU0apjRS|z<#~@EpYxpR%qHVNKH~y@+ zy|2Ttnlb~0jH{U|C_(Pi@LV>nTO19HhlNg4#W*sRj=PzT4p|y0g3O|muf zzwfLy-AF;!BTQl|6xUDqq2hgT9UMk$bEE9#=2WBs)hd}`jqsXsIe!YoMG*<@BoEwG zLA-uM=~yX;nJFAs*48eSH*WO>k3iJf`<`UpH#K*T$EKuf8vu|}eOPvo$2;xEu=vM^rYK?s1Z-xIdq0iG7IMFCX1yhFxZ={I<_mVjC@6y1;@ z`MTN^4KY%n#QkP4y2_VP+eDboG?e{``R1-W6LYP&_&?kKC|VAghSbS#=lI|<{KxJO zhQRY-w^zrZ)=k%*AaS>8TqYG?UK=n6*km`3Uy&j~BQnr@a=su*;@KZsZ|-*qJ@c+p z_&5(<_nBM?3Pm=dcFwRaeBX-m2RK>QzrQxE*jx*!{$qh-Kc;#_Z*?i?Bf9(n$|tTv z#HPw|RI(|c0`aJ_lyLEff%I(WiSV=8+~=h8?M(X%uZtJ429=jWfc4OEb;qJyw$;a{ z*XAsj1S0=aN4MKgk|5`gr?_m2*{+j?YUiqr`HCOX7~M0}u_WfDN9#*8xgK-(vu}3r zL>_sKkvW;B#CWJC4x4%;=8q>t@@wXlFn&Y4>xKu?2A65adgO_~MX-yp^C-%4{=vWF z$Sdofk4C~ehlBjkiW%m=TZz65zwrT?+&y^SeId{jwzZjCcp&@n&(^EGp#`pGSj4l1 z-NaCkj=DvQk1-&?Z+w$~&$RDaPBdYIlcves^b3%M?wYsa=|0(mb-S(gJr&!>7^e{! zdfEL{<%qFnWto5XORj4Xuv8Z+dS?65wcvL>GmANVO#188EWTa}{{injs{;3+Cj>Le&j-8Z&&*tADNJ;}|_pjlVX{BK3ZA;@7Qt(n? z;rDd{n#A=#@3&pGWypRU<9{=YEIBG~`HO+~B@i3%HxR~S3-|ojR4+~eqMB~2_k;f4 zz`iyGMUa2Gmjz>Qo%wME@{hb)f2&zv?xcY%962)yHp7Q0U=-g}o#37uy5orvRLo@$ z|MI#vyyJ=!%VeM^UMkDO=;H`s&l1Vy=y%48GJ#>fu@*Gq(}DEdFCTpmwYJ~ayIK>^ zmQ^<<8I#|SeFNHa-9Et!_#}KqFyXa`D`DOs&e(ozxm>%Fht)DUSOYznd?1;^b@QeT z^O2!?T4d^M_+oy<6X&`ryN7Qk9C>-8856_*otD~Te*XKL!h1Aj@V|#|s`5Xg#S}G1 z!+o+)=*PW$nSOPQLNzmvTC2`EU}bD%wm^aIwFPMMWm;tz_d^Ves#`tkE6(wY1%k_0db0II}#GFnw1F4BK_-Amy#s-=wrkgZV3spGGOYqed#fU%KCb;R_A z`|;`lufIig_cVw>Bb2v9^o)mDRCJX#n&GI#>ealo1gHE%@^#|wll9nb$1FQg1&r(y zA$Ie@!72)eNsb*M5po8K=(>^78kK!tibA!78UpDP z23hg{GRw=#ki$LctVMd|;ssy$Eb_Vv9u70^u2K#X#@d%yYkuDNc*AB{QLF=#(h3eyCgpC$_TOZw2&)qBx506j zD(@ePjwP?U?r#(6SqL^!7=Mxfoe3^jYuqa@y(@_P9#4w6wSG_Q6f4|1J~Mv>v^q4- z!Pb}>O&J+A32Wu1;S22ML7z3`zExWL=FqV90CN~@rPCye3eUj_om?p?{!T9i1&jL^ z!PlNE!P85MQ~dO&;HS7+hfq1~f9Xh2*{-+lrL3&$kJZLSo=(ensPyj6y9Mo6+0HAP z#g!i}f=A#o2^R06Bl~8unR^q}Yp@R|8Z>8qb|*1{A|LSVbu_2^{!oTNqgO=-jV4H+W)0O4#Pf0;!w)lbbys5h6KC8H#__NUX_Y4 z+ip?Mxe@P7{urI-RHRHk!-8T7eA z_m?=YLj*gBS4Rp=RpMom2VRo6JkX2S$&r6Z_JX{N_+|Ay4txB)csJe~az|XQs0o-# z{KE(9NM1Kp%%4?LE!YdKl%Z`ln5YVVK6C!_F3xEkz`aB*LXn>l%E0J5_iHWQrSsbK zn9uL=%IPUeTsYO9nLB9fyJW#S!(bvB2A?nG5rdyXuLN5}Ovz$hQO+ak63?2M#v5a_ zBq31i@)~l|6QwY4dv;=7piQ8%+o4;LlfG*<8CQo;i@F+^EL|G1dJBHb{+igiwj4N=Hnr;=`;R)5ig^= z!Ze@k)^NtC>jNwIF~s*84D7mMUW#TDM!X9(2wE`I3cuk>`e4TC+))#;Z$aQLuxk>) zLUOp4!~x?e$HA*qDrVrD>8%8$#3ZR0g5}X9p_Ku>?7i)F`^g5G{mi5S!TJ~YSA8SC zU2n6>0=b1>gm#|R!Y&6)567}lGe!@XZ(5a%sucbTM6*JY4(xmu1EmjgF~@oJWkFZEYr*LJB^ju zMFggqK<6&99K^z|RT`49MrWl6l8I37Yw43WC3xk|+(BDmUm0*et=vh`jFB(;b-r~Z zTH*JjjN|~-G7>IfrjQ2dXJw5%+_OW*5;jwzC>tK{%;~tfkHASX-%Cn=j0Dw(OVos% z$0_@@AB*JlN%CK~j*o5sJkOMRl*D~fAR>yr7S`|LHQ>%l6WBbti#UPM*|ucLdE(|5 zM<1cq!QgR(rs*Ni;-HC1oi1l>6nx+buQz^;JXj%DEUVbT?V~}_3cxMNBodU8Z^lHa zVcP4Tj7`je&R*e`%29WNYsPcG_R_sO-)K36)7#1%t>$!`LzR5uGN5$)nsmqd3~?sp zcZmpa=AqLM*n-#=9DbfMdzBcz#^y?ld&j-@#=*YWQNxr1tEph;T~9a=O5SS&L}!(u z!CDNolYvx-U6fjya>r$&X8W=&F0tQvBYXnYdT4L{^@!KdWN3dxIKzYo$1O)eZ$F*= z{ao2=L4@{;L>5y>flJXhI$A&=rbweSa?M{@ZLK8hD~%$-fp=ra8s_VQ-{mbTh~kS8 zI-@Q_MqTGz^WQ44IGGar9f@|_Vg8)WAX%0znm0)S!7j_y?Zrc|qmM@#7hoED#|U>j#FWUVtBAqZGp z;!(?!EVt`9hHm!uG69xTczQOPVJ*)SwzYnc<<)a%z3({^{6EG zZ?tW(LVH|d*b&?PzZlTXG!ORt_D za<8$QA*ykg>U*+6v>BH5_dMM4mtN{t=?i7g3%SvMzs7x8FWzV<|XWx7D4s`(Ho~K3#>MDlC*wdfA%BVt>Z(&~}C8^(HZEmkklf9^Rb$b>QbP$vf>4M-c5 zT=LNNbD$ENq1R}cjINwiLJZpSZ8&*Y^JdjJsTsPFhwPHdxT6hXX;E3YCfH@L-zN(8 zKFY6~*XPcKhw*dk!+UZfgjnE%;dWSLJQ;;L^_@iO^z28 zW-;DTk-9andsWPaRUm4TFh9KgTwn&%HH5`S2Le97w;_Po9TM(QabujYL z61tE%`|M9`N79rIGNN$4o?xD^y3{^W0HSA4u9qqYRKj41gdn}ha&)?IEu_sMfd$+- z;b|V^|m0U%t9LY74bx8ClJr-t{Al1Rg)47pbdWch?dRJ0&qdX~|+&ao-F zyeZFp+Rk-|dAw&zhwf56?H3E3TLeq(Om~#e?a5^TJ^n{1LWn<)?$HS&`T7u< z5KphkCw;bEDM%fZ#2x06B)kE)Uu)P$2cHw#gNV07CCmm?KLN&IK}TUZVcQPImg(Iuqda3NkeEgQ}5o)zL%THY!0 zXhhjmhC61ME+0{nU=knVa3Jt^PbvC01=>g zBl)x9)*f`1}F@ zT3hKdkMLRSD*>|FraT^(I*QWr^LxkXql4?$Z~-B|{rjnA%UqQ!H3ZodT`D>qALpcC zK#ezaK*S}*A zcH`X8-@@4SYOr~LL+kv+nOB`URg;#PL|aSNr-$6`lAGDRa*MCmY@3vq`ryMWr;p*&e+`e+3=G zNW;jCNH>LR-m8qX;wYsw0~2j65~3b5rOQuKDz}qhwyMs05Em-E4xtFDEw5nen z4yYI^n$$dEgizh`e&3i{vsey3waFT?UGEDnhh{jtqIEf)EhxPdTr6g|0Gcuy!0cMU zMVy3Ucs$QGWKirhY4e>q=CEY@O88#yR?MK;(1W`~0XMSiART_zJc+>z{)()3iv}^p z$@js$X5g}EkcErIENV$T!Rr1QmZ3Z^_l~+Bvr`ZroYi*1+0uE|0>=4tPHb?7GS9sJ zG#cBli7U}&tw9cWmhcZ0dBdGftp>5*>$ z*pUIrs(<@~f}3gIL@k$VH$CjiX8B8HNT%0a zGMtL!<8hEcg$Vrq9(!>nZkLN)w64#zim#>~eitOb`8Yg)N>7#0-O!s9NZ8`jn$L~m znV01@`RT5Ju~*sblxpIS?nD2OG!bCrMXhe_a)+2HLK6ptvgI@zLHfC_OF~kNbqGoF z-4NqX=1Ez61(;w60vR%l(ob7NoX5kd#Uz)XB@#6DAW1AH1}CntCUP>_f2~x;ls~|B zf~~?(^UH*S`}`Uwp0s`ozVQ3Md3k^|jtMv5J}&nmNzMbGVEKCr`(bs7xTplE7Z$lK zYMpUwto38sFlz2>`SWA+xtfG^4JYp4*K-gbxG*ux5DP|YxMN^O#bQ=-spz}3cz%DG ztF7B{#MS+dG*)9uz%mw#&f!@!0fO?)?M;oYGJ;Aj0arhx$Km9?+zA|A+~gz+remtt zKHU}H?g|e)7N)$oT+98Ua1?W_=>e(>dDa2YAZQtp4J{w!|MB4e!e%TeVl#Hy$86kz z?qH~NrO4qvtiUqLvRY-SCJxc z2+xlI;t3uloW;;idEV?ltztT$33#T}Fg>2aGX6zDvFSCmqR7e;@a3t|<~n|B?tU8& zUFF*W(g6cO^b8TK0GJPnUJs75hH6dMM z0CgCmK3kxZd`*0a6SFwDd;|{o5mnO zFi3f`_GXhPkO>qDyuA+jJefqW(9Nd`w|8YsXw0HSNIMiCl~1(i0l35mUweo$;iSud|@QPs6jM<=2bEuX%$o&HV;dgeC??&~F?!zNKj z8c8-z0dfmWTRfUh9+4$stdNM}CF&h@j`_Bw;4`&l9{+8gEkY@yeEqAa=GV-6sIJXP zy(S^}AAx(Iv?=BA_@j*^XoXD_`k8>5MQvNutN=mFNO*gTAzlHGb}S?y87gy6Fkln^ z8O0Gz@@|WJF7q}&Zdwuw#m8Uc!vVjX0h%G7;?glB^!n2AR{Em!su}aS&Y!I}k?WZV zP}r4(Y4 zG5H>FZRbNW(?avWWMHEE8CGucbAcdN|L(kfw=)mvAgnDO6Z(iZmwXB*aqLISb$0)J zPP>`}<&6yMB_Pt#tVC924leu(f|~9fuE4Z!E3l&KebJhXzGdgN2YfxDORGMDqJO>2 zlFMfXzVya_P{fxCVO!qG@v0K2SG^bre}0A> zmG0eELDxfxQ_jOF)asy9EL`Y3InlP+qWjL_+e>O4|0VNRpcW9Fu?dstkT4I{=!~tQ z%4z#*Cdl} zPL%GBt|4rn=jHM!9Hidu56W<1R+6v-M8KSLILW{oo)Fz8L|N|Wea8HtDvFy(6gk8* zqt?c>(%+JP(TvjfQ?{1K#1Sfpv3g&d0$XE*aa5=4-lo_LK12xHhWE*^=T zBQi`Ey!616++yX}01EvO&w6k>s7SGNAZNYj&=KSGjrpYDC8j$q4Jn2PA)SFl&&fz} zKGJ7V^Ftki6{EFfrTf!nx#$QHu$wNHTthWQT z;8@Ic&e=5dkkB}_M-XbuKdSMFb@{x=omnpsf->?CP*h@cEZ7*F)V?Pn!Nm;J_EC*& zxn=n$J?hb70AdX#dPt8ZuHLRmVvy6sxUM%1<{r6954;eSzR>+*pWrz74iX9_Vs;9N z(x z@~%vGmEmw}Ua4Q>_dSRg@Rj{Lz|)y~B$%3T(}=h)v`c24(k^X<3iWFGxfF~1f=0^t zg<#_`Y*sM6hD{wNla<>9_*O$`gbZa5VyUD`@bsxlK#|H-MLHEj3h9#T7Qe|57-8~Z zT0u_UO~Wx@JqPZhSL^=h-gQEdXy+P7KTqRiC$6_Qhy}uWShQ_LOfqJ=cMZkIJ&S~I zf)y+|twBAV>l~5Z9>IPVrt1mKK*jfXc(Rc8e!#Dvn!QoTR48+7Pe7Vo)H6(nQo_Ms zO2n3Zs~96LS!!_@#0F%Ph}D&1S_zT$q8|BZdhn7y4qtd^NxfguTv6`e1RbPGknbS$ z;8H+H`0#w9KOc9C?ef<{o^m1+*Xw4{>SWtYK=ENtiCd6Ad^UGf1&;F2q3Cts^s#Ya zREuD7?ogm)a+VEbLU0kM=U|~bcJaRfgn{reKcA5X29YmAse5HsCLW zNF26t2?6LAMUJl#Fymr8IXzU%=1|#7sn?3}{N1w>?7gsAPjF7HZrMSFa?iLP+pF{p zQM@_Egi=@*h-h3FD}dIK@SL7d9wkl-WL?qRt(E*N@bCR3v+p$gPPh#%kVND_9(y?@ z=rO6@6kc%}fP4gvD!TD-!F|=4K$G;vr^bD3gIv(_iSe>?GV1sXZenpB7k+aYr-y3O zqOdLgWgdwqznXyUUp7AvIO<8rA32{0!+Anj14*vAY|`0FW2Vdor-}0!$rO+OGn5`w z@|>~j9%e|ZJi!mqD5$`m2`@^mNvh*g@xX8@96D|4U$geVxgkqahq6q=`GBak(8S9Z z%35L=S56#8JjXG`)|65)J~Q#!*XkeQ_$D)I3`~vRzfVVwB4{mkD>J^$7s!3Mx`*II z7K>|8d!O~^g%LsLwL^ZyG?(K@mcRhwx}< z86>|%H|?972o$xfW`HtnTE-w1iFIrV78|t^$8ME1RGq`$y!W99UY{*fXsDW|`((~` z3nf-+&UMW)O}lH>U`6RWq&Kh+Z2h4P@}8PRPq0F!NZKhUsx>dT>VT-R<{aF5*sz61 zeMVUW>?Jz9L@p-AQ)IT0bZbQyyz5tt1V!y5X5)HP&Vl}Q-XBgi%~844c zE#=!BGSj>y&${ExBNO~AD)1BMqF?C~fNCtzV33n3Y$ri=GHr}#kE+PpBKkM7a)S|& zB3KSAj{FO(9-4hYU5z|XZn;rQ}bTC;A$9$ z!~B7Dn=@cJCNSxh+fl{ikvhv|gv8hDubA#o%v%l$i_uu2Z>ZmnV)6?5!}v`@5<5nsh=#~TXTPfB{dEi>eOZs_lWr@?2GO>2|8nXY*Swd zLkiiH4G=a=ORAgACl@R2$IDTID;eD#3d*vLQV~D6^ZzVG+itihUq|OM@fPh(0GAM^ zYT4Ah6uA~7#d#0vEYM+f5q&xGj(9Ur)F){+{Y@U)^WPi<#tG8^}xs^_rS#M>AZ$AoH>I^^ln&RPCjZh(7!zXMcCd;*6jE zPk6Wc2Q{~8b7=o^kqLZP%}YEWc0MH(15AygRyp@1@OHf@v|N4c`>o=qf+m^Ao*MC>Um7eWS}t3fvJK}VYSrZFFXEzgI$8l$C#D9R3z*W| z{URH<{8#nS=InzRul*KvwpL>e=}a|sh3NPddzoWBC`k*$Udau@?#t|RE9FDE^`C}F zm>{jvv8|xbYw2Wd{~FGoCe!GB-X>a5(l7zT2B0L+c+;P%zd7@|XnhqUrV3KM?B563 z8gN#u}I72Yx&MalMX zNu}nURPe*q%ECi|LC(anPsGDEzyA}9{(1w+S@omfh2LO|+(oi)26yACWC*MSb!fHS zDEN?Sr#`k|3@9tifrayrI6oKlFgy!Kqk@tX3DRG1E*w%0B3vuOesRKo?77KDltMg- zp9N`FAu*sG35|S!ySUwt#o!Lm$xeN^kg37D7zER=3`%S*zkg(g2dLul;=6PsL8*>W zw5zs>DEJ|l#xL}}!M>6^8zTJpwB+?=eet&FS((`Iy^>D>eq9~=Jr8m8t$MX}W;KX9 zt(01Y(JEES8y919>bpSt`VPOMi^_b|l(z(yx=0*?gh^0&XE3iCM)W8Z(98k`E<`eb zb&|M9FekPpjrmi=RE8t9xVcUeP`|lM;wxoh8hj5zs4JqP{(t{uWZ0I5_k65y zVIK5mF=NkwKw75rjINLOm$xY?jN{P9zmv^Y`)*ar9H}R<(pB05%zovaO?qY%X;WCa z9mw)n=_jT;?unnVH2|ET8uRxRE8UQ9@`aCLelBX1EJBpm=chx=-TU7-d1HCiE?Rcb zy1O&IC0VqiB)(-ZBe1f_k7lK3$8pEBceH2|29lkL@bo$tQ`!6@Cm$@`_&jEs5~<~{ zMFm?g*XR3!a1cDmKTA-2|B})#n9;}ceIz27nk>sbC-~;OEFW&T08+Mea2QiY&G0Rm z{^f9-_wBSKn{9Fhs>m}2?kfhd&&d9N5mb@D^JiSJMd+K+ju?DN(b}|XqC0zA|){PM#+mwQLZiK2@L z18wgfNoKV89Icw_=KhNwtLpzbXLj;|7VrS=A7tVJ{|=%2$xwD~;!t=nO7!)&8~zq) z`H|t$(`M;xL02>6X4=7cIsc>Sti$Q>|NkwghA}x^(+tziFicE$_tD)Pn;E9Nd-8~* z^BB`P?HtX~-R*bYpWpTU_qfhK=eoSEXFcv4SB0~!H-EN*oqg%iF5-I?0clbS*~Gtg z$$i}Ps)!v6UcD zo&0jCfuZ4n&8|D&v43e0Db)F^nJou};u|xDwHLd*Nwb`xra%pm<6_14Kin4SZ5(30 zT)Qg~#!MckS0KG!9ZT)reA5j`f?2S%+7F-v&pE5jv^Xg6Pq`1zWGNU`>H_|C zKfUf|qk$0br&so|9;QU6^z-2o8QtTlXBMAOzvpL|8vPJMM}Y*SNhdursy!AO4aH;X z{O0z;7l`=}$B|EgO`E?NJO1Bc{bafoZg+ZboD1TJ+nrmlw)^C*ag$wL<43t0mBinb znkIt(<}7->OsW?2kj50xAHZtuoRW%#cewsqKjRNM>H$I zy@_0r!Vuo-W?sc;9Qajgm*kmfQf$zXY;pAUa1Q@suJ1KQoa$@uywa}V`k{B>&t#9e z8#0+|%+uGmRnA7HHF81!2horNYBBIf)9wM3fXhiJkt*^x0Rx|A-;jSAPxAQ+q|aU; zQ{UhjCU_cKnZM1|w%5=1Y1{HZQ7dx+?>*O<046Fs~jlU?|#%`IrMcFt+ zH%Mo-=6&rMm*=#&Dtl05vP=A6)f2B2;C)O880FUd{c}$d)LG#c^vyk+lpCmB`5g?h zThK3<{TzDn*yAN2)ANrW%jV+MzH?eZi4LUh-w$HkM&gOFIz*m6O7XoIPIQ$V$^stG z<8bWKgc9SLgWtX!yLRglHbr?m=ZcW?FY)@oI?h!+%R5>2s?P`Do#%S3%=*5jiQ0mu zI=7_Gm*n}y6i7gigM%0IcQRMbr3$w9j$Nn3jO#00=^zVihonmX>a-?h!}en`_DS(- zo_dnEMd=jYsLdJjVHsJpoSt90F0%963pfAfJpJu`jMJ>V;13?Qn)v&d?v1dOA|tkR zPC~{nW18<}t4yR3J$hH+A5dD!OlXAxj<(9TKX`wQOsk4(kM8w4ZG4Zsrm%l}yh0q>hy~#!ldU6t{DTr39BWQV zkfPj?ohZclM2C(2MO|sT8znZTc3!NX@4E0cfoFuAUu?_$@hL&?5l&vax=k|1V&l9q zfLqBDGvk9R1Fi)v8!a3Cu-fPBAE7AA!27Fch{N=x_-0s7~QCfjn zWP{`dO^I*h1RyjCuZJAZ>H~OQBSs|!6AE{mQgUYR)7xV{2>y*Hv1Xv(B2?+M{J!RV z?$i9qU$Ee4<(thLz3_K@I_}K`>vjn}ucbu^aD)2^<_K;PfyNjFk!#{V^_pxnp(Z5o z=SW&j{N2sN;auL+r~t^+mP*7Zp{7?;<^CEw`n*bGYLb3=r*BLJ-nMGg7I3c(D|k?? z#m6JJwcHf0kecI#rfYiYdc>SN6NWWTdAp?6HWZZnd+xL-dAiF;bNqek#G(1;-H0-y zk<-SMcS>)BiN4ADCf${kGDHreKQY!VqP4E3n)|f`-kumj|7I-p{zAIFqJoV=)Vm76 zts!x#BGNOt3(uWGg!p?9FxIULPNy*P1tYG}uj#kAGBZ}KLt^XW{NKyzOnGx{`$y=H zH&bz3!%l#alA|HwSa_(f_tD4Nza=5HE>oA&Sl@RZElZ)(-1B-OpuS2l!<;}W+S>w& zKc$fH9V0o7SF7ZbQ&-6Xy3Zdu{&yf6^x<-6#q36Ix=fI)bpsWzb>p6}33T_MA+Kh? zz;|i0(jrP3Ffy;%8sKl_<1sT{bu-tb=5s{ldQv^%=;Vj&JcTcGi0%1hV|MwXGTb&_ z)5rISYrS)}sQLEjtJ?6O$^85EGkTaoRDAt{Sk z{P97b9|HI!d>*ZvuP4DWrh2m#rV#i@Dfc(T!`fp{t^57(UkbH9YCW_|nygdd;%5QL znxiyWA_50o{b7kR;>~*Hw^18W`awz5X77d_T@u`z*j4I=sd>tV*03e(=EHxE@U~4wBW1(7>LC*&b<1x9$*|0R5q26a zwxbyXo|wO6DNXwo5J^V{RT25ul`=fK==#5=y_l#3wpwJX^uKi9zl_#3C1O_+RWs9) z4bj-B)fiagl$_}Z)Iyc|w2Z}k!eiJIxuQSf{{BT~QA!h%|8(Cxp>J-T0diWhOH5$P z8KqF(^eWyk6%5Xgp^@mWh|pR@o#1W=5$n?U{ZreUj_5ZvVc?LrS!sUwB#UZYhwn00 zWM*{f={Kpb?YVYrNUUy$f&7$UAemtP^_X!_uoc2rxa86Q^{C; z*%683_A;v z%(*NVo^cECG-De%UM32L!|-Wc`uaM8Z|^HqwYXn0H(W50DS9LW1#Q{?F#9${xy7mR zx^fWIXS74jo@Gu5m&c;#_`{S*M=K>}=;9+!LPZ1x1Q+QrsH|3>$9+0*we24wMu}Zk zj9j!)E72G%Y!J&}f`4d^w?_0AKR-aIQt>F(=;_K`89Ikq)lI%f7wd*h-k|&Hn=FB`SmEs}fs0n#BQ>8@&OZuu!6jT@Tvfc_YcyAuI3a>r>VPql+Ooo!4Ygpw|U41*g zL`5q*+V5TqmkRxeY1kQ*9NR^{PV|pNZ~xy{W3(i<{)g|oSjIr*yCcGe+w_v*(_^6_ z4E$h3f^fWCnYFf)rMtP0D(2w?0ts3}rKx?ohE0sQBkXIV=)MToj^v}NNn9UgMz|+VfW;V-{8>Hyb z$^U7yLSBh|O8jJ#sra!LtpkJ7H*~W}=^z4etiJH_bpKMNYS4f#(j6YUFmND2$deh} zQ}!?DH)0e7_1nqWfwmRXn@&js?D*ZcKDR%fj~HKk%v%4RA4cP8^Vo(^gB{fkP+tgi z7s_xAP^vw=Cg=S-FdpjSeYeHLxyf#jc}o{FfzIRFlMdJ;UHgR-oc;IWJbRRM1N>A> zThF~$f8jDB8fIy?m{Vl3gdQ-hB4}+V4V6keJ}02wCk*U)6Be*it)iAJI>p-zBt3%B z%xj9@qcW?s@2KZ9iv#|$iDf>=A7baPGCcDx=E=eEdvie9V5Or52`>b5QV8ZXmWdIt9m1wq zc8Yh$I%OMdQtb=ZX>05EpRv~8EA;>ss_C*rT^g1!0ju1PrzyIIVv;t(q?Fd|QC2(; zd-UvFHI0;KUREd6bNrQmR!r;1Nj_>Xp7x`#o9X~?F4!I8P#2;r{=7e$osihCpR@rP z+sZA*%n5Vb6j{YgsdYOz$)><-3@v*jJe#ts{2%WBJ?M9wYd#E&pg3B8%^)A~K25#s zfXNAmHh9}0c}njg=MOufruV!iH;d1Q?c6J$T2uytNzS=Pi{Ws1ThO9<1(#{p5?A{n z@6O82u#2&M5!wPGw*3ii4Tt56`=M)`;Y)q|iBX$bskwIu+B5o6sUam`TKYPMGS0{) zo;UrC(OXVrTc}7}Y-%8DgaDFd{zAAd^j}r{>yTejwe`=)j>aECfH^_et*Yh2&z0l; z3Y&8J4Yg8AfHU;Z3Q{&a82b(dQlM*Lxs1c_BZFM%Y?=ttfkw5TFNoWgJu%8duCd+ISS&gD}EM z{^P~0S$2E5p2ifBbPpP?m*;%7)i1lIe@Dj+pFg1Y=z1t78Gljz(^Oj z0VsbqePwnfFpQ(b4x!(QP*_Tvj@TT|VruJhL3>h0FMOl8=D~ryjmbf?cyrlyQ0+l$ zsz+Ix7u=?jC$apWDoHZH?(X5LN}lgEog49Q!d z@g+R^aM@?YEA+!fNC3tb*m+rGN5Dt30l7SZGax^fMl!glYKhd&ZA)0xYVb_}Lai#0 z*vyUjVIaF15qDr7xdW6dglcX3B6-NPwZ8E`-SwlT=};Ho_kBY(nIe&ZeJxFcv*c;5 zY3~&0fn;-iy{y%3&C82*p&B4aY?qdnBbk`zO)vQD#HBv4y}w8qv+K_Yq+$i$;cUZB z*L#=sux=il;6R?~+{>nvJP_+D0Tc<}33qR+vj|#Sv~OR@F$ISiP{Vj{G)>bb+-Rnj zNkg2JQ%5F!W|6Q`H34qafn{)qoNB z8KpfIjh@zPzEzi?Gyx5&fCBsxe9B$gG&6(EN`)6}2gd2u!oe{>AqfSNPIg_BXY6}GA z3iPs(XzM$BC8->)uHFw5Zcl?X0Bkk&i532pS}VViJ3(c6*RMyM8j9Y>)pevv)P4mI zE)>-p;S8YT3C=A#%+@;&y<*=3PxZ%A*l1VkK;7QoSD(TGR>TF}Xj z1I?N@xIG`${*l~(W44-pe+m02_br$h$##I+C!5N9p#U7VcRO zuJ_q%pj{6!UfBDjCb!LF**voBgbxcPX~D)iQYqKnsQA!Ket_fQLY;T?X2nD(MSE{87-53FrNm`_=5K`=(tRls9-uD690q2e2x_;a~7F z=3=s5l`qs!?}^=X`jZKKq8535es+4}C%IlE+@ z+%5(bxSuv*VLA_xo$r|`j9^o z&$pJMPcxd0Zx{w_%2uCLj*LLTJrdD!cj(?{y4+wMaB{D~e z!x8t392Y*xyBR#OQ>iRIpDqNc6V366lQqaw^PhL(@Ya$QkU_XDACNdDI0u!1tExwP! zWO4d95})EAEt`|WTFy|6cT{l@7Z5tys;*b-%BB^g9bqCG92CU}F7bfyX~^ ze|-e_&+e3qW!gx(1?}uaCHbLe(zJb_X!}YMNiMh6 zeTa;R9MMPo_-HRF>0H6kF!Ok>D$HJYVw|lxlm>~%Q(1*?Z7l<*NnDdAyLCGbLh6tm znwtq2LpL+HhvpRk-*2G&F+kg`%r0BRew!+qM zYu#@t_lBA3$Tf4v9002W!JC^_Tw7ziWz+MPg}YewUG@zEhW_qhUO}fj4R3T5k1-_- zKw@og6loK?Ra2zUijTF<6jBrj7yPmEp)ly0v}!5q7lqH_b?37v!qIe8{8kMti!V9~%nMCC{N^*>x+4;9iI7le zYwKctKV)T}@NoPQ`o##T=yYs+xDj9ScJc=(^1TSTR!+m4#UDZv)^dx(0hcG_u6*FfrChFtvKY#L6m07lm&yCgs*6 z>3tq=zd247b7L+&{z!?|C92eOp!VA$5HRmSJBrgSC4F%AKK4U2WVi=T+oNiSCn#`N z3YeU;=LkBHn$YbVQPupjG{~d1U@)HO3j_C~y)a0SVb_~#7t zC?xN1A!yz;O=s@hfr}++&j?}_ic-NhdBHwg{)scw9D4D3zO|9+8OXKR( zvA`7>hd8?3XL&D&>4FGKE6DRzjh0Ug@jnpF9Cs_&si|;wK88elW z*86dnB_jG_aVsv86awzW#{P@MD1#VKRG(!hRO_MdOb#GBDn|W~qm5`M@I6tq+_>0- zBSK?&$jTgXx7Y1?EO{* zhwN9M$c8x?As3+u9o(}IKo)96Gv?20Wks>>7ZjmdLivN65k08tc9Jn;2w?Yw_eLah z8#>!)4U)PM#vncSg}xC<+2Pkr(_Tk9B-Ks-WN&G4k2Nw1^-gJ|iYBT_;xU~Hu38R- zv#@ZnhvXz3*4oRP`cfFBIOLPc342E~Cgt=zsTxZ`{JY-aEMJp(fCS>j{px)b@{~r> zb&6L+>o2CJ_le}!soCVs$-E?%iG^?*gDhD-L+l|EyBptVSHt=mP-rTiALqxQLNJn8 zw}=(>IEFaVn8($xeihFAeEh9V5ukgEYkx&mqZCz(><_gaRa5eyyBXf48_SQ7mp!!* z2RAJB42hol3&x|bc%Tn7oV+f*ifWyt^bGjB7hKY21fk6$!7^OOE^T|i8=OK*>-=eC zsP&$l*^ovzhM#A3O}$PuX8PLy^@QUrKa9d$AV(bSy*JjHmxX`?G;e;aQ`|pB-F)A8 zsKP&^B%sk_eIoqH7>ai4-Y064aOwH=C*yzbs}zDj!9AohMx3L7`12$(Q|P;nMpM|L zih_ClyW6HD7fscHZT*rmH^kq>w4jO;2#7J_EuZ__4 z%$4WxZ8&+n^{Y~4__*?Un?H=O?N^}Z&%luMF>JW?Ee~jX`yFgY>-!tQHu3F{sfw0a z-I1qS4jmm_DrbFWYI}6%FW_p0U(Fjl44|E|uBMlShXp#)JirrrncnK3IhLCax1)@V zx|h!*ySi2$%DR|h#`XuwpF-qfI9xoO}((_{m8z*s9bXJX}w1!y7+ho zumhbYx8=JL7k1fO>bqN=M5tq3aiXl!D8g1DEVui*)mF@~ds+@W8F&XzqY8yKyU+mb z=apN@>k+O*C_1T;8Dg5{W1WzFvUG=Hu#_*RF5{VxyRYmd;fWP#JbD{Woa)6KHW%h{OAutt4z=lL+ZH)1<3 z+ILnM9-R#)uf;XPLsx4edG&Z`E7^>A=X>=NEC?Q$fW-(BTC(v=5sk3KivI=Ye`2S@ z^AYYe`i7bN>w}%{xmob!XsYtU`7n0C(U{;Nc5Bz8B=AS*?_YKAVp1SjKS3$5)o*wr zgCfLU;3sqe$2Yk48HZb%YITmS-ODaZ&pIHUtIPTc=z0<}j1*7OC^h}yFmUJ|jWsPV z@ZSEQ`1%==_c${*Qiu`J%At2FTx*i5@a~M``(tyKhv(wndN?I(?mQ#DB7UP z0J(T#>Z$Yjp27;*0KP;}wAP1mt((s+Lq@?4bTv7UA)*t}{( z4*rFR{Btj)cPc3V#xTf3emV@&SIgtfCtbVBQ&UAq{VLK1hclksE!!YGL$}kd9!Cdl zA&|}^9|7l$KRV@NG!>9M@9o9ws}Zi|X6q7Bvjuf#zQ{3j!G2;}wYjEPg*@&nGWB22 z@`MXsj7YR-WIkb4a82o%86Bh_>+h&X6ob*a`FiBnGC!yy7LqvwiP^6YiaAloR{Yd)Pc{SFpc(SrRPxp}@V zX@(jJn;ZuVo#HF?Rx@j{Dy&Qe)ycxV;PcShR&@l_w$qv^q>5zp@DK@UEVtCWn~zFu zFi%_?i?2ZnBdt9Pg|Q}87wsimMImnI7}wxaeR-&;Ey?1f+ML7j58d0(qaBhCLJS0C zQGjifN*doptwr`vX|3|LSHFDTeh)Y=wx&~i#V~P=RUXLSOdp8;0qJ*{peeQzhcwbB z>tt3O)mFB*Z<3N&;fdGuMW90}9cFZ`ShNCTGV2t!d0&zKy0v)F;NbY8j#yz@mBa;y zDLg-ynaQq-ZT`2!L&7*JV8DNtAW0h&dlI_zn!?Q!A68pEY!c!e9y83|WDQ2k5jN%W zictLX{xfOXpJ}*(pb2&J*G>!rbDJ)>_Uu9xe(7Q+{%U=c0qcs@*c8YEXssj+)$WE( z@mr@Fmxvo@o#!w)Rna=*93Yrbs&!t=w%@fsrrHNITl*$B#AF;2=od68yCTMPx`cvwOdkra3^Q6#LAIDI ztXUHqv5^)=j*wtNE%=KDK0Aure20dVUO^R5AqG0)+zKbzLCj$lXc|eaXCtTHu)jvS zM9_)mx5sj9>m{<_OI-*ZXt^qu-^B8bgJ~1LKM%=i$&-}iv=>K^I?@H$$mA;*d#q26 z^}beXi#7gifoue}@5xD3x_a*1V3gYw>HEbGtt;qAeb$Eq)4vyO^MV~#TQ(@b>Q99^ zOe6Zq5Ek*TQ$DFFEK#44_|R_CL{Es;ng@Z`0ms5h50ju~I@uqKYG_g$Civm+9}k;P zg0H#tbqa4wA1zpeQZch*6wUE#rX_NngSIx@xLf&)m*d#EoxK`0GsL+nQ=%c)+F2jY z)B|?c6(|Q&nHR>(<4{fER!cTn)>`4|6roeFA^?KuQ|@nt>atOMxXUC zboYby854q^f!i6Sur6?BInieXtXEK`JlYHCEdrM2zrfm&mx^Q9ov=~8T$~?=vrD;a zbZ7<=Ig6KtpI@uPw@v1RqD?poOak|a+fS8E!H6yWa%=zcOXyi94lf!lq)7axA38`7 zFl_Cffojf7?VY}aZVL!5Ptf);)t`Sw(%K>_?-uLz=827uPx(0Jh2xI9e({2Ip?E0i zC>o`cx$Lq1q-%%6MzBiH9$|vW%Z9 zNb!?wBYh5+=GivR73A=baNR&1MUTyhOVfnu-QcFch>rBDO1wo8i{#<9>Q1YKVI4Fe zI-CArk?Dt?B$d&*LiWxsA+wXbm&ej9j3iPRPZ&qwX@0%rjY2c6!o`P)a#yL;_0T|M zjUkAP2`-{h3vNH_r^vBIBVqQtyZH6~zx~qlbu(nq2l8MnE^3ls6ej1s1mWA#cnW#m z38B0tt^OvIU%UfHY^6NB+*ETKq2^Xk6YMtMr!X?J0`qoRo{EVjHmeOP^2p$@(^x2?m8A8!9k+I5P4!y$6Fm6T(+ z1sTplqvdsfJ&&taOcnBY<~#vCsDYSNujRQtYCtfwSx5N0^(QcHunjtt*DCG^IIGY9 zG*9O#{5M4aqXnA!M2mcUPTwS({%Y5b*6|uyjSmJ5{@v4)jMBg%z0D;)#VOrPty}!q*49rltF5aaObY_41O#Fww(PW#( zYgm^VZuPqwITz(;KN#%@)9feC8&*J7^E>^7xseo?Ut+_MK()o@%)~0G zFEaq{sddW-J$afVA82Q^m+9mz8|eUU2|Ns5GFKX)%g!~JaurFGyQ#347+!faO?<#n z!#Z$Yf9~j{p9HFAOA0q=?9Oqd;0ab68Tx66e-;H>#f8ggz(vTy9a^i7<_6tewC6l2 zG|ZriAR`?IHp8@{Bm={bK}|vL59TOs`Lir5*)yv;6m^N3RpH>J*jn7vIqrH8Dh9Ga{*WOcA2i!tYV_N(U&+)OjfZ9~Z&xt3(@ zBuULil9)k)|5G9eMOisBDxAeHJJF;^iX^mR@{imZV)4Ibgt-~)mH};Y z9kLL;YS28|(p2?6c1}yFs!ee)g*>Bjs2!cK;mlBrv-er%dZctVShZ>@!AHV=8~Pkc zmb4wbeVldZ08Mq(9cnLW=a)Xzx*lP68{PI4l39O{dVhlt=XQ>>=br19bHn=ee=Gof zePIpxw8am=?NKb2-}5yvTERc;vvYw*oDYJwATh)iWe4iJTPyz8ut0^lJ8BjH+MWF@ zv5cO0ozTY>%{4b7&O6t_vgbyZgVoK^x*0~UobLtjGM3`{{22rVt2$>|qU6~#fWy^p zrH+#~R}1Bo(ejV!ZpQoE(M=cydm43Eq&inapMhEO$mjLeL2atofZ_bV0JO52HpZ$#?EGI~=(htnKc_ zE^Apn5$^61nyFfk+deikeV9=HfzHUH)Fb84(qTgPk$XRGg>(Nh5ZR=uZ}}xT)*5{ic;}y zS?T7%l%3Ym+~St{J}{9_x0nwCzz<)#Y}Ub;z&*Y&8mnu0b&YiTcz4lgRyM<#Sis@e zgKU=61vCt~#<302*@`Nu2&p-RYmEHZD~|3(jsDXEY(MDMV!97rRGTO{3EUAK3XXj3 zD^csi8Vz1U_cIDS>r)j7UMCJrn7^?lIau4HAZN&H~POgEIaMplv_XEH6>qt6uJL5 zQd@L^6#~neM{KNow8Zz0?E?niJPVRSi*m2?>x3nmulx;tmojeJK}WC*F@N%VOG7_r zfP~XjfzRC+!+QT1PwFSA0f$r9QJ*DrxbW|P^{w~M|8`puKZPq86T7byk+@5q`)1g` zKMH%@H(Xw|XWN5?HX|e=&a?R*=PjKQL!0{-`eI!+&QD#v1zlTi-hWigf-lOEgr$1BnI^%~Q;!EE=%( zvt=!`vm>q58o6-;L^A6K+_rVqvF;cr}MwFf+==MH9A3e>k>wN%TsHLPB(GLXPM)L*v z+5xX6gQ?-M<*goPv>ehRX>Q2)HFbh3fIOo%{0YB$87wYHzf`;9?+ zNKNN+8etw+R@XJ@eKBS=mA_|aHYV(pKji4P87oPwDMgRktW-|dyV!+w= zz$pn?R7y=7Wb+TvcUd+Y7~a3D{ufYwQbjO1As*5Pycl;^ak~67FcqVDK8ZgS9<%e) zv$e3{GYXX~irysh22UEV+WB7qz)tw4nWK-S+|R$cY%o0cRdl~j^|;5SDxvSVT?Wkw z`4*i|?6s9?N|O0V<0+Pq7J!eCj{FRZ3$i1`EI9#m@`8p?O`tlHxD^@wL*tx zf2j4Ue-aS5S*y@#U6t(X;a&2ZXu%m9pTAJK`aAt(~uJl#)6T&9% zdBShfL0aIgV)G!w44|c3l6#HAp#bF57;q_t@XHBQ;0Ilsz>-_btlV{TmseczwZRfWjZ#hg7 ze6G-8afl`Q?4<-qmitNj8A#Y*K$4|oXv(52tnXxn9bfh4?H$U816}PkW<9eqjx#HB zjhb_Q=tz}(=JbY%aB32_VB7ROY}k*zO8z)#6N&>0*a-}>6I?vBKB$VQ(g^SzpMH+v zZ;~dl%1fzA+~6`bW3L958+BAv_h)~3u-Y?VPbHRW3az(3dzaN&(4$k$N*vFYR;=R2 z9zDcq4skSWvB^M6^{-^BNvPa-jm%Qg)Yf3|{yRC!Z?roH{3azRBl^MqRSHSkMim`{ zu95*OjL^U!d2lEVDpjmHrP_EXmrEjkMw$TImJPey=4+D0I~E+4NDggj7DWX8(9$^@ zV9xC-B&-iYeY8rRQgzIEfg1TMed8LaBa_Ha802$QNU<-zmo($|Z$pRm#rK2XAnmeQ zc_rphB~^BudEbcp428kf1Onz9cfieBa}kH-QN%b(>O^1`9^`kG*5-FxZ`BM^kNbXH zQDh$~#F0BG(0DY&q{=DpaOF^iB$p?IGm%WPIIH(tT(2MuZuk0*=yXBbnG0X!l(sZJ zWW!XG!&C__#;Shd`8O)54imJdP=h7UnA#cY-}3c`gqeNHZT}dJ(^gT~b%JoZuTjOW;%bUfu! zvEyuJp{c6+ZI}u;KR*9=me9~>;CA2rgwL3A%`&MwVqm@>%?;gy`dmxCTm+zS4lJez z4d_9DPsVwVH#6k|R)ZfJHRr%!kcRDAxzWqtpMJ|Y-yLZJOaU1cs zy7SuLXtqs8>XF=CPJHzFHbQa%P=j9WU#IlD1~r~G2@A?_!*R42t$kG!`~;vVd%B1x z)zP@zd-2)4a(4^-`Ls6?(j>Wb#%zq!eSR8vpdfG~gpDXn6tK(+KCh9xrY||w-_&D_ z3pj9lc$$Eg)J4=*SH%8YSsdp3h<7a4Y2)W&E#$_gILMzD@oy&-vd2zrZ zK}0j6L|;f@?A9~G8&32|=hP3aXK&=$;Q#Mk{I3XXGD5+fk~}v$T_rPArDG%buW3H! z0B+YTEw`q8h5ffHS}-S+dLl0CHqaq2b}{%D2L8v20bi+CG#D+E5T0{MUYhet;AO7V z$$Ym20Ha!+4zVxCcT?#XV+Z!pSL<+#Qxtb|{Btg6ngUg`rl#1YiA9f4**S3rfNb=% zt(SCC(4>CX8xY4EFe4SpBiY%+>{ds^D&r=aZcNiHnetK=f56@J>-x~s=OP`x=oDvM z|I04XX;RfG2CI*e?k;MF>S(JhIc%-es5+G;x!3DpPh;!Xr4BLeKF$K1GP;XOG`c}m z<&BLSX{+w7+lKE)W}|xEXVsgRqzqc`86nq9mvSRGTO6c6DeKddo%F%}oGgh2GbT<*= zOlo|oT`GlGuLfHylW;e|T-uj|YiI(@a=oGUC`0|ieYA7Le72*W!!lAcEuo6Z$t+*% z;nZHkZ#A<=y5AiEL=jsA@Y#|!enna|z zheZzgO5*<3(l+I1$>2`{2B$xd=+(k4+j3tcH4+2D!G!9G3Fq9%IWITuFJ4Nc2#{iy zyK+FP(OJrZ%gaskbq|8=exSjLrmmly+~USGgY!iWv4>ATLy7l?9W^gj%vK(5C8;*Z z;^)}0>u{xY1iPG!l+Z+(?4xn~1%04v31Rv5HMu8E5zAx)st7~UMjT8XRdq;Y+(l}5 z2JP`?Nm%}X9e6Fbd0opMsYM_f;@FyG8Pez_#GnsW{qir)C}0i08=o|idC>aQPXB8v z&TWvC&PSKdr&tz&8@erzoZX<)bqrNi+o288%85AZb$uq?g~8Z>YvttwM3K_%f4$49 zeh#6;#9i<^FE14u;pm3zqx?bc`pOYLRavRjf)}54nmP^5~xRQSl*L zNnRHq+2IA&kr_wooWan0E|q**%=N#ww>9_5MABZe+b}tgV((hNUb8;Kkh)Q7<&eMo z1;|s^k;X(5%yxHcIDBpY1QVMpeumtSpfPYiNwX9^jwiK?+^-$mK_`q z76=uUSj#~6UDYQChC8>pso+h)x+t`re5tcS~v(TArE^4w>%^A$KDCqcvGA{JUAd5X1Q7 zglM5FKhw%a$y^rwZ=;?6P04Jx*1V|-EEX>M0~DP8rTA51lKmJ@9uXJ8UsGU;{1WYNOIrd3K%#$zg)#?=l@VUYIxK1a z2Ua=SF2vFEi)%k9EA?ugF7G;SlNpdYUal5EzS-fz}>Y1$Mm`8i@^>t)+1qI@3CoPtjlxiULk+U z`6Ns7PNSA9SC^)z(+L4%dRUV@h!h zPv<5Ydw?{X10N1%7QyMWx%%Yj;Nr1_!{o75#xph;$}D$eWiYc>@^^=r6_l$@Oph_3 zn9(LUx(yO3w)EEubGgse{oFq+bi6kR72VK*)?RaX*fsbZK8VhR#1asZiZe>dYxNU% zj=DTFQje}c3e8R~ax^%*6aD-3@hnVVcRU54jiOVcr*Y0Z8qH(9fd4jHizxBR{hRWt zCASVyHqUGXeO;MmLK3c%I|&8K)q-u6>K#@iN`E;i>)Qbo0p%zQDXmbUPxzcf`Fa|g z*Cgy3M74)r{au-dZcxRm*JP8cv-}~?V(hwxLkB1ywJ@SQmbyqteNtcx4*FlJQ(%kE z(2M$hAgF;LzSST~heHhO&K;x-bKeS1y;rBSt|zU$m;S*~%*#82_k{Lhn0sz(g?%}u z(OJRArx_5;2CDechtsN4)`#WgMV!jqoB~gI=ZJD`4FE9Qkg=sLh&x?B_H7t))my(w z$(vVXD3h8qZ$zPMl(dtMCH6U>n{Q`&2Z?dNgR$I!OUwGB?;Xu^iMo0BDkj4sHjZVh z+DS;OjK0$Aw%xfq85dU!bC_;|!4(=eoqz5x_icL{<_#Br5DOTR44)bq7Rlk}@V9@Q z8G<&pgI3<81?*4)V-OkAhqr0EuTq=zGaBbAeFU$PfK3*&{>X#MAP?p%p2iHfd~2N8 zo!_sA_?guLYUlLR3dfsvs5`4Ps+(=c4Svg!w;YT$6vYbnJ9k~B3G5`P9nEN})^*Ac zLo4#X1~At&YJ(}cXM&FCU-DakCiMlw_xR*oK7VbPqS3!smrp*k8*?%%UpD5sOXuE? z6>5x;leg8NJIAtcf>OfW@LQ>mEf0p3td`s4j})ARf3ptu$G)@Tlg!%Lt})R6J3SwG zx~V<}WpOpt{Ed<}khacfESy%J^Wvx|uD|RLa2#{7#WP6U%n((HrZ21oeC}o!Mf#>k z>2?5W7U>h(D+l_KDbHAbbVe>_nKoE^VpNXyz4R`)Q;V)`5W`l#Vwb&#h2X>H+Z+GlX#?Q);Q{LDvi1xpSKc(9X^s5QZBXS1YE2u^&(rX8o4P6|^y-{o8rfZ+UPUFzuxHae2K1Kz9bJ^Wc zy&Dr;{Q{6yhJ*<`EP>6~I8J6n3NMUxQ7jRMv=L%Ht9}nh0ywNo5SRwbveCxM89(W% z5#}_`08A#p7Ov=|Dbf{`d&-|TN7B^`hbD$7+ck&hPSlqK-ntw5e}AFUo=-g|8HX?h zIvPJ`gbR8jwe3wG9dsFi#`-AcN8VtnvOM%3t}5@`YGjg}ZIgf_LmgXxjpzvyt=|SH zyyA~nfDpCWr<`(G;_T;oy2^cg_np>en2xhi0Q>uBHV zTwzegBDF9#i}GW*N=EAg8zEp`;yJCKwpQD}@e^zbQ|-NFJA(-$t2mHOrabv`F?Tgj z>1bSc9m5DJ{S857s##x~pjLb56nb6p*95Rte1f%gAZYWL`eZ^h!#SSy2A{B#h<7UeA$A(on2shW^rnFusnx5I7d zC5^S{!q}~V({2(U@@l;lhG#(kw}ly6v&Ym;t~=%&&|2&(d^>yNypNe3ADe(3_p8;a z;n%XFx+U?BX;L(!i9rq?CH#Tuv4~;*KvXpIeaPP^&(kWR!93|iF9wZ-<<&y;{U?6e z`^vAwcr0)zntWzx_GNP*Itrq1NEC?K$8bi7z(jy%lj)cx;Y1QGqgN7cEnJ-7RiTW9 z))G<0R&jy$y%fyIqGeyzKNL&c2ZPD0{0u@XfHt0p)dHdoGYgA71rygIoQwD&;(Z{Z zAtMa-94W%QZIKRpC%#)q-LSu`I4cYGgr4X8qMWEeUo)!#CG*kRT^srPYGj%agiXLi zp#kf%?fqf0ptZHz$9Gj)U9oXm9j)l)sqTC+^h5AcgZwGA2hXD3yS~|ZnT~nK%bWka z9k85x{;r<H3vH&7O~fZoTK>gAvD*57905a@;@>MUR+nGzO4og@Jyr!|MIdfk)#EDU4AUSH?}}tnr{vCi?w(V@24N$ErX5wm zpCO%Qw-pAjr+R4ub~౰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

S9RuFch?vpA?*m+r{F5DD*Ji?W##>Mz#Fiv0 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�Gl9%O>Zn!@cmpr~-Z(~vQDd%m@c~(RGp|7zx zAS`a)CdGccV%`heZ~SZG@0Ng~FSZ2)mS^oh%%AjWIO$2^Xc}y@EVKY<2YQ-Jzw$L} znXIk+zKE0~)*4a&(rAz{zYH1qP_z{|==Z=0)?B`$x$l*3;Jw;Fy|UI)wO(BB;gtxj zY*EpNe>;-WfPWuWO`zUlE`%y!snvX=zxeoLogKmJleCRXU+y#C+U3!y{cIgjmR|l) z?jt)Q?ZZ?2x3q>J%M!FKwdG{~Cy@5ou+0`1D}kx6TXLhfPpYOc7u+G|7~>+Lh-MM= z-QiI*KyZ87tUCKG^~Z#xji=~hp3ym8`%~|BK`Ngf*qjDWlpoy1A&J&rNM*-5K2;+b>o#HZUl%h6t% z^ASkG6@$?>k2?}W@aZ4=mMcQ9m-oa6TWI4tPa%XA9e!K9;DOez<;MyP!%)Bq6HxuK z#KI51q(e4@C(j9?nl5+=6}4K4RDjUItewNhS3mFhi_mHXCv8msd?YCj7pzV#%Adar zBx>&B|Ae>V^u-rG(mm_+EI#CSL$hz2FyOn6x(S@Pqbp?RX|5gF*XId{5hC$^cs$01 zs_a<)_pyi2n|WQ;=EE1cKtob*hJKI$XoP$|Vdzoz)_+>4C1J|4VGoehlvw!h9X{q* ze`noeTR1SGl(86F$DeZSGkwi)16X%kN`_r)wAZvpAjKlTZGVE}y*vr8N9V-Ei@4c> zc0*R8k7As)j5s1tXdYt}?K(nFc*nLNXUeObBsUqTzBjR}7TM4{;Xl7hOMmlwVutHa zb$^iqJ)njoZme)Iq_<}ht&-H4*W=?ti+Q@6?WLW-aHH@32a8cDL|KA9={%cV6pQ|5 zYTMQPGob&i$srrlKWwF7Y`#2%f9RluTv%|2fuFhikoaiiUsO`71IDG%j?|Kxd0q6N z{a=jIYr@LE-4O(0)U{O{ZUR`#E2fY9jGZ_dq6LrC{d*zw*SntAlj{F9S5e(ir!=7!4wSSR6A))eeT>(DeXCJ)&1o&A4OIFGUQtLqrOgJ;p zK&3)}I4OsVr6skml;v`#vz%I6JQkQLpqqr8SPh8~T~zcu`{A4`%g*_=Evs08!TgKR z2n!GBpKAzslL86Ft(?QXPCz9gbe(|ed1%*4vNR<+6-Gjurp@264`QTQo=M_B((2iV z@L89Lyv{#$VFB#x@KKHGYd;E5H#S!La!YSW`m7bszbMKIg0G`J=MINcQ^wT)9(6!$ zVNd?ly7_}t^DIa*_|G8Y;0*iV6=a&J{<+4*dDiC{jmh2I4Z6n)us1bX&v<7+4;lA> zfXJv4)sCAR=4I4Y|8yRT<@rg(a5a3d*EQA8wVBlW@~@d6-34Xs{`4nl3Hs}rp9ggG zVuSdmy=ydS77SSSd9YRUYSKX^tD!VbR45)j2uQ?fxlX1Dtg&lbv+@OpTy^99=diw7 zB;{ZHenRhY@u!#(c#{nYr5v6kX5rUC6Sj2*+ss%-F+$NiL@8-c`z#=Uj#g@|_-~5@ z@{#t?Ndt-8FlDB=1(Dw=#(1yEw^lfSy3pbLKV)2B+n@-IU25r#dp&E1^DGqLkGIQB z+d)FznY)Oy{dZe9cxr(#lr1{X1R+sb0H?Ta_Nyz>EZd01+qGR-DOblfqUB%#B*m>i zVRc;(6#zREJbpX-=&#~rv0Y00bFq&bKHk9!PXcNPUkIb1Q{=o)G@u_;%CV?&`_5OX=9n1dp9UELQv#iUd$s);f<^Eq9(?vm7C@#3 z*x*@$Kmt@GUD{A99GHR#e$Q3YG8SU6b&4_-5wLI!8G=qxj`|{Z{+SNFd57Ip_C`j*vhyEXA_Q!USdkBSCS30_{22JbfTU|d%I+J@Y?rsCZg3zksD0+v7cE&RQFZpTIQmJ5{*_QTb4cjZx6=+J z4as|a2oHDgOYxqzAHhqZdy9As30~QbEae#gI+d@`>J_EZ8_)i1^{<%t)uenzYrO&! z|7FYQ5+i-?;q$|99UmP&iZ(@T3(baPSjj)ZhiBB7bip9KD}cTjiDtb@{oE|7YrU|M zyKUnDH&uGS@u!b!=sxQ{X{r8mJqS7v_V;<-!$_!vDNJt8wQ%*U`k0U{KCIEop3Et$ z*;#-!TQoj2sJLikE~RQduS*XrY`mm6Pw`bz*T%qnlIqk;3`{H>&{K-jNsQw*N1+qb z_ddWi7^2fP2d#7daW4e8&q9zO=&9Q1<$$Gye@_d#N{5AWh7TFMc>MkPHcmB`{SSj) z2lV$CM-98?zHd8U{|KX82uA4Ju8Q{e1lRX~OLS#m^ymNf&+093@%mQtBmDn;>wi80 zfk3+0Y8n6Y!Ta;LLfJUPJi{CQjHhQ}lw2Jok@tf^G3U={ZN0GUIs A2LJ#7 literal 0 HcmV?d00001 diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 15523229..1471f9b8 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -100,6 +100,7 @@ const allProjects = [ "**/*.ssg.spec.ts", "**/*.page-context.spec.ts", "**/*.wealthreview.spec.ts", + "**/*.media.spec.ts", ], }, }, @@ -166,6 +167,7 @@ export default defineConfig({ actionTimeout: 15_000, navigationTimeout: 30_000, baseURL: "http://localhost:3000", + viewport: { width: 1280, height: 900 }, }, webServer: webServers, projects, diff --git a/e2e/tests/smoke.cms.spec.ts b/e2e/tests/smoke.cms.spec.ts index a970c070..4a935f95 100644 --- a/e2e/tests/smoke.cms.spec.ts +++ b/e2e/tests/smoke.cms.spec.ts @@ -1,5 +1,13 @@ import { expect, test } from "@playwright/test"; +// Ignore network/resource 404s from image thumbnail loading (local adapter +// serves uploads as static Next.js files; the preview may 404 in +// production-mode test runs). Only capture JS runtime errors. +function isRealConsoleError(text: string): boolean { + if (text.startsWith("Failed to load resource:")) return false; + return true; +} + const emptySelector = '[data-testid="empty-state"]'; const errorSelector = '[data-testid="error-placeholder"]'; @@ -452,7 +460,8 @@ test.describe("CMS Image Upload", () => { test("image upload field is rendered in product form", async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); }); await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); @@ -469,7 +478,8 @@ test.describe("CMS Image Upload", () => { test("can upload an image and see preview", async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); }); await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); @@ -491,11 +501,10 @@ test.describe("CMS Image Upload", () => { const imagePreview = page.locator('[data-testid="image-preview"]'); await expect(imagePreview).toBeVisible({ timeout: 10000 }); - // The preview should show the mock URL (placehold.co/400/png) from the uploadImage override - await expect(imagePreview).toHaveAttribute( - "src", - /placehold\.co|data:image/, - ); + // The preview should show a real URL from the media plugin upload endpoint + const previewSrc = await imagePreview.getAttribute("src"); + expect(previewSrc).toBeTruthy(); + expect(previewSrc).not.toBe(""); expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( [], @@ -505,7 +514,8 @@ test.describe("CMS Image Upload", () => { test("can remove uploaded image", async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); }); await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); @@ -542,7 +552,8 @@ test.describe("CMS Image Upload", () => { test("create product with image upload", async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); }); await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); @@ -591,7 +602,8 @@ test.describe("CMS Image Upload", () => { test("edit product preserves uploaded image", async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); }); // First create a product with an image @@ -647,118 +659,3 @@ test.describe("CMS Image Upload", () => { ); }); }); - -test.describe("CMS Custom Field Components", () => { - test("uses custom file field component from fieldComponents override", async ({ - page, - }) => { - const errors: string[] = []; - page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); - }); - - await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - - // All examples have a custom file field component with data-testid="custom-file-field" - const customFileField = page.locator('[data-testid="custom-file-field"]'); - await expect(customFileField).toBeVisible({ timeout: 5000 }); - - // Verify the custom component has the image upload input - const imageUploadInput = customFileField.locator( - '[data-testid="image-upload-input"]', - ); - await expect(imageUploadInput).toBeAttached(); - - expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( - [], - ); - }); - - test("custom file component can upload and preview image", async ({ - page, - }) => { - const errors: string[] = []; - page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); - }); - - await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - - // Find the custom file field - const customFileField = page.locator('[data-testid="custom-file-field"]'); - await expect(customFileField).toBeVisible({ timeout: 5000 }); - - // Upload an image using the custom component - const imageUploadInput = customFileField.locator( - '[data-testid="image-upload-input"]', - ); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); - - const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - await imageUploadInput.setInputFiles({ - name: "custom-upload.png", - mimeType: "image/png", - buffer: Buffer.from(testImageBase64, "base64"), - }); - - // Wait for preview to appear in the custom component - const imagePreview = customFileField.locator( - '[data-testid="image-preview"]', - ); - await expect(imagePreview).toBeVisible({ timeout: 10000 }); - - // Verify the mock URL is used (placehold.co from mockUploadFile) - await expect(imagePreview).toHaveAttribute("src", /placehold\.co/); - - expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( - [], - ); - }); - - test("custom file component can remove uploaded image", async ({ page }) => { - const errors: string[] = []; - page.on("console", (msg) => { - if (msg.type() === "error") errors.push(msg.text()); - }); - - await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - - const customFileField = page.locator('[data-testid="custom-file-field"]'); - await expect(customFileField).toBeVisible({ timeout: 5000 }); - - // Upload an image - const imageUploadInput = customFileField.locator( - '[data-testid="image-upload-input"]', - ); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); - - const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - await imageUploadInput.setInputFiles({ - name: "to-remove.png", - mimeType: "image/png", - buffer: Buffer.from(testImageBase64, "base64"), - }); - - // Wait for preview - const imagePreview = customFileField.locator( - '[data-testid="image-preview"]', - ); - await expect(imagePreview).toBeVisible({ timeout: 10000 }); - - // Click remove button - const removeButton = customFileField.locator( - '[data-testid="remove-image-button"]', - ); - await removeButton.click(); - - // Preview should be hidden, upload input should reappear - await expect(imagePreview).not.toBeVisible(); - await expect(imageUploadInput).toBeAttached(); - - expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( - [], - ); - }); -}); diff --git a/e2e/tests/smoke.media.spec.ts b/e2e/tests/smoke.media.spec.ts new file mode 100644 index 00000000..d35fa8ea --- /dev/null +++ b/e2e/tests/smoke.media.spec.ts @@ -0,0 +1,292 @@ +import { expect, test, type Page } from "@playwright/test"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +// Load the test image from fixtures once +const testImageBuffer = readFileSync( + resolve(__dirname, "../fixtures/test-image.png"), +); + +// Filter function for console errors: ignore network/resource 404s from image +// thumbnail loading (expected with local adapter in Next.js production mode). +// Only capture JS runtime errors. +function isRealConsoleError(text: string): boolean { + if (text.startsWith("Failed to load resource:")) return false; + return true; +} + +// Helper: open media picker popover in the page +async function openMediaPicker(page: Page) { + const triggerBtn = page.locator('[data-testid="open-media-picker"]').first(); + await expect(triggerBtn).toBeVisible({ timeout: 10000 }); + await triggerBtn.click(); + // Wait for the popover content to appear (Media Library header) + await expect(page.getByText("Media Library")).toBeVisible({ timeout: 5000 }); +} + +// Helper: upload a file inside the open MediaPicker (Upload tab) +async function uploadInMediaPicker(page: Page) { + // Switch to Upload tab + await page.getByRole("tab", { name: /upload/i }).click(); + + // Find the hidden file input inside the upload tab + const fileInput = page.locator('[data-testid="media-upload-input"]').first(); + await expect(fileInput).toBeAttached({ timeout: 5000 }); + await fileInput.setInputFiles({ + name: "test-image.png", + mimeType: "image/png", + buffer: testImageBuffer, + }); + + // Wait for upload to complete — a thumbnail should appear in the Browse tab + await page.getByRole("tab", { name: /browse/i }).click(); + // The uploaded asset should appear in the grid + await expect( + page.locator('[data-testid="media-asset-item"]').first(), + ).toBeVisible({ timeout: 15000 }); +} + +// Helper: select first asset and confirm +async function selectFirstAsset(page: Page) { + const firstAsset = page.locator('[data-testid="media-asset-item"]').first(); + await expect(firstAsset).toBeVisible({ timeout: 10000 }); + await firstAsset.click(); + // Click the Select button in the footer (targeted by testid to avoid ambiguity) + const selectBtn = page.locator('[data-testid="media-select-button"]'); + await expect(selectBtn).toBeVisible({ timeout: 3000 }); + await selectBtn.click(); + // Popover should close + await expect(page.getByText("Media Library")).not.toBeVisible({ + timeout: 5000, + }); +} + +test.describe("Media Plugin — direct upload via MediaPicker", () => { + test("MediaPicker trigger is visible on blog new post page", async ({ + page, + }) => { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); + }); + + await page.goto("/pages/blog/new", { waitUntil: "networkidle" }); + await expect(page.locator('[data-testid="new-post-page"]')).toBeVisible(); + + // The image picker trigger should be visible adjacent to the markdown editor + const trigger = page + .locator('[data-testid="image-picker-trigger"]') + .first(); + await expect(trigger).toBeVisible({ timeout: 10000 }); + + expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); + }); + + test("MediaPicker trigger is visible on CMS product form", async ({ + page, + }) => { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); + }); + + await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); + + // The image picker trigger should be visible inside the file upload field + const trigger = page + .locator('[data-testid="image-picker-trigger"]') + .first(); + await expect(trigger).toBeVisible({ timeout: 10000 }); + + expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); + }); + + test("can upload image via MediaPicker on CMS product form and save it", async ({ + page, + }) => { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); + }); + + const testRunId = Date.now().toString(36); + const productName = `Media Test ${testRunId}`; + + await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); + + // Fill required fields + await page.locator('input[name="name"]').fill(productName); + await page + .locator('textarea[name="description"]') + .fill("A product with media picker image"); + await page.locator('input[name="price"]').fill("49.99"); + + // Category select (required) + const categorySelect = page.locator('button[role="combobox"]').first(); + await categorySelect.click(); + await page.locator('[role="option"]').first().waitFor({ state: "visible" }); + await page.locator('[role="option"]').first().click(); + + // Open the MediaPicker from inside the file upload field + await openMediaPicker(page); + + // Upload an image via the Upload tab + await uploadInMediaPicker(page); + + // Select the uploaded asset + await selectFirstAsset(page); + + // After selection the image preview should appear + const imagePreview = page.locator('[data-testid="image-preview"]'); + await expect(imagePreview).toBeVisible({ timeout: 10000 }); + + // The preview src should be a real URL (from the local storage adapter), not a mock placeholder + const previewSrc = await imagePreview.getAttribute("src"); + expect(previewSrc).toBeTruthy(); + expect(previewSrc).not.toContain("placehold.co"); + + // Submit the form + await page.locator('button[type="submit"]').click(); + await page.waitForURL(/\/pages\/cms\/product$/, { timeout: 15000 }); + + expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); + }); + + test("can upload image via MediaPicker Upload tab on blog new post form", async ({ + page, + }) => { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); + }); + + await page.goto("/pages/blog/new", { waitUntil: "networkidle" }); + await expect(page.locator('[data-testid="new-post-page"]')).toBeVisible(); + + // Wait for markdown editor to load + await page.waitForSelector(".milkdown-custom", { state: "visible" }); + await page.waitForTimeout(500); + + // Open the MediaPicker (trigger is adjacent to editor) + await openMediaPicker(page); + + // Upload a new image + await uploadInMediaPicker(page); + + // Select it — this inserts the image URL into the editor + await selectFirstAsset(page); + + // The editor should now contain an image — verify via markdown content + // (Milkdown renders images as tags inside the contenteditable) + await page.waitForTimeout(500); + const editorImages = page.locator(".milkdown-custom [contenteditable] img"); + await expect(editorImages.first()).toBeVisible({ timeout: 10000 }); + + // The image src should be a real URL (not a placeholder) + const imgSrc = await editorImages.first().getAttribute("src"); + expect(imgSrc).toBeTruthy(); + expect(imgSrc).not.toContain("placehold.co"); + + expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); + }); + + test("can select previously uploaded image from Browse tab", async ({ + page, + }) => { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); + }); + + // Navigate to CMS product form + await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); + + // First upload an image via the MediaPicker UI so it appears in the Browse tab later + await openMediaPicker(page); + await uploadInMediaPicker(page); + // Close the picker without selecting (click Cancel) + await page + .getByRole("button", { name: /cancel/i }) + .last() + .click(); + await expect(page.getByText("Media Library")).not.toBeVisible({ + timeout: 5000, + }); + + // Fill required fields + const testRunId = Date.now().toString(36); + await page.locator('input[name="name"]').fill(`Browse Test ${testRunId}`); + await page + .locator('textarea[name="description"]') + .fill("Testing browse tab"); + await page.locator('input[name="price"]').fill("9.99"); + + const categorySelect = page.locator('button[role="combobox"]').first(); + await categorySelect.click(); + await page.locator('[role="option"]').first().waitFor({ state: "visible" }); + await page.locator('[role="option"]').first().click(); + + // Reopen MediaPicker — the previously uploaded asset should appear in Browse tab + await openMediaPicker(page); + + // The previously uploaded asset should be visible in the Browse grid + const assetItem = page.locator('[data-testid="media-asset-item"]').first(); + await expect(assetItem).toBeVisible({ timeout: 10000 }); + + // Select it + await selectFirstAsset(page); + + // Preview should appear + const imagePreview = page.locator('[data-testid="image-preview"]'); + await expect(imagePreview).toBeVisible({ timeout: 10000 }); + + const previewSrc = await imagePreview.getAttribute("src"); + expect(previewSrc).toBeTruthy(); + expect(previewSrc).not.toContain("placehold.co"); + + expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); + }); + + test("can paste a URL via MediaPicker URL tab on CMS form", async ({ + page, + }) => { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error" && isRealConsoleError(msg.text())) + errors.push(msg.text()); + }); + + const testUrl = "https://placehold.co/200/png"; + + await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); + + // Open MediaPicker + await openMediaPicker(page); + + // Switch to URL tab + await page.getByRole("tab", { name: /url/i }).click(); + + // Fill in the URL input + const urlInput = page.locator('[data-testid="media-url-input"]'); + await expect(urlInput).toBeVisible({ timeout: 5000 }); + await urlInput.fill(testUrl); + + // Confirm + await page.getByRole("button", { name: /use url/i }).click(); + + // Popover closes and preview is shown + await expect(page.getByText("Media Library")).not.toBeVisible({ + timeout: 5000, + }); + const imagePreview = page.locator('[data-testid="image-preview"]'); + await expect(imagePreview).toBeVisible({ timeout: 5000 }); + await expect(imagePreview).toHaveAttribute("src", testUrl); + + expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); + }); +}); diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore index 5ef6a520..73b1cc70 100644 --- a/examples/nextjs/.gitignore +++ b/examples/nextjs/.gitignore @@ -23,6 +23,7 @@ # misc .DS_Store *.pem +/public/uploads/** # debug npm-debug.log* diff --git a/examples/nextjs/app/cms-example/page.tsx b/examples/nextjs/app/cms-example/page.tsx index 264a8379..66b27a90 100644 --- a/examples/nextjs/app/cms-example/page.tsx +++ b/examples/nextjs/app/cms-example/page.tsx @@ -13,10 +13,10 @@ import { getOrCreateQueryClient } from "@/lib/query-client" import type { CMSTypes } from "@/lib/cms-schemas" // Get base URL - works on both server and client -const getBaseURL = () => - typeof window !== 'undefined' - ? (process.env.NEXT_PUBLIC_BASE_URL || window.location.origin) - : (process.env.BASE_URL || "http://localhost:3000") +const getBaseURL = () => + typeof window !== 'undefined' + ? (process.env.NEXT_PUBLIC_BASE_URL || window.location.origin) + : (process.env.BASE_URL || "http://localhost:3000") // Mock file upload function async function mockUploadFile(file: File): Promise { @@ -29,7 +29,7 @@ async function mockUploadFile(file: File): Promise { // Shared Next.js Image wrapper function NextImageWrapper(props: React.ImgHTMLAttributes) { const { alt = "", src = "", width, height, ...rest } = props - + if (!width || !height) { return ( @@ -43,7 +43,7 @@ function NextImageWrapper(props: React.ImgHTMLAttributes) { ) } - + return ( {alt}("product", { limit: PAGE_SIZE }) if (typesLoading || itemsLoading) { @@ -110,9 +110,9 @@ function CMSExampleContent() { ) : (

{contentTypes.map((type) => ( -
{type.name} @@ -138,30 +138,43 @@ function CMSExampleContent() { <>
{items.map((item) => ( -
-
- {/* No more type guards needed - parsedData is fully typed! */} -

- {item.parsedData.name} -

-

- Slug: {item.slug} -

- {item.parsedData.description && ( -

- {item.parsedData.description} +

+
+ {item.parsedData.name} +
+ +
+ {/* No more type guards needed - parsedData is fully typed! */} +

+ {item.parsedData.name} +

+

+ Slug: {item.slug}

- )} - {item.parsedData.featured && ( - - Featured - - )} + {item.parsedData.description && ( +

+ {item.parsedData.description} +

+ )} + {item.parsedData.featured && ( + + Featured + + )} +
diff --git a/examples/nextjs/app/pages/layout.tsx b/examples/nextjs/app/pages/layout.tsx index c264a844..ddcc578e 100644 --- a/examples/nextjs/app/pages/layout.tsx +++ b/examples/nextjs/app/pages/layout.tsx @@ -18,35 +18,26 @@ import { defaultComponentRegistry } from "@btst/stack/plugins/ui-builder/client" import type { KanbanPluginOverrides } from "@btst/stack/plugins/kanban/client" import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" import { CommentThread } from "@btst/stack/plugins/comments/client/components" +import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" +import { MediaPicker, ImageInputField, uploadMediaFile } from "@btst/stack/plugins/media/client/components" import { resolveUser, searchUsers } from "@/lib/mock-users" +import { Button } from "@/components/ui/button" // Get base URL - works on both server and client // On server: uses process.env.BASE_URL // On client: uses NEXT_PUBLIC_BASE_URL or falls back to window.location.origin (which will be correct) -const getBaseURL = () => - typeof window !== 'undefined' - ? (process.env.NEXT_PUBLIC_BASE_URL || window.location.origin) - : (process.env.BASE_URL || "http://localhost:3000") +const getBaseURL = () => + typeof window !== 'undefined' + ? (process.env.NEXT_PUBLIC_BASE_URL || window.location.origin) + : (process.env.BASE_URL || "http://localhost:3000") -// Mock file upload URLs -const MOCK_IMAGE_URL = "https://placehold.co/400/png" -const MOCK_FILE_URL = "https://example-files.online-convert.com/document/txt/example.txt" -// Mock file upload function that returns appropriate URL based on file type -async function mockUploadFile(file: File): Promise { - console.log("uploadFile", file.name, file.type) - // Return image placeholder for images, txt file URL for other file types - if (file.type.startsWith("image/")) { - return MOCK_IMAGE_URL - } - return MOCK_FILE_URL -} // Shared Next.js Image wrapper for plugins // Handles both cases: with explicit dimensions or using fill mode function NextImageWrapper(props: React.ImgHTMLAttributes) { const { alt = "", src = "", width, height, ...rest } = props - + // Use fill mode if width or height are not provided if (!width || !height) { return ( @@ -61,7 +52,7 @@ function NextImageWrapper(props: React.ImgHTMLAttributes) { ) } - + return ( {alt} getOrCreateQueryClient()) const baseURL = getBaseURL() + const uploadImage = React.useCallback( + async (file: File) => { + const asset = await uploadMediaFile(file, baseURL) + return asset.url; + }, [baseURL]); + return ( @@ -112,7 +110,9 @@ export default function ExampleLayout({ apiBasePath: "/api/data", navigate: (path) => router.push(path), refresh: () => router.refresh(), - uploadImage: mockUploadFile, + uploadImage, + imagePicker: ImagePicker, + imageInputField: ImageInputField, Image: NextImageWrapper, // Wire comments into the bottom of each blog post postBottomSlot: (post) => ( @@ -129,29 +129,29 @@ export default function ExampleLayout({ ), // Lifecycle Hooks - called during route rendering onRouteRender: async (routeName, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onRouteRender: Route rendered:`, routeName, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onRouteRender: Route rendered:`, routeName, context.path); }, onRouteError: async (routeName, error, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onRouteError: Route error:`, routeName, error.message, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onRouteError: Route error:`, routeName, error.message, context.path); }, onBeforePostsPageRendered: (context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforePostsPageRendered: checking access for`, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforePostsPageRendered: checking access for`, context.path); return true; }, onBeforeDraftsPageRendered: (context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeDraftsPageRendered: checking auth for`, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeDraftsPageRendered: checking auth for`, context.path); return true; }, onBeforeNewPostPageRendered: (context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeNewPostPageRendered: checking permissions for`, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeNewPostPageRendered: checking permissions for`, context.path); return true; }, onBeforeEditPostPageRendered: (slug, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeEditPostPageRendered: checking permissions for`, slug, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeEditPostPageRendered: checking permissions for`, slug, context.path); return true; }, onBeforePostPageRendered: (slug, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforePostPageRendered: checking access for`, slug, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforePostPageRendered: checking access for`, slug, context.path); return true; }, }, @@ -161,7 +161,7 @@ export default function ExampleLayout({ apiBasePath: "/api/data", navigate: (path) => router.push(path), refresh: () => router.refresh(), - uploadFile: mockUploadFile, + uploadFile: uploadImage, Link: ({ href, ...props }) => , Image: NextImageWrapper, chatSuggestions: [ @@ -173,10 +173,10 @@ export default function ExampleLayout({ ], // Lifecycle hooks onRouteRender: async (routeName, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] AI Chat route:`, routeName, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] AI Chat route:`, routeName, context.path); }, onRouteError: async (routeName, error, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] AI Chat error:`, routeName, error.message); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] AI Chat error:`, routeName, error.message); }, }, cms: { @@ -184,71 +184,17 @@ export default function ExampleLayout({ apiBasePath: "/api/data", navigate: (path) => router.push(path), refresh: () => router.refresh(), - uploadImage: mockUploadFile, + uploadImage, + imagePicker: ImagePicker, + imageInputField: ImageInputField, Link: ({ href, ...props }) => , Image: NextImageWrapper, - // Custom field components for CMS forms - // These override the default auto-form field types - fieldComponents: { - // Override "file" to use uploadImage from context - file: ({ field, label, isRequired, fieldConfigItem, fieldProps }) => { - const [preview, setPreview] = React.useState(field.value || null); - const [uploading, setUploading] = React.useState(false); - // Sync preview with field.value when it changes (e.g., when editing an existing item) - React.useEffect(() => { - const normalizedValue = field.value || null; - if (normalizedValue !== preview) { - setPreview(normalizedValue); - } - }, [field.value, preview]); - return ( -
- - {!preview ? ( - { - const file = e.target.files?.[0]; - if (file) { - setUploading(true); - try { - const url = await mockUploadFile(file); - setPreview(url); - field.onChange(url); - } finally { - setUploading(false); - } - } - }} - className="block w-full text-sm" - /> - ) : ( -
- Preview - -
- )} - {fieldConfigItem?.description && ( -

{String(fieldConfigItem.description)}

- )} -
- ); - }, - }, // Lifecycle hooks onRouteRender: async (routeName, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] CMS route:`, routeName, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] CMS route:`, routeName, context.path); }, onRouteError: async (routeName, error, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] CMS error:`, routeName, error.message); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] CMS error:`, routeName, error.message); }, }, "form-builder": { @@ -259,10 +205,10 @@ export default function ExampleLayout({ Link: ({ href, ...props }) => , // Lifecycle hooks onRouteRender: async (routeName, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] Form Builder route:`, routeName, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] Form Builder route:`, routeName, context.path); }, onRouteError: async (routeName, error, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] Form Builder error:`, routeName, error.message); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] Form Builder error:`, routeName, error.message); }, }, "ui-builder": { @@ -279,6 +225,9 @@ export default function ExampleLayout({ navigate: (path) => router.push(path), refresh: () => router.refresh(), Link: ({ href, ...props }) => , + + uploadImage, + imagePicker: ImagePicker, // User resolution for assignees resolveUser, searchUsers, @@ -296,17 +245,17 @@ export default function ExampleLayout({ ), // Lifecycle hooks onRouteRender: async (routeName, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] Kanban route:`, routeName, context.path); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] Kanban route:`, routeName, context.path); }, onRouteError: async (routeName, error, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] Kanban error:`, routeName, error.message); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] Kanban error:`, routeName, error.message); }, onBeforeBoardsPageRendered: (context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeBoardsPageRendered`); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeBoardsPageRendered`); return true; }, onBeforeBoardPageRendered: (boardId, context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeBoardPageRendered:`, boardId); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeBoardPageRendered:`, boardId); return true; }, }, @@ -317,17 +266,26 @@ export default function ExampleLayout({ currentUserId: "olliethedev", defaultCommentPageSize: 5, resourceLinks: { - "blog-post": (slug) => `/pages/blog/${slug}`, + "blog-post": (slug) => `/pages/blog/${ slug }`, }, onBeforeModerationPageRendered: (context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeModerationPageRendered`); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeModerationPageRendered`); return true; // In production: check admin role }, onBeforeUserCommentsPageRendered: (context) => { - console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeUserCommentsPageRendered`); + console.log(`[${ context.isSSR ? 'SSR' : 'CSR' }] onBeforeUserCommentsPageRendered`); return true; // In production: check authenticated session }, - } + }, + media: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + queryClient, + uploadMode: "direct", + navigate: (path) => router.push(path), + Link: ({ href, ...props }) => , + Image: NextImageWrapper, + }, }} > {children} @@ -349,3 +307,20 @@ export default function ExampleLayout({ ) } +const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { + return ( + + Browse Media + + } + accept={["image/*"]} + onSelect={(assets) => onSelect(assets[0].url)} + /> + ) +} diff --git a/examples/nextjs/lib/stack-client.tsx b/examples/nextjs/lib/stack-client.tsx index 5dcccd8c..6db093a8 100644 --- a/examples/nextjs/lib/stack-client.tsx +++ b/examples/nextjs/lib/stack-client.tsx @@ -8,6 +8,7 @@ import { uiBuilderClientPlugin, defaultComponentRegistry } from "@btst/stack/plu import { routeDocsClientPlugin } from "@btst/stack/plugins/route-docs/client" import { kanbanClientPlugin } from "@btst/stack/plugins/kanban/client" import { commentsClientPlugin } from "@btst/stack/plugins/comments/client" +import { mediaClientPlugin } from "@btst/stack/plugins/media/client" import { QueryClient } from "@tanstack/react-query" // Get base URL function - works on both server and client @@ -185,6 +186,15 @@ export const getStackClient = ( queryClient: queryClient, headers: options?.headers, }), + // Media plugin — registers the /media library route + media: mediaClientPlugin({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + siteBaseURL: baseURL, + siteBasePath: "/pages", + queryClient: queryClient, + headers: options?.headers, + }), } }) } diff --git a/examples/nextjs/lib/stack.ts b/examples/nextjs/lib/stack.ts index 21047ea7..c673be29 100644 --- a/examples/nextjs/lib/stack.ts +++ b/examples/nextjs/lib/stack.ts @@ -9,6 +9,7 @@ import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api" import { openApiBackendPlugin } from "@btst/stack/plugins/open-api/api" import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api" import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api" +import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api" import { UI_BUILDER_CONTENT_TYPE } from "@btst/stack/plugins/ui-builder" import { openai } from "@ai-sdk/openai" import { tool } from "ai" @@ -440,6 +441,12 @@ Keep all responses concise. Do not discuss the technology stack or internal tool revalidatePath("/pages/ssg-kanban", "page"); }, }), + // Media plugin for asset management + media: mediaBackendPlugin({ + storageAdapter: localAdapter(), + // Allow external URLs for testing (e.g. placehold.co used by e2e smoke tests) + allowedUrlPrefixes: ["https://placehold.co"], + }), }, adapter: (db) => createMemoryAdapter(db)({}) }) diff --git a/examples/react-router/app/lib/stack-client.tsx b/examples/react-router/app/lib/stack-client.tsx index 12ede79c..2aa9647f 100644 --- a/examples/react-router/app/lib/stack-client.tsx +++ b/examples/react-router/app/lib/stack-client.tsx @@ -7,6 +7,7 @@ import { commentsClientPlugin } from "@btst/stack/plugins/comments/client" import { uiBuilderClientPlugin, defaultComponentRegistry } from "@btst/stack/plugins/ui-builder/client" import { routeDocsClientPlugin } from "@btst/stack/plugins/route-docs/client" import { kanbanClientPlugin } from "@btst/stack/plugins/kanban/client" +import { mediaClientPlugin } from "@btst/stack/plugins/media/client" import { QueryClient } from "@tanstack/react-query" // Get base URL function - works on both server and client @@ -142,6 +143,14 @@ export const getStackClient = (queryClient: QueryClient) => { siteBasePath: "/pages", queryClient: queryClient, }), + // Media plugin — registers the /media library route + media: mediaClientPlugin({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + siteBaseURL: baseURL, + siteBasePath: "/pages", + queryClient: queryClient, + }), } }) } diff --git a/examples/react-router/app/lib/stack.ts b/examples/react-router/app/lib/stack.ts index 17965fb6..441882a9 100644 --- a/examples/react-router/app/lib/stack.ts +++ b/examples/react-router/app/lib/stack.ts @@ -7,6 +7,7 @@ import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api" import { openApiBackendPlugin } from "@btst/stack/plugins/open-api/api" import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api" import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api" +import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api" import { UI_BUILDER_CONTENT_TYPE } from "@btst/stack/plugins/ui-builder" import { openai } from "@ai-sdk/openai" @@ -210,6 +211,10 @@ const { handler, dbSchema } = stack({ return ctx?.headers?.get?.("x-user-id") ?? null }, }), + // Media plugin for asset management + media: mediaBackendPlugin({ + storageAdapter: localAdapter(), + }), }, adapter: (db) => createMemoryAdapter(db)({}) }) diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index dc7ccb6a..91f11530 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -1,5 +1,5 @@ // app/routes/__root.tsx -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Outlet, Link, useNavigate } from "react-router"; import { StackProvider } from "@btst/stack/context" import type { BlogPluginOverrides } from "@btst/stack/plugins/blog/client" @@ -10,7 +10,11 @@ import type { FormBuilderPluginOverrides } from "@btst/stack/plugins/form-builde import type { KanbanPluginOverrides } from "@btst/stack/plugins/kanban/client" import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" import { CommentThread } from "@btst/stack/plugins/comments/client/components" +import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" +import { MediaPicker, ImageInputField, uploadMediaFile } from "@btst/stack/plugins/media/client/components" +import { Button } from "../../components/ui/button" import { resolveUser, searchUsers } from "../../lib/mock-users" +import { getOrCreateQueryClient } from "../../lib/query-client" // Get base URL function - works on both server and client // On server: uses process.env.BASE_URL @@ -20,19 +24,6 @@ const getBaseURL = () => ? (import.meta.env.VITE_BASE_URL || window.location.origin) : (process.env.BASE_URL || "http://localhost:5173") -// Mock file upload URLs -const MOCK_IMAGE_URL = "https://placehold.co/400/png" -const MOCK_FILE_URL = "https://example-files.online-convert.com/document/txt/example.txt" - -// Mock file upload function that returns appropriate URL based on file type -async function mockUploadFile(file: File): Promise { - console.log("uploadFile", file.name, file.type) - // Return image placeholder for images, txt file URL for other file types - if (file.type.startsWith("image/")) { - return MOCK_IMAGE_URL - } - return MOCK_FILE_URL -} // Define the shape of all plugin overrides type PluginOverrides = { @@ -42,6 +33,7 @@ async function mockUploadFile(file: File): Promise { "form-builder": FormBuilderPluginOverrides, kanban: KanbanPluginOverrides, comments: CommentsPluginOverrides, + media: MediaPluginOverrides, } export default function Layout() { @@ -49,6 +41,13 @@ export default function Layout() { const baseURL = getBaseURL() console.log("baseURL", baseURL) const navigate = useNavigate() + const [queryClient] = useState(() => getOrCreateQueryClient()) + + const uploadImage = async (file: File) => { + const asset = await uploadMediaFile(file, baseURL) + return asset.url + } + return ( @@ -58,7 +57,9 @@ export default function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => navigate(href), - uploadImage: mockUploadFile, + uploadImage, + imagePicker: ImagePicker, + imageInputField: ImageInputField, Link: ({ href, children, className, ...props }) => ( {children} @@ -110,7 +111,7 @@ export default function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => navigate(href), - uploadFile: mockUploadFile, + uploadFile: uploadImage, Link: ({ href, children, className, ...props }) => ( {children} @@ -124,68 +125,14 @@ export default function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => navigate(href), - uploadImage: mockUploadFile, + uploadImage, + imagePicker: ImagePicker, + imageInputField: ImageInputField, Link: ({ href, children, className, ...props }) => ( {children} ), - // Custom field components for CMS forms - // These override the default auto-form field types - fieldComponents: { - // Override "file" to use uploadImage from context - file: ({ field, label, isRequired, fieldConfigItem }) => { - const [preview, setPreview] = useState(field.value || null); - const [uploading, setUploading] = useState(false); - // Sync preview with field.value when it changes (e.g., when editing an existing item) - useEffect(() => { - const normalizedValue = field.value || null; - if (normalizedValue !== preview) { - setPreview(normalizedValue); - } - }, [field.value, preview]); - return ( -
- - {!preview ? ( - { - const file = e.target.files?.[0]; - if (file) { - setUploading(true); - try { - const url = await mockUploadFile(file); - setPreview(url); - field.onChange(url); - } finally { - setUploading(false); - } - } - }} - className="block w-full text-sm" - /> - ) : ( -
- Preview - -
- )} - {fieldConfigItem?.description && ( -

{String(fieldConfigItem.description)}

- )} -
- ); - }, - }, onRouteRender: async (routeName, context) => { console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] CMS route:`, routeName, context.path); }, @@ -215,6 +162,8 @@ export default function Layout() { {children} ), + uploadImage, + imagePicker: ImagePicker, // User resolution for assignees resolveUser, searchUsers, @@ -252,7 +201,19 @@ export default function Layout() { console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeUserCommentsPageRendered`); return true; // In production: check authenticated session }, - } + }, + media: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + queryClient, + uploadMode: "direct", + navigate: (href) => navigate(href), + Link: ({ href, children, className, ...props }) => ( + + {children} + + ), + }, }} > @@ -273,3 +234,22 @@ export default function Layout() { ); } + +const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { + return ( + + Browse Media + + } + accept={["image/*"]} + onSelect={(assets) => onSelect(assets[0].url)} + /> + ) +} diff --git a/examples/tanstack/src/lib/stack-client.tsx b/examples/tanstack/src/lib/stack-client.tsx index 043ce077..1af362f5 100644 --- a/examples/tanstack/src/lib/stack-client.tsx +++ b/examples/tanstack/src/lib/stack-client.tsx @@ -7,6 +7,7 @@ import { uiBuilderClientPlugin, defaultComponentRegistry } from "@btst/stack/plu import { routeDocsClientPlugin } from "@btst/stack/plugins/route-docs/client" import { kanbanClientPlugin } from "@btst/stack/plugins/kanban/client" import { commentsClientPlugin } from "@btst/stack/plugins/comments/client" +import { mediaClientPlugin } from "@btst/stack/plugins/media/client" import { QueryClient } from "@tanstack/react-query" // Get base URL function - works on both server and client @@ -142,6 +143,14 @@ export const getStackClient = (queryClient: QueryClient) => { siteBasePath: "/pages", queryClient: queryClient, }), + // Media plugin — registers the /media library route + media: mediaClientPlugin({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + siteBaseURL: baseURL, + siteBasePath: "/pages", + queryClient: queryClient, + }), } }) } diff --git a/examples/tanstack/src/lib/stack.ts b/examples/tanstack/src/lib/stack.ts index a0d84b65..e0d2cf44 100644 --- a/examples/tanstack/src/lib/stack.ts +++ b/examples/tanstack/src/lib/stack.ts @@ -7,6 +7,7 @@ import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api" import { openApiBackendPlugin } from "@btst/stack/plugins/open-api/api" import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api" import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api" +import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api" import { UI_BUILDER_CONTENT_TYPE } from "@btst/stack/plugins/ui-builder" import { openai } from "@ai-sdk/openai" @@ -209,6 +210,10 @@ const { handler, dbSchema } = stack({ return ctx?.headers?.get?.("x-user-id") ?? null }, }), + // Media plugin for asset management + media: mediaBackendPlugin({ + storageAdapter: localAdapter(), + }), }, adapter: (db) => createMemoryAdapter(db)({}) }) diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index f45412f5..332011b4 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from "react" import { StackProvider } from "@btst/stack/context" import { QueryClientProvider } from "@tanstack/react-query" import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -10,6 +9,9 @@ import type { FormBuilderPluginOverrides } from "@btst/stack/plugins/form-builde import type { KanbanPluginOverrides } from "@btst/stack/plugins/kanban/client" import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" import { CommentThread } from "@btst/stack/plugins/comments/client/components" +import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" +import { MediaPicker, ImageInputField, uploadMediaFile } from "@btst/stack/plugins/media/client/components" +import { Button } from "../../components/ui/button" import { resolveUser, searchUsers } from "../../lib/mock-users" import { Link, useRouter, Outlet, createFileRoute } from "@tanstack/react-router" @@ -21,20 +23,6 @@ const getBaseURL = () => ? (import.meta.env.VITE_BASE_URL || window.location.origin) : (process.env.BASE_URL || "http://localhost:3000") -// Mock file upload URLs -const MOCK_IMAGE_URL = "https://placehold.co/400/png" -const MOCK_FILE_URL = "https://example-files.online-convert.com/document/txt/example.txt" - -// Mock file upload function that returns appropriate URL based on file type -async function mockUploadFile(file: File): Promise { - console.log("uploadFile", file.name, file.type) - // Return image placeholder for images, txt file URL for other file types - if (file.type.startsWith("image/")) { - return MOCK_IMAGE_URL - } - return MOCK_FILE_URL -} - // Define the shape of all plugin overrides type PluginOverrides = { blog: BlogPluginOverrides, @@ -43,6 +31,7 @@ type PluginOverrides = { "form-builder": FormBuilderPluginOverrides, kanban: KanbanPluginOverrides, comments: CommentsPluginOverrides, + media: MediaPluginOverrides, } export const Route = createFileRoute('/pages')({ @@ -54,11 +43,16 @@ export const Route = createFileRoute('/pages')({ function Layout() { const router = useRouter() - const context = Route.useRouteContext() + const routeContext = Route.useRouteContext() const baseURL = getBaseURL() + const uploadImage = async (file: File) => { + const asset = await uploadMediaFile(file, baseURL) + return asset.url + } + return ( - + basePath="/pages" @@ -67,7 +61,9 @@ function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => router.navigate({ href }), - uploadImage: mockUploadFile, + uploadImage, + imagePicker: ImagePicker, + imageInputField: ImageInputField, Link: ({ href, children, className, ...props }) => ( {children} @@ -119,7 +115,7 @@ function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => router.navigate({ href }), - uploadFile: mockUploadFile, + uploadFile: uploadImage, Link: ({ href, children, className, ...props }) => ( {children} @@ -133,68 +129,14 @@ function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => router.navigate({ href }), - uploadImage: mockUploadFile, + uploadImage, + imagePicker: ImagePicker, + imageInputField: ImageInputField, Link: ({ href, children, className, ...props }) => ( {children} ), - // Custom field components for CMS forms - // These override the default auto-form field types - fieldComponents: { - // Override "file" to use uploadImage from context - file: ({ field, label, isRequired, fieldConfigItem }) => { - const [preview, setPreview] = useState(field.value || null); - const [uploading, setUploading] = useState(false); - // Sync preview with field.value when it changes (e.g., when editing an existing item) - useEffect(() => { - const normalizedValue = field.value || null; - if (normalizedValue !== preview) { - setPreview(normalizedValue); - } - }, [field.value, preview]); - return ( -
- - {!preview ? ( - { - const file = e.target.files?.[0]; - if (file) { - setUploading(true); - try { - const url = await mockUploadFile(file); - setPreview(url); - field.onChange(url); - } finally { - setUploading(false); - } - } - }} - className="block w-full text-sm" - /> - ) : ( -
- Preview - -
- )} - {fieldConfigItem?.description && ( -

{String(fieldConfigItem.description)}

- )} -
- ); - }, - }, onRouteRender: async (routeName, context) => { console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] CMS route:`, routeName, context.path); }, @@ -224,6 +166,8 @@ function Layout() { {children} ), + uploadImage, + imagePicker: ImagePicker, // User resolution for assignees resolveUser, searchUsers, @@ -261,7 +205,19 @@ function Layout() { console.log(`[${context.isSSR ? 'SSR' : 'CSR'}] onBeforeUserCommentsPageRendered`); return true; // In production: check authenticated session }, - } + }, + media: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + queryClient: routeContext.queryClient, + uploadMode: "direct", + navigate: (href) => router.navigate({ href }), + Link: ({ href, children, className, ...props }) => ( + + {children} + + ), + }, }} > @@ -283,3 +239,21 @@ function Layout() { ) } +const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { + return ( + + Browse Media + + } + accept={["image/*"]} + onSelect={(assets) => onSelect(assets[0].url)} + /> + ) +} diff --git a/packages/stack/build.config.ts b/packages/stack/build.config.ts index 1dd43178..27715027 100644 --- a/packages/stack/build.config.ts +++ b/packages/stack/build.config.ts @@ -117,6 +117,10 @@ export default defineBuildConfig({ "./src/plugins/comments/query-keys.ts", // media plugin entries "./src/plugins/media/api/index.ts", + "./src/plugins/media/client/index.ts", + "./src/plugins/media/client/components/index.tsx", + "./src/plugins/media/client/hooks/index.tsx", + "./src/plugins/media/query-keys.ts", "./src/components/auto-form/index.ts", "./src/components/stepped-auto-form/index.ts", "./src/components/multi-select/index.ts", diff --git a/packages/stack/knip.json b/packages/stack/knip.json index 64e1f770..c31759d8 100644 --- a/packages/stack/knip.json +++ b/packages/stack/knip.json @@ -40,7 +40,16 @@ "src/plugins/kanban/client/components/index.tsx", "src/plugins/kanban/client/hooks/index.tsx", "src/plugins/kanban/query-keys.ts", + "src/plugins/comments/api/index.ts", + "src/plugins/comments/client/index.ts", + "src/plugins/comments/client/components/index.tsx", + "src/plugins/comments/client/hooks/index.tsx", + "src/plugins/comments/query-keys.ts", "src/plugins/media/api/index.ts", + "src/plugins/media/client/index.ts", + "src/plugins/media/client/components/index.tsx", + "src/plugins/media/client/hooks/index.tsx", + "src/plugins/media/query-keys.ts", "build.config.ts", "vitest.config.mts", "scripts/build-registry.ts", diff --git a/packages/stack/package.json b/packages/stack/package.json index cb0ba9c0..92da185d 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -424,6 +424,47 @@ "default": "./dist/plugins/media/api/index.cjs" } }, + "./plugins/media/client": { + "import": { + "types": "./dist/plugins/media/client/index.d.ts", + "default": "./dist/plugins/media/client/index.mjs" + }, + "require": { + "types": "./dist/plugins/media/client/index.d.cts", + "default": "./dist/plugins/media/client/index.cjs" + } + }, + "./plugins/media/client/components": { + "import": { + "types": "./dist/plugins/media/client/components/index.d.ts", + "default": "./dist/plugins/media/client/components/index.mjs" + }, + "require": { + "types": "./dist/plugins/media/client/components/index.d.cts", + "default": "./dist/plugins/media/client/components/index.cjs" + } + }, + "./plugins/media/client/hooks": { + "import": { + "types": "./dist/plugins/media/client/hooks/index.d.ts", + "default": "./dist/plugins/media/client/hooks/index.mjs" + }, + "require": { + "types": "./dist/plugins/media/client/hooks/index.d.cts", + "default": "./dist/plugins/media/client/hooks/index.cjs" + } + }, + "./plugins/media/query-keys": { + "import": { + "types": "./dist/plugins/media/query-keys.d.ts", + "default": "./dist/plugins/media/query-keys.mjs" + }, + "require": { + "types": "./dist/plugins/media/query-keys.d.cts", + "default": "./dist/plugins/media/query-keys.cjs" + } + }, + "./plugins/media/css": "./dist/plugins/media/client.css", "./plugins/route-docs/client": { "import": { "types": "./dist/plugins/route-docs/client/index.d.ts", @@ -623,6 +664,18 @@ "plugins/media/api": [ "./dist/plugins/media/api/index.d.ts" ], + "plugins/media/client": [ + "./dist/plugins/media/client/index.d.ts" + ], + "plugins/media/client/components": [ + "./dist/plugins/media/client/components/index.d.ts" + ], + "plugins/media/client/hooks": [ + "./dist/plugins/media/client/hooks/index.d.ts" + ], + "plugins/media/query-keys": [ + "./dist/plugins/media/query-keys.d.ts" + ], "plugins/route-docs/client": [ "./dist/plugins/route-docs/client/index.d.ts" ], diff --git a/packages/stack/src/plugins/blog/client/components/forms/image-field.tsx b/packages/stack/src/plugins/blog/client/components/forms/image-field.tsx index b3886714..8c553df8 100644 --- a/packages/stack/src/plugins/blog/client/components/forms/image-field.tsx +++ b/packages/stack/src/plugins/blog/client/components/forms/image-field.tsx @@ -28,13 +28,44 @@ export function FeaturedImageField({ const fileInputRef = useRef(null); const [isUploading, setIsUploading] = useState(false); - const { uploadImage, Image, localization } = usePluginOverrides< - BlogPluginOverrides, - Partial - >("blog", { localization: BLOG_LOCALIZATION }); + const { + uploadImage, + Image, + localization, + imageInputField: ImageInput, + } = usePluginOverrides>( + "blog", + { localization: BLOG_LOCALIZATION }, + ); const ImageComponent = Image ? Image : DefaultImage; + // When a custom imageInput component is provided via overrides, delegate to it. + if (ImageInput) { + return ( + + + {localization.BLOG_FORMS_FEATURED_IMAGE_LABEL} + {isRequired && ( + + {" "} + {localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK} + + )} + + + + + + + + ); + } + const handleImageUpload = async ( event: React.ChangeEvent, ) => { diff --git a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx index 385ac9e1..f985d0fe 100644 --- a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +++ b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx @@ -1,4 +1,5 @@ "use client"; +import { useCallback, useRef } from "react"; import { usePluginOverrides } from "@btst/stack/context"; import type { BlogPluginOverrides } from "../../overrides"; import { BLOG_LOCALIZATION } from "../../localization"; @@ -6,24 +7,78 @@ import { MarkdownEditor, type MarkdownEditorProps } from "./markdown-editor"; type MarkdownEditorWithOverridesProps = Omit< MarkdownEditorProps, - "uploadImage" | "placeholder" + | "uploadImage" + | "placeholder" + | "insertImageRef" + | "openMediaPickerForImageBlock" >; export function MarkdownEditorWithOverrides( props: MarkdownEditorWithOverridesProps, ) { - const { uploadImage, localization } = usePluginOverrides< - BlogPluginOverrides, - Partial - >("blog", { - localization: BLOG_LOCALIZATION, - }); + const { + uploadImage, + imagePicker: ImagePickerTrigger, + localization, + } = usePluginOverrides>( + "blog", + { localization: BLOG_LOCALIZATION }, + ); + + const insertImageRef = useRef<((url: string) => void) | null>(null); + // Holds the Crepe-image-block `setUrl` callback while the picker is open. + const pendingInsertUrlRef = useRef<((url: string) => void) | null>(null); + // Ref to the trigger wrapper so we can programmatically click the picker button. + const triggerContainerRef = useRef(null); + + // Single onSelect handler for ImagePickerTrigger. + // URLs are encoded here before being forwarded to either destination. + const handleSelect = useCallback((url: string) => { + const encodedUrl = encodeURI(url); + if (pendingInsertUrlRef.current) { + // Crepe image block flow: set the URL into the block's link input. + pendingInsertUrlRef.current(encodedUrl); + pendingInsertUrlRef.current = null; + } else { + // Normal flow: insert image at end of markdown content. + insertImageRef.current?.(encodedUrl); + } + }, []); + + // Called by MarkdownEditor's click interceptor when the user clicks a Crepe + // image-block upload placeholder. + const openMediaPickerForImageBlock = useCallback( + (setUrl: (url: string) => void) => { + pendingInsertUrlRef.current = setUrl; + // Programmatically click the visible picker trigger button. + const btn = triggerContainerRef.current?.querySelector( + '[data-testid="open-media-picker"]', + ) as HTMLButtonElement | null; + btn?.click(); + }, + [], + ); return ( - +
+ + {ImagePickerTrigger && ( +
+ +
+ )} +
); } diff --git a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx index 1cebce5e..6d3fe4a8 100644 --- a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx +++ b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx @@ -8,7 +8,12 @@ import { editorViewCtx, parserCtx } from "@milkdown/kit/core"; import { listener, listenerCtx } from "@milkdown/kit/plugin/listener"; import { Slice } from "@milkdown/kit/prose/model"; import { Selection } from "@milkdown/kit/prose/state"; -import { useLayoutEffect, useRef, useState } from "react"; +import { + useLayoutEffect, + useRef, + useState, + type MutableRefObject, +} from "react"; export interface MarkdownEditorProps { value?: string; @@ -18,6 +23,19 @@ export interface MarkdownEditorProps { uploadImage?: (file: File) => Promise; /** Placeholder text shown when the editor is empty. */ placeholder?: string; + /** + * Optional ref that will be populated with an `insertImage(url)` function. + * Call `insertImageRef.current?.(url)` to programmatically insert an image. + * The URL is expected to be already encoded by the caller. + */ + insertImageRef?: MutableRefObject<((url: string) => void) | null>; + /** + * When provided, clicking the Crepe image block's upload area opens a media + * picker instead of the native file dialog. The callback receives a `setUrl` + * function — call it with the chosen URL to set it into the image block. + * The URL passed to `setUrl` is expected to be already encoded by the caller. + */ + openMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void; } export function MarkdownEditor({ @@ -26,6 +44,8 @@ export function MarkdownEditor({ className, uploadImage, placeholder = "Write something...", + insertImageRef, + openMediaPickerForImageBlock, }: MarkdownEditorProps) { const containerRef = useRef(null); const crepeRef = useRef(null); @@ -33,6 +53,9 @@ export function MarkdownEditor({ const [isReady, setIsReady] = useState(false); const onChangeRef = useRef(onChange); const initialValueRef = useRef(value ?? ""); + const openMediaPickerRef = useRef( + openMediaPickerForImageBlock, + ); type ThrottledFn = ((markdown: string) => void) & { cancel?: () => void; flush?: () => void; @@ -40,12 +63,24 @@ export function MarkdownEditor({ const throttledOnChangeRef = useRef(null); onChangeRef.current = onChange; + openMediaPickerRef.current = openMediaPickerForImageBlock; useLayoutEffect(() => { if (crepeRef.current) return; const container = containerRef.current; if (!container) return; + const hasMediaPicker = !!openMediaPickerRef.current; + + const imageBlockConfig: Record = {}; + if (uploadImage) { + imageBlockConfig.onUpload = async (file: File) => uploadImage(file); + } + if (hasMediaPicker) { + imageBlockConfig.blockUploadPlaceholderText = "Media Picker"; + imageBlockConfig.inlineUploadPlaceholderText = "Media Picker"; + } + const crepe = new Crepe({ root: container, defaultValue: initialValueRef.current, @@ -53,19 +88,47 @@ export function MarkdownEditor({ [CrepeFeature.Placeholder]: { text: placeholder, }, - ...(uploadImage - ? { - [CrepeFeature.ImageBlock]: { - onUpload: async (file: File) => { - const url = await uploadImage(file); - return url; - }, - }, - } + ...(Object.keys(imageBlockConfig).length > 0 + ? { [CrepeFeature.ImageBlock]: imageBlockConfig } : {}), }, }); + // Intercept clicks on Crepe image-block upload placeholders so that the + // native file dialog is suppressed and the media picker is opened instead. + const interceptHandler = (e: MouseEvent) => { + if (!openMediaPickerRef.current) return; + const target = e.target as Element; + // Only intercept clicks inside the upload placeholder area. + const inPlaceholder = target.closest(".image-edit .placeholder"); + if (!inPlaceholder) return; + // Let the hidden file itself through (shouldn't receive clicks normally). + if ((target as HTMLElement).matches("input")) return; + + e.preventDefault(); + e.stopPropagation(); + + const imageEdit = inPlaceholder.closest(".image-edit"); + const linkInput = imageEdit?.querySelector( + ".link-input-area", + ) as HTMLInputElement | null; + + openMediaPickerRef.current((url: string) => { + if (!linkInput) return; + // Use the native setter so Vue's reactivity picks up the change. + const nativeSetter = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + "value", + )?.set; + nativeSetter?.call(linkInput, url); + linkInput.dispatchEvent(new Event("input", { bubbles: true })); + linkInput.dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", bubbles: true }), + ); + }); + }; + container.addEventListener("click", interceptHandler, true); + // Prepare throttled onChange once per editor instance throttledOnChangeRef.current = throttle((markdown: string) => { if (onChangeRef.current) onChangeRef.current(markdown); @@ -86,6 +149,7 @@ export function MarkdownEditor({ crepeRef.current = crepe; return () => { + container.removeEventListener("click", interceptHandler, true); try { isReadyRef.current = false; throttledOnChangeRef.current?.cancel?.(); @@ -133,6 +197,38 @@ export function MarkdownEditor({ }); }, [value, isReady]); + // Expose insertImage via ref so the parent can insert images programmatically + useLayoutEffect(() => { + if (!insertImageRef) return; + insertImageRef.current = (url: string) => { + if (!crepeRef.current || !isReadyRef.current) return; + try { + const currentMarkdown = crepeRef.current.getMarkdown?.() ?? ""; + const imageMarkdown = `\n\n![](${url})\n\n`; + const newMarkdown = currentMarkdown.trimEnd() + imageMarkdown; + crepeRef.current.editor.action((ctx) => { + const view = ctx.get(editorViewCtx); + const parser = ctx.get(parserCtx); + const doc = parser(newMarkdown); + if (!doc) return; + const state = view.state; + const tr = state.tr.replace( + 0, + state.doc.content.size, + new Slice(doc.content, 0, 0), + ); + view.dispatch(tr); + }); + if (onChangeRef.current) onChangeRef.current(newMarkdown); + } catch { + // Editor may not be ready yet + } + }; + return () => { + if (insertImageRef) insertImageRef.current = null; + }; + }, [insertImageRef]); + return (
); diff --git a/packages/stack/src/plugins/blog/client/overrides.ts b/packages/stack/src/plugins/blog/client/overrides.ts index c1d543ed..79f6ab6e 100644 --- a/packages/stack/src/plugins/blog/client/overrides.ts +++ b/packages/stack/src/plugins/blog/client/overrides.ts @@ -2,6 +2,18 @@ import type { SerializedPost } from "../types"; import type { ComponentType, ReactNode } from "react"; import type { BlogLocalization } from "./localization"; +/** + * Props for the overridable blog featured image input component. + */ +export interface BlogImageInputFieldProps { + /** Current image URL value */ + value: string; + /** Called when the image URL changes */ + onChange: (value: string) => void; + /** Whether the field is required */ + isRequired?: boolean; +} + /** * Context passed to lifecycle hooks */ @@ -51,6 +63,50 @@ export interface BlogPluginOverrides { * Function used to upload an image and return its URL. */ uploadImage: (file: File) => Promise; + /** + * Optional custom component for the featured image field. + * + * When provided it replaces the default file-upload input entirely. + * The component receives `value` (current URL string) and `onChange` (setter). + * + * Typical use case: render a preview when a value is set, and a media-picker + * trigger when no value is set. + * + * @example + * ```tsx + * imageInputField: ({ value, onChange }) => + * value ? ( + *
+ * Preview + * Change} accept={["image/*"]} + * onSelect={(assets) => onChange(assets[0].url)} /> + *
+ * ) : ( + * Browse media} accept={["image/*"]} + * onSelect={(assets) => onChange(assets[0].url)} /> + * ) + * ``` + */ + imageInputField?: ComponentType; + + /** + * Optional trigger component for a media picker. + * When provided, it is rendered adjacent to the Markdown editor and allows + * users to browse and select previously uploaded assets. + * Receives `onSelect(url)` — insert the chosen URL into the editor. + * + * @example + * ```tsx + * imagePicker: ({ onSelect }) => ( + * Browse media} + * accept={["image/*"]} + * onSelect={(assets) => onSelect(assets[0].url)} + * /> + * ) + * ``` + */ + imagePicker?: ComponentType<{ onSelect: (url: string) => void }>; /** * Localization object for the blog plugin */ diff --git a/packages/stack/src/plugins/cms/client/components/forms/content-form.tsx b/packages/stack/src/plugins/cms/client/components/forms/content-form.tsx index 158cee9a..de59c6a5 100644 --- a/packages/stack/src/plugins/cms/client/components/forms/content-form.tsx +++ b/packages/stack/src/plugins/cms/client/components/forms/content-form.tsx @@ -56,6 +56,12 @@ function buildFieldConfigFromJsonSchema( string, React.ComponentType >, + imagePicker?: React.ComponentType<{ onSelect: (url: string) => void }>, + imageInputField?: React.ComponentType<{ + value: string; + onChange: (value: string) => void; + isRequired?: boolean; + }>, ): FieldConfig> { // Get base config from shared utility (handles fieldType from JSON Schema) const baseConfig = buildFieldConfigBase(jsonSchema, fieldComponents); @@ -73,14 +79,14 @@ function buildFieldConfigFromJsonSchema( // Handle "file" fieldType when there's NO custom component for "file" if (prop.fieldType === "file" && !fieldComponents?.["file"]) { // Use CMSFileUpload as the default file component - if (!uploadImage) { - // Show a clear error message if uploadImage is not provided + if (!uploadImage && !imageInputField) { + // Show a clear error message if neither uploadImage nor imageInputField is provided baseConfig[key] = { ...baseConfig[key], fieldType: () => (
- File upload requires an uploadImage function in CMS - overrides. + File upload requires an uploadImage or{" "} + imageInputField function in CMS overrides.
), }; @@ -88,7 +94,12 @@ function buildFieldConfigFromJsonSchema( baseConfig[key] = { ...baseConfig[key], fieldType: (props: AutoFormInputComponentProps) => ( - + Promise.resolve(""))} + imageInputField={imageInputField} + imagePicker={imagePicker} + /> ), }; } @@ -151,6 +162,8 @@ export function ContentForm({ const { localization: customLocalization, uploadImage, + imagePicker, + imageInputField, fieldComponents, } = usePluginOverrides("cms"); const localization = { ...CMS_LOCALIZATION, ...customLocalization }; @@ -214,8 +227,14 @@ export function ContentForm({ // Build field config for AutoForm (fieldType is now embedded in jsonSchema) const fieldConfig = useMemo( () => - buildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents), - [jsonSchema, uploadImage, fieldComponents], + buildFieldConfigFromJsonSchema( + jsonSchema, + uploadImage, + fieldComponents, + imagePicker, + imageInputField, + ), + [jsonSchema, uploadImage, fieldComponents, imagePicker, imageInputField], ); // Find the field to use for slug auto-generation diff --git a/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx b/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx index 54f8b973..e1dd02e3 100644 --- a/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx +++ b/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx @@ -1,6 +1,12 @@ "use client"; -import { useState, useCallback, useEffect, type ChangeEvent } from "react"; +import { + useState, + useCallback, + useEffect, + type ChangeEvent, + type ComponentType, +} from "react"; import { toast } from "sonner"; import type { AutoFormInputComponentProps } from "@workspace/ui/components/auto-form/types"; import { Input } from "@workspace/ui/components/input"; @@ -23,6 +29,20 @@ export interface CMSFileUploadProps extends AutoFormInputComponentProps { * This is required - consumers must provide an upload implementation. */ uploadImage: (file: File) => Promise; + /** + * Optional custom component for the image field. + * When provided, it replaces the default file-upload input entirely. + */ + imageInputField?: ComponentType<{ + value: string; + onChange: (value: string) => void; + isRequired?: boolean; + }>; + /** + * Optional trigger component for a media picker. + * When provided, it is rendered as a "Browse media" option. + */ + imagePicker?: ComponentType<{ onSelect: (url: string) => void }>; } /** @@ -54,6 +74,8 @@ export function CMSFileUpload({ fieldProps, field, uploadImage, + imageInputField: ImageInputField, + imagePicker: ImagePickerTrigger, }: CMSFileUploadProps) { // Exclude showLabel and value from props spread // File inputs cannot have their value set programmatically (browser security) @@ -63,6 +85,29 @@ export function CMSFileUpload({ ...safeFieldProps } = fieldProps; const showLabel = _showLabel === undefined ? true : _showLabel; + + // When a custom imageInputField component is provided via overrides, delegate to it. + if (ImageInputField) { + return ( + + {showLabel && ( + + )} + + + + + + + ); + } const [isUploading, setIsUploading] = useState(false); const [previewUrl, setPreviewUrl] = useState( field.value || null, @@ -116,19 +161,31 @@ export function CMSFileUpload({ )} {!previewUrl && ( -
- - {isUploading && ( -
- +
+
+ + {isUploading && ( +
+ +
+ )} +
+ {ImagePickerTrigger && ( +
+ { + setPreviewUrl(url); + field.onChange(url); + }} + />
)}
diff --git a/packages/stack/src/plugins/cms/client/overrides.ts b/packages/stack/src/plugins/cms/client/overrides.ts index e65d9dc6..5c1a9267 100644 --- a/packages/stack/src/plugins/cms/client/overrides.ts +++ b/packages/stack/src/plugins/cms/client/overrides.ts @@ -2,6 +2,18 @@ import type { ComponentType } from "react"; import type { CMSLocalization } from "./localization"; import type { AutoFormInputComponentProps } from "@workspace/ui/components/auto-form/types"; +/** + * Props for the overridable CMS image input field component. + */ +export interface CmsImageInputFieldProps { + /** Current image URL value */ + value: string; + /** Called when the image URL changes */ + onChange: (value: string) => void; + /** Whether the field is required */ + isRequired?: boolean; +} + /** * Context passed to lifecycle hooks */ @@ -51,6 +63,48 @@ export interface CMSPluginOverrides { */ uploadImage?: (file: File) => Promise; + /** + * Optional custom component for image fields (fieldType: "file"). + * + * When provided it replaces the default file-upload input entirely. + * The component receives `value` (current URL string) and `onChange` (setter). + * + * @example + * ```tsx + * imageInputField: ({ value, onChange }) => + * value ? ( + *
+ * Preview + * Change} accept={["image/*"]} + * onSelect={(assets) => onChange(assets[0].url)} /> + *
+ * ) : ( + * Browse media} accept={["image/*"]} + * onSelect={(assets) => onChange(assets[0].url)} /> + * ) + * ``` + */ + imageInputField?: ComponentType; + + /** + * Optional trigger component for a media picker. + * When provided, it is rendered inside the default "file" field component as a + * "Browse media" option, letting users select a previously uploaded asset. + * Receives `onSelect(url)` — the URL is set as the field value. + * + * @example + * ```tsx + * imagePicker: ({ onSelect }) => ( + * Browse media} + * accept={["image/*"]} + * onSelect={(assets) => onSelect(assets[0].url)} + * /> + * ) + * ``` + */ + imagePicker?: ComponentType<{ onSelect: (url: string) => void }>; + /** * Custom field components for AutoForm fields. * diff --git a/packages/stack/src/plugins/kanban/client/components/forms/board-form.tsx b/packages/stack/src/plugins/kanban/client/components/forms/board-form.tsx index b2c2ed83..1105d3bc 100644 --- a/packages/stack/src/plugins/kanban/client/components/forms/board-form.tsx +++ b/packages/stack/src/plugins/kanban/client/components/forms/board-form.tsx @@ -50,7 +50,7 @@ export function BoardForm({ board, onClose, onSuccess }: BoardFormProps) { }; return ( -
+
("kanban"); const { createTask, updateTask, @@ -155,7 +159,7 @@ export function TaskForm({ }; return ( - +
diff --git a/packages/stack/src/plugins/kanban/client/overrides.ts b/packages/stack/src/plugins/kanban/client/overrides.ts index 9c90cc33..5e8f8502 100644 --- a/packages/stack/src/plugins/kanban/client/overrides.ts +++ b/packages/stack/src/plugins/kanban/client/overrides.ts @@ -73,6 +73,30 @@ export interface KanbanPluginOverrides { */ headers?: HeadersInit; + /** + * Function used to upload an image from the task description editor and return its URL. + * Wired as the `uploader` prop of MinimalTiptapEditor — handles drag-drop image uploads. + */ + uploadImage?: (file: File) => Promise; + + /** + * Optional trigger component for a media picker. + * When provided, it appears inside the image insertion dialog of the task description editor, + * letting users browse and select previously uploaded assets. + * + * @example + * ```tsx + * imagePicker: ({ onSelect }) => ( + * Browse media} + * accept={["image/*"]} + * onSelect={(assets) => onSelect(assets[0].url)} + * /> + * ) + * ``` + */ + imagePicker?: ComponentType<{ onSelect: (url: string) => void }>; + // ============ User Resolution (required for assignee features) ============ /** diff --git a/packages/stack/src/plugins/media/__tests__/plugin.test.ts b/packages/stack/src/plugins/media/__tests__/plugin.test.ts index 10b59021..5a4eb3d1 100644 --- a/packages/stack/src/plugins/media/__tests__/plugin.test.ts +++ b/packages/stack/src/plugins/media/__tests__/plugin.test.ts @@ -215,17 +215,34 @@ async function createAssetViaApi( }; } -function invokeEndpoint( +async function parseRequestBody( + request: Request, +): Promise | undefined> { + const contentType = request.headers.get("content-type") ?? ""; + if (contentType.includes("multipart/form-data")) { + const formData = await request.formData(); + const body: Record = {}; + formData.forEach((value, key) => { + body[key] = value; + }); + return body; + } + return undefined; +} + +async function invokeEndpoint( backend: ReturnType, endpointKey: string, request: Request, ) { + const body = await parseRequestBody(request); return (backend.router as any).endpoints[endpointKey]({ request, headers: request.headers, method: request.method, params: {}, query: {}, + body, asResponse: true, }); } diff --git a/packages/stack/src/plugins/media/api/plugin.ts b/packages/stack/src/plugins/media/api/plugin.ts index 220e3f5d..3bac9ffc 100644 --- a/packages/stack/src/plugins/media/api/plugin.ts +++ b/packages/stack/src/plugins/media/api/plugin.ts @@ -555,6 +555,13 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) => "/media/upload", { method: "POST", + metadata: { + // Tell Better Call this endpoint accepts multipart/form-data so it + // parses the body into a FormData object and exposes it as ctx.body. + // Without this, Better Call may pre-read the body stream and calling + // ctx.request.formData() afterwards fails with "Body already read". + allowedMediaTypes: ["multipart/form-data"], + }, }, async (ctx) => { if (!isDirectAdapter(storageAdapter)) { @@ -564,21 +571,36 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) => }); } - if (!ctx.request) { + // Better Call parses multipart/form-data into a plain object on ctx.body, + // where each field's value is preserved as-is (File instances for file fields). + const body = ctx.body as Record | undefined; + + if (!body || typeof body !== "object") { throw ctx.error(400, { - message: "Request object is not available", + message: "Expected multipart/form-data request body", }); } - const formData = await ctx.request.formData(); - const file = formData.get("file"); + const fileRaw = body.file; - if (!file || !(file instanceof File)) { + // Use a duck-type check instead of instanceof File to avoid + // cross-module-boundary failures (e.g. undici's File vs globalThis.File). + if ( + !fileRaw || + typeof fileRaw !== "object" || + typeof (fileRaw as any).arrayBuffer !== "function" + ) { throw ctx.error(400, { message: "Missing 'file' field in form data", }); } + // Safe to treat as a File-like object after the duck-type check above. + const file = fileRaw as Pick< + File, + "name" | "type" | "size" | "arrayBuffer" + >; + const context: MediaApiContext = { headers: ctx.headers }; if (hooks?.onBeforeUpload) { @@ -606,8 +628,7 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) => } const buffer = Buffer.from(await file.arrayBuffer()); - const folderId = - (formData.get("folderId") as string | undefined) ?? undefined; + const folderId = (body.folderId as string | undefined) ?? undefined; if (folderId) { const folder = await getFolderById(adapter, folderId); diff --git a/packages/stack/src/plugins/media/client.css b/packages/stack/src/plugins/media/client.css new file mode 100644 index 00000000..b43b676b --- /dev/null +++ b/packages/stack/src/plugins/media/client.css @@ -0,0 +1 @@ +/* Media plugin client styles — included in consumer CSS via @import "@btst/stack/plugins/media/css" */ diff --git a/packages/stack/src/plugins/media/client/components/index.tsx b/packages/stack/src/plugins/media/client/components/index.tsx new file mode 100644 index 00000000..0d3caf46 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/index.tsx @@ -0,0 +1,7 @@ +export { + MediaPicker, + ImageInputField, + type MediaPickerProps, + uploadMediaFile, +} from "./media-picker"; +export { LibraryPageComponent } from "./pages/library-page"; diff --git a/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx b/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx new file mode 100644 index 00000000..35e42216 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx @@ -0,0 +1,96 @@ +import { useDeleteAsset } from "../../hooks/use-media"; +import type { SerializedAsset } from "../../../types"; +import { cn } from "@workspace/ui/lib/utils"; +import { File, Check, Trash2 } from "lucide-react"; +import { isImage, formatBytes } from "./utils"; +import { usePluginOverrides } from "@btst/stack/context"; +import type { MediaPluginOverrides } from "../../overrides"; + +export function AssetCard({ + asset, + selected, + onToggle, +}: { + asset: SerializedAsset; + selected: boolean; + onToggle: () => void; +}) { + const { mutateAsync: deleteAsset } = useDeleteAsset(); + const { Image: ImageComponent } = usePluginOverrides< + MediaPluginOverrides, + Partial + >("media", {}); + + return ( +
(e.key === "Enter" || e.key === " ") && onToggle()} + className={cn( + "group relative cursor-pointer rounded-md border bg-muted/30 p-1 transition-all hover:border-ring hover:shadow-sm", + selected && "border-ring ring-1 ring-ring", + )} + > + {/* Thumbnail */} +
+ {isImage(asset.mimeType) ? ( + ImageComponent ? ( + + ) : ( + {asset.alt + ) + ) : ( + + )} +
+ + {/* Name + size */} +
+

+ {asset.originalName} +

+

+ {formatBytes(asset.size)} +

+
+ + {/* Selection indicator */} + {selected && ( +
+ +
+ )} + + {/* Delete button (on hover) */} + +
+ ); +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx b/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx new file mode 100644 index 00000000..3d3915e9 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx @@ -0,0 +1,109 @@ +import { useState, useRef } from "react"; +import { useAssets } from "../../hooks/use-media"; +import type { SerializedAsset } from "../../../types"; +import { Input } from "@workspace/ui/components/input"; +import { Button } from "@workspace/ui/components/button"; +import { Loader2, Search, X, Image } from "lucide-react"; +import { AssetCard } from "./asset-card"; +import { matchesAccept } from "./utils"; + +export function BrowseTab({ + folderId, + selected, + multiple, + accept, + onToggle, +}: { + folderId: string | null; + selected: SerializedAsset[]; + multiple: boolean; + accept?: string[]; + onToggle: (asset: SerializedAsset) => void; +}) { + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const debounceRef = useRef | null>(null); + + const handleSearch = (v: string) => { + setSearch(v); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => setDebouncedSearch(v), 300); + }; + + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = + useAssets({ + folderId: folderId ?? undefined, + query: debouncedSearch || undefined, + limit: 40, + }); + + const allAssets = data?.pages.flatMap((p) => p.items) ?? []; + const filtered = accept + ? allAssets.filter((a) => matchesAccept(a.mimeType, accept)) + : allAssets; + + return ( +
+
+ + handleSearch(e.target.value)} + placeholder="Search files…" + className="h-8 pl-7 text-sm" + /> + {search && ( + + )} +
+ + {isLoading ? ( +
+ +
+ ) : filtered.length === 0 ? ( +
+ +

No files found

+
+ ) : ( +
+
+ {filtered.map((asset) => ( + s.id === asset.id)} + onToggle={() => onToggle(asset)} + /> + ))} +
+ {hasNextPage && ( +
+ +
+ )} +
+ )} +
+ ); +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx b/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx new file mode 100644 index 00000000..12ea1a42 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx @@ -0,0 +1,188 @@ +import { useState } from "react"; +import { + useFolders, + useCreateFolder, + useDeleteFolder, +} from "../../hooks/use-media"; +import type { SerializedFolder } from "../../../types"; +import { FolderPlus } from "lucide-react"; +import { Input } from "@workspace/ui/components/input"; +import { Check, Folder, Trash2, ChevronRight, FolderOpen } from "lucide-react"; +import { cn } from "@workspace/ui/lib/utils"; + +export function FolderTree({ + selectedId, + onSelect, +}: { + selectedId: string | null; + onSelect: (id: string | null) => void; +}) { + const { data: rootFoldersRaw = [] } = useFolders(null); + const rootFolders = + rootFoldersRaw as import("../../../types").SerializedFolder[]; + const [newFolderName, setNewFolderName] = useState(""); + const [isCreating, setIsCreating] = useState(false); + const { mutateAsync: createFolder } = useCreateFolder(); + const { mutateAsync: deleteFolder } = useDeleteFolder(); + + const handleCreateFolder = async () => { + const name = newFolderName.trim(); + if (!name) return; + try { + await createFolder({ name, parentId: selectedId ?? undefined }); + setNewFolderName(""); + setIsCreating(false); + } catch (err) { + console.error("[btst/media] Failed to create folder", err); + } + }; + + return ( +
+
+ + Folders + + +
+ + {isCreating && ( +
+ setNewFolderName(e.target.value)} + placeholder="Folder name" + className="h-6 text-xs" + onKeyDown={(e) => { + if (e.key === "Enter") void handleCreateFolder(); + if (e.key === "Escape") setIsCreating(false); + }} + /> + +
+ )} + +
+ {/* All assets (root) */} + + + {rootFolders.map((folder) => ( + + ))} +
+ + {selectedId && ( +
+ +
+ )} +
+ ); +} + +function FolderTreeItem({ + folder, + selectedId, + onSelect, + depth = 0, +}: { + folder: SerializedFolder; + selectedId: string | null; + onSelect: (id: string | null) => void; + depth?: number; +}) { + const [expanded, setExpanded] = useState(false); + const { data: children = [] } = useFolders(folder.id); + + return ( +
+ + {expanded && + children.map((child) => ( + + ))} +
+ ); +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx new file mode 100644 index 00000000..486d3155 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -0,0 +1,331 @@ +"use client"; +import { useState, type ReactNode } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@workspace/ui/components/popover"; +import { Button } from "@workspace/ui/components/button"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@workspace/ui/components/tabs"; +import { Image, Upload, Link, X } from "lucide-react"; +import type { SerializedAsset } from "../../../types"; +import { FolderTree } from "./folder-tree"; +import { BrowseTab } from "./browse-tab"; +import { UploadTab } from "./upload-tab"; +import { UrlTab } from "./url-tab"; +import type { MediaPluginOverrides } from "../../overrides"; +import { usePluginOverrides } from "@btst/stack/context"; + +export interface MediaPickerProps { + /** + * Element that triggers opening the picker. Required. + */ + trigger: ReactNode; + /** + * Called when the user confirms their selection. + */ + onSelect: (assets: SerializedAsset[]) => void; + /** + * Allow multiple selection. + * @default false + */ + multiple?: boolean; + /** + * Filter displayed assets by MIME type prefix (e.g. "image/"). + */ + accept?: string[]; +} + +/** + * MediaPicker — a Popover-based media browser. + * + * Reads API config from the `media` plugin overrides context (set up in StackProvider). + * Must be rendered inside a `StackProvider` that includes media overrides. + * + * @example + * ```tsx + * Browse media} + * accept={["image/*"]} + * onSelect={(assets) => form.setValue("image", assets[0].url)} + * /> + * ``` + */ +export function MediaPicker({ + trigger, + onSelect, + multiple = false, + accept, +}: MediaPickerProps) { + const [open, setOpen] = useState(false); + const [selectedFolder, setSelectedFolder] = useState(null); + const [selectedAssets, setSelectedAssets] = useState([]); + const [activeTab, setActiveTab] = useState<"browse" | "upload" | "url">( + "browse", + ); + + const handleClose = () => { + setOpen(false); + setSelectedAssets([]); + }; + + const handleConfirm = () => { + if (selectedAssets.length === 0) return; + // Copy selection before clearing; defer onSelect so the popover has time + // to start its close animation before any parent state updates that might + // unmount this component (e.g. CMSFileUpload hiding when previewUrl is set). + const toSelect = [...selectedAssets]; + handleClose(); + setTimeout(() => onSelect(toSelect), 0); + }; + + const handleToggleAsset = (asset: SerializedAsset) => { + if (multiple) { + setSelectedAssets((prev) => + prev.some((a) => a.id === asset.id) + ? prev.filter((a) => a.id !== asset.id) + : [...prev, asset], + ); + } else { + setSelectedAssets([asset]); + } + }; + + const handleUploaded = (asset: SerializedAsset) => { + if (multiple) { + setSelectedAssets((prev) => [...prev, asset]); + } else { + setSelectedAssets([asset]); + setActiveTab("browse"); + } + }; + + const handleUrlRegistered = (asset: SerializedAsset) => { + // Close the popover first, then notify parent — same deferral as handleConfirm. + const toSelect = asset; + handleClose(); + setTimeout(() => onSelect([toSelect]), 0); + }; + + return ( + { + if (!v) handleClose(); + else setOpen(true); + }} + > + {trigger} + +
+ {/* Header */} +
+ Media Library + +
+ + {/* Body */} +
+ {/* Folder sidebar */} +
+ +
+ + {/* Main panel */} +
+ setActiveTab(v as any)} + className="flex flex-1 flex-col min-h-0" + > + + + + Browse + + + + Upload + + + + URL + + + +
+ + + + + + + + + +
+
+
+
+ + {/* Footer */} +
+ + {selectedAssets.length > 0 + ? `${selectedAssets.length} selected` + : "Click a file to select it"} + +
+ + +
+
+
+
+
+ ); +} + +/** + * ImageInputField — a component that displays an image preview and a media picker button. + */ +export function ImageInputField({ + value, + onChange, +}: { + value: string; + onChange: (v: string) => void; +}) { + const { Image: ImageComponent } = usePluginOverrides< + MediaPluginOverrides, + Partial + >("media", {}); + + if (value) { + return ( +
+ {ImageComponent ? ( + + ) : ( + Featured image preview + )} +
+ + Change Image + + } + accept={["image/*"]} + onSelect={(assets) => onChange(assets[0]?.url ?? "")} + /> + +
+
+ ); + } + return ( + + Browse Media + + } + accept={["image/*"]} + onSelect={(assets) => onChange(assets[0]?.url ?? "")} + /> + ); +} + +/** + * Upload a file via the media plugin's direct upload endpoint + */ +export async function uploadMediaFile( + file: File, + baseURL: string, +): Promise { + const formData = new FormData(); + formData.append("file", file); + const res = await fetch(`${baseURL}/api/data/media/upload`, { + method: "POST", + headers: { + "Content-Type": "multipart/form-data", + }, + body: formData, + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Upload failed"); + } + const asset = (await res.json()) as SerializedAsset; + return asset; +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx b/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx new file mode 100644 index 00000000..cbf526f9 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx @@ -0,0 +1,108 @@ +import { useState, useCallback, useRef } from "react"; +import { useUploadAsset } from "../../hooks/use-media"; +import type { SerializedAsset } from "../../../types"; +import { Button } from "@workspace/ui/components/button"; +import { Loader2, Upload } from "lucide-react"; +import { cn } from "@workspace/ui/lib/utils"; +import { matchesAccept } from "./utils"; + +export function UploadTab({ + folderId, + accept, + onUploaded, +}: { + folderId: string | null; + accept?: string[]; + onUploaded: (asset: SerializedAsset) => void; +}) { + const [dragging, setDragging] = useState(false); + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + const { mutateAsync: uploadAsset } = useUploadAsset(); + + const acceptAttr = accept?.join(",") ?? undefined; + + const handleFiles = useCallback( + async (files: FileList | File[]) => { + const fileArr = Array.from(files); + if (fileArr.length === 0) return; + setError(null); + setUploading(true); + try { + for (const file of fileArr) { + if (accept && !matchesAccept(file.type, accept)) { + setError(`File type ${file.type} is not accepted.`); + continue; + } + const asset = await uploadAsset({ + file, + folderId: folderId ?? undefined, + }); + onUploaded(asset); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Upload failed"); + } finally { + setUploading(false); + } + }, + [accept, folderId, uploadAsset, onUploaded], + ); + + return ( +
+
{ + e.preventDefault(); + setDragging(true); + }} + onDragLeave={() => setDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setDragging(false); + void handleFiles(e.dataTransfer.files); + }} + className={cn( + "flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed transition-colors", + dragging ? "border-ring bg-ring/5" : "border-muted-foreground/30", + )} + > + {uploading ? ( + <> + +

Uploading…

+ + ) : ( + <> + +
+

Drop files here

+

+ or click to browse +

+
+ + + )} +
+ {error &&

{error}

} + e.target.files && handleFiles(e.target.files)} + /> +
+ ); +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx b/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx new file mode 100644 index 00000000..0e12bcf3 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { useRegisterAsset } from "../../hooks/use-media"; +import type { SerializedAsset } from "../../../types"; +import { Input } from "@workspace/ui/components/input"; +import { Button } from "@workspace/ui/components/button"; +import { Loader2, Check } from "lucide-react"; + +export function UrlTab({ + folderId, + onRegistered, +}: { + folderId: string | null; + onRegistered: (asset: SerializedAsset) => void; +}) { + const [url, setUrl] = useState(""); + const [error, setError] = useState(null); + const { mutateAsync: registerAsset, isPending } = useRegisterAsset(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + const trimmed = url.trim(); + if (!trimmed) return; + try { + const filename = trimmed.split("/").pop() ?? "asset"; + const asset = await registerAsset({ + url: trimmed, + filename, + folderId: folderId ?? undefined, + }); + setUrl(""); + onRegistered(asset); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to register URL"); + } + }; + + return ( +
+

+ Paste a public URL to register it as an asset without uploading a file. +

+ +
+ setUrl(e.target.value)} + placeholder="https://example.com/image.png" + className="flex-1" + data-testid="media-url-input" + autoFocus + /> + +
+ {error &&

{error}

} + +
+ ); +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/utils.ts b/packages/stack/src/plugins/media/client/components/media-picker/utils.ts new file mode 100644 index 00000000..b15d3e92 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/utils.ts @@ -0,0 +1,17 @@ +export function matchesAccept(mimeType: string, accept?: string[]) { + if (!accept || accept.length === 0) return true; + return accept.some((a) => { + if (a.endsWith("/*")) return mimeType.startsWith(a.slice(0, -1)); + return mimeType === a; + }); +} + +export function isImage(mimeType: string) { + return mimeType.startsWith("image/"); +} + +export function formatBytes(bytes: number) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} diff --git a/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx b/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx new file mode 100644 index 00000000..edaacbf5 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx @@ -0,0 +1,493 @@ +"use client"; +import { useState, useCallback, useRef, type ComponentType } from "react"; +import { + useAssets, + useDeleteAsset, + useFolders, + useUploadAsset, + useCreateFolder, +} from "../../hooks/use-media"; +import type { SerializedAsset, SerializedFolder } from "../../../types"; +import { Button } from "@workspace/ui/components/button"; +import { Input } from "@workspace/ui/components/input"; +import { + Folder, + FolderOpen, + Image, + File as FileIcon, + Upload, + Trash2, + Search, + X, + ChevronRight, + Loader2, + FolderPlus, + Check, + Copy, +} from "lucide-react"; +import { cn } from "@workspace/ui/lib/utils"; +import { toast } from "sonner"; +import { usePluginOverrides } from "@btst/stack/context"; +import type { MediaPluginOverrides } from "../../overrides"; +import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle"; + +function formatBytes(bytes: number) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +function FolderTreeItem({ + folder, + selectedId, + onSelect, + depth = 0, +}: { + folder: SerializedFolder; + selectedId: string | null; + onSelect: (id: string | null) => void; + depth?: number; +}) { + const [expanded, setExpanded] = useState(false); + const { data: childrenRaw = [] } = useFolders(folder.id); + const children = childrenRaw as SerializedFolder[]; + const { mutateAsync: deleteFolder } = useDeleteFolder(); + + return ( +
+
+ +
+ {expanded && + children.map((child) => ( + + ))} +
+ ); +} + +function useDeleteFolder() { + const { mutateAsync } = useDeleteAsset(); + // Reuse hook but for folders — separate import at top handles this + const { + useDeleteFolder: _useDeleteFolder, + } = require("../../hooks/use-media"); + return _useDeleteFolder(); +} + +function LibrarySidebar({ + selectedFolder, + onSelect, +}: { + selectedFolder: string | null; + onSelect: (id: string | null) => void; +}) { + const { data: rootFoldersRaw = [] } = useFolders(null); + const rootFolders = rootFoldersRaw as SerializedFolder[]; + const [newFolderName, setNewFolderName] = useState(""); + const [isCreating, setIsCreating] = useState(false); + const { mutateAsync: createFolder, isPending } = useCreateFolder(); + + const handleCreate = async () => { + const name = newFolderName.trim(); + if (!name) return; + try { + await createFolder({ name, parentId: selectedFolder ?? undefined }); + setNewFolderName(""); + setIsCreating(false); + toast.success("Folder created"); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to create folder", + ); + } + }; + + return ( +
+
+ + Folders + + +
+ {isCreating && ( +
+ setNewFolderName(e.target.value)} + placeholder="Folder name" + className="h-7 text-xs" + onKeyDown={(e) => { + if (e.key === "Enter") void handleCreate(); + if (e.key === "Escape") setIsCreating(false); + }} + /> + +
+ )} +
+ + {rootFolders.map((folder) => ( + + ))} +
+
+ ); +} + +function AssetCard({ + asset, + onDelete, + ImageComponent, + apiBaseURL, +}: { + asset: SerializedAsset; + onDelete: (id: string) => void; + ImageComponent?: ComponentType< + React.ImgHTMLAttributes & Record + >; + apiBaseURL: string; +}) { + const isImg = asset.mimeType.startsWith("image/"); + + const copyUrl = () => { + let fullUrl: string; + try { + // new URL() handles both absolute and relative URLs and encodes + // special characters (spaces, non-ASCII) in the path correctly. + fullUrl = new URL(asset.url, apiBaseURL).href; + } catch { + fullUrl = asset.url; + } + navigator.clipboard + .writeText(fullUrl) + .then(() => toast.success("URL copied")); + }; + + return ( +
+
+ {isImg ? ( + ImageComponent ? ( + + ) : ( + {asset.alt + ) + ) : ( + + )} +
+
+

+ {asset.originalName} +

+

+ {asset.mimeType} · {formatBytes(asset.size)} +

+

+ {asset.url} +

+
+
+ + +
+
+ ); +} + +export function LibraryPage() { + const overrides = usePluginOverrides< + MediaPluginOverrides, + Partial + >("media", {}); + + useRouteLifecycle({ + routeName: "library", + context: { + path: "/media", + isSSR: typeof window === "undefined", + }, + overrides, + beforeRenderHook: (overrides, context) => { + if (overrides.onBeforeLibraryPageRendered) { + return overrides.onBeforeLibraryPageRendered(context); + } + return true; + }, + }); + + const [selectedFolder, setSelectedFolder] = useState(null); + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const debounceRef = useRef | null>(null); + const [dragging, setDragging] = useState(false); + const fileInputRef = useRef(null); + const { mutateAsync: uploadAsset, isPending: isUploading } = useUploadAsset(); + const { mutateAsync: deleteAsset } = useDeleteAsset(); + const { Image: ImageComponent, apiBaseURL = "" } = overrides; + + const handleSearch = (v: string) => { + setSearch(v); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => setDebouncedSearch(v), 300); + }; + + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = + useAssets({ + folderId: selectedFolder ?? undefined, + query: debouncedSearch || undefined, + limit: 40, + }); + + const assets = data?.pages.flatMap((p) => p.items) ?? []; + + const handleUpload = useCallback( + async (files: FileList | File[]) => { + const arr = Array.from(files); + for (const file of arr) { + try { + await uploadAsset({ file, folderId: selectedFolder ?? undefined }); + toast.success(`Uploaded ${file.name}`); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Upload failed"); + } + } + }, + [selectedFolder, uploadAsset], + ); + + const handleDelete = async (id: string) => { + if (!confirm("Delete this asset?")) return; + try { + await deleteAsset(id); + toast.success("Deleted"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Delete failed"); + } + }; + + return ( +
+ + +
{ + e.preventDefault(); + setDragging(true); + }} + onDragLeave={() => setDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setDragging(false); + void handleUpload(e.dataTransfer.files); + }} + > + {/* Toolbar */} +
+
+ + handleSearch(e.target.value)} + placeholder="Search files…" + className="h-8 pl-8" + /> + {search && ( + + )} +
+ + e.target.files && handleUpload(e.target.files)} + /> +
+ + {/* Drop overlay */} + {dragging && ( +
+
+ +

Drop files to upload

+
+
+ )} + + {/* Asset grid */} +
+ {isLoading ? ( +
+ +
+ ) : assets.length === 0 ? ( +
+ +

+ No files yet. Drag & drop or click Upload. +

+
+ ) : ( +
+ {assets.map((asset) => ( + + ))} +
+ )} + {hasNextPage && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/packages/stack/src/plugins/media/client/components/pages/library-page.tsx b/packages/stack/src/plugins/media/client/components/pages/library-page.tsx new file mode 100644 index 00000000..aec7a8e7 --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/pages/library-page.tsx @@ -0,0 +1,40 @@ +"use client"; +import { lazy } from "react"; +import { usePluginOverrides } from "@btst/stack/context"; +import { ComposedRoute } from "@btst/stack/client/components"; +import type { MediaPluginOverrides } from "../../overrides"; +import { Loader2 } from "lucide-react"; + +const LibraryPage = lazy(() => + import("./library-page.internal").then((m) => ({ default: m.LibraryPage })), +); + +function LibraryLoading() { + return ( +
+ +
+ ); +} + +function LibraryError({ error }: { error: Error }) { + return ( +
+

{error.message}

+
+ ); +} + +export function LibraryPageComponent() { + usePluginOverrides("media"); + return ( + null} + onError={(error) => console.error("[btst/media] Library error:", error)} + /> + ); +} diff --git a/packages/stack/src/plugins/media/client/hooks/index.tsx b/packages/stack/src/plugins/media/client/hooks/index.tsx new file mode 100644 index 00000000..8daddbc2 --- /dev/null +++ b/packages/stack/src/plugins/media/client/hooks/index.tsx @@ -0,0 +1,9 @@ +export { + useAssets, + useFolders, + useUploadAsset, + useRegisterAsset, + useDeleteAsset, + useCreateFolder, + useDeleteFolder, +} from "./use-media"; diff --git a/packages/stack/src/plugins/media/client/hooks/use-media.tsx b/packages/stack/src/plugins/media/client/hooks/use-media.tsx new file mode 100644 index 00000000..f7b11284 --- /dev/null +++ b/packages/stack/src/plugins/media/client/hooks/use-media.tsx @@ -0,0 +1,411 @@ +"use client"; +import { + useInfiniteQuery, + useQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { usePluginOverrides } from "@btst/stack/context"; +import { createApiClient } from "@btst/stack/plugins/client"; +import type { MediaApiRouter } from "../../api/plugin"; +import type { MediaPluginOverrides } from "../overrides"; +import { createMediaQueryKeys } from "../../query-keys"; +import type { AssetListParams } from "../../api/getters"; +import type { SerializedAsset, SerializedFolder } from "../../types"; +import { compressImage } from "../utils/image-compression"; + +function useMediaConfig() { + return usePluginOverrides("media"); +} + +function useMediaApiClient() { + const { apiBaseURL, apiBasePath, headers } = useMediaConfig(); + const client = createApiClient({ + baseURL: apiBaseURL, + basePath: apiBasePath, + }); + return { client, headers }; +} + +/** + * Infinite-scroll list of assets, optionally filtered by folder / MIME type / search. + */ +export function useAssets(params?: AssetListParams) { + const { client, headers } = useMediaApiClient(); + const queries = createMediaQueryKeys(client, headers); + const { queryClient } = useMediaConfig(); + + const limit = params?.limit ?? 20; + + return useInfiniteQuery( + { + ...queries.mediaAssets.list(params), + initialPageParam: 0, + refetchOnMount: "always", + getNextPageParam: ( + lastPage: { + items: SerializedAsset[]; + total: number; + limit?: number; + offset?: number; + }, + _allPages: any[], + lastPageParam: number, + ) => { + const offset = (lastPage.offset ?? 0) + lastPage.items.length; + return offset < lastPage.total ? offset : undefined; + }, + }, + queryClient, + ); +} + +/** + * List of folders, optionally filtered by parentId. + * Pass `null` for root-level folders, `undefined` for all folders. + */ +export function useFolders(parentId?: string | null) { + const { client, headers } = useMediaApiClient(); + const queries = createMediaQueryKeys(client, headers); + const { queryClient } = useMediaConfig(); + + return useQuery( + { + ...queries.mediaFolders.list(parentId), + }, + queryClient, + ); +} + +/** + * Upload an asset — adapter-aware. Handles direct, S3, and Vercel Blob flows. + */ +export function useUploadAsset() { + const { + apiBaseURL, + apiBasePath, + headers, + uploadMode = "direct", + imageCompression, + queryClient: qc, + } = useMediaConfig(); + const reactQueryClient = useQueryClient(qc); + + return useMutation( + { + mutationFn: async ({ + file, + folderId, + }: { + file: File; + folderId?: string; + }): Promise => { + const processedFile = + imageCompression === false + ? file + : await compressImage( + file, + imageCompression ?? { + maxWidth: 2048, + maxHeight: 2048, + quality: 0.85, + }, + ); + + const base = `${apiBaseURL}${apiBasePath}`; + const headersObj = new Headers(headers as HeadersInit | undefined); + + if (uploadMode === "direct") { + const formData = new FormData(); + formData.append("file", processedFile); + if (folderId) formData.append("folderId", folderId); + const res = await fetch(`${base}/media/upload`, { + method: "POST", + headers: headersObj, + body: formData, + }); + if (!res.ok) { + const err = await res + .json() + .catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Upload failed"); + } + return res.json(); + } + + if (uploadMode === "s3") { + const tokenRes = await fetch(`${base}/media/upload/token`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: processedFile.name, + mimeType: processedFile.type, + size: processedFile.size, + folderId, + }), + }); + if (!tokenRes.ok) { + const err = await tokenRes + .json() + .catch(() => ({ message: tokenRes.statusText })); + throw new Error(err.message ?? "Failed to get upload token"); + } + const token = (await tokenRes.json()) as { + type: "presigned-url"; + payload: { + uploadUrl: string; + publicUrl: string; + key: string; + method: "PUT"; + headers: Record; + }; + }; + + const putRes = await fetch(token.payload.uploadUrl, { + method: "PUT", + headers: token.payload.headers, + body: processedFile, + }); + if (!putRes.ok) throw new Error("Failed to upload to S3"); + + const assetRes = await fetch(`${base}/media/assets`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: processedFile.name, + originalName: file.name, + mimeType: processedFile.type, + size: processedFile.size, + url: token.payload.publicUrl, + folderId, + }), + }); + if (!assetRes.ok) { + const err = await assetRes + .json() + .catch(() => ({ message: assetRes.statusText })); + throw new Error(err.message ?? "Failed to register asset"); + } + return assetRes.json(); + } + + if (uploadMode === "vercel-blob") { + // Dynamic import keeps @vercel/blob/client optional + const { upload } = await import("@vercel/blob/client"); + const blob = await upload(processedFile.name, processedFile, { + access: "public", + handleUploadUrl: `${base}/media/upload/vercel-blob`, + clientPayload: JSON.stringify({ + mimeType: processedFile.type, + size: processedFile.size, + }), + }); + const assetRes = await fetch(`${base}/media/assets`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: processedFile.name, + originalName: file.name, + mimeType: processedFile.type, + size: processedFile.size, + url: blob.url, + folderId, + }), + }); + if (!assetRes.ok) { + const err = await assetRes + .json() + .catch(() => ({ message: assetRes.statusText })); + throw new Error(err.message ?? "Failed to register asset"); + } + return assetRes.json(); + } + + throw new Error(`Unknown uploadMode: ${uploadMode}`); + }, + onSuccess: () => { + reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] }); + }, + }, + qc, + ); +} + +/** + * Register an asset URL directly (for when the URL already exists). + */ +export function useRegisterAsset() { + const { + apiBaseURL, + apiBasePath, + headers, + queryClient: qc, + } = useMediaConfig(); + const reactQueryClient = useQueryClient(qc); + + return useMutation( + { + mutationFn: async (input: { + url: string; + filename: string; + mimeType?: string; + size?: number; + folderId?: string; + }): Promise => { + const base = `${apiBaseURL}${apiBasePath}`; + const headersObj = new Headers(headers as HeadersInit | undefined); + const res = await fetch(`${base}/media/assets`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: input.filename, + originalName: input.filename, + mimeType: input.mimeType ?? "application/octet-stream", + size: input.size ?? 0, + url: input.url, + folderId: input.folderId, + }), + }); + if (!res.ok) { + const err = await res + .json() + .catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Failed to register asset"); + } + return res.json(); + }, + onSuccess: () => { + reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] }); + }, + }, + qc, + ); +} + +/** + * Delete an asset by ID. + */ +export function useDeleteAsset() { + const { + apiBaseURL, + apiBasePath, + headers, + queryClient: qc, + } = useMediaConfig(); + const reactQueryClient = useQueryClient(qc); + + return useMutation( + { + mutationFn: async (id: string) => { + const base = `${apiBaseURL}${apiBasePath}`; + const headersObj = new Headers(headers as HeadersInit | undefined); + const res = await fetch(`${base}/media/assets/${id}`, { + method: "DELETE", + headers: headersObj, + }); + if (!res.ok) { + const err = await res + .json() + .catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Delete failed"); + } + }, + onSuccess: () => { + reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] }); + }, + }, + qc, + ); +} + +/** + * Create a new folder. + */ +export function useCreateFolder() { + const { + apiBaseURL, + apiBasePath, + headers, + queryClient: qc, + } = useMediaConfig(); + const reactQueryClient = useQueryClient(qc); + + return useMutation( + { + mutationFn: async (input: { + name: string; + parentId?: string; + }): Promise => { + const base = `${apiBaseURL}${apiBasePath}`; + const headersObj = new Headers(headers as HeadersInit | undefined); + const res = await fetch(`${base}/media/folders`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + if (!res.ok) { + const err = await res + .json() + .catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Failed to create folder"); + } + return res.json(); + }, + onSuccess: () => { + reactQueryClient.invalidateQueries({ queryKey: ["mediaFolders"] }); + }, + }, + qc, + ); +} + +/** + * Delete a folder by ID. + */ +export function useDeleteFolder() { + const { + apiBaseURL, + apiBasePath, + headers, + queryClient: qc, + } = useMediaConfig(); + const reactQueryClient = useQueryClient(qc); + + return useMutation( + { + mutationFn: async (id: string) => { + const base = `${apiBaseURL}${apiBasePath}`; + const headersObj = new Headers(headers as HeadersInit | undefined); + const res = await fetch(`${base}/media/folders/${id}`, { + method: "DELETE", + headers: headersObj, + }); + if (!res.ok) { + const err = await res + .json() + .catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Failed to delete folder"); + } + }, + onSuccess: () => { + reactQueryClient.invalidateQueries({ queryKey: ["mediaFolders"] }); + }, + }, + qc, + ); +} diff --git a/packages/stack/src/plugins/media/client/index.ts b/packages/stack/src/plugins/media/client/index.ts new file mode 100644 index 00000000..4cb8ca40 --- /dev/null +++ b/packages/stack/src/plugins/media/client/index.ts @@ -0,0 +1,2 @@ +export { mediaClientPlugin } from "./plugin"; +export type { MediaPluginOverrides, MediaUploadMode } from "./overrides"; diff --git a/packages/stack/src/plugins/media/client/overrides.ts b/packages/stack/src/plugins/media/client/overrides.ts new file mode 100644 index 00000000..a62ca9f6 --- /dev/null +++ b/packages/stack/src/plugins/media/client/overrides.ts @@ -0,0 +1,127 @@ +import type { ComponentType } from "react"; +import type { QueryClient } from "@tanstack/react-query"; +import type { ImageCompressionOptions } from "./utils/image-compression"; + +/** + * Upload mode — must match the storage adapter configured in mediaBackendPlugin. + * - `"direct"` — local filesystem adapter, files are uploaded via `POST /media/upload` + * - `"s3"` — AWS S3 / R2 / MinIO, the client fetches a presigned token then PUTs directly to S3 + * - `"vercel-blob"` — Vercel Blob, uses the `@vercel/blob/client` SDK for direct upload + */ +export type MediaUploadMode = "direct" | "s3" | "vercel-blob"; + +/** + * Overridable components and functions for the Media plugin. + * + * External consumers provide these when registering the media client plugin + * via the StackProvider overrides. + */ +export interface MediaPluginOverrides { + /** + * Base URL for API calls (e.g., "http://localhost:3000"). + */ + apiBaseURL: string; + + /** + * Path where the API is mounted (e.g., "/api/data"). + */ + apiBasePath: string; + + /** + * React Query client — used by the MediaPicker to cache and fetch assets. + */ + queryClient: QueryClient; + + /** + * Upload mode — must match the storageAdapter configured in mediaBackendPlugin. + * @default "direct" + */ + uploadMode?: MediaUploadMode; + + /** + * Optional headers to pass with API requests (e.g., for SSR auth). + */ + headers?: HeadersInit; + + /** + * Navigation function for programmatic navigation. + */ + navigate: (path: string) => void | Promise; + + /** + * Link component for navigation within the media library page. + */ + Link?: ComponentType & Record>; + + /** + * Image component for rendering asset thumbnails and previews. + * + * When provided, replaces the default `` element in asset cards, + * the media library grid, and the ImageInputField preview. Use this + * to plug in Next.js `` for automatic optimisation. + * + * @example + * ```tsx + * Image: (props) => + * ``` + */ + Image?: ComponentType< + React.ImgHTMLAttributes & Record + >; + + /** + * Client-side image compression applied before upload via the Canvas API. + * + * Images are scaled down to fit within `maxWidth` × `maxHeight` (preserving + * aspect ratio) and re-encoded at `quality`. SVG and GIF files are always + * passed through unchanged. + * + * Set to `false` to disable compression entirely. + * + * @default { maxWidth: 2048, maxHeight: 2048, quality: 0.85 } + */ + imageCompression?: ImageCompressionOptions | false; + + // ============ Lifecycle Hooks ============ + + /** + * Called when a media route is rendered. + */ + onRouteRender?: ( + routeName: string, + context: MediaRouteContext, + ) => void | Promise; + + /** + * Called when a media route encounters an error. + */ + onRouteError?: ( + routeName: string, + error: Error, + context: MediaRouteContext, + ) => void | Promise; + + /** + * Called before the media library page is rendered. + * Return `false` to prevent rendering (e.g., redirect unauthenticated users). + * + * @example + * ```ts + * media: { + * onBeforeLibraryPageRendered: (context) => !!currentUser?.isAdmin, + * onRouteError: (routeName, error, context) => navigate("/login"), + * } + * ``` + */ + onBeforeLibraryPageRendered?: (context: MediaRouteContext) => boolean; +} + +export interface MediaRouteContext { + /** Current route path */ + path: string; + /** Route parameters */ + params?: Record; + /** Whether rendering on server (true) or client (false) */ + isSSR: boolean; + [key: string]: unknown; +} diff --git a/packages/stack/src/plugins/media/client/plugin.tsx b/packages/stack/src/plugins/media/client/plugin.tsx new file mode 100644 index 00000000..5d729a36 --- /dev/null +++ b/packages/stack/src/plugins/media/client/plugin.tsx @@ -0,0 +1,171 @@ +import { + defineClientPlugin, + createApiClient, + isConnectionError, +} from "@btst/stack/plugins/client"; +import { createRoute } from "@btst/yar"; +import type { QueryClient } from "@tanstack/react-query"; +import { LibraryPageComponent } from "./components/pages/library-page"; +import { createMediaQueryKeys } from "../query-keys"; +import type { MediaApiRouter } from "../api/plugin"; + +export interface MediaLoaderContext { + path: string; + isSSR: boolean; + apiBaseURL: string; + apiBasePath: string; + headers?: HeadersInit; +} + +export interface MediaClientHooks { + /** Called before the media library data is fetched during SSR. Throw to cancel. */ + beforeLoadLibrary?: (context: MediaLoaderContext) => Promise | void; + + /** Called after the media library data is fetched during SSR. */ + afterLoadLibrary?: (context: MediaLoaderContext) => Promise | void; + + /** Called when an error occurs during the SSR loader. */ + onLoadError?: ( + error: Error, + context: MediaLoaderContext, + ) => Promise | void; +} + +export interface MediaClientConfig { + /** Base URL for API calls (e.g., "http://localhost:3000") */ + apiBaseURL: string; + /** Path where the API is mounted (e.g., "/api/data") */ + apiBasePath: string; + /** Base URL of your site for SEO meta tags */ + siteBaseURL: string; + /** Path where pages are mounted (e.g., "/pages") */ + siteBasePath: string; + /** React Query client — used by the SSR loader to prefetch data */ + queryClient: QueryClient; + /** Optional headers forwarded with SSR API requests (e.g. auth cookies) */ + headers?: HeadersInit; + /** Optional lifecycle hooks for the media client plugin */ + hooks?: MediaClientHooks; +} + +/** + * Media client plugin. + * Registers the /media library route. + * + * Configure overrides in StackProvider: + * ```tsx + * + * ``` + * + * @example + * ```ts + * import { mediaClientPlugin } from "@btst/stack/plugins/media/client" + * + * const clientPlugins = [ + * mediaClientPlugin({ apiBaseURL, apiBasePath, siteBaseURL, siteBasePath, queryClient }), + * // ...other plugins + * ] + * ``` + */ +export const mediaClientPlugin = (config: MediaClientConfig) => + defineClientPlugin({ + name: "media", + + routes: () => ({ + library: createRoute("/media", () => ({ + PageComponent: LibraryPageComponent, + loader: createMediaLibraryLoader(config), + meta: createMediaLibraryMeta(config), + })), + }), + }); + +function createMediaLibraryLoader(config: MediaClientConfig) { + return async () => { + if (typeof window === "undefined") { + const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config; + + const context: MediaLoaderContext = { + path: "/media", + isSSR: true, + apiBaseURL, + apiBasePath, + headers, + }; + + try { + if (hooks?.beforeLoadLibrary) { + await hooks.beforeLoadLibrary(context); + } + + const client = createApiClient({ + baseURL: apiBaseURL, + basePath: apiBasePath, + }); + const queries = createMediaQueryKeys(client, headers); + + // Prefetch initial asset grid (infinite query — root folder, default limit) + await queryClient.prefetchInfiniteQuery({ + ...queries.mediaAssets.list({ limit: 40 }), + initialPageParam: 0, + }); + + // Prefetch root-level folders for the sidebar tree + await queryClient.prefetchQuery(queries.mediaFolders.list(null)); + + if (hooks?.afterLoadLibrary) { + await hooks.afterLoadLibrary(context); + } + + const queryState = queryClient.getQueryState( + queries.mediaAssets.list({ limit: 40 }).queryKey, + ); + if (queryState?.error && hooks?.onLoadError) { + const error = + queryState.error instanceof Error + ? queryState.error + : new Error(String(queryState.error)); + await hooks.onLoadError(error, context); + } + } catch (error) { + if (isConnectionError(error)) { + console.warn( + "[btst/media] route.loader() failed — no server running at build time. " + + "The media library does not support SSG.", + ); + } + if (hooks?.onLoadError) { + await hooks.onLoadError(error as Error, context); + } + } + } + }; +} + +function createMediaLibraryMeta(config: MediaClientConfig) { + return () => { + const { siteBaseURL, siteBasePath } = config; + const fullUrl = `${siteBaseURL}${siteBasePath}/media`; + const title = "Media Library"; + + return [ + { title }, + { name: "title", content: title }, + { name: "description", content: "Manage your media assets" }, + { name: "robots", content: "noindex, nofollow" }, + + // Open Graph + { property: "og:type", content: "website" }, + { property: "og:title", content: title }, + { + property: "og:description", + content: "Manage your media assets", + }, + { property: "og:url", content: fullUrl }, + + // Twitter + { name: "twitter:card", content: "summary" }, + { name: "twitter:title", content: title }, + ]; + }; +} diff --git a/packages/stack/src/plugins/media/client/utils/image-compression.ts b/packages/stack/src/plugins/media/client/utils/image-compression.ts new file mode 100644 index 00000000..b7111d8d --- /dev/null +++ b/packages/stack/src/plugins/media/client/utils/image-compression.ts @@ -0,0 +1,131 @@ +/** + * Canvas-based client-side image compression. + * + * Skips SVG and GIF (vector data / animation would be lost on a canvas round-trip). + * All other image/* types are scaled down to fit within maxWidth × maxHeight + * (preserving aspect ratio) and re-encoded at the configured quality. + */ + +export interface ImageCompressionOptions { + /** + * Maximum width in pixels. Images wider than this are scaled down. + * @default 2048 + */ + maxWidth?: number; + + /** + * Maximum height in pixels. Images taller than this are scaled down. + * @default 2048 + */ + maxHeight?: number; + + /** + * Encoding quality (0–1). Applies to JPEG and WebP. + * @default 0.85 + */ + quality?: number; + + /** + * Output MIME type. Defaults to the source image's MIME type. + * Set to `"image/webp"` for better compression at the cost of format change. + */ + outputFormat?: string; +} + +function loadImage(file: File): Promise { + return new Promise((resolve, reject) => { + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + URL.revokeObjectURL(url); + resolve(img); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error(`Failed to load image: ${file.name}`)); + }; + img.src = url; + }); +} + +const SKIP_TYPES = new Set(["image/svg+xml", "image/gif"]); + +/** + * Compresses an image file client-side using the Canvas API. + * + * Returns the original file unchanged if: + * - The file is not an image + * - The MIME type is SVG or GIF (would lose vector data / animation) + * - The browser does not support canvas (SSR guard) + */ +export async function compressImage( + file: File, + options: ImageCompressionOptions = {}, +): Promise { + if (!file.type.startsWith("image/") || SKIP_TYPES.has(file.type)) { + return file; + } + + // SSR guard — canvas is only available in the browser + if (typeof document === "undefined") return file; + + const { + maxWidth = 2048, + maxHeight = 2048, + quality = 0.85, + outputFormat, + } = options; + + const img = await loadImage(file); + + let { width, height } = img; + + const needsResize = width > maxWidth || height > maxHeight; + const needsFormatChange = + outputFormat !== undefined && outputFormat !== file.type; + + // Skip canvas entirely if the image is already within the limits and no + // format conversion is needed — re-encoding a small image can make it larger. + if (!needsResize && !needsFormatChange) return file; + + // Scale down proportionally if either dimension exceeds the max + if (needsResize) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width = Math.round(width * ratio); + height = Math.round(height * ratio); + } + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + if (!ctx) return file; + + ctx.drawImage(img, 0, 0, width, height); + + const mimeType = outputFormat ?? file.type; + + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => { + if (!blob) { + reject(new Error("canvas.toBlob returned null")); + return; + } + + // Preserve the original filename, updating extension only if + // the output format changed from the source. + let name = file.name; + if (outputFormat && outputFormat !== file.type) { + const ext = outputFormat.split("/")[1] ?? "jpg"; + name = name.replace(/\.[^.]+$/, `.${ext}`); + } + + resolve(new File([blob], name, { type: mimeType })); + }, + mimeType, + quality, + ); + }); +} diff --git a/packages/stack/src/plugins/media/query-keys.ts b/packages/stack/src/plugins/media/query-keys.ts new file mode 100644 index 00000000..d892e33d --- /dev/null +++ b/packages/stack/src/plugins/media/query-keys.ts @@ -0,0 +1,96 @@ +import { + mergeQueryKeys, + createQueryKeys, +} from "@lukemorales/query-key-factory"; +import { createApiClient } from "@btst/stack/plugins/client"; +import type { MediaApiRouter } from "./api/plugin"; +import type { SerializedAsset, SerializedFolder } from "./types"; +import { assetListDiscriminator } from "./api/query-key-defs"; +import type { AssetListParams } from "./api/getters"; + +function isErrorResponse(response: unknown): response is { error: unknown } { + return ( + typeof response === "object" && + response !== null && + "error" in response && + (response as Record).error !== null && + (response as Record).error !== undefined + ); +} + +function toError(error: unknown): Error { + if (error instanceof Error) return error; + if (typeof error === "object" && error !== null) { + const errorObj = error as Record; + const message = + (typeof errorObj.message === "string" ? errorObj.message : null) || + JSON.stringify(error); + const err = new Error(message); + Object.assign(err, error); + return err; + } + return new Error(String(error)); +} + +export function createMediaQueryKeys( + client: ReturnType>, + headers?: HeadersInit, +) { + return mergeQueryKeys( + createQueryKeys("mediaAssets", { + list: (params?: AssetListParams) => ({ + queryKey: [assetListDiscriminator(params)], + queryFn: async ({ pageParam }: { pageParam?: number }) => { + const response = await (client as any)("/media/assets", { + method: "GET", + query: { + folderId: params?.folderId, + mimeType: params?.mimeType, + query: params?.query, + offset: pageParam ?? params?.offset ?? 0, + limit: params?.limit ?? 20, + }, + headers, + }); + if (isErrorResponse(response)) throw toError(response.error); + const data = (response as any).data as { + items: SerializedAsset[]; + total: number; + limit?: number; + offset?: number; + }; + return data; + }, + }), + detail: (id: string) => ({ + queryKey: [id], + queryFn: async () => { + const response = await (client as any)("/media/assets", { + method: "GET", + query: { id }, + headers, + }); + if (isErrorResponse(response)) throw toError(response.error); + return (response as any).data as SerializedAsset | null; + }, + }), + }), + createQueryKeys("mediaFolders", { + list: (parentId?: string | null) => ({ + queryKey: [parentId ?? "root"], + queryFn: async () => { + const response = await (client as any)("/media/folders", { + method: "GET", + query: + parentId !== undefined ? { parentId: parentId ?? undefined } : {}, + headers, + }); + if (isErrorResponse(response)) throw toError(response.error); + return (response as any).data as SerializedFolder[]; + }, + }), + }), + ); +} + +export type MediaQueryKeys = ReturnType; diff --git a/packages/stack/src/plugins/media/schemas.ts b/packages/stack/src/plugins/media/schemas.ts index 052b9b17..9af0b820 100644 --- a/packages/stack/src/plugins/media/schemas.ts +++ b/packages/stack/src/plugins/media/schemas.ts @@ -12,7 +12,8 @@ export const createAssetSchema = z.object({ filename: z.string().min(1), originalName: z.string().min(1), mimeType: z.string().min(1), - size: z.number().int().positive(), + // Allow 0 for URL-registered assets where size is unknown at registration time. + size: z.number().int().min(0), url: z.string().url(), folderId: z.string().optional(), alt: z.string().optional(), diff --git a/packages/stack/src/plugins/media/style.css b/packages/stack/src/plugins/media/style.css new file mode 100644 index 00000000..7513c307 --- /dev/null +++ b/packages/stack/src/plugins/media/style.css @@ -0,0 +1 @@ +@source "./client/**/*.{ts,tsx}"; diff --git a/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.tsx b/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.tsx index 8a13b4e0..77976c10 100644 --- a/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.tsx +++ b/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.tsx @@ -1,17 +1,22 @@ import * as React from "react" import type { Editor } from "@tiptap/react" +import type { ComponentType } from "react" import { Button } from "@workspace/ui/components/button" import { Label } from "@workspace/ui/components/label" import { Input } from "@workspace/ui/components/input" +import { Separator } from "@workspace/ui/components/separator" interface ImageEditBlockProps { editor: Editor close: () => void + /** Optional trigger for a media library picker. When provided, rendered as a "Browse media" section. */ + imagePickerTrigger?: ComponentType<{ onSelect: (url: string) => void }> } export const ImageEditBlock: React.FC = ({ editor, close, + imagePickerTrigger: ImagePickerTrigger, }) => { const fileInputRef = React.useRef(null) const [link, setLink] = React.useState("") @@ -79,6 +84,22 @@ export const ImageEditBlock: React.FC = ({ + {ImagePickerTrigger && ( + <> + +
+ + { + editor.commands.setImages([{ src: url }]) + close() + }} + /> +
+ + )} { editor: Editor + imagePickerTrigger?: ComponentType<{ onSelect: (url: string) => void }> } -const ImageEditDialog = ({ editor, size, variant }: ImageEditDialogProps) => { +const ImageEditDialog = ({ editor, size, variant, imagePickerTrigger }: ImageEditDialogProps) => { const [open, setOpen] = useState(false) return ( @@ -41,7 +43,7 @@ const ImageEditDialog = ({ editor, size, variant }: ImageEditDialogProps) => { Upload an image from your computer - setOpen(false)} /> + setOpen(false)} imagePickerTrigger={imagePickerTrigger} /> ) diff --git a/packages/ui/src/components/minimal-tiptap/components/section/five.tsx b/packages/ui/src/components/minimal-tiptap/components/section/five.tsx index c325e909..0623b7a2 100644 --- a/packages/ui/src/components/minimal-tiptap/components/section/five.tsx +++ b/packages/ui/src/components/minimal-tiptap/components/section/five.tsx @@ -1,5 +1,6 @@ import * as React from "react" import type { Editor } from "@tiptap/react" +import type { ComponentType } from "react" import type { FormatAction } from "../../types" import type { toggleVariants } from "@workspace/ui/components/toggle" import type { VariantProps } from "class-variance-authority" @@ -56,6 +57,7 @@ interface SectionFiveProps extends VariantProps { editor: Editor activeActions?: InsertElementAction[] mainActionCount?: number + imagePickerTrigger?: ComponentType<{ onSelect: (url: string) => void }> } export const SectionFive: React.FC = ({ @@ -64,11 +66,12 @@ export const SectionFive: React.FC = ({ mainActionCount = 0, size, variant, + imagePickerTrigger, }) => { return ( <> - + void className?: string editorContentClassName?: string + /** + * Optional trigger component for a media picker. + * When provided, it appears inside the image insertion dialog as a "Browse media" section. + * Receives `onSelect(url)` — the URL is inserted as an image node. + */ + imagePickerTrigger?: ComponentType<{ onSelect: (url: string) => void }> } -const Toolbar = ({ editor }: { editor: Editor }) => ( +const Toolbar = ({ + editor, + imagePickerTrigger, +}: { + editor: Editor + imagePickerTrigger?: ComponentType<{ onSelect: (url: string) => void }> +}) => (
@@ -63,6 +76,7 @@ const Toolbar = ({ editor }: { editor: Editor }) => ( editor={editor} activeActions={["codeBlock", "blockquote", "horizontalRule"]} mainActionCount={0} + imagePickerTrigger={imagePickerTrigger} />
@@ -73,6 +87,7 @@ export const MinimalTiptapEditor = ({ onChange, className, editorContentClassName, + imagePickerTrigger, ...props }: MinimalTiptapProps) => { const editor = useMinimalTiptapEditor({ @@ -91,6 +106,7 @@ export const MinimalTiptapEditor = ({ editor={editor} className={className} editorContentClassName={editorContentClassName} + imagePickerTrigger={imagePickerTrigger} /> ) @@ -104,6 +120,7 @@ export const MainMinimalTiptapEditor = ({ editor: providedEditor, className, editorContentClassName, + imagePickerTrigger, }: MinimalTiptapProps & { editor: Editor }) => { const { editor } = useTiptapEditor(providedEditor) @@ -121,7 +138,7 @@ export const MainMinimalTiptapEditor = ({ className )} > - + Date: Fri, 20 Mar 2026 11:55:26 -0400 Subject: [PATCH 02/29] fix: remove hidden class from media picker button to ensure visibility --- examples/react-router/app/routes/pages/_layout.tsx | 1 - examples/tanstack/src/routes/pages/route.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index 91f11530..0734bb15 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -243,7 +243,6 @@ const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { variant="outline" size="sm" data-testid="open-media-picker" - className="hidden" > Browse Media diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index 332011b4..d4147d16 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -247,7 +247,6 @@ const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { variant="outline" size="sm" data-testid="open-media-picker" - className="hidden" > Browse Media From fec7455ed357e49410340be631997ab34120d6cc Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 11:57:41 -0400 Subject: [PATCH 03/29] fix: remove unnecessary Content-Type header from media file upload request --- .../src/plugins/media/client/components/media-picker/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx index 486d3155..39afcff4 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -317,9 +317,6 @@ export async function uploadMediaFile( formData.append("file", file); const res = await fetch(`${baseURL}/api/data/media/upload`, { method: "POST", - headers: { - "Content-Type": "multipart/form-data", - }, body: formData, }); if (!res.ok) { From 78434111042f4b1d3671f051eac2bbfee44e62f3 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 11:58:01 -0400 Subject: [PATCH 04/29] feat: integrate folder deletion functionality into media library page --- .../client/components/pages/library-page.internal.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx b/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx index edaacbf5..29394ea7 100644 --- a/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx +++ b/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx @@ -3,6 +3,7 @@ import { useState, useCallback, useRef, type ComponentType } from "react"; import { useAssets, useDeleteAsset, + useDeleteFolder, useFolders, useUploadAsset, useCreateFolder, @@ -102,15 +103,6 @@ function FolderTreeItem({ ); } -function useDeleteFolder() { - const { mutateAsync } = useDeleteAsset(); - // Reuse hook but for folders — separate import at top handles this - const { - useDeleteFolder: _useDeleteFolder, - } = require("../../hooks/use-media"); - return _useDeleteFolder(); -} - function LibrarySidebar({ selectedFolder, onSelect, From d6e63b900337b00ca23cc6619e86e18aba45f536 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:07:44 -0400 Subject: [PATCH 05/29] feat: enhance file validation in media backend plugin to ensure proper file attributes --- .../stack/src/plugins/media/api/plugin.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/stack/src/plugins/media/api/plugin.ts b/packages/stack/src/plugins/media/api/plugin.ts index 3bac9ffc..29471015 100644 --- a/packages/stack/src/plugins/media/api/plugin.ts +++ b/packages/stack/src/plugins/media/api/plugin.ts @@ -595,6 +595,28 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) => }); } + if ( + typeof (fileRaw as any).size !== "number" || + (fileRaw as any).size < 0 + ) { + throw ctx.error(400, { + message: "File 'size' is missing or invalid", + }); + } + if ( + typeof (fileRaw as any).name !== "string" || + !(fileRaw as any).name + ) { + throw ctx.error(400, { + message: "File 'name' is missing or invalid", + }); + } + if (typeof (fileRaw as any).type !== "string") { + throw ctx.error(400, { + message: "File 'type' is missing or invalid", + }); + } + // Safe to treat as a File-like object after the duck-type check above. const file = fileRaw as Pick< File, @@ -628,7 +650,10 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) => } const buffer = Buffer.from(await file.arrayBuffer()); - const folderId = (body.folderId as string | undefined) ?? undefined; + const folderId = + typeof body.folderId === "string" && body.folderId + ? body.folderId + : undefined; if (folderId) { const folder = await getFolderById(adapter, folderId); From 0ead2a2ef5de1548bb1fe9b2226e795cebe51f3c Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:09:44 -0400 Subject: [PATCH 06/29] fix: update URL validation in media asset schema to use httpUrl for improved accuracy --- packages/stack/src/plugins/media/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/src/plugins/media/schemas.ts b/packages/stack/src/plugins/media/schemas.ts index 9af0b820..ee1147a1 100644 --- a/packages/stack/src/plugins/media/schemas.ts +++ b/packages/stack/src/plugins/media/schemas.ts @@ -14,7 +14,7 @@ export const createAssetSchema = z.object({ mimeType: z.string().min(1), // Allow 0 for URL-registered assets where size is unknown at registration time. size: z.number().int().min(0), - url: z.string().url(), + url: z.httpUrl(), folderId: z.string().optional(), alt: z.string().optional(), }); From fb09292e869bc007c80f1fdb668d98b6d9887f28 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:21:52 -0400 Subject: [PATCH 07/29] build: move media adapters from main entry --- packages/stack/build.config.ts | 2 ++ packages/stack/package.json | 26 +++++++++++++++++++ packages/stack/src/plugins/media/api/index.ts | 10 ------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/stack/build.config.ts b/packages/stack/build.config.ts index 27715027..86fce7e8 100644 --- a/packages/stack/build.config.ts +++ b/packages/stack/build.config.ts @@ -117,6 +117,8 @@ export default defineBuildConfig({ "./src/plugins/comments/query-keys.ts", // media plugin entries "./src/plugins/media/api/index.ts", + "./src/plugins/media/api/adapters/s3.ts", + "./src/plugins/media/api/adapters/vercel-blob.ts", "./src/plugins/media/client/index.ts", "./src/plugins/media/client/components/index.tsx", "./src/plugins/media/client/hooks/index.tsx", diff --git a/packages/stack/package.json b/packages/stack/package.json index 92da185d..1bd0dab9 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -424,6 +424,26 @@ "default": "./dist/plugins/media/api/index.cjs" } }, + "./plugins/media/api/adapters/s3": { + "import": { + "types": "./dist/plugins/media/api/adapters/s3.d.ts", + "default": "./dist/plugins/media/api/adapters/s3.mjs" + }, + "require": { + "types": "./dist/plugins/media/api/adapters/s3.d.cts", + "default": "./dist/plugins/media/api/adapters/s3.cjs" + } + }, + "./plugins/media/api/adapters/vercel-blob": { + "import": { + "types": "./dist/plugins/media/api/adapters/vercel-blob.d.ts", + "default": "./dist/plugins/media/api/adapters/vercel-blob.mjs" + }, + "require": { + "types": "./dist/plugins/media/api/adapters/vercel-blob.d.cts", + "default": "./dist/plugins/media/api/adapters/vercel-blob.cjs" + } + }, "./plugins/media/client": { "import": { "types": "./dist/plugins/media/client/index.d.ts", @@ -664,6 +684,12 @@ "plugins/media/api": [ "./dist/plugins/media/api/index.d.ts" ], + "plugins/media/api/adapters/s3": [ + "./dist/plugins/media/api/adapters/s3.d.ts" + ], + "plugins/media/api/adapters/vercel-blob": [ + "./dist/plugins/media/api/adapters/vercel-blob.d.ts" + ], "plugins/media/client": [ "./dist/plugins/media/client/index.d.ts" ], diff --git a/packages/stack/src/plugins/media/api/index.ts b/packages/stack/src/plugins/media/api/index.ts index 603edc11..789fca25 100644 --- a/packages/stack/src/plugins/media/api/index.ts +++ b/packages/stack/src/plugins/media/api/index.ts @@ -30,16 +30,6 @@ export { type LocalStorageAdapterOptions, } from "./adapters/local"; -export { - s3Adapter, - type S3StorageAdapterOptions, -} from "./adapters/s3"; - -export { - vercelBlobAdapter, - type VercelBlobStorageAdapterOptions, -} from "./adapters/vercel-blob"; - export type { StorageAdapter, DirectStorageAdapter, From d0f1c8c9b521d8b3e8dd39e1ea0e145d2fdd04ba Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:23:45 -0400 Subject: [PATCH 08/29] refactor: reorganize image input handling in CMSFileUpload component for improved clarity and functionality --- .../client/components/forms/file-upload.tsx | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx b/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx index e1dd02e3..b76f7a68 100644 --- a/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx +++ b/packages/stack/src/plugins/cms/client/components/forms/file-upload.tsx @@ -86,28 +86,7 @@ export function CMSFileUpload({ } = fieldProps; const showLabel = _showLabel === undefined ? true : _showLabel; - // When a custom imageInputField component is provided via overrides, delegate to it. - if (ImageInputField) { - return ( - - {showLabel && ( - - )} - - - - - - - ); - } + // All hooks must be called unconditionally before any early return. const [isUploading, setIsUploading] = useState(false); const [previewUrl, setPreviewUrl] = useState( field.value || null, @@ -125,7 +104,6 @@ export function CMSFileUpload({ const file = e.target.files?.[0]; if (!file) return; - // Check if it's an image if (!file.type.startsWith("image/")) { toast.error("Please select an image file"); return; @@ -151,6 +129,29 @@ export function CMSFileUpload({ field.onChange(""); }, [field]); + // When a custom imageInputField component is provided via overrides, delegate to it. + if (ImageInputField) { + return ( + + {showLabel && ( + + )} + + + + + + + ); + } + return ( {showLabel && ( From 95288964a474aa46774276b8d45d07e364e4037c Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:26:32 -0400 Subject: [PATCH 09/29] refactor: export FolderTreeItem component and clean up library page imports for better modularity --- .../components/media-picker/folder-tree.tsx | 2 +- .../pages/library-page.internal.tsx | 76 +------------------ 2 files changed, 3 insertions(+), 75 deletions(-) diff --git a/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx b/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx index 12ea1a42..22766e9c 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx @@ -128,7 +128,7 @@ export function FolderTree({ ); } -function FolderTreeItem({ +export function FolderTreeItem({ folder, selectedId, onSelect, diff --git a/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx b/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx index 29394ea7..cac7fb99 100644 --- a/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx +++ b/packages/stack/src/plugins/media/client/components/pages/library-page.internal.tsx @@ -3,7 +3,6 @@ import { useState, useCallback, useRef, type ComponentType } from "react"; import { useAssets, useDeleteAsset, - useDeleteFolder, useFolders, useUploadAsset, useCreateFolder, @@ -13,14 +12,12 @@ import { Button } from "@workspace/ui/components/button"; import { Input } from "@workspace/ui/components/input"; import { Folder, - FolderOpen, Image, File as FileIcon, Upload, Trash2, Search, X, - ChevronRight, Loader2, FolderPlus, Check, @@ -31,77 +28,8 @@ import { toast } from "sonner"; import { usePluginOverrides } from "@btst/stack/context"; import type { MediaPluginOverrides } from "../../overrides"; import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle"; - -function formatBytes(bytes: number) { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / 1024 / 1024).toFixed(1)} MB`; -} - -function FolderTreeItem({ - folder, - selectedId, - onSelect, - depth = 0, -}: { - folder: SerializedFolder; - selectedId: string | null; - onSelect: (id: string | null) => void; - depth?: number; -}) { - const [expanded, setExpanded] = useState(false); - const { data: childrenRaw = [] } = useFolders(folder.id); - const children = childrenRaw as SerializedFolder[]; - const { mutateAsync: deleteFolder } = useDeleteFolder(); - - return ( -
-
- -
- {expanded && - children.map((child) => ( - - ))} -
- ); -} +import { formatBytes } from "../media-picker/utils"; +import { FolderTreeItem } from "../media-picker/folder-tree"; function LibrarySidebar({ selectedFolder, From 7d38e265e30e857384a47deafbcf14feafef1ab0 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:26:44 -0400 Subject: [PATCH 10/29] refactor: update uploadMediaFile function to accept apiBasePath parameter for improved flexibility in media uploads --- examples/nextjs/app/pages/layout.tsx | 2 +- examples/react-router/app/routes/pages/_layout.tsx | 2 +- examples/tanstack/src/routes/pages/route.tsx | 2 +- .../media/client/components/media-picker/index.tsx | 8 ++++++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/nextjs/app/pages/layout.tsx b/examples/nextjs/app/pages/layout.tsx index ddcc578e..7f160c56 100644 --- a/examples/nextjs/app/pages/layout.tsx +++ b/examples/nextjs/app/pages/layout.tsx @@ -89,7 +89,7 @@ export default function ExampleLayout({ const uploadImage = React.useCallback( async (file: File) => { - const asset = await uploadMediaFile(file, baseURL) + const asset = await uploadMediaFile(file, baseURL, "/api/data") return asset.url; }, [baseURL]); diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index 0734bb15..0e1ed352 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -44,7 +44,7 @@ export default function Layout() { const [queryClient] = useState(() => getOrCreateQueryClient()) const uploadImage = async (file: File) => { - const asset = await uploadMediaFile(file, baseURL) + const asset = await uploadMediaFile(file, baseURL, "/api/data") return asset.url } diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index d4147d16..75e88c48 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -47,7 +47,7 @@ function Layout() { const baseURL = getBaseURL() const uploadImage = async (file: File) => { - const asset = await uploadMediaFile(file, baseURL) + const asset = await uploadMediaFile(file, baseURL, "/api/data") return asset.url } diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx index 39afcff4..021562bc 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -307,15 +307,19 @@ export function ImageInputField({ } /** - * Upload a file via the media plugin's direct upload endpoint + * Upload a file via the media plugin's direct upload endpoint. + * @param file - The file to upload. + * @param baseURL - The base URL of the server (e.g. `https://example.com`). + * @param apiBasePath - The API base path configured for the media plugin (e.g. `/api/v2`). Defaults to `/api/data`. */ export async function uploadMediaFile( file: File, baseURL: string, + apiBasePath: string, ): Promise { const formData = new FormData(); formData.append("file", file); - const res = await fetch(`${baseURL}/api/data/media/upload`, { + const res = await fetch(`${baseURL}${apiBasePath}/media/upload`, { method: "POST", body: formData, }); From bc4567d215bb6b13cd0664771b2f1959c1841e63 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:35:00 -0400 Subject: [PATCH 11/29] refactor: enhance ImageInputField component with data-testid attributes and improve button structure for better accessibility --- .../media/client/components/media-picker/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx index 021562bc..3100ab22 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -264,12 +264,14 @@ export function ImageInputField({ className="h-auto w-full max-w-xs rounded-md border object-cover" width={400} height={300} + data-testid="image-preview" /> ) : ( Featured image preview )}
@@ -282,7 +284,12 @@ export function ImageInputField({ accept={["image/*"]} onSelect={(assets) => onChange(assets[0]?.url ?? "")} /> -
From e164728e55919e176d6a378e8d6cff2eca177a01 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 12:40:53 -0400 Subject: [PATCH 12/29] refactor: conditionally render image in CMSExampleContent component to prevent empty image elements --- examples/nextjs/app/cms-example/page.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/nextjs/app/cms-example/page.tsx b/examples/nextjs/app/cms-example/page.tsx index 66b27a90..ecba229a 100644 --- a/examples/nextjs/app/cms-example/page.tsx +++ b/examples/nextjs/app/cms-example/page.tsx @@ -145,16 +145,18 @@ function CMSExampleContent() { >
-
- {item.parsedData.name} -
+ {item.parsedData.image && ( +
+ {item.parsedData.name} +
+ )}
{/* No more type guards needed - parsedData is fully typed! */} From 17a9e573740fcc463b053af1911aa067e59b391a Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 13:29:17 -0400 Subject: [PATCH 13/29] refactor: update external registry URLs to point to the new image upload configuration for improved media handling --- packages/stack/registry/btst-blog.json | 8 ++++---- packages/stack/registry/btst-cms.json | 6 +++--- packages/stack/registry/btst-kanban.json | 8 ++++---- packages/stack/registry/registry.json | 2 +- packages/stack/scripts/build-registry.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/stack/registry/btst-blog.json b/packages/stack/registry/btst-blog.json index f3f6ca66..22aeffd6 100644 --- a/packages/stack/registry/btst-blog.json +++ b/packages/stack/registry/btst-blog.json @@ -58,7 +58,7 @@ { "path": "btst/blog/client/components/forms/image-field.tsx", "type": "registry:component", - "content": "import { Button } from \"@/components/ui/button\";\nimport {\n\tFormControl,\n\tFormDescription,\n\tFormItem,\n\tFormLabel,\n\tFormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { Loader2, Upload } from \"lucide-react\";\nimport { useRef, useState } from \"react\";\nimport { toast } from \"sonner\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\n\nexport function FeaturedImageField({\n\tisRequired,\n\tvalue,\n\tonChange,\n\tsetFeaturedImageUploading,\n}: {\n\tisRequired?: boolean;\n\tvalue?: string;\n\tonChange: (value: string) => void;\n\tsetFeaturedImageUploading: (uploading: boolean) => void;\n}) {\n\tconst fileInputRef = useRef(null);\n\tconst [isUploading, setIsUploading] = useState(false);\n\n\tconst { uploadImage, Image, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", { localization: BLOG_LOCALIZATION });\n\n\tconst ImageComponent = Image ? Image : DefaultImage;\n\n\tconst handleImageUpload = async (\n\t\tevent: React.ChangeEvent,\n\t) => {\n\t\tconst file = event.target.files?.[0];\n\t\tif (!file) return;\n\n\t\tif (!file.type.startsWith(\"image/\")) {\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_ERROR_NOT_IMAGE);\n\t\t\treturn;\n\t\t}\n\n\t\tif (file.size > 4 * 1024 * 1024) {\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_ERROR_TOO_LARGE);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tsetIsUploading(true);\n\t\t\tsetFeaturedImageUploading(true);\n\t\t\tconst url = await uploadImage(file);\n\t\t\tonChange(url);\n\t\t\ttoast.success(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_SUCCESS);\n\t\t} catch (error) {\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_FAILURE);\n\t\t\tconsole.error(\"Failed to upload image:\", error);\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_FAILURE);\n\t\t} finally {\n\t\t\tsetIsUploading(false);\n\t\t\tsetFeaturedImageUploading(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_LABEL}\n\t\t\t\t{isRequired && (\n\t\t\t\t\t\n\t\t\t\t\t\t{\" \"}\n\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t onChange(e.target.value)}\n\t\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isUploading ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_UPLOADING_BUTTON}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_UPLOAD_BUTTON}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{isUploading && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_UPLOADING_TEXT}\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t\t{value && !isUploading && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t
\n\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n\nfunction DefaultImage({\n\tsrc,\n\talt,\n\tclassName,\n\twidth,\n\theight,\n}: {\n\tsrc: string;\n\talt: string;\n\tclassName?: string;\n\twidth?: number;\n\theight?: number;\n}) {\n\treturn (\n\t\t\n\t);\n}\n", + "content": "import { Button } from \"@/components/ui/button\";\nimport {\n\tFormControl,\n\tFormDescription,\n\tFormItem,\n\tFormLabel,\n\tFormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { Loader2, Upload } from \"lucide-react\";\nimport { useRef, useState } from \"react\";\nimport { toast } from \"sonner\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\n\nexport function FeaturedImageField({\n\tisRequired,\n\tvalue,\n\tonChange,\n\tsetFeaturedImageUploading,\n}: {\n\tisRequired?: boolean;\n\tvalue?: string;\n\tonChange: (value: string) => void;\n\tsetFeaturedImageUploading: (uploading: boolean) => void;\n}) {\n\tconst fileInputRef = useRef(null);\n\tconst [isUploading, setIsUploading] = useState(false);\n\n\tconst {\n\t\tuploadImage,\n\t\tImage,\n\t\tlocalization,\n\t\timageInputField: ImageInput,\n\t} = usePluginOverrides>(\n\t\t\"blog\",\n\t\t{ localization: BLOG_LOCALIZATION },\n\t);\n\n\tconst ImageComponent = Image ? Image : DefaultImage;\n\n\t// When a custom imageInput component is provided via overrides, delegate to it.\n\tif (ImageInput) {\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_LABEL}\n\t\t\t\t\t{isRequired && (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{\" \"}\n\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK}\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n\n\tconst handleImageUpload = async (\n\t\tevent: React.ChangeEvent,\n\t) => {\n\t\tconst file = event.target.files?.[0];\n\t\tif (!file) return;\n\n\t\tif (!file.type.startsWith(\"image/\")) {\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_ERROR_NOT_IMAGE);\n\t\t\treturn;\n\t\t}\n\n\t\tif (file.size > 4 * 1024 * 1024) {\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_ERROR_TOO_LARGE);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tsetIsUploading(true);\n\t\t\tsetFeaturedImageUploading(true);\n\t\t\tconst url = await uploadImage(file);\n\t\t\tonChange(url);\n\t\t\ttoast.success(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_SUCCESS);\n\t\t} catch (error) {\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_FAILURE);\n\t\t\tconsole.error(\"Failed to upload image:\", error);\n\t\t\ttoast.error(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_FAILURE);\n\t\t} finally {\n\t\t\tsetIsUploading(false);\n\t\t\tsetFeaturedImageUploading(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_LABEL}\n\t\t\t\t{isRequired && (\n\t\t\t\t\t\n\t\t\t\t\t\t{\" \"}\n\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t onChange(e.target.value)}\n\t\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isUploading ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_UPLOADING_BUTTON}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_UPLOAD_BUTTON}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{isUploading && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{localization.BLOG_FORMS_FEATURED_IMAGE_UPLOADING_TEXT}\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t\t{value && !isUploading && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t
\n\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n\nfunction DefaultImage({\n\tsrc,\n\talt,\n\tclassName,\n\twidth,\n\theight,\n}: {\n\tsrc: string;\n\talt: string;\n\tclassName?: string;\n\twidth?: number;\n\theight?: number;\n}) {\n\treturn (\n\t\t\n\t);\n}\n", "target": "src/components/btst/blog/client/components/forms/image-field.tsx" }, { @@ -70,13 +70,13 @@ { "path": "btst/blog/client/components/forms/markdown-editor-with-overrides.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { MarkdownEditor, type MarkdownEditorProps } from \"./markdown-editor\";\n\ntype MarkdownEditorWithOverridesProps = Omit<\n\tMarkdownEditorProps,\n\t\"uploadImage\" | \"placeholder\"\n>;\n\nexport function MarkdownEditorWithOverrides(\n\tprops: MarkdownEditorWithOverridesProps,\n) {\n\tconst { uploadImage, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\n\treturn (\n\t\t\n\t);\n}\n", + "content": "\"use client\";\nimport { useCallback, useRef } from \"react\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { MarkdownEditor, type MarkdownEditorProps } from \"./markdown-editor\";\n\ntype MarkdownEditorWithOverridesProps = Omit<\n\tMarkdownEditorProps,\n\t| \"uploadImage\"\n\t| \"placeholder\"\n\t| \"insertImageRef\"\n\t| \"openMediaPickerForImageBlock\"\n>;\n\nexport function MarkdownEditorWithOverrides(\n\tprops: MarkdownEditorWithOverridesProps,\n) {\n\tconst {\n\t\tuploadImage,\n\t\timagePicker: ImagePickerTrigger,\n\t\tlocalization,\n\t} = usePluginOverrides>(\n\t\t\"blog\",\n\t\t{ localization: BLOG_LOCALIZATION },\n\t);\n\n\tconst insertImageRef = useRef<((url: string) => void) | null>(null);\n\t// Holds the Crepe-image-block `setUrl` callback while the picker is open.\n\tconst pendingInsertUrlRef = useRef<((url: string) => void) | null>(null);\n\t// Ref to the trigger wrapper so we can programmatically click the picker button.\n\tconst triggerContainerRef = useRef(null);\n\n\t// Single onSelect handler for ImagePickerTrigger.\n\t// URLs are encoded here before being forwarded to either destination.\n\tconst handleSelect = useCallback((url: string) => {\n\t\tconst encodedUrl = encodeURI(url);\n\t\tif (pendingInsertUrlRef.current) {\n\t\t\t// Crepe image block flow: set the URL into the block's link input.\n\t\t\tpendingInsertUrlRef.current(encodedUrl);\n\t\t\tpendingInsertUrlRef.current = null;\n\t\t} else {\n\t\t\t// Normal flow: insert image at end of markdown content.\n\t\t\tinsertImageRef.current?.(encodedUrl);\n\t\t}\n\t}, []);\n\n\t// Called by MarkdownEditor's click interceptor when the user clicks a Crepe\n\t// image-block upload placeholder.\n\tconst openMediaPickerForImageBlock = useCallback(\n\t\t(setUrl: (url: string) => void) => {\n\t\t\tpendingInsertUrlRef.current = setUrl;\n\t\t\t// Programmatically click the visible picker trigger button.\n\t\t\tconst btn = triggerContainerRef.current?.querySelector(\n\t\t\t\t'[data-testid=\"open-media-picker\"]',\n\t\t\t) as HTMLButtonElement | null;\n\t\t\tbtn?.click();\n\t\t},\n\t\t[],\n\t);\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t{ImagePickerTrigger && (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/forms/markdown-editor-with-overrides.tsx" }, { "path": "btst/blog/client/components/forms/markdown-editor.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { Crepe, CrepeFeature } from \"@milkdown/crepe\";\nimport \"@milkdown/crepe/theme/common/style.css\";\nimport \"./markdown-editor-styles.css\";\n\nimport { cn, throttle } from \"../../../utils\";\nimport { editorViewCtx, parserCtx } from \"@milkdown/kit/core\";\nimport { listener, listenerCtx } from \"@milkdown/kit/plugin/listener\";\nimport { Slice } from \"@milkdown/kit/prose/model\";\nimport { Selection } from \"@milkdown/kit/prose/state\";\nimport { useLayoutEffect, useRef, useState } from \"react\";\n\nexport interface MarkdownEditorProps {\n\tvalue?: string;\n\tonChange?: (markdown: string) => void;\n\tclassName?: string;\n\t/** Optional image upload handler. When provided, enables image upload in the editor. */\n\tuploadImage?: (file: File) => Promise;\n\t/** Placeholder text shown when the editor is empty. */\n\tplaceholder?: string;\n}\n\nexport function MarkdownEditor({\n\tvalue,\n\tonChange,\n\tclassName,\n\tuploadImage,\n\tplaceholder = \"Write something...\",\n}: MarkdownEditorProps) {\n\tconst containerRef = useRef(null);\n\tconst crepeRef = useRef(null);\n\tconst isReadyRef = useRef(false);\n\tconst [isReady, setIsReady] = useState(false);\n\tconst onChangeRef = useRef(onChange);\n\tconst initialValueRef = useRef(value ?? \"\");\n\ttype ThrottledFn = ((markdown: string) => void) & {\n\t\tcancel?: () => void;\n\t\tflush?: () => void;\n\t};\n\tconst throttledOnChangeRef = useRef(null);\n\n\tonChangeRef.current = onChange;\n\n\tuseLayoutEffect(() => {\n\t\tif (crepeRef.current) return;\n\t\tconst container = containerRef.current;\n\t\tif (!container) return;\n\n\t\tconst crepe = new Crepe({\n\t\t\troot: container,\n\t\t\tdefaultValue: initialValueRef.current,\n\t\t\tfeatureConfigs: {\n\t\t\t\t[CrepeFeature.Placeholder]: {\n\t\t\t\t\ttext: placeholder,\n\t\t\t\t},\n\t\t\t\t...(uploadImage\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t[CrepeFeature.ImageBlock]: {\n\t\t\t\t\t\t\t\tonUpload: async (file: File) => {\n\t\t\t\t\t\t\t\t\tconst url = await uploadImage(file);\n\t\t\t\t\t\t\t\t\treturn url;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t});\n\n\t\t// Prepare throttled onChange once per editor instance\n\t\tthrottledOnChangeRef.current = throttle((markdown: string) => {\n\t\t\tif (onChangeRef.current) onChangeRef.current(markdown);\n\t\t}, 200);\n\n\t\tcrepe.editor\n\t\t\t.config((ctx) => {\n\t\t\t\tctx.get(listenerCtx).markdownUpdated((_, markdown) => {\n\t\t\t\t\tthrottledOnChangeRef.current?.(markdown);\n\t\t\t\t});\n\t\t\t})\n\t\t\t.use(listener);\n\n\t\tcrepe.create().then(() => {\n\t\t\tisReadyRef.current = true;\n\t\t\tsetIsReady(true);\n\t\t});\n\t\tcrepeRef.current = crepe;\n\n\t\treturn () => {\n\t\t\ttry {\n\t\t\t\tisReadyRef.current = false;\n\t\t\t\tthrottledOnChangeRef.current?.cancel?.();\n\t\t\t\tthrottledOnChangeRef.current = null;\n\t\t\t\tcrepe.destroy();\n\t\t\t} finally {\n\t\t\t\tcrepeRef.current = null;\n\t\t\t}\n\t\t};\n\t}, []);\n\n\tuseLayoutEffect(() => {\n\t\tif (!isReady) return;\n\t\tif (!crepeRef.current) return;\n\t\tif (typeof value !== \"string\") return;\n\n\t\tlet currentMarkdown: string | undefined;\n\t\ttry {\n\t\t\tcurrentMarkdown = crepeRef.current?.getMarkdown?.();\n\t\t} catch {\n\t\t\t// Editor may not have finished initializing its view/state; skip sync for now\n\t\t\treturn;\n\t\t}\n\n\t\tif (currentMarkdown === value) return;\n\n\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\tif (view?.hasFocus?.() === true) return;\n\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\tconst doc = parser(value);\n\t\t\tif (!doc) return;\n\n\t\t\tconst state = view.state;\n\t\t\tconst selection = state.selection;\n\t\t\tconst from = selection.from;\n\n\t\t\tlet tr = state.tr;\n\t\t\ttr = tr.replace(0, state.doc.content.size, new Slice(doc.content, 0, 0));\n\n\t\t\tconst docSize = doc.content.size;\n\t\t\tconst safeFrom = Math.max(1, Math.min(from, Math.max(1, docSize - 2)));\n\t\t\ttr = tr.setSelection(Selection.near(tr.doc.resolve(safeFrom)));\n\t\t\tview.dispatch(tr);\n\t\t});\n\t}, [value, isReady]);\n\n\treturn (\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\nimport { Crepe, CrepeFeature } from \"@milkdown/crepe\";\nimport \"@milkdown/crepe/theme/common/style.css\";\nimport \"./markdown-editor-styles.css\";\n\nimport { cn, throttle } from \"../../../utils\";\nimport { editorViewCtx, parserCtx } from \"@milkdown/kit/core\";\nimport { listener, listenerCtx } from \"@milkdown/kit/plugin/listener\";\nimport { Slice } from \"@milkdown/kit/prose/model\";\nimport { Selection } from \"@milkdown/kit/prose/state\";\nimport {\n\tuseLayoutEffect,\n\tuseRef,\n\tuseState,\n\ttype MutableRefObject,\n} from \"react\";\n\nexport interface MarkdownEditorProps {\n\tvalue?: string;\n\tonChange?: (markdown: string) => void;\n\tclassName?: string;\n\t/** Optional image upload handler. When provided, enables image upload in the editor. */\n\tuploadImage?: (file: File) => Promise;\n\t/** Placeholder text shown when the editor is empty. */\n\tplaceholder?: string;\n\t/**\n\t * Optional ref that will be populated with an `insertImage(url)` function.\n\t * Call `insertImageRef.current?.(url)` to programmatically insert an image.\n\t * The URL is expected to be already encoded by the caller.\n\t */\n\tinsertImageRef?: MutableRefObject<((url: string) => void) | null>;\n\t/**\n\t * When provided, clicking the Crepe image block's upload area opens a media\n\t * picker instead of the native file dialog. The callback receives a `setUrl`\n\t * function — call it with the chosen URL to set it into the image block.\n\t * The URL passed to `setUrl` is expected to be already encoded by the caller.\n\t */\n\topenMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void;\n}\n\nexport function MarkdownEditor({\n\tvalue,\n\tonChange,\n\tclassName,\n\tuploadImage,\n\tplaceholder = \"Write something...\",\n\tinsertImageRef,\n\topenMediaPickerForImageBlock,\n}: MarkdownEditorProps) {\n\tconst containerRef = useRef(null);\n\tconst crepeRef = useRef(null);\n\tconst isReadyRef = useRef(false);\n\tconst [isReady, setIsReady] = useState(false);\n\tconst onChangeRef = useRef(onChange);\n\tconst initialValueRef = useRef(value ?? \"\");\n\tconst openMediaPickerRef = useRef(\n\t\topenMediaPickerForImageBlock,\n\t);\n\ttype ThrottledFn = ((markdown: string) => void) & {\n\t\tcancel?: () => void;\n\t\tflush?: () => void;\n\t};\n\tconst throttledOnChangeRef = useRef(null);\n\n\tonChangeRef.current = onChange;\n\topenMediaPickerRef.current = openMediaPickerForImageBlock;\n\n\tuseLayoutEffect(() => {\n\t\tif (crepeRef.current) return;\n\t\tconst container = containerRef.current;\n\t\tif (!container) return;\n\n\t\tconst hasMediaPicker = !!openMediaPickerRef.current;\n\n\t\tconst imageBlockConfig: Record = {};\n\t\tif (uploadImage) {\n\t\t\timageBlockConfig.onUpload = async (file: File) => uploadImage(file);\n\t\t}\n\t\tif (hasMediaPicker) {\n\t\t\timageBlockConfig.blockUploadPlaceholderText = \"Media Picker\";\n\t\t\timageBlockConfig.inlineUploadPlaceholderText = \"Media Picker\";\n\t\t}\n\n\t\tconst crepe = new Crepe({\n\t\t\troot: container,\n\t\t\tdefaultValue: initialValueRef.current,\n\t\t\tfeatureConfigs: {\n\t\t\t\t[CrepeFeature.Placeholder]: {\n\t\t\t\t\ttext: placeholder,\n\t\t\t\t},\n\t\t\t\t...(Object.keys(imageBlockConfig).length > 0\n\t\t\t\t\t? { [CrepeFeature.ImageBlock]: imageBlockConfig }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t});\n\n\t\t// Intercept clicks on Crepe image-block upload placeholders so that the\n\t\t// native file dialog is suppressed and the media picker is opened instead.\n\t\tconst interceptHandler = (e: MouseEvent) => {\n\t\t\tif (!openMediaPickerRef.current) return;\n\t\t\tconst target = e.target as Element;\n\t\t\t// Only intercept clicks inside the upload placeholder area.\n\t\t\tconst inPlaceholder = target.closest(\".image-edit .placeholder\");\n\t\t\tif (!inPlaceholder) return;\n\t\t\t// Let the hidden file itself through (shouldn't receive clicks normally).\n\t\t\tif ((target as HTMLElement).matches(\"input\")) return;\n\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\n\t\t\tconst imageEdit = inPlaceholder.closest(\".image-edit\");\n\t\t\tconst linkInput = imageEdit?.querySelector(\n\t\t\t\t\".link-input-area\",\n\t\t\t) as HTMLInputElement | null;\n\n\t\t\topenMediaPickerRef.current((url: string) => {\n\t\t\t\tif (!linkInput) return;\n\t\t\t\t// Use the native setter so Vue's reactivity picks up the change.\n\t\t\t\tconst nativeSetter = Object.getOwnPropertyDescriptor(\n\t\t\t\t\tHTMLInputElement.prototype,\n\t\t\t\t\t\"value\",\n\t\t\t\t)?.set;\n\t\t\t\tnativeSetter?.call(linkInput, url);\n\t\t\t\tlinkInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n\t\t\t\tlinkInput.dispatchEvent(\n\t\t\t\t\tnew KeyboardEvent(\"keydown\", { key: \"Enter\", bubbles: true }),\n\t\t\t\t);\n\t\t\t});\n\t\t};\n\t\tcontainer.addEventListener(\"click\", interceptHandler, true);\n\n\t\t// Prepare throttled onChange once per editor instance\n\t\tthrottledOnChangeRef.current = throttle((markdown: string) => {\n\t\t\tif (onChangeRef.current) onChangeRef.current(markdown);\n\t\t}, 200);\n\n\t\tcrepe.editor\n\t\t\t.config((ctx) => {\n\t\t\t\tctx.get(listenerCtx).markdownUpdated((_, markdown) => {\n\t\t\t\t\tthrottledOnChangeRef.current?.(markdown);\n\t\t\t\t});\n\t\t\t})\n\t\t\t.use(listener);\n\n\t\tcrepe.create().then(() => {\n\t\t\tisReadyRef.current = true;\n\t\t\tsetIsReady(true);\n\t\t});\n\t\tcrepeRef.current = crepe;\n\n\t\treturn () => {\n\t\t\tcontainer.removeEventListener(\"click\", interceptHandler, true);\n\t\t\ttry {\n\t\t\t\tisReadyRef.current = false;\n\t\t\t\tthrottledOnChangeRef.current?.cancel?.();\n\t\t\t\tthrottledOnChangeRef.current = null;\n\t\t\t\tcrepe.destroy();\n\t\t\t} finally {\n\t\t\t\tcrepeRef.current = null;\n\t\t\t}\n\t\t};\n\t}, []);\n\n\tuseLayoutEffect(() => {\n\t\tif (!isReady) return;\n\t\tif (!crepeRef.current) return;\n\t\tif (typeof value !== \"string\") return;\n\n\t\tlet currentMarkdown: string | undefined;\n\t\ttry {\n\t\t\tcurrentMarkdown = crepeRef.current?.getMarkdown?.();\n\t\t} catch {\n\t\t\t// Editor may not have finished initializing its view/state; skip sync for now\n\t\t\treturn;\n\t\t}\n\n\t\tif (currentMarkdown === value) return;\n\n\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\tif (view?.hasFocus?.() === true) return;\n\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\tconst doc = parser(value);\n\t\t\tif (!doc) return;\n\n\t\t\tconst state = view.state;\n\t\t\tconst selection = state.selection;\n\t\t\tconst from = selection.from;\n\n\t\t\tlet tr = state.tr;\n\t\t\ttr = tr.replace(0, state.doc.content.size, new Slice(doc.content, 0, 0));\n\n\t\t\tconst docSize = doc.content.size;\n\t\t\tconst safeFrom = Math.max(1, Math.min(from, Math.max(1, docSize - 2)));\n\t\t\ttr = tr.setSelection(Selection.near(tr.doc.resolve(safeFrom)));\n\t\t\tview.dispatch(tr);\n\t\t});\n\t}, [value, isReady]);\n\n\t// Expose insertImage via ref so the parent can insert images programmatically\n\tuseLayoutEffect(() => {\n\t\tif (!insertImageRef) return;\n\t\tinsertImageRef.current = (url: string) => {\n\t\t\tif (!crepeRef.current || !isReadyRef.current) return;\n\t\t\ttry {\n\t\t\t\tconst currentMarkdown = crepeRef.current.getMarkdown?.() ?? \"\";\n\t\t\t\tconst imageMarkdown = `\\n\\n![](${url})\\n\\n`;\n\t\t\t\tconst newMarkdown = currentMarkdown.trimEnd() + imageMarkdown;\n\t\t\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\t\t\tconst doc = parser(newMarkdown);\n\t\t\t\t\tif (!doc) return;\n\t\t\t\t\tconst state = view.state;\n\t\t\t\t\tconst tr = state.tr.replace(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tstate.doc.content.size,\n\t\t\t\t\t\tnew Slice(doc.content, 0, 0),\n\t\t\t\t\t);\n\t\t\t\t\tview.dispatch(tr);\n\t\t\t\t});\n\t\t\t\tif (onChangeRef.current) onChangeRef.current(newMarkdown);\n\t\t\t} catch {\n\t\t\t\t// Editor may not be ready yet\n\t\t\t}\n\t\t};\n\t\treturn () => {\n\t\t\tif (insertImageRef) insertImageRef.current = null;\n\t\t};\n\t}, [insertImageRef]);\n\n\treturn (\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/forms/markdown-editor.tsx" }, { @@ -352,7 +352,7 @@ { "path": "btst/blog/client/overrides.ts", "type": "registry:lib", - "content": "import type { SerializedPost } from \"../types\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { BlogLocalization } from \"./localization\";\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { slug: \"my-post\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: any;\n}\n\n/**\n * Overridable components and functions for the Blog plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface BlogPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Post card component for displaying a post\n\t */\n\tPostCard?: ComponentType<{\n\t\tpost: SerializedPost;\n\t}>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Function used to upload an image and return its URL.\n\t */\n\tuploadImage: (file: File) => Promise;\n\t/**\n\t * Localization object for the blog plugin\n\t */\n\tlocalization?: BlogLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'posts', 'post', 'newPost')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the posts list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforePostsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug\n\t * @param context - Route context\n\t */\n\tonBeforePostPageRendered?: (slug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the new post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewPostPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the edit post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug being edited\n\t * @param context - Route context\n\t */\n\tonBeforeEditPostPageRendered?: (\n\t\tslug: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the drafts page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDraftsPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered below the blog post body.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the blog plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * blog: {\n\t * postBottomSlot: (post) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tpostBottomSlot?: (post: SerializedPost) => ReactNode;\n}\n", + "content": "import type { SerializedPost } from \"../types\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { BlogLocalization } from \"./localization\";\n\n/**\n * Props for the overridable blog featured image input component.\n */\nexport interface BlogImageInputFieldProps {\n\t/** Current image URL value */\n\tvalue: string;\n\t/** Called when the image URL changes */\n\tonChange: (value: string) => void;\n\t/** Whether the field is required */\n\tisRequired?: boolean;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { slug: \"my-post\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: any;\n}\n\n/**\n * Overridable components and functions for the Blog plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface BlogPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Post card component for displaying a post\n\t */\n\tPostCard?: ComponentType<{\n\t\tpost: SerializedPost;\n\t}>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Function used to upload an image and return its URL.\n\t */\n\tuploadImage: (file: File) => Promise;\n\t/**\n\t * Optional custom component for the featured image field.\n\t *\n\t * When provided it replaces the default file-upload input entirely.\n\t * The component receives `value` (current URL string) and `onChange` (setter).\n\t *\n\t * Typical use case: render a preview when a value is set, and a media-picker\n\t * trigger when no value is set.\n\t *\n\t * @example\n\t * ```tsx\n\t * imageInputField: ({ value, onChange }) =>\n\t * value ? (\n\t *
\n\t * \"Preview\"\n\t * Change} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t *
\n\t * ) : (\n\t * Browse media} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t * )\n\t * ```\n\t */\n\timageInputField?: ComponentType;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered adjacent to the Markdown editor and allows\n\t * users to browse and select previously uploaded assets.\n\t * Receives `onSelect(url)` — insert the chosen URL into the editor.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\t/**\n\t * Localization object for the blog plugin\n\t */\n\tlocalization?: BlogLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'posts', 'post', 'newPost')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the posts list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforePostsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug\n\t * @param context - Route context\n\t */\n\tonBeforePostPageRendered?: (slug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the new post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewPostPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the edit post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug being edited\n\t * @param context - Route context\n\t */\n\tonBeforeEditPostPageRendered?: (\n\t\tslug: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the drafts page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDraftsPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered below the blog post body.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the blog plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * blog: {\n\t * postBottomSlot: (post) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tpostBottomSlot?: (post: SerializedPost) => ReactNode;\n}\n", "target": "src/components/btst/blog/client/overrides.ts" }, { diff --git a/packages/stack/registry/btst-cms.json b/packages/stack/registry/btst-cms.json index ea31783d..94c80b2f 100644 --- a/packages/stack/registry/btst-cms.json +++ b/packages/stack/registry/btst-cms.json @@ -49,13 +49,13 @@ { "path": "btst/cms/client/components/forms/content-form.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { useState, useMemo, useEffect, useRef } from \"react\";\nimport { z } from \"zod\";\nimport { SteppedAutoForm } from \"@/components/ui/auto-form/stepped-auto-form\";\nimport type {\n\tFieldConfig,\n\tAutoFormInputComponentProps,\n} from \"@/components/ui/auto-form/types\";\nimport { buildFieldConfigFromJsonSchema as buildFieldConfigBase } from \"@/components/ui/auto-form/helpers\";\nimport { formSchemaToZod } from \"@/lib/schema-converter\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { CMSPluginOverrides } from \"../../overrides\";\nimport type { SerializedContentType, RelationConfig } from \"../../../types\";\nimport { slugify } from \"../../../utils\";\nimport { CMS_LOCALIZATION } from \"../../localization\";\nimport { CMSFileUpload } from \"./file-upload\";\nimport { RelationField } from \"./relation-field\";\n\ninterface ContentFormProps {\n\tcontentType: SerializedContentType;\n\tinitialData?: Record;\n\tinitialSlug?: string;\n\tisEditing?: boolean;\n\tonSubmit: (data: {\n\t\tslug: string;\n\t\tdata: Record;\n\t}) => Promise;\n\tonCancel?: () => void;\n}\n\n/**\n * Build field configuration for AutoForm with CMS-specific file upload handling.\n *\n * Uses the shared buildFieldConfigFromJsonSchema from auto-form/helpers as a base,\n * then adds special handling for \"file\" fieldType to inject CMSFileUpload component\n * ONLY if no custom component is provided via fieldComponents.\n *\n * @param jsonSchema - The JSON Schema from the content type (with fieldType embedded in properties)\n * @param uploadImage - The uploadImage function from overrides (for file fields)\n * @param fieldComponents - Custom field components from overrides\n */\ninterface JsonSchemaProperty {\n\tfieldType?: string;\n\trelation?: RelationConfig;\n\t[key: string]: unknown;\n}\n\nfunction buildFieldConfigFromJsonSchema(\n\tjsonSchema: Record,\n\tuploadImage?: (file: File) => Promise,\n\tfieldComponents?: Record<\n\t\tstring,\n\t\tReact.ComponentType\n\t>,\n): FieldConfig> {\n\t// Get base config from shared utility (handles fieldType from JSON Schema)\n\tconst baseConfig = buildFieldConfigBase(jsonSchema, fieldComponents);\n\n\t// Apply CMS-specific handling for special fieldTypes ONLY if no custom component exists\n\t// Custom fieldComponents take priority - don't override if user provided one\n\tconst properties = jsonSchema.properties as Record<\n\t\tstring,\n\t\tJsonSchemaProperty\n\t>;\n\n\tif (!properties) return baseConfig;\n\n\tfor (const [key, prop] of Object.entries(properties)) {\n\t\t// Handle \"file\" fieldType when there's NO custom component for \"file\"\n\t\tif (prop.fieldType === \"file\" && !fieldComponents?.[\"file\"]) {\n\t\t\t// Use CMSFileUpload as the default file component\n\t\t\tif (!uploadImage) {\n\t\t\t\t// Show a clear error message if uploadImage is not provided\n\t\t\t\tbaseConfig[key] = {\n\t\t\t\t\t...baseConfig[key],\n\t\t\t\t\tfieldType: () => (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\tFile upload requires an uploadImage function in CMS\n\t\t\t\t\t\t\toverrides.\n\t\t\t\t\t\t
\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tbaseConfig[key] = {\n\t\t\t\t\t...baseConfig[key],\n\t\t\t\t\tfieldType: (props: AutoFormInputComponentProps) => (\n\t\t\t\t\t\t\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Handle \"relation\" fieldType when there's NO custom component for \"relation\"\n\t\tif (\n\t\t\tprop.fieldType === \"relation\" &&\n\t\t\tprop.relation &&\n\t\t\t!fieldComponents?.[\"relation\"]\n\t\t) {\n\t\t\tconst relationConfig = prop.relation;\n\t\t\tbaseConfig[key] = {\n\t\t\t\t...baseConfig[key],\n\t\t\t\tfieldType: (props: AutoFormInputComponentProps) => (\n\t\t\t\t\t\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\t}\n\n\treturn baseConfig;\n}\n\n/**\n * Determine the first string field in the schema for slug auto-generation\n */\nfunction findSlugSourceField(\n\tjsonSchema: Record,\n): string | null {\n\tconst properties = jsonSchema.properties as Record;\n\tif (!properties) return null;\n\n\t// Look for common name fields first\n\tconst priorityFields = [\"name\", \"title\", \"heading\", \"label\"];\n\tfor (const field of priorityFields) {\n\t\tif (properties[field]?.type === \"string\") {\n\t\t\treturn field;\n\t\t}\n\t}\n\n\t// Fall back to first string field\n\tfor (const [key, value] of Object.entries(properties)) {\n\t\tif (value.type === \"string\") {\n\t\t\treturn key;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport function ContentForm({\n\tcontentType,\n\tinitialData = {},\n\tinitialSlug = \"\",\n\tisEditing = false,\n\tonSubmit,\n\tonCancel,\n}: ContentFormProps) {\n\tconst {\n\t\tlocalization: customLocalization,\n\t\tuploadImage,\n\t\tfieldComponents,\n\t} = usePluginOverrides(\"cms\");\n\tconst localization = { ...CMS_LOCALIZATION, ...customLocalization };\n\n\tconst [slug, setSlug] = useState(initialSlug);\n\tconst [slugManuallyEdited, setSlugManuallyEdited] = useState(isEditing);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [formData, setFormData] =\n\t\tuseState>(initialData);\n\tconst [slugError, setSlugError] = useState(null);\n\tconst [submitError, setSubmitError] = useState(null);\n\n\t// Track if we've already synced prefill data to avoid overwriting user input\n\tconst hasSyncedPrefillRef = useRef(false);\n\n\t// Sync formData with initialData when it changes\n\t// This handles both:\n\t// 1. Editing mode: always sync when item data is loaded (isEditing=true)\n\t// 2. Create mode: only sync prefill data ONCE to avoid overwriting user input\n\t// useState only uses the initial value on mount, so we need this effect for updates\n\tuseEffect(() => {\n\t\tconst hasData = Object.keys(initialData).length > 0;\n\t\t// In edit mode, always sync (user is loading existing data)\n\t\t// In create mode, only sync prefill data once\n\t\tconst shouldSync = hasData && (isEditing || !hasSyncedPrefillRef.current);\n\n\t\tif (shouldSync) {\n\t\t\tsetFormData(initialData);\n\t\t\tif (!isEditing) {\n\t\t\t\thasSyncedPrefillRef.current = true;\n\t\t\t}\n\t\t}\n\t}, [initialData, isEditing]);\n\n\t// Also sync slug when initialSlug changes\n\tuseEffect(() => {\n\t\tif (isEditing && initialSlug) {\n\t\t\tsetSlug(initialSlug);\n\t\t}\n\t}, [initialSlug, isEditing]);\n\n\t// Parse JSON Schema (now includes fieldType embedded in properties)\n\tconst jsonSchema = useMemo(() => {\n\t\ttry {\n\t\t\treturn JSON.parse(contentType.jsonSchema) as Record;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}, [contentType.jsonSchema]);\n\n\t// Convert JSON Schema to Zod schema using formSchemaToZod utility\n\t// This properly handles date fields (format: \"date-time\") and min/max date constraints\n\tconst zodSchema = useMemo(() => {\n\t\ttry {\n\t\t\treturn formSchemaToZod(jsonSchema);\n\t\t} catch {\n\t\t\treturn z.object({});\n\t\t}\n\t}, [jsonSchema]);\n\n\t// Build field config for AutoForm (fieldType is now embedded in jsonSchema)\n\tconst fieldConfig = useMemo(\n\t\t() =>\n\t\t\tbuildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents),\n\t\t[jsonSchema, uploadImage, fieldComponents],\n\t);\n\n\t// Find the field to use for slug auto-generation\n\tconst slugSourceField = useMemo(\n\t\t() => findSlugSourceField(jsonSchema),\n\t\t[jsonSchema],\n\t);\n\n\t// Handle form value changes for slug auto-generation\n\tconst handleValuesChange = (values: Record) => {\n\t\tsetFormData(values);\n\n\t\t// Auto-generate slug from source field if not manually edited\n\t\tif (!isEditing && !slugManuallyEdited && slugSourceField) {\n\t\t\tconst sourceValue = values[slugSourceField];\n\t\t\tif (typeof sourceValue === \"string\" && sourceValue.trim()) {\n\t\t\t\tsetSlug(slugify(sourceValue));\n\t\t\t}\n\t\t}\n\t};\n\n\t// Handle form submission\n\tconst handleSubmit = async (data: Record) => {\n\t\tsetSlugError(null);\n\t\tsetSubmitError(null);\n\n\t\tif (!slug.trim()) {\n\t\t\tsetSlugError(\"Slug is required\");\n\t\t\treturn;\n\t\t}\n\n\t\tsetIsSubmitting(true);\n\t\ttry {\n\t\t\tawait onSubmit({ slug, data });\n\t\t} catch (error) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : localization.CMS_TOAST_ERROR;\n\t\t\tsetSubmitError(message);\n\t\t} finally {\n\t\t\tsetIsSubmitting(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t{/* Slug field */}\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{!isEditing && (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{slugManuallyEdited\n\t\t\t\t\t\t\t\t? localization.CMS_EDITOR_SLUG_MANUAL\n\t\t\t\t\t\t\t\t: localization.CMS_EDITOR_SLUG_AUTO}\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t {\n\t\t\t\t\t\tsetSlug(e.target.value);\n\t\t\t\t\t\tsetSlugError(null);\n\t\t\t\t\t\tif (!isEditing) {\n\t\t\t\t\t\t\tsetSlugManuallyEdited(true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t\tdisabled={isEditing}\n\t\t\t\t\tplaceholder={\n\t\t\t\t\t\tslugSourceField\n\t\t\t\t\t\t\t? `Auto-generated from ${slugSourceField}`\n\t\t\t\t\t\t\t: \"Enter slug...\"\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t{slugError &&

{slugError}

}\n\t\t\t\t

\n\t\t\t\t\t{localization.CMS_LABEL_SLUG_DESCRIPTION}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t{/* Submit error message */}\n\t\t\t{submitError && (\n\t\t\t\t
\n\t\t\t\t\t

{submitError}

\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t{/* Dynamic form from Zod schema */}\n\t\t\t{/* Uses SteppedAutoForm which automatically handles both single-step and multi-step content types */}\n\t\t\t}\n\t\t\t\tvalues={formData as any}\n\t\t\t\tonValuesChange={handleValuesChange as any}\n\t\t\t\tonSubmit={handleSubmit as any}\n\t\t\t\tfieldConfig={fieldConfig as any}\n\t\t\t\tisSubmitting={isSubmitting}\n\t\t\t\tsubmitButtonText={\n\t\t\t\t\tisSubmitting\n\t\t\t\t\t\t? localization.CMS_STATUS_SAVING\n\t\t\t\t\t\t: localization.CMS_BUTTON_SAVE\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{onCancel && (\n\t\t\t\t\t\n\t\t\t\t\t\t{localization.CMS_BUTTON_CANCEL}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { useState, useMemo, useEffect, useRef } from \"react\";\nimport { z } from \"zod\";\nimport { SteppedAutoForm } from \"@/components/ui/auto-form/stepped-auto-form\";\nimport type {\n\tFieldConfig,\n\tAutoFormInputComponentProps,\n} from \"@/components/ui/auto-form/types\";\nimport { buildFieldConfigFromJsonSchema as buildFieldConfigBase } from \"@/components/ui/auto-form/helpers\";\nimport { formSchemaToZod } from \"@/lib/schema-converter\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { CMSPluginOverrides } from \"../../overrides\";\nimport type { SerializedContentType, RelationConfig } from \"../../../types\";\nimport { slugify } from \"../../../utils\";\nimport { CMS_LOCALIZATION } from \"../../localization\";\nimport { CMSFileUpload } from \"./file-upload\";\nimport { RelationField } from \"./relation-field\";\n\ninterface ContentFormProps {\n\tcontentType: SerializedContentType;\n\tinitialData?: Record;\n\tinitialSlug?: string;\n\tisEditing?: boolean;\n\tonSubmit: (data: {\n\t\tslug: string;\n\t\tdata: Record;\n\t}) => Promise;\n\tonCancel?: () => void;\n}\n\n/**\n * Build field configuration for AutoForm with CMS-specific file upload handling.\n *\n * Uses the shared buildFieldConfigFromJsonSchema from auto-form/helpers as a base,\n * then adds special handling for \"file\" fieldType to inject CMSFileUpload component\n * ONLY if no custom component is provided via fieldComponents.\n *\n * @param jsonSchema - The JSON Schema from the content type (with fieldType embedded in properties)\n * @param uploadImage - The uploadImage function from overrides (for file fields)\n * @param fieldComponents - Custom field components from overrides\n */\ninterface JsonSchemaProperty {\n\tfieldType?: string;\n\trelation?: RelationConfig;\n\t[key: string]: unknown;\n}\n\nfunction buildFieldConfigFromJsonSchema(\n\tjsonSchema: Record,\n\tuploadImage?: (file: File) => Promise,\n\tfieldComponents?: Record<\n\t\tstring,\n\t\tReact.ComponentType\n\t>,\n\timagePicker?: React.ComponentType<{ onSelect: (url: string) => void }>,\n\timageInputField?: React.ComponentType<{\n\t\tvalue: string;\n\t\tonChange: (value: string) => void;\n\t\tisRequired?: boolean;\n\t}>,\n): FieldConfig> {\n\t// Get base config from shared utility (handles fieldType from JSON Schema)\n\tconst baseConfig = buildFieldConfigBase(jsonSchema, fieldComponents);\n\n\t// Apply CMS-specific handling for special fieldTypes ONLY if no custom component exists\n\t// Custom fieldComponents take priority - don't override if user provided one\n\tconst properties = jsonSchema.properties as Record<\n\t\tstring,\n\t\tJsonSchemaProperty\n\t>;\n\n\tif (!properties) return baseConfig;\n\n\tfor (const [key, prop] of Object.entries(properties)) {\n\t\t// Handle \"file\" fieldType when there's NO custom component for \"file\"\n\t\tif (prop.fieldType === \"file\" && !fieldComponents?.[\"file\"]) {\n\t\t\t// Use CMSFileUpload as the default file component\n\t\t\tif (!uploadImage && !imageInputField) {\n\t\t\t\t// Show a clear error message if neither uploadImage nor imageInputField is provided\n\t\t\t\tbaseConfig[key] = {\n\t\t\t\t\t...baseConfig[key],\n\t\t\t\t\tfieldType: () => (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\tFile upload requires an uploadImage or{\" \"}\n\t\t\t\t\t\t\timageInputField function in CMS overrides.\n\t\t\t\t\t\t
\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tbaseConfig[key] = {\n\t\t\t\t\t...baseConfig[key],\n\t\t\t\t\tfieldType: (props: AutoFormInputComponentProps) => (\n\t\t\t\t\t\t Promise.resolve(\"\"))}\n\t\t\t\t\t\t\timageInputField={imageInputField}\n\t\t\t\t\t\t\timagePicker={imagePicker}\n\t\t\t\t\t\t/>\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Handle \"relation\" fieldType when there's NO custom component for \"relation\"\n\t\tif (\n\t\t\tprop.fieldType === \"relation\" &&\n\t\t\tprop.relation &&\n\t\t\t!fieldComponents?.[\"relation\"]\n\t\t) {\n\t\t\tconst relationConfig = prop.relation;\n\t\t\tbaseConfig[key] = {\n\t\t\t\t...baseConfig[key],\n\t\t\t\tfieldType: (props: AutoFormInputComponentProps) => (\n\t\t\t\t\t\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\t}\n\n\treturn baseConfig;\n}\n\n/**\n * Determine the first string field in the schema for slug auto-generation\n */\nfunction findSlugSourceField(\n\tjsonSchema: Record,\n): string | null {\n\tconst properties = jsonSchema.properties as Record;\n\tif (!properties) return null;\n\n\t// Look for common name fields first\n\tconst priorityFields = [\"name\", \"title\", \"heading\", \"label\"];\n\tfor (const field of priorityFields) {\n\t\tif (properties[field]?.type === \"string\") {\n\t\t\treturn field;\n\t\t}\n\t}\n\n\t// Fall back to first string field\n\tfor (const [key, value] of Object.entries(properties)) {\n\t\tif (value.type === \"string\") {\n\t\t\treturn key;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport function ContentForm({\n\tcontentType,\n\tinitialData = {},\n\tinitialSlug = \"\",\n\tisEditing = false,\n\tonSubmit,\n\tonCancel,\n}: ContentFormProps) {\n\tconst {\n\t\tlocalization: customLocalization,\n\t\tuploadImage,\n\t\timagePicker,\n\t\timageInputField,\n\t\tfieldComponents,\n\t} = usePluginOverrides(\"cms\");\n\tconst localization = { ...CMS_LOCALIZATION, ...customLocalization };\n\n\tconst [slug, setSlug] = useState(initialSlug);\n\tconst [slugManuallyEdited, setSlugManuallyEdited] = useState(isEditing);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [formData, setFormData] =\n\t\tuseState>(initialData);\n\tconst [slugError, setSlugError] = useState(null);\n\tconst [submitError, setSubmitError] = useState(null);\n\n\t// Track if we've already synced prefill data to avoid overwriting user input\n\tconst hasSyncedPrefillRef = useRef(false);\n\n\t// Sync formData with initialData when it changes\n\t// This handles both:\n\t// 1. Editing mode: always sync when item data is loaded (isEditing=true)\n\t// 2. Create mode: only sync prefill data ONCE to avoid overwriting user input\n\t// useState only uses the initial value on mount, so we need this effect for updates\n\tuseEffect(() => {\n\t\tconst hasData = Object.keys(initialData).length > 0;\n\t\t// In edit mode, always sync (user is loading existing data)\n\t\t// In create mode, only sync prefill data once\n\t\tconst shouldSync = hasData && (isEditing || !hasSyncedPrefillRef.current);\n\n\t\tif (shouldSync) {\n\t\t\tsetFormData(initialData);\n\t\t\tif (!isEditing) {\n\t\t\t\thasSyncedPrefillRef.current = true;\n\t\t\t}\n\t\t}\n\t}, [initialData, isEditing]);\n\n\t// Also sync slug when initialSlug changes\n\tuseEffect(() => {\n\t\tif (isEditing && initialSlug) {\n\t\t\tsetSlug(initialSlug);\n\t\t}\n\t}, [initialSlug, isEditing]);\n\n\t// Parse JSON Schema (now includes fieldType embedded in properties)\n\tconst jsonSchema = useMemo(() => {\n\t\ttry {\n\t\t\treturn JSON.parse(contentType.jsonSchema) as Record;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}, [contentType.jsonSchema]);\n\n\t// Convert JSON Schema to Zod schema using formSchemaToZod utility\n\t// This properly handles date fields (format: \"date-time\") and min/max date constraints\n\tconst zodSchema = useMemo(() => {\n\t\ttry {\n\t\t\treturn formSchemaToZod(jsonSchema);\n\t\t} catch {\n\t\t\treturn z.object({});\n\t\t}\n\t}, [jsonSchema]);\n\n\t// Build field config for AutoForm (fieldType is now embedded in jsonSchema)\n\tconst fieldConfig = useMemo(\n\t\t() =>\n\t\t\tbuildFieldConfigFromJsonSchema(\n\t\t\t\tjsonSchema,\n\t\t\t\tuploadImage,\n\t\t\t\tfieldComponents,\n\t\t\t\timagePicker,\n\t\t\t\timageInputField,\n\t\t\t),\n\t\t[jsonSchema, uploadImage, fieldComponents, imagePicker, imageInputField],\n\t);\n\n\t// Find the field to use for slug auto-generation\n\tconst slugSourceField = useMemo(\n\t\t() => findSlugSourceField(jsonSchema),\n\t\t[jsonSchema],\n\t);\n\n\t// Handle form value changes for slug auto-generation\n\tconst handleValuesChange = (values: Record) => {\n\t\tsetFormData(values);\n\n\t\t// Auto-generate slug from source field if not manually edited\n\t\tif (!isEditing && !slugManuallyEdited && slugSourceField) {\n\t\t\tconst sourceValue = values[slugSourceField];\n\t\t\tif (typeof sourceValue === \"string\" && sourceValue.trim()) {\n\t\t\t\tsetSlug(slugify(sourceValue));\n\t\t\t}\n\t\t}\n\t};\n\n\t// Handle form submission\n\tconst handleSubmit = async (data: Record) => {\n\t\tsetSlugError(null);\n\t\tsetSubmitError(null);\n\n\t\tif (!slug.trim()) {\n\t\t\tsetSlugError(\"Slug is required\");\n\t\t\treturn;\n\t\t}\n\n\t\tsetIsSubmitting(true);\n\t\ttry {\n\t\t\tawait onSubmit({ slug, data });\n\t\t} catch (error) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : localization.CMS_TOAST_ERROR;\n\t\t\tsetSubmitError(message);\n\t\t} finally {\n\t\t\tsetIsSubmitting(false);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t{/* Slug field */}\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{!isEditing && (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{slugManuallyEdited\n\t\t\t\t\t\t\t\t? localization.CMS_EDITOR_SLUG_MANUAL\n\t\t\t\t\t\t\t\t: localization.CMS_EDITOR_SLUG_AUTO}\n\t\t\t\t\t\t\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t {\n\t\t\t\t\t\tsetSlug(e.target.value);\n\t\t\t\t\t\tsetSlugError(null);\n\t\t\t\t\t\tif (!isEditing) {\n\t\t\t\t\t\t\tsetSlugManuallyEdited(true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t\tdisabled={isEditing}\n\t\t\t\t\tplaceholder={\n\t\t\t\t\t\tslugSourceField\n\t\t\t\t\t\t\t? `Auto-generated from ${slugSourceField}`\n\t\t\t\t\t\t\t: \"Enter slug...\"\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t{slugError &&

{slugError}

}\n\t\t\t\t

\n\t\t\t\t\t{localization.CMS_LABEL_SLUG_DESCRIPTION}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t{/* Submit error message */}\n\t\t\t{submitError && (\n\t\t\t\t
\n\t\t\t\t\t

{submitError}

\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t{/* Dynamic form from Zod schema */}\n\t\t\t{/* Uses SteppedAutoForm which automatically handles both single-step and multi-step content types */}\n\t\t\t}\n\t\t\t\tvalues={formData as any}\n\t\t\t\tonValuesChange={handleValuesChange as any}\n\t\t\t\tonSubmit={handleSubmit as any}\n\t\t\t\tfieldConfig={fieldConfig as any}\n\t\t\t\tisSubmitting={isSubmitting}\n\t\t\t\tsubmitButtonText={\n\t\t\t\t\tisSubmitting\n\t\t\t\t\t\t? localization.CMS_STATUS_SAVING\n\t\t\t\t\t\t: localization.CMS_BUTTON_SAVE\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{onCancel && (\n\t\t\t\t\t\n\t\t\t\t\t\t{localization.CMS_BUTTON_CANCEL}\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t
\n\t);\n}\n", "target": "src/components/btst/cms/client/components/forms/content-form.tsx" }, { "path": "btst/cms/client/components/forms/file-upload.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { useState, useCallback, useEffect, type ChangeEvent } from \"react\";\nimport { toast } from \"sonner\";\nimport type { AutoFormInputComponentProps } from \"@/components/ui/auto-form/types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tFormControl,\n\tFormItem,\n\tFormMessage,\n} from \"@/components/ui/form\";\nimport { Trash2, Loader2 } from \"lucide-react\";\nimport AutoFormLabel from \"@/components/ui/auto-form/common/label\";\nimport AutoFormTooltip from \"@/components/ui/auto-form/common/tooltip\";\n\n/**\n * Props for the CMSFileUpload component\n */\nexport interface CMSFileUploadProps extends AutoFormInputComponentProps {\n\t/**\n\t * Function to upload an image file and return the URL.\n\t * This is required - consumers must provide an upload implementation.\n\t */\n\tuploadImage: (file: File) => Promise;\n}\n\n/**\n * Default file upload component for CMS image fields.\n *\n * This component:\n * - Accepts image files via file input\n * - Uses the required uploadImage prop to upload and get a URL\n * - Shows a preview of the uploaded image\n * - Allows removing the uploaded image\n *\n * You can use this component directly in your fieldComponents override,\n * or create your own custom component using this as a reference.\n *\n * @example\n * ```tsx\n * // Use the default component with your upload function\n * fieldComponents: {\n * file: (props) => (\n * \n * ),\n * }\n * ```\n */\nexport function CMSFileUpload({\n\tlabel,\n\tisRequired,\n\tfieldConfigItem,\n\tfieldProps,\n\tfield,\n\tuploadImage,\n}: CMSFileUploadProps) {\n\t// Exclude showLabel and value from props spread\n\t// File inputs cannot have their value set programmatically (browser security)\n\tconst {\n\t\tshowLabel: _showLabel,\n\t\tvalue: _value,\n\t\t...safeFieldProps\n\t} = fieldProps;\n\tconst showLabel = _showLabel === undefined ? true : _showLabel;\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [previewUrl, setPreviewUrl] = useState(\n\t\tfield.value || null,\n\t);\n\n\tuseEffect(() => {\n\t\tconst normalizedValue = field.value || null;\n\t\tif (normalizedValue !== previewUrl) {\n\t\t\tsetPreviewUrl(normalizedValue);\n\t\t}\n\t}, [field.value, previewUrl]);\n\n\tconst handleFileChange = useCallback(\n\t\tasync (e: ChangeEvent) => {\n\t\t\tconst file = e.target.files?.[0];\n\t\t\tif (!file) return;\n\n\t\t\t// Check if it's an image\n\t\t\tif (!file.type.startsWith(\"image/\")) {\n\t\t\t\ttoast.error(\"Please select an image file\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetIsUploading(true);\n\t\t\ttry {\n\t\t\t\tconst url = await uploadImage(file);\n\t\t\t\tsetPreviewUrl(url);\n\t\t\t\tfield.onChange(url);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Image upload failed:\", error);\n\t\t\t\ttoast.error(\"Failed to upload image\");\n\t\t\t} finally {\n\t\t\t\tsetIsUploading(false);\n\t\t\t}\n\t\t},\n\t\t[field, uploadImage],\n\t);\n\n\tconst handleRemove = useCallback(() => {\n\t\tsetPreviewUrl(null);\n\t\tfield.onChange(\"\");\n\t}, [field]);\n\n\treturn (\n\t\t\n\t\t\t{showLabel && (\n\t\t\t\t\n\t\t\t)}\n\t\t\t{!previewUrl && (\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t{isUploading && (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t)}\n\t\t\t{previewUrl && (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport {\n\tuseState,\n\tuseCallback,\n\tuseEffect,\n\ttype ChangeEvent,\n\ttype ComponentType,\n} from \"react\";\nimport { toast } from \"sonner\";\nimport type { AutoFormInputComponentProps } from \"@/components/ui/auto-form/types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tFormControl,\n\tFormItem,\n\tFormMessage,\n} from \"@/components/ui/form\";\nimport { Trash2, Loader2 } from \"lucide-react\";\nimport AutoFormLabel from \"@/components/ui/auto-form/common/label\";\nimport AutoFormTooltip from \"@/components/ui/auto-form/common/tooltip\";\n\n/**\n * Props for the CMSFileUpload component\n */\nexport interface CMSFileUploadProps extends AutoFormInputComponentProps {\n\t/**\n\t * Function to upload an image file and return the URL.\n\t * This is required - consumers must provide an upload implementation.\n\t */\n\tuploadImage: (file: File) => Promise;\n\t/**\n\t * Optional custom component for the image field.\n\t * When provided, it replaces the default file-upload input entirely.\n\t */\n\timageInputField?: ComponentType<{\n\t\tvalue: string;\n\t\tonChange: (value: string) => void;\n\t\tisRequired?: boolean;\n\t}>;\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered as a \"Browse media\" option.\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n}\n\n/**\n * Default file upload component for CMS image fields.\n *\n * This component:\n * - Accepts image files via file input\n * - Uses the required uploadImage prop to upload and get a URL\n * - Shows a preview of the uploaded image\n * - Allows removing the uploaded image\n *\n * You can use this component directly in your fieldComponents override,\n * or create your own custom component using this as a reference.\n *\n * @example\n * ```tsx\n * // Use the default component with your upload function\n * fieldComponents: {\n * file: (props) => (\n * \n * ),\n * }\n * ```\n */\nexport function CMSFileUpload({\n\tlabel,\n\tisRequired,\n\tfieldConfigItem,\n\tfieldProps,\n\tfield,\n\tuploadImage,\n\timageInputField: ImageInputField,\n\timagePicker: ImagePickerTrigger,\n}: CMSFileUploadProps) {\n\t// Exclude showLabel and value from props spread\n\t// File inputs cannot have their value set programmatically (browser security)\n\tconst {\n\t\tshowLabel: _showLabel,\n\t\tvalue: _value,\n\t\t...safeFieldProps\n\t} = fieldProps;\n\tconst showLabel = _showLabel === undefined ? true : _showLabel;\n\n\t// All hooks must be called unconditionally before any early return.\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [previewUrl, setPreviewUrl] = useState(\n\t\tfield.value || null,\n\t);\n\n\tuseEffect(() => {\n\t\tconst normalizedValue = field.value || null;\n\t\tif (normalizedValue !== previewUrl) {\n\t\t\tsetPreviewUrl(normalizedValue);\n\t\t}\n\t}, [field.value, previewUrl]);\n\n\tconst handleFileChange = useCallback(\n\t\tasync (e: ChangeEvent) => {\n\t\t\tconst file = e.target.files?.[0];\n\t\t\tif (!file) return;\n\n\t\t\tif (!file.type.startsWith(\"image/\")) {\n\t\t\t\ttoast.error(\"Please select an image file\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetIsUploading(true);\n\t\t\ttry {\n\t\t\t\tconst url = await uploadImage(file);\n\t\t\t\tsetPreviewUrl(url);\n\t\t\t\tfield.onChange(url);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Image upload failed:\", error);\n\t\t\t\ttoast.error(\"Failed to upload image\");\n\t\t\t} finally {\n\t\t\t\tsetIsUploading(false);\n\t\t\t}\n\t\t},\n\t\t[field, uploadImage],\n\t);\n\n\tconst handleRemove = useCallback(() => {\n\t\tsetPreviewUrl(null);\n\t\tfield.onChange(\"\");\n\t}, [field]);\n\n\t// When a custom imageInputField component is provided via overrides, delegate to it.\n\tif (ImageInputField) {\n\t\treturn (\n\t\t\t\n\t\t\t\t{showLabel && (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n\n\treturn (\n\t\t\n\t\t\t{showLabel && (\n\t\t\t\t\n\t\t\t)}\n\t\t\t{!previewUrl && (\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{isUploading && (\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t{ImagePickerTrigger && (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\tsetPreviewUrl(url);\n\t\t\t\t\t\t\t\t\t\tfield.onChange(url);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t)}\n\t\t\t{previewUrl && (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n", "target": "src/components/btst/cms/client/components/forms/file-upload.tsx" }, { @@ -199,7 +199,7 @@ { "path": "btst/cms/client/overrides.ts", "type": "registry:lib", - "content": "import type { ComponentType } from \"react\";\nimport type { CMSLocalization } from \"./localization\";\nimport type { AutoFormInputComponentProps } from \"@/components/ui/auto-form/types\";\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { typeSlug: \"product\", id: \"123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the CMS plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface CMSPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\n\t/**\n\t * Function used to upload an image and return its URL.\n\t * Used by the default \"file\" field component.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Custom field components for AutoForm fields.\n\t *\n\t * These map field type names to React components. Use these to:\n\t * - Override built-in field types (checkbox, date, select, radio, switch, textarea, file, number, fallback)\n\t * - Add custom field types for your content types\n\t *\n\t * The component receives AutoFormInputComponentProps which includes:\n\t * - field: react-hook-form field controller\n\t * - label: the field label\n\t * - isRequired: whether the field is required\n\t * - fieldConfigItem: the field config (description, inputProps, etc.)\n\t * - fieldProps: additional props from fieldConfig.inputProps\n\t * - zodItem: the Zod schema for this field\n\t *\n\t * @example\n\t * ```tsx\n\t * fieldComponents: {\n\t * // Override the file type with custom S3 upload\n\t * file: ({ field, label, isRequired }) => (\n\t * \n\t * ),\n\t * // Add a custom rich text editor\n\t * richText: ({ field, label }) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tfieldComponents?: Record>;\n\n\t/**\n\t * Localization object for the CMS plugin\n\t */\n\tlocalization?: CMSLocalization;\n\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'dashboard', 'contentList', 'contentEditor')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the dashboard page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDashboardRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param context - Route context\n\t */\n\tonBeforeListRendered?: (typeSlug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content editor page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param id - The content item ID (null for new items)\n\t * @param context - Route context\n\t */\n\tonBeforeEditorRendered?: (\n\t\ttypeSlug: string,\n\t\tid: string | null,\n\t\tcontext: RouteContext,\n\t) => boolean;\n}\n", + "content": "import type { ComponentType } from \"react\";\nimport type { CMSLocalization } from \"./localization\";\nimport type { AutoFormInputComponentProps } from \"@/components/ui/auto-form/types\";\n\n/**\n * Props for the overridable CMS image input field component.\n */\nexport interface CmsImageInputFieldProps {\n\t/** Current image URL value */\n\tvalue: string;\n\t/** Called when the image URL changes */\n\tonChange: (value: string) => void;\n\t/** Whether the field is required */\n\tisRequired?: boolean;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { typeSlug: \"product\", id: \"123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the CMS plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface CMSPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\n\t/**\n\t * Function used to upload an image and return its URL.\n\t * Used by the default \"file\" field component.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Optional custom component for image fields (fieldType: \"file\").\n\t *\n\t * When provided it replaces the default file-upload input entirely.\n\t * The component receives `value` (current URL string) and `onChange` (setter).\n\t *\n\t * @example\n\t * ```tsx\n\t * imageInputField: ({ value, onChange }) =>\n\t * value ? (\n\t *
\n\t * \"Preview\"\n\t * Change} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t *
\n\t * ) : (\n\t * Browse media} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t * )\n\t * ```\n\t */\n\timageInputField?: ComponentType;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered inside the default \"file\" field component as a\n\t * \"Browse media\" option, letting users select a previously uploaded asset.\n\t * Receives `onSelect(url)` — the URL is set as the field value.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\n\t/**\n\t * Custom field components for AutoForm fields.\n\t *\n\t * These map field type names to React components. Use these to:\n\t * - Override built-in field types (checkbox, date, select, radio, switch, textarea, file, number, fallback)\n\t * - Add custom field types for your content types\n\t *\n\t * The component receives AutoFormInputComponentProps which includes:\n\t * - field: react-hook-form field controller\n\t * - label: the field label\n\t * - isRequired: whether the field is required\n\t * - fieldConfigItem: the field config (description, inputProps, etc.)\n\t * - fieldProps: additional props from fieldConfig.inputProps\n\t * - zodItem: the Zod schema for this field\n\t *\n\t * @example\n\t * ```tsx\n\t * fieldComponents: {\n\t * // Override the file type with custom S3 upload\n\t * file: ({ field, label, isRequired }) => (\n\t * \n\t * ),\n\t * // Add a custom rich text editor\n\t * richText: ({ field, label }) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tfieldComponents?: Record>;\n\n\t/**\n\t * Localization object for the CMS plugin\n\t */\n\tlocalization?: CMSLocalization;\n\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'dashboard', 'contentList', 'contentEditor')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the dashboard page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDashboardRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param context - Route context\n\t */\n\tonBeforeListRendered?: (typeSlug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content editor page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param id - The content item ID (null for new items)\n\t * @param context - Route context\n\t */\n\tonBeforeEditorRendered?: (\n\t\ttypeSlug: string,\n\t\tid: string | null,\n\t\tcontext: RouteContext,\n\t) => boolean;\n}\n", "target": "src/components/btst/cms/client/overrides.ts" }, { diff --git a/packages/stack/registry/btst-kanban.json b/packages/stack/registry/btst-kanban.json index 18f06e5f..04662319 100644 --- a/packages/stack/registry/btst-kanban.json +++ b/packages/stack/registry/btst-kanban.json @@ -20,7 +20,7 @@ "card", "dialog", "dropdown-menu", - "https://raw.githubusercontent.com/olliethedev/shadcn-minimal-tiptap/refs/heads/feat/markdown/registry/block-registry.json", + "https://raw.githubusercontent.com/olliethedev/shadcn-minimal-tiptap/refs/heads/feat/image-upload-config/registry/block-registry.json", "input", "label", "select", @@ -49,7 +49,7 @@ { "path": "btst/kanban/client/components/forms/board-form.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport { useBoardMutations } from \"@btst/stack/plugins/kanban/client/hooks\";\nimport type { SerializedBoard } from \"../../../types\";\n\ninterface BoardFormProps {\n\tboard?: SerializedBoard;\n\tonClose: () => void;\n\tonSuccess: (boardId: string) => void;\n}\n\nexport function BoardForm({ board, onClose, onSuccess }: BoardFormProps) {\n\tconst isEditing = !!board;\n\tconst { createBoard, updateBoard, isCreating, isUpdating } =\n\t\tuseBoardMutations();\n\n\tconst [name, setName] = useState(board?.name || \"\");\n\tconst [description, setDescription] = useState(board?.description || \"\");\n\tconst [error, setError] = useState(null);\n\n\tconst isPending = isCreating || isUpdating;\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\n\t\tif (!name.trim()) {\n\t\t\tsetError(\"Name is required\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tif (isEditing && board) {\n\t\t\t\tawait updateBoard(board.id, { name, description });\n\t\t\t\tonSuccess(board.id);\n\t\t\t} else {\n\t\t\t\tconst newBoard = await createBoard({ name, description });\n\t\t\t\tif (newBoard?.id) {\n\t\t\t\t\tonSuccess(newBoard.id);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"An error occurred\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t) =>\n\t\t\t\t\t\tsetName(e.target.value)\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder=\"e.g., Project Alpha\"\n\t\t\t\t\tdisabled={isPending}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t) =>\n\t\t\t\t\t\tsetDescription(e.target.value)\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder=\"Describe your board...\"\n\t\t\t\t\tdisabled={isPending}\n\t\t\t\t\trows={3}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t{error && (\n\t\t\t\t
\n\t\t\t\t\t{error}\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tCancel\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport { useBoardMutations } from \"@btst/stack/plugins/kanban/client/hooks\";\nimport type { SerializedBoard } from \"../../../types\";\n\ninterface BoardFormProps {\n\tboard?: SerializedBoard;\n\tonClose: () => void;\n\tonSuccess: (boardId: string) => void;\n}\n\nexport function BoardForm({ board, onClose, onSuccess }: BoardFormProps) {\n\tconst isEditing = !!board;\n\tconst { createBoard, updateBoard, isCreating, isUpdating } =\n\t\tuseBoardMutations();\n\n\tconst [name, setName] = useState(board?.name || \"\");\n\tconst [description, setDescription] = useState(board?.description || \"\");\n\tconst [error, setError] = useState(null);\n\n\tconst isPending = isCreating || isUpdating;\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\n\t\tif (!name.trim()) {\n\t\t\tsetError(\"Name is required\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tif (isEditing && board) {\n\t\t\t\tawait updateBoard(board.id, { name, description });\n\t\t\t\tonSuccess(board.id);\n\t\t\t} else {\n\t\t\t\tconst newBoard = await createBoard({ name, description });\n\t\t\t\tif (newBoard?.id) {\n\t\t\t\t\tonSuccess(newBoard.id);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"An error occurred\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t) =>\n\t\t\t\t\t\tsetName(e.target.value)\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder=\"e.g., Project Alpha\"\n\t\t\t\t\tdisabled={isPending}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t) =>\n\t\t\t\t\t\tsetDescription(e.target.value)\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder=\"Describe your board...\"\n\t\t\t\t\tdisabled={isPending}\n\t\t\t\t\trows={3}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t{error && (\n\t\t\t\t
\n\t\t\t\t\t{error}\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tCancel\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n}\n", "target": "src/components/btst/kanban/client/components/forms/board-form.tsx" }, { @@ -61,7 +61,7 @@ { "path": "btst/kanban/client/components/forms/task-form.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Trash2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n\tSelect,\n\tSelectContent,\n\tSelectItem,\n\tSelectTrigger,\n\tSelectValue,\n} from \"@/components/ui/select\";\nimport { MinimalTiptapEditor } from \"@/components/ui/minimal-tiptap\";\nimport SearchSelect from \"@/components/ui/search-select\";\nimport { useTaskMutations, useSearchUsers } from \"@btst/stack/plugins/kanban/client/hooks\";\nimport { PRIORITY_OPTIONS } from \"../../../utils\";\nimport type {\n\tSerializedColumn,\n\tSerializedTask,\n\tPriority,\n} from \"../../../types\";\n\ninterface TaskFormProps {\n\tcolumnId: string;\n\tboardId: string;\n\ttaskId?: string;\n\ttask?: SerializedTask;\n\tcolumns: SerializedColumn[];\n\tonClose: () => void;\n\tonSuccess: () => void;\n\tonDelete?: () => void;\n}\n\nexport function TaskForm({\n\tcolumnId,\n\tboardId,\n\ttaskId,\n\ttask,\n\tcolumns,\n\tonClose,\n\tonSuccess,\n\tonDelete,\n}: TaskFormProps) {\n\tconst isEditing = !!taskId;\n\tconst {\n\t\tcreateTask,\n\t\tupdateTask,\n\t\tmoveTask,\n\t\tisCreating,\n\t\tisUpdating,\n\t\tisDeleting,\n\t\tisMoving,\n\t} = useTaskMutations();\n\n\tconst [title, setTitle] = useState(task?.title || \"\");\n\tconst [description, setDescription] = useState(task?.description || \"\");\n\tconst [priority, setPriority] = useState(\n\t\ttask?.priority || \"MEDIUM\",\n\t);\n\tconst [selectedColumnId, setSelectedColumnId] = useState(\n\t\ttask?.columnId || columnId,\n\t);\n\tconst [assigneeId, setAssigneeId] = useState(task?.assigneeId || \"\");\n\tconst [error, setError] = useState(null);\n\n\t// Fetch available users for assignment\n\tconst { data: users = [] } = useSearchUsers(\"\", boardId);\n\tconst userOptions = [\n\t\t{ value: \"\", label: \"Unassigned\" },\n\t\t...users.map((user) => ({ value: user.id, label: user.name })),\n\t];\n\n\tconst isPending = isCreating || isUpdating || isDeleting || isMoving;\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\n\t\tif (!title.trim()) {\n\t\t\tsetError(\"Title is required\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tif (isEditing && taskId) {\n\t\t\t\tconst isColumnChanging =\n\t\t\t\t\ttask?.columnId && selectedColumnId !== task.columnId;\n\n\t\t\t\tif (isColumnChanging) {\n\t\t\t\t\t// When changing columns, we need two operations:\n\t\t\t\t\t// 1. Update task properties (title, description, priority, assigneeId)\n\t\t\t\t\t// 2. Move task to new column with proper order calculation\n\t\t\t\t\t//\n\t\t\t\t\t// To avoid partial failure confusion, we attempt both operations\n\t\t\t\t\t// but provide clear messaging if one succeeds and the other fails.\n\n\t\t\t\t\t// First update the task properties (title, description, priority, assigneeId)\n\t\t\t\t\t// If this fails, nothing is saved and the outer catch handles it\n\t\t\t\t\tawait updateTask(taskId, {\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tpriority,\n\t\t\t\t\t\tassigneeId: assigneeId || null,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Then move the task to the new column with calculated order\n\t\t\t\t\t// Place at the end of the destination column\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst targetColumn = columns.find((c) => c.id === selectedColumnId);\n\t\t\t\t\t\tconst targetTasks = targetColumn?.tasks || [];\n\t\t\t\t\t\tconst targetOrder =\n\t\t\t\t\t\t\ttargetTasks.length > 0\n\t\t\t\t\t\t\t\t? Math.max(...targetTasks.map((t) => t.order)) + 1\n\t\t\t\t\t\t\t\t: 0;\n\n\t\t\t\t\t\tawait moveTask(taskId, selectedColumnId, targetOrder);\n\t\t\t\t\t} catch (moveErr) {\n\t\t\t\t\t\t// Properties were saved but column move failed\n\t\t\t\t\t\t// Provide specific error message about partial success\n\t\t\t\t\t\tconst moveErrorMsg =\n\t\t\t\t\t\t\tmoveErr instanceof Error ? moveErr.message : \"Unknown error\";\n\t\t\t\t\t\tsetError(\n\t\t\t\t\t\t\t`Task properties were saved, but moving to the new column failed: ${moveErrorMsg}. ` +\n\t\t\t\t\t\t\t\t`You can try dragging the task to the desired column.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\t// Don't call onSuccess since the operation wasn't fully completed\n\t\t\t\t\t\t// but also don't throw - we want to show the specific error\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Same column - just update the task properties\n\t\t\t\t\tawait updateTask(taskId, {\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tpriority,\n\t\t\t\t\t\tcolumnId: selectedColumnId,\n\t\t\t\t\t\tassigneeId: assigneeId || null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tawait createTask({\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tpriority,\n\t\t\t\t\tcolumnId: selectedColumnId,\n\t\t\t\t\tassigneeId: assigneeId || undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t\tonSuccess();\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"An error occurred\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t) =>\n\t\t\t\t\t\tsetTitle(e.target.value)\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder=\"e.g., Fix login bug\"\n\t\t\t\t\tdisabled={isPending}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t setPriority(v as Priority)}\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{PRIORITY_OPTIONS.map((option) => (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{option.label}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\tsetDescription(typeof value === \"string\" ? value : \"\")\n\t\t\t\t\t}\n\t\t\t\t\toutput=\"markdown\"\n\t\t\t\t\tplaceholder=\"Describe the task...\"\n\t\t\t\t\tclassName=\"min-h-[150px]\"\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t{error && (\n\t\t\t\t
\n\t\t\t\t\t{error}\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tCancel\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t{isEditing && onDelete && (\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tDelete\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Trash2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n\tSelect,\n\tSelectContent,\n\tSelectItem,\n\tSelectTrigger,\n\tSelectValue,\n} from \"@/components/ui/select\";\nimport { MinimalTiptapEditor } from \"@/components/ui/minimal-tiptap\";\nimport SearchSelect from \"@/components/ui/search-select\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { useTaskMutations, useSearchUsers } from \"@btst/stack/plugins/kanban/client/hooks\";\nimport type { KanbanPluginOverrides } from \"../../overrides\";\nimport { PRIORITY_OPTIONS } from \"../../../utils\";\nimport type {\n\tSerializedColumn,\n\tSerializedTask,\n\tPriority,\n} from \"../../../types\";\n\ninterface TaskFormProps {\n\tcolumnId: string;\n\tboardId: string;\n\ttaskId?: string;\n\ttask?: SerializedTask;\n\tcolumns: SerializedColumn[];\n\tonClose: () => void;\n\tonSuccess: () => void;\n\tonDelete?: () => void;\n}\n\nexport function TaskForm({\n\tcolumnId,\n\tboardId,\n\ttaskId,\n\ttask,\n\tcolumns,\n\tonClose,\n\tonSuccess,\n\tonDelete,\n}: TaskFormProps) {\n\tconst isEditing = !!taskId;\n\tconst { uploadImage, imagePicker: imagePickerTrigger } =\n\t\tusePluginOverrides(\"kanban\");\n\tconst {\n\t\tcreateTask,\n\t\tupdateTask,\n\t\tmoveTask,\n\t\tisCreating,\n\t\tisUpdating,\n\t\tisDeleting,\n\t\tisMoving,\n\t} = useTaskMutations();\n\n\tconst [title, setTitle] = useState(task?.title || \"\");\n\tconst [description, setDescription] = useState(task?.description || \"\");\n\tconst [priority, setPriority] = useState(\n\t\ttask?.priority || \"MEDIUM\",\n\t);\n\tconst [selectedColumnId, setSelectedColumnId] = useState(\n\t\ttask?.columnId || columnId,\n\t);\n\tconst [assigneeId, setAssigneeId] = useState(task?.assigneeId || \"\");\n\tconst [error, setError] = useState(null);\n\n\t// Fetch available users for assignment\n\tconst { data: users = [] } = useSearchUsers(\"\", boardId);\n\tconst userOptions = [\n\t\t{ value: \"\", label: \"Unassigned\" },\n\t\t...users.map((user) => ({ value: user.id, label: user.name })),\n\t];\n\n\tconst isPending = isCreating || isUpdating || isDeleting || isMoving;\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\n\t\tif (!title.trim()) {\n\t\t\tsetError(\"Title is required\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tif (isEditing && taskId) {\n\t\t\t\tconst isColumnChanging =\n\t\t\t\t\ttask?.columnId && selectedColumnId !== task.columnId;\n\n\t\t\t\tif (isColumnChanging) {\n\t\t\t\t\t// When changing columns, we need two operations:\n\t\t\t\t\t// 1. Update task properties (title, description, priority, assigneeId)\n\t\t\t\t\t// 2. Move task to new column with proper order calculation\n\t\t\t\t\t//\n\t\t\t\t\t// To avoid partial failure confusion, we attempt both operations\n\t\t\t\t\t// but provide clear messaging if one succeeds and the other fails.\n\n\t\t\t\t\t// First update the task properties (title, description, priority, assigneeId)\n\t\t\t\t\t// If this fails, nothing is saved and the outer catch handles it\n\t\t\t\t\tawait updateTask(taskId, {\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tpriority,\n\t\t\t\t\t\tassigneeId: assigneeId || null,\n\t\t\t\t\t});\n\n\t\t\t\t\t// Then move the task to the new column with calculated order\n\t\t\t\t\t// Place at the end of the destination column\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst targetColumn = columns.find((c) => c.id === selectedColumnId);\n\t\t\t\t\t\tconst targetTasks = targetColumn?.tasks || [];\n\t\t\t\t\t\tconst targetOrder =\n\t\t\t\t\t\t\ttargetTasks.length > 0\n\t\t\t\t\t\t\t\t? Math.max(...targetTasks.map((t) => t.order)) + 1\n\t\t\t\t\t\t\t\t: 0;\n\n\t\t\t\t\t\tawait moveTask(taskId, selectedColumnId, targetOrder);\n\t\t\t\t\t} catch (moveErr) {\n\t\t\t\t\t\t// Properties were saved but column move failed\n\t\t\t\t\t\t// Provide specific error message about partial success\n\t\t\t\t\t\tconst moveErrorMsg =\n\t\t\t\t\t\t\tmoveErr instanceof Error ? moveErr.message : \"Unknown error\";\n\t\t\t\t\t\tsetError(\n\t\t\t\t\t\t\t`Task properties were saved, but moving to the new column failed: ${moveErrorMsg}. ` +\n\t\t\t\t\t\t\t\t`You can try dragging the task to the desired column.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\t// Don't call onSuccess since the operation wasn't fully completed\n\t\t\t\t\t\t// but also don't throw - we want to show the specific error\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Same column - just update the task properties\n\t\t\t\t\tawait updateTask(taskId, {\n\t\t\t\t\t\ttitle,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tpriority,\n\t\t\t\t\t\tcolumnId: selectedColumnId,\n\t\t\t\t\t\tassigneeId: assigneeId || null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tawait createTask({\n\t\t\t\t\ttitle,\n\t\t\t\t\tdescription,\n\t\t\t\t\tpriority,\n\t\t\t\t\tcolumnId: selectedColumnId,\n\t\t\t\t\tassigneeId: assigneeId || undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t\tonSuccess();\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"An error occurred\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t) =>\n\t\t\t\t\t\tsetTitle(e.target.value)\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder=\"e.g., Fix login bug\"\n\t\t\t\t\tdisabled={isPending}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t setPriority(v as Priority)}\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{PRIORITY_OPTIONS.map((option) => (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{option.label}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\tsetDescription(typeof value === \"string\" ? value : \"\")\n\t\t\t\t\t}\n\t\t\t\t\toutput=\"markdown\"\n\t\t\t\t\tplaceholder=\"Describe the task...\"\n\t\t\t\t\tclassName=\"min-h-[150px]\"\n\t\t\t\t\tuploader={uploadImage}\n\t\t\t\t\timagePickerTrigger={imagePickerTrigger}\n\t\t\t\t/>\n\t\t\t
\n\n\t\t\t{error && (\n\t\t\t\t
\n\t\t\t\t\t{error}\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tCancel\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t{isEditing && onDelete && (\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tDelete\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t
\n\t);\n}\n", "target": "src/components/btst/kanban/client/components/forms/task-form.tsx" }, { @@ -193,7 +193,7 @@ { "path": "btst/kanban/client/overrides.ts", "type": "registry:lib", - "content": "import type { ComponentType, ReactNode } from \"react\";\nimport type { KanbanLocalization } from \"./localization\";\nimport type { SerializedTask } from \"../types\";\n\n/**\n * User information for assignee display/selection\n * Framework-agnostic - consumers map their auth system to this shape\n */\nexport interface KanbanUser {\n\tid: string;\n\tname: string;\n\tavatarUrl?: string;\n\temail?: string;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { boardId: \"abc123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the Kanban plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface KanbanPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Localization object for the kanban plugin\n\t */\n\tlocalization?: KanbanLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// ============ User Resolution (required for assignee features) ============\n\n\t/**\n\t * Resolve user info from an assigneeId\n\t * Called when rendering task cards/forms that have an assignee\n\t * Return null for unknown users (will show fallback UI)\n\t */\n\tresolveUser: (\n\t\tuserId: string,\n\t) => Promise | KanbanUser | null;\n\n\t/**\n\t * Search/list users available for assignment\n\t * Called when user opens the assignee picker\n\t * @param query - Search query (empty string for initial load)\n\t * @param boardId - Optional board context for scoped user lists\n\t */\n\tsearchUsers: (\n\t\tquery: string,\n\t\tboardId?: string,\n\t) => Promise | KanbanUser[];\n\n\t// ============ Lifecycle Hooks (optional) ============\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'boards', 'board', 'newBoard')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the boards list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeBoardsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param boardId - The board ID\n\t * @param context - Route context\n\t */\n\tonBeforeBoardPageRendered?: (\n\t\tboardId: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the new board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewBoardPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered at the bottom of the task detail dialog.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the kanban plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * kanban: {\n\t * taskDetailBottomSlot: (task) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\ttaskDetailBottomSlot?: (task: SerializedTask) => ReactNode;\n}\n", + "content": "import type { ComponentType, ReactNode } from \"react\";\nimport type { KanbanLocalization } from \"./localization\";\nimport type { SerializedTask } from \"../types\";\n\n/**\n * User information for assignee display/selection\n * Framework-agnostic - consumers map their auth system to this shape\n */\nexport interface KanbanUser {\n\tid: string;\n\tname: string;\n\tavatarUrl?: string;\n\temail?: string;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { boardId: \"abc123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the Kanban plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface KanbanPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Localization object for the kanban plugin\n\t */\n\tlocalization?: KanbanLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t/**\n\t * Function used to upload an image from the task description editor and return its URL.\n\t * Wired as the `uploader` prop of MinimalTiptapEditor — handles drag-drop image uploads.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it appears inside the image insertion dialog of the task description editor,\n\t * letting users browse and select previously uploaded assets.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\n\t// ============ User Resolution (required for assignee features) ============\n\n\t/**\n\t * Resolve user info from an assigneeId\n\t * Called when rendering task cards/forms that have an assignee\n\t * Return null for unknown users (will show fallback UI)\n\t */\n\tresolveUser: (\n\t\tuserId: string,\n\t) => Promise | KanbanUser | null;\n\n\t/**\n\t * Search/list users available for assignment\n\t * Called when user opens the assignee picker\n\t * @param query - Search query (empty string for initial load)\n\t * @param boardId - Optional board context for scoped user lists\n\t */\n\tsearchUsers: (\n\t\tquery: string,\n\t\tboardId?: string,\n\t) => Promise | KanbanUser[];\n\n\t// ============ Lifecycle Hooks (optional) ============\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'boards', 'board', 'newBoard')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the boards list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeBoardsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param boardId - The board ID\n\t * @param context - Route context\n\t */\n\tonBeforeBoardPageRendered?: (\n\t\tboardId: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the new board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewBoardPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered at the bottom of the task detail dialog.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the kanban plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * kanban: {\n\t * taskDetailBottomSlot: (task) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\ttaskDetailBottomSlot?: (task: SerializedTask) => ReactNode;\n}\n", "target": "src/components/btst/kanban/client/overrides.ts" }, { diff --git a/packages/stack/registry/registry.json b/packages/stack/registry/registry.json index fcc8f8d1..e4381a05 100644 --- a/packages/stack/registry/registry.json +++ b/packages/stack/registry/registry.json @@ -159,7 +159,7 @@ "card", "dialog", "dropdown-menu", - "https://raw.githubusercontent.com/olliethedev/shadcn-minimal-tiptap/refs/heads/feat/markdown/registry/block-registry.json", + "https://raw.githubusercontent.com/olliethedev/shadcn-minimal-tiptap/refs/heads/feat/image-upload-config/registry/block-registry.json", "input", "label", "select", diff --git a/packages/stack/scripts/build-registry.ts b/packages/stack/scripts/build-registry.ts index b34f3a5b..e14d4ac0 100644 --- a/packages/stack/scripts/build-registry.ts +++ b/packages/stack/scripts/build-registry.ts @@ -57,7 +57,7 @@ const EXTERNAL_REGISTRY_COMPONENTS: Record = { "form-builder": "https://raw.githubusercontent.com/better-stack-ai/form-builder/refs/heads/main/registry/form-builder.json", "minimal-tiptap": - "https://raw.githubusercontent.com/olliethedev/shadcn-minimal-tiptap/refs/heads/feat/markdown/registry/block-registry.json", + "https://raw.githubusercontent.com/olliethedev/shadcn-minimal-tiptap/refs/heads/feat/image-upload-config/registry/block-registry.json", "ui-builder": "https://raw.githubusercontent.com/olliethedev/ui-builder/refs/heads/main/registry/block-registry.json", }; From b1e42c4dc5508d31db415ad313dba7960857c42d Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 14:01:09 -0400 Subject: [PATCH 14/29] refactor: update URL handling in media adapters and markdown editor to ensure proper percent-encoding for improved consistency --- .../forms/markdown-editor-with-overrides.tsx | 8 ++++---- .../client/components/forms/markdown-editor.tsx | 4 ++-- .../stack/src/plugins/media/api/adapters/local.ts | 13 ++++++++++--- packages/stack/src/plugins/media/api/adapters/s3.ts | 12 ++++++++++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx index f985d0fe..e30eec54 100644 --- a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +++ b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx @@ -32,16 +32,16 @@ export function MarkdownEditorWithOverrides( const triggerContainerRef = useRef(null); // Single onSelect handler for ImagePickerTrigger. - // URLs are encoded here before being forwarded to either destination. + // URLs returned by the media plugin are already percent-encoded at the + // source (storage adapter), so no additional encoding is applied here. const handleSelect = useCallback((url: string) => { - const encodedUrl = encodeURI(url); if (pendingInsertUrlRef.current) { // Crepe image block flow: set the URL into the block's link input. - pendingInsertUrlRef.current(encodedUrl); + pendingInsertUrlRef.current(url); pendingInsertUrlRef.current = null; } else { // Normal flow: insert image at end of markdown content. - insertImageRef.current?.(encodedUrl); + insertImageRef.current?.(url); } }, []); diff --git a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx index 6d3fe4a8..92c2c713 100644 --- a/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx +++ b/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.tsx @@ -26,14 +26,14 @@ export interface MarkdownEditorProps { /** * Optional ref that will be populated with an `insertImage(url)` function. * Call `insertImageRef.current?.(url)` to programmatically insert an image. - * The URL is expected to be already encoded by the caller. + * The URL must be a valid, percent-encoded URL (storage adapters guarantee this). */ insertImageRef?: MutableRefObject<((url: string) => void) | null>; /** * When provided, clicking the Crepe image block's upload area opens a media * picker instead of the native file dialog. The callback receives a `setUrl` * function — call it with the chosen URL to set it into the image block. - * The URL passed to `setUrl` is expected to be already encoded by the caller. + * The URL must be a valid, percent-encoded URL (storage adapters guarantee this). */ openMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void; } diff --git a/packages/stack/src/plugins/media/api/adapters/local.ts b/packages/stack/src/plugins/media/api/adapters/local.ts index 23714158..e098776d 100644 --- a/packages/stack/src/plugins/media/api/adapters/local.ts +++ b/packages/stack/src/plugins/media/api/adapters/local.ts @@ -51,13 +51,20 @@ export function localAdapter( await fs.writeFile(filePath, buffer); - const url = `${publicPath.replace(/\/$/, "")}/${storedFilename}`; + // Percent-encode the filename segment so the returned URL is always a + // valid URL — e.g. spaces become %20. The raw storedFilename is used for + // the filesystem path; the encoded form is what gets stored in the DB and + // served to clients. + const url = `${publicPath.replace(/\/$/, "")}/${encodeURIComponent(storedFilename)}`; return { url }; }, async delete(url: string): Promise { - const filename = url.split("/").pop(); - if (!filename) return; + // The stored URL has an encoded filename (e.g. "my%20file.png"); decode + // it back to the raw filesystem name before building the file path. + const encodedFilename = url.split("/").pop(); + if (!encodedFilename) return; + const filename = decodeURIComponent(encodedFilename); const filePath = path.join(uploadDir, filename); try { diff --git a/packages/stack/src/plugins/media/api/adapters/s3.ts b/packages/stack/src/plugins/media/api/adapters/s3.ts index 4b3b2257..ec17f9a5 100644 --- a/packages/stack/src/plugins/media/api/adapters/s3.ts +++ b/packages/stack/src/plugins/media/api/adapters/s3.ts @@ -159,7 +159,13 @@ export function s3Adapter(options: S3StorageAdapterOptions): S3StorageAdapter { }); const uploadUrl = await buildSignedUrl(client, command, { expiresIn }); - const publicUrl = `${publicBaseUrl.replace(/\/$/, "")}/${key}`; + + // Percent-encode each path segment so the stored public URL is always + // valid. The raw `key` is used for the S3 key (which the AWS SDK + // handles separately); the encoded form is what gets stored in the DB + // and returned to clients. + const encodedKey = key.split("/").map(encodeURIComponent).join("/"); + const publicUrl = `${publicBaseUrl.replace(/\/$/, "")}/${encodedKey}`; return { type: "presigned-url", @@ -180,9 +186,11 @@ export function s3Adapter(options: S3StorageAdapterOptions): S3StorageAdapter { ]); const base = publicBaseUrl.replace(/\/$/, ""); - const key = url.startsWith(base) + const encodedKey = url.startsWith(base) ? url.slice(base.length + 1) : (url.split("/").pop() ?? url); + // Decode the percent-encoded key back to the raw S3 object key. + const key = decodeURIComponent(encodedKey); await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); }, From e7a338401624080aa392cdd973aa5a111d08db0a Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 14:24:32 -0400 Subject: [PATCH 15/29] refactor: implement MediaPicker integration in CMS image upload tests for improved image handling and user experience --- e2e/tests/smoke.cms.spec.ts | 135 ++++++++++-------- e2e/tests/smoke.media.spec.ts | 12 +- examples/nextjs/app/pages/layout.tsx | 15 +- examples/react-router/.gitignore | 3 + .../react-router/app/routes/pages/_layout.tsx | 12 +- examples/tanstack/src/routes/pages/route.tsx | 12 +- .../client/components/media-picker/index.tsx | 44 ++++-- 7 files changed, 151 insertions(+), 82 deletions(-) diff --git a/e2e/tests/smoke.cms.spec.ts b/e2e/tests/smoke.cms.spec.ts index 4a935f95..8962584e 100644 --- a/e2e/tests/smoke.cms.spec.ts +++ b/e2e/tests/smoke.cms.spec.ts @@ -1,4 +1,11 @@ -import { expect, test } from "@playwright/test"; +import { expect, test, type Page } from "@playwright/test"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +// Shared test image buffer loaded once for the whole module +const testImageBuffer = readFileSync( + resolve(__dirname, "../fixtures/test-image.png"), +); // Ignore network/resource 404s from image thumbnail loading (local adapter // serves uploads as static Next.js files; the preview may 404 in @@ -453,6 +460,49 @@ test.describe("CMS Plugin", () => { }); }); +// ─── MediaPicker helpers (reused across CMS image upload tests) ───────────── + +/** Open the MediaPicker popover via the `open-media-picker` trigger. */ +async function openMediaPicker(page: Page) { + const triggerBtn = page.locator('[data-testid="open-media-picker"]').first(); + await expect(triggerBtn).toBeVisible({ timeout: 10000 }); + await triggerBtn.click(); + await expect(page.getByText("Media Library")).toBeVisible({ timeout: 5000 }); +} + +/** + * Inside an open MediaPicker, switch to the Upload tab and set a test image, + * then switch to Browse to wait for the uploaded asset to appear. + */ +async function uploadInMediaPicker(page: Page) { + await page.getByRole("tab", { name: /upload/i }).click(); + const fileInput = page.locator('[data-testid="media-upload-input"]').first(); + await expect(fileInput).toBeAttached({ timeout: 5000 }); + await fileInput.setInputFiles({ + name: "test-product-image.png", + mimeType: "image/png", + buffer: testImageBuffer, + }); + // Switch to Browse and wait for the uploaded thumbnail to appear + await page.getByRole("tab", { name: /browse/i }).click(); + await expect( + page.locator('[data-testid="media-asset-item"]').first(), + ).toBeVisible({ timeout: 15000 }); +} + +/** Click the first asset in the Browse grid, then confirm selection. */ +async function selectFirstAsset(page: Page) { + await page.locator('[data-testid="media-asset-item"]').first().click(); + const selectBtn = page.locator('[data-testid="media-select-button"]'); + await expect(selectBtn).toBeVisible({ timeout: 3000 }); + await selectBtn.click(); + await expect(page.getByText("Media Library")).not.toBeVisible({ + timeout: 5000, + }); +} + +// ─── CMS Image Upload tests ────────────────────────────────────────────────── + test.describe("CMS Image Upload", () => { // Generate unique ID for each test run to avoid slug collisions const testRunId = Date.now().toString(36); @@ -466,9 +516,9 @@ test.describe("CMS Image Upload", () => { await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - // Should show the image upload input - const imageUploadInput = page.locator('[data-testid="image-upload-input"]'); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); + // The MediaPicker trigger button should be visible in the image field + const trigger = page.locator('[data-testid="open-media-picker"]').first(); + await expect(trigger).toBeVisible({ timeout: 5000 }); expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( [], @@ -484,27 +534,20 @@ test.describe("CMS Image Upload", () => { await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - // Wait for the image upload input to be visible - const imageUploadInput = page.locator('[data-testid="image-upload-input"]'); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); - - // Upload a test image file - const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - await imageUploadInput.setInputFiles({ - name: "test-product-image.png", - mimeType: "image/png", - buffer: Buffer.from(testImageBase64, "base64"), - }); + // Open the MediaPicker and upload via the Upload tab + await openMediaPicker(page); + await uploadInMediaPicker(page); + await selectFirstAsset(page); - // Wait for the preview to appear + // After selection the image preview should appear const imagePreview = page.locator('[data-testid="image-preview"]'); await expect(imagePreview).toBeVisible({ timeout: 10000 }); - // The preview should show a real URL from the media plugin upload endpoint + // The preview should show a real URL (not a mock placeholder) const previewSrc = await imagePreview.getAttribute("src"); expect(previewSrc).toBeTruthy(); expect(previewSrc).not.toBe(""); + expect(previewSrc).not.toContain("placehold.co"); expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( [], @@ -520,29 +563,23 @@ test.describe("CMS Image Upload", () => { await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - // Upload an image first - const imageUploadInput = page.locator('[data-testid="image-upload-input"]'); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); - - const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - await imageUploadInput.setInputFiles({ - name: "to-remove.png", - mimeType: "image/png", - buffer: Buffer.from(testImageBase64, "base64"), - }); + // Upload via MediaPicker + await openMediaPicker(page); + await uploadInMediaPicker(page); + await selectFirstAsset(page); // Wait for preview const imagePreview = page.locator('[data-testid="image-preview"]'); await expect(imagePreview).toBeVisible({ timeout: 10000 }); // Click remove button - const removeButton = page.locator('[data-testid="remove-image-button"]'); - await removeButton.click(); + await page.locator('[data-testid="remove-image-button"]').click(); - // Preview should be hidden, upload input should reappear + // Preview should be gone; the Browse Media trigger should reappear await expect(imagePreview).not.toBeVisible(); - await expect(imageUploadInput).toBeAttached(); + await expect( + page.locator('[data-testid="open-media-picker"]').first(), + ).toBeVisible(); expect(errors, `Console errors detected: \n${errors.join("\n")}`).toEqual( [], @@ -572,17 +609,10 @@ test.describe("CMS Image Upload", () => { await page.locator('[role="option"]').first().waitFor({ state: "visible" }); await page.locator('[role="option"]').first().click(); - // Upload an image - const imageUploadInput = page.locator('[data-testid="image-upload-input"]'); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); - - const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - await imageUploadInput.setInputFiles({ - name: "product-image.png", - mimeType: "image/png", - buffer: Buffer.from(testImageBase64, "base64"), - }); + // Upload an image via MediaPicker + await openMediaPicker(page); + await uploadInMediaPicker(page); + await selectFirstAsset(page); // Wait for preview const imagePreview = page.locator('[data-testid="image-preview"]'); @@ -622,17 +652,10 @@ test.describe("CMS Image Upload", () => { await page.locator('[role="option"]').first().waitFor({ state: "visible" }); await page.locator('[role="option"]').first().click(); - // Upload an image - const imageUploadInput = page.locator('[data-testid="image-upload-input"]'); - await expect(imageUploadInput).toBeAttached({ timeout: 5000 }); - - const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - await imageUploadInput.setInputFiles({ - name: "original-image.png", - mimeType: "image/png", - buffer: Buffer.from(testImageBase64, "base64"), - }); + // Upload an image via MediaPicker + await openMediaPicker(page); + await uploadInMediaPicker(page); + await selectFirstAsset(page); // Wait for preview let imagePreview = page.locator('[data-testid="image-preview"]'); @@ -650,7 +673,7 @@ test.describe("CMS Image Upload", () => { const row = page.locator(`tr:has-text("${expectedSlug}")`); await row.locator("button:has(svg.lucide-pencil)").click(); - // On edit page, image preview should still be visible + // On edit page, image preview should still be visible (loaded from DB) imagePreview = page.locator('[data-testid="image-preview"]'); await expect(imagePreview).toBeVisible({ timeout: 10000 }); diff --git a/e2e/tests/smoke.media.spec.ts b/e2e/tests/smoke.media.spec.ts index d35fa8ea..889d1801 100644 --- a/e2e/tests/smoke.media.spec.ts +++ b/e2e/tests/smoke.media.spec.ts @@ -74,10 +74,8 @@ test.describe("Media Plugin — direct upload via MediaPicker", () => { await page.goto("/pages/blog/new", { waitUntil: "networkidle" }); await expect(page.locator('[data-testid="new-post-page"]')).toBeVisible(); - // The image picker trigger should be visible adjacent to the markdown editor - const trigger = page - .locator('[data-testid="image-picker-trigger"]') - .first(); + // ImageInputField renders a "Browse Media" button (open-media-picker) when no image is set + const trigger = page.locator('[data-testid="open-media-picker"]').first(); await expect(trigger).toBeVisible({ timeout: 10000 }); expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); @@ -94,10 +92,8 @@ test.describe("Media Plugin — direct upload via MediaPicker", () => { await page.goto("/pages/cms/product/new", { waitUntil: "networkidle" }); - // The image picker trigger should be visible inside the file upload field - const trigger = page - .locator('[data-testid="image-picker-trigger"]') - .first(); + // ImageInputField renders a "Browse Media" button (open-media-picker) when no image is set + const trigger = page.locator('[data-testid="open-media-picker"]').first(); await expect(trigger).toBeVisible({ timeout: 10000 }); expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); diff --git a/examples/nextjs/app/pages/layout.tsx b/examples/nextjs/app/pages/layout.tsx index 7f160c56..5c37c3c0 100644 --- a/examples/nextjs/app/pages/layout.tsx +++ b/examples/nextjs/app/pages/layout.tsx @@ -93,6 +93,19 @@ export default function ExampleLayout({ return asset.url; }, [baseURL]); + // For chat file attachments we embed as a data URL so OpenAI can read the + // content directly — a local /uploads/... path is not reachable from OpenAI's servers. + const uploadFileForChat = React.useCallback( + (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = () => reject(new Error("Failed to read file")) + reader.readAsDataURL(file) + }), + [], + ); + return ( @@ -161,7 +174,7 @@ export default function ExampleLayout({ apiBasePath: "/api/data", navigate: (path) => router.push(path), refresh: () => router.refresh(), - uploadFile: uploadImage, + uploadFile: uploadFileForChat, Link: ({ href, ...props }) => , Image: NextImageWrapper, chatSuggestions: [ diff --git a/examples/react-router/.gitignore b/examples/react-router/.gitignore index 039ee62d..afa2f3bf 100644 --- a/examples/react-router/.gitignore +++ b/examples/react-router/.gitignore @@ -5,3 +5,6 @@ # React Router /.react-router/ /build/ + +#misc +/public/uploads/** diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index 0e1ed352..4b36cd96 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -48,6 +48,16 @@ export default function Layout() { return asset.url } + // For chat file attachments we embed as a data URL so OpenAI can read the + // content directly — a local /uploads/... path is not reachable from OpenAI's servers. + const uploadFileForChat = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = () => reject(new Error("Failed to read file")) + reader.readAsDataURL(file) + }) + return ( @@ -111,7 +121,7 @@ export default function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => navigate(href), - uploadFile: uploadImage, + uploadFile: uploadFileForChat, Link: ({ href, children, className, ...props }) => ( {children} diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index 75e88c48..cd8f274d 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -51,6 +51,16 @@ function Layout() { return asset.url } + // For chat file attachments we embed as a data URL so OpenAI can read the + // content directly — a local /uploads/... path is not reachable from OpenAI's servers. + const uploadFileForChat = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = () => reject(new Error("Failed to read file")) + reader.readAsDataURL(file) + }) + return ( @@ -115,7 +125,7 @@ function Layout() { apiBaseURL: baseURL, apiBasePath: "/api/data", navigate: (href) => router.navigate({ href }), - uploadFile: uploadImage, + uploadFile: uploadFileForChat, Link: ({ href, children, className, ...props }) => ( {children} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx index 3100ab22..bd9b2f0f 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -240,7 +240,11 @@ export function MediaPicker({ } /** - * ImageInputField — a component that displays an image preview and a media picker button. + * ImageInputField — displays an image preview with change/remove actions, or a + * "Browse Media" button that opens the full MediaPicker popover (Browse / Upload / URL tabs). + * + * Upload mode, folder selection, and multi-mode cloud support are all handled inside + * the MediaPicker's UploadTab — this component is purely a thin wrapper. */ export function ImageInputField({ value, @@ -277,7 +281,12 @@ export function ImageInputField({
+ } @@ -288,6 +297,7 @@ export function ImageInputField({ type="button" variant="destructive" size="sm" + data-testid="remove-image-button" onClick={() => onChange("")} > Remove @@ -296,20 +306,24 @@ export function ImageInputField({
); } + return ( - - Browse Media - - } - accept={["image/*"]} - onSelect={(assets) => onChange(assets[0]?.url ?? "")} - /> +
+ + Browse Media + + } + accept={["image/*"]} + onSelect={(assets) => onChange(assets[0]?.url ?? "")} + /> +
); } From e953d62ab7565bc6067158a2cb4c220292be5d74 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 14:24:43 -0400 Subject: [PATCH 16/29] feat: add btst-media plugin with components and types for enhanced media handling in the stack --- packages/stack/registry/btst-blog.json | 4 +- packages/stack/registry/btst-media.json | 104 +++++++++++++++++++++++ packages/stack/registry/registry.json | 18 ++++ packages/stack/scripts/build-registry.ts | 13 +++ packages/stack/scripts/test-registry.sh | 6 +- 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 packages/stack/registry/btst-media.json diff --git a/packages/stack/registry/btst-blog.json b/packages/stack/registry/btst-blog.json index 22aeffd6..f1319296 100644 --- a/packages/stack/registry/btst-blog.json +++ b/packages/stack/registry/btst-blog.json @@ -70,13 +70,13 @@ { "path": "btst/blog/client/components/forms/markdown-editor-with-overrides.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { useCallback, useRef } from \"react\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { MarkdownEditor, type MarkdownEditorProps } from \"./markdown-editor\";\n\ntype MarkdownEditorWithOverridesProps = Omit<\n\tMarkdownEditorProps,\n\t| \"uploadImage\"\n\t| \"placeholder\"\n\t| \"insertImageRef\"\n\t| \"openMediaPickerForImageBlock\"\n>;\n\nexport function MarkdownEditorWithOverrides(\n\tprops: MarkdownEditorWithOverridesProps,\n) {\n\tconst {\n\t\tuploadImage,\n\t\timagePicker: ImagePickerTrigger,\n\t\tlocalization,\n\t} = usePluginOverrides>(\n\t\t\"blog\",\n\t\t{ localization: BLOG_LOCALIZATION },\n\t);\n\n\tconst insertImageRef = useRef<((url: string) => void) | null>(null);\n\t// Holds the Crepe-image-block `setUrl` callback while the picker is open.\n\tconst pendingInsertUrlRef = useRef<((url: string) => void) | null>(null);\n\t// Ref to the trigger wrapper so we can programmatically click the picker button.\n\tconst triggerContainerRef = useRef(null);\n\n\t// Single onSelect handler for ImagePickerTrigger.\n\t// URLs are encoded here before being forwarded to either destination.\n\tconst handleSelect = useCallback((url: string) => {\n\t\tconst encodedUrl = encodeURI(url);\n\t\tif (pendingInsertUrlRef.current) {\n\t\t\t// Crepe image block flow: set the URL into the block's link input.\n\t\t\tpendingInsertUrlRef.current(encodedUrl);\n\t\t\tpendingInsertUrlRef.current = null;\n\t\t} else {\n\t\t\t// Normal flow: insert image at end of markdown content.\n\t\t\tinsertImageRef.current?.(encodedUrl);\n\t\t}\n\t}, []);\n\n\t// Called by MarkdownEditor's click interceptor when the user clicks a Crepe\n\t// image-block upload placeholder.\n\tconst openMediaPickerForImageBlock = useCallback(\n\t\t(setUrl: (url: string) => void) => {\n\t\t\tpendingInsertUrlRef.current = setUrl;\n\t\t\t// Programmatically click the visible picker trigger button.\n\t\t\tconst btn = triggerContainerRef.current?.querySelector(\n\t\t\t\t'[data-testid=\"open-media-picker\"]',\n\t\t\t) as HTMLButtonElement | null;\n\t\t\tbtn?.click();\n\t\t},\n\t\t[],\n\t);\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t{ImagePickerTrigger && (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\nimport { useCallback, useRef } from \"react\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { MarkdownEditor, type MarkdownEditorProps } from \"./markdown-editor\";\n\ntype MarkdownEditorWithOverridesProps = Omit<\n\tMarkdownEditorProps,\n\t| \"uploadImage\"\n\t| \"placeholder\"\n\t| \"insertImageRef\"\n\t| \"openMediaPickerForImageBlock\"\n>;\n\nexport function MarkdownEditorWithOverrides(\n\tprops: MarkdownEditorWithOverridesProps,\n) {\n\tconst {\n\t\tuploadImage,\n\t\timagePicker: ImagePickerTrigger,\n\t\tlocalization,\n\t} = usePluginOverrides>(\n\t\t\"blog\",\n\t\t{ localization: BLOG_LOCALIZATION },\n\t);\n\n\tconst insertImageRef = useRef<((url: string) => void) | null>(null);\n\t// Holds the Crepe-image-block `setUrl` callback while the picker is open.\n\tconst pendingInsertUrlRef = useRef<((url: string) => void) | null>(null);\n\t// Ref to the trigger wrapper so we can programmatically click the picker button.\n\tconst triggerContainerRef = useRef(null);\n\n\t// Single onSelect handler for ImagePickerTrigger.\n\t// URLs returned by the media plugin are already percent-encoded at the\n\t// source (storage adapter), so no additional encoding is applied here.\n\tconst handleSelect = useCallback((url: string) => {\n\t\tif (pendingInsertUrlRef.current) {\n\t\t\t// Crepe image block flow: set the URL into the block's link input.\n\t\t\tpendingInsertUrlRef.current(url);\n\t\t\tpendingInsertUrlRef.current = null;\n\t\t} else {\n\t\t\t// Normal flow: insert image at end of markdown content.\n\t\t\tinsertImageRef.current?.(url);\n\t\t}\n\t}, []);\n\n\t// Called by MarkdownEditor's click interceptor when the user clicks a Crepe\n\t// image-block upload placeholder.\n\tconst openMediaPickerForImageBlock = useCallback(\n\t\t(setUrl: (url: string) => void) => {\n\t\t\tpendingInsertUrlRef.current = setUrl;\n\t\t\t// Programmatically click the visible picker trigger button.\n\t\t\tconst btn = triggerContainerRef.current?.querySelector(\n\t\t\t\t'[data-testid=\"open-media-picker\"]',\n\t\t\t) as HTMLButtonElement | null;\n\t\t\tbtn?.click();\n\t\t},\n\t\t[],\n\t);\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t{ImagePickerTrigger && (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/forms/markdown-editor-with-overrides.tsx" }, { "path": "btst/blog/client/components/forms/markdown-editor.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { Crepe, CrepeFeature } from \"@milkdown/crepe\";\nimport \"@milkdown/crepe/theme/common/style.css\";\nimport \"./markdown-editor-styles.css\";\n\nimport { cn, throttle } from \"../../../utils\";\nimport { editorViewCtx, parserCtx } from \"@milkdown/kit/core\";\nimport { listener, listenerCtx } from \"@milkdown/kit/plugin/listener\";\nimport { Slice } from \"@milkdown/kit/prose/model\";\nimport { Selection } from \"@milkdown/kit/prose/state\";\nimport {\n\tuseLayoutEffect,\n\tuseRef,\n\tuseState,\n\ttype MutableRefObject,\n} from \"react\";\n\nexport interface MarkdownEditorProps {\n\tvalue?: string;\n\tonChange?: (markdown: string) => void;\n\tclassName?: string;\n\t/** Optional image upload handler. When provided, enables image upload in the editor. */\n\tuploadImage?: (file: File) => Promise;\n\t/** Placeholder text shown when the editor is empty. */\n\tplaceholder?: string;\n\t/**\n\t * Optional ref that will be populated with an `insertImage(url)` function.\n\t * Call `insertImageRef.current?.(url)` to programmatically insert an image.\n\t * The URL is expected to be already encoded by the caller.\n\t */\n\tinsertImageRef?: MutableRefObject<((url: string) => void) | null>;\n\t/**\n\t * When provided, clicking the Crepe image block's upload area opens a media\n\t * picker instead of the native file dialog. The callback receives a `setUrl`\n\t * function — call it with the chosen URL to set it into the image block.\n\t * The URL passed to `setUrl` is expected to be already encoded by the caller.\n\t */\n\topenMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void;\n}\n\nexport function MarkdownEditor({\n\tvalue,\n\tonChange,\n\tclassName,\n\tuploadImage,\n\tplaceholder = \"Write something...\",\n\tinsertImageRef,\n\topenMediaPickerForImageBlock,\n}: MarkdownEditorProps) {\n\tconst containerRef = useRef(null);\n\tconst crepeRef = useRef(null);\n\tconst isReadyRef = useRef(false);\n\tconst [isReady, setIsReady] = useState(false);\n\tconst onChangeRef = useRef(onChange);\n\tconst initialValueRef = useRef(value ?? \"\");\n\tconst openMediaPickerRef = useRef(\n\t\topenMediaPickerForImageBlock,\n\t);\n\ttype ThrottledFn = ((markdown: string) => void) & {\n\t\tcancel?: () => void;\n\t\tflush?: () => void;\n\t};\n\tconst throttledOnChangeRef = useRef(null);\n\n\tonChangeRef.current = onChange;\n\topenMediaPickerRef.current = openMediaPickerForImageBlock;\n\n\tuseLayoutEffect(() => {\n\t\tif (crepeRef.current) return;\n\t\tconst container = containerRef.current;\n\t\tif (!container) return;\n\n\t\tconst hasMediaPicker = !!openMediaPickerRef.current;\n\n\t\tconst imageBlockConfig: Record = {};\n\t\tif (uploadImage) {\n\t\t\timageBlockConfig.onUpload = async (file: File) => uploadImage(file);\n\t\t}\n\t\tif (hasMediaPicker) {\n\t\t\timageBlockConfig.blockUploadPlaceholderText = \"Media Picker\";\n\t\t\timageBlockConfig.inlineUploadPlaceholderText = \"Media Picker\";\n\t\t}\n\n\t\tconst crepe = new Crepe({\n\t\t\troot: container,\n\t\t\tdefaultValue: initialValueRef.current,\n\t\t\tfeatureConfigs: {\n\t\t\t\t[CrepeFeature.Placeholder]: {\n\t\t\t\t\ttext: placeholder,\n\t\t\t\t},\n\t\t\t\t...(Object.keys(imageBlockConfig).length > 0\n\t\t\t\t\t? { [CrepeFeature.ImageBlock]: imageBlockConfig }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t});\n\n\t\t// Intercept clicks on Crepe image-block upload placeholders so that the\n\t\t// native file dialog is suppressed and the media picker is opened instead.\n\t\tconst interceptHandler = (e: MouseEvent) => {\n\t\t\tif (!openMediaPickerRef.current) return;\n\t\t\tconst target = e.target as Element;\n\t\t\t// Only intercept clicks inside the upload placeholder area.\n\t\t\tconst inPlaceholder = target.closest(\".image-edit .placeholder\");\n\t\t\tif (!inPlaceholder) return;\n\t\t\t// Let the hidden file itself through (shouldn't receive clicks normally).\n\t\t\tif ((target as HTMLElement).matches(\"input\")) return;\n\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\n\t\t\tconst imageEdit = inPlaceholder.closest(\".image-edit\");\n\t\t\tconst linkInput = imageEdit?.querySelector(\n\t\t\t\t\".link-input-area\",\n\t\t\t) as HTMLInputElement | null;\n\n\t\t\topenMediaPickerRef.current((url: string) => {\n\t\t\t\tif (!linkInput) return;\n\t\t\t\t// Use the native setter so Vue's reactivity picks up the change.\n\t\t\t\tconst nativeSetter = Object.getOwnPropertyDescriptor(\n\t\t\t\t\tHTMLInputElement.prototype,\n\t\t\t\t\t\"value\",\n\t\t\t\t)?.set;\n\t\t\t\tnativeSetter?.call(linkInput, url);\n\t\t\t\tlinkInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n\t\t\t\tlinkInput.dispatchEvent(\n\t\t\t\t\tnew KeyboardEvent(\"keydown\", { key: \"Enter\", bubbles: true }),\n\t\t\t\t);\n\t\t\t});\n\t\t};\n\t\tcontainer.addEventListener(\"click\", interceptHandler, true);\n\n\t\t// Prepare throttled onChange once per editor instance\n\t\tthrottledOnChangeRef.current = throttle((markdown: string) => {\n\t\t\tif (onChangeRef.current) onChangeRef.current(markdown);\n\t\t}, 200);\n\n\t\tcrepe.editor\n\t\t\t.config((ctx) => {\n\t\t\t\tctx.get(listenerCtx).markdownUpdated((_, markdown) => {\n\t\t\t\t\tthrottledOnChangeRef.current?.(markdown);\n\t\t\t\t});\n\t\t\t})\n\t\t\t.use(listener);\n\n\t\tcrepe.create().then(() => {\n\t\t\tisReadyRef.current = true;\n\t\t\tsetIsReady(true);\n\t\t});\n\t\tcrepeRef.current = crepe;\n\n\t\treturn () => {\n\t\t\tcontainer.removeEventListener(\"click\", interceptHandler, true);\n\t\t\ttry {\n\t\t\t\tisReadyRef.current = false;\n\t\t\t\tthrottledOnChangeRef.current?.cancel?.();\n\t\t\t\tthrottledOnChangeRef.current = null;\n\t\t\t\tcrepe.destroy();\n\t\t\t} finally {\n\t\t\t\tcrepeRef.current = null;\n\t\t\t}\n\t\t};\n\t}, []);\n\n\tuseLayoutEffect(() => {\n\t\tif (!isReady) return;\n\t\tif (!crepeRef.current) return;\n\t\tif (typeof value !== \"string\") return;\n\n\t\tlet currentMarkdown: string | undefined;\n\t\ttry {\n\t\t\tcurrentMarkdown = crepeRef.current?.getMarkdown?.();\n\t\t} catch {\n\t\t\t// Editor may not have finished initializing its view/state; skip sync for now\n\t\t\treturn;\n\t\t}\n\n\t\tif (currentMarkdown === value) return;\n\n\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\tif (view?.hasFocus?.() === true) return;\n\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\tconst doc = parser(value);\n\t\t\tif (!doc) return;\n\n\t\t\tconst state = view.state;\n\t\t\tconst selection = state.selection;\n\t\t\tconst from = selection.from;\n\n\t\t\tlet tr = state.tr;\n\t\t\ttr = tr.replace(0, state.doc.content.size, new Slice(doc.content, 0, 0));\n\n\t\t\tconst docSize = doc.content.size;\n\t\t\tconst safeFrom = Math.max(1, Math.min(from, Math.max(1, docSize - 2)));\n\t\t\ttr = tr.setSelection(Selection.near(tr.doc.resolve(safeFrom)));\n\t\t\tview.dispatch(tr);\n\t\t});\n\t}, [value, isReady]);\n\n\t// Expose insertImage via ref so the parent can insert images programmatically\n\tuseLayoutEffect(() => {\n\t\tif (!insertImageRef) return;\n\t\tinsertImageRef.current = (url: string) => {\n\t\t\tif (!crepeRef.current || !isReadyRef.current) return;\n\t\t\ttry {\n\t\t\t\tconst currentMarkdown = crepeRef.current.getMarkdown?.() ?? \"\";\n\t\t\t\tconst imageMarkdown = `\\n\\n![](${url})\\n\\n`;\n\t\t\t\tconst newMarkdown = currentMarkdown.trimEnd() + imageMarkdown;\n\t\t\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\t\t\tconst doc = parser(newMarkdown);\n\t\t\t\t\tif (!doc) return;\n\t\t\t\t\tconst state = view.state;\n\t\t\t\t\tconst tr = state.tr.replace(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tstate.doc.content.size,\n\t\t\t\t\t\tnew Slice(doc.content, 0, 0),\n\t\t\t\t\t);\n\t\t\t\t\tview.dispatch(tr);\n\t\t\t\t});\n\t\t\t\tif (onChangeRef.current) onChangeRef.current(newMarkdown);\n\t\t\t} catch {\n\t\t\t\t// Editor may not be ready yet\n\t\t\t}\n\t\t};\n\t\treturn () => {\n\t\t\tif (insertImageRef) insertImageRef.current = null;\n\t\t};\n\t}, [insertImageRef]);\n\n\treturn (\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\nimport { Crepe, CrepeFeature } from \"@milkdown/crepe\";\nimport \"@milkdown/crepe/theme/common/style.css\";\nimport \"./markdown-editor-styles.css\";\n\nimport { cn, throttle } from \"../../../utils\";\nimport { editorViewCtx, parserCtx } from \"@milkdown/kit/core\";\nimport { listener, listenerCtx } from \"@milkdown/kit/plugin/listener\";\nimport { Slice } from \"@milkdown/kit/prose/model\";\nimport { Selection } from \"@milkdown/kit/prose/state\";\nimport {\n\tuseLayoutEffect,\n\tuseRef,\n\tuseState,\n\ttype MutableRefObject,\n} from \"react\";\n\nexport interface MarkdownEditorProps {\n\tvalue?: string;\n\tonChange?: (markdown: string) => void;\n\tclassName?: string;\n\t/** Optional image upload handler. When provided, enables image upload in the editor. */\n\tuploadImage?: (file: File) => Promise;\n\t/** Placeholder text shown when the editor is empty. */\n\tplaceholder?: string;\n\t/**\n\t * Optional ref that will be populated with an `insertImage(url)` function.\n\t * Call `insertImageRef.current?.(url)` to programmatically insert an image.\n\t * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).\n\t */\n\tinsertImageRef?: MutableRefObject<((url: string) => void) | null>;\n\t/**\n\t * When provided, clicking the Crepe image block's upload area opens a media\n\t * picker instead of the native file dialog. The callback receives a `setUrl`\n\t * function — call it with the chosen URL to set it into the image block.\n\t * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).\n\t */\n\topenMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void;\n}\n\nexport function MarkdownEditor({\n\tvalue,\n\tonChange,\n\tclassName,\n\tuploadImage,\n\tplaceholder = \"Write something...\",\n\tinsertImageRef,\n\topenMediaPickerForImageBlock,\n}: MarkdownEditorProps) {\n\tconst containerRef = useRef(null);\n\tconst crepeRef = useRef(null);\n\tconst isReadyRef = useRef(false);\n\tconst [isReady, setIsReady] = useState(false);\n\tconst onChangeRef = useRef(onChange);\n\tconst initialValueRef = useRef(value ?? \"\");\n\tconst openMediaPickerRef = useRef(\n\t\topenMediaPickerForImageBlock,\n\t);\n\ttype ThrottledFn = ((markdown: string) => void) & {\n\t\tcancel?: () => void;\n\t\tflush?: () => void;\n\t};\n\tconst throttledOnChangeRef = useRef(null);\n\n\tonChangeRef.current = onChange;\n\topenMediaPickerRef.current = openMediaPickerForImageBlock;\n\n\tuseLayoutEffect(() => {\n\t\tif (crepeRef.current) return;\n\t\tconst container = containerRef.current;\n\t\tif (!container) return;\n\n\t\tconst hasMediaPicker = !!openMediaPickerRef.current;\n\n\t\tconst imageBlockConfig: Record = {};\n\t\tif (uploadImage) {\n\t\t\timageBlockConfig.onUpload = async (file: File) => uploadImage(file);\n\t\t}\n\t\tif (hasMediaPicker) {\n\t\t\timageBlockConfig.blockUploadPlaceholderText = \"Media Picker\";\n\t\t\timageBlockConfig.inlineUploadPlaceholderText = \"Media Picker\";\n\t\t}\n\n\t\tconst crepe = new Crepe({\n\t\t\troot: container,\n\t\t\tdefaultValue: initialValueRef.current,\n\t\t\tfeatureConfigs: {\n\t\t\t\t[CrepeFeature.Placeholder]: {\n\t\t\t\t\ttext: placeholder,\n\t\t\t\t},\n\t\t\t\t...(Object.keys(imageBlockConfig).length > 0\n\t\t\t\t\t? { [CrepeFeature.ImageBlock]: imageBlockConfig }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t});\n\n\t\t// Intercept clicks on Crepe image-block upload placeholders so that the\n\t\t// native file dialog is suppressed and the media picker is opened instead.\n\t\tconst interceptHandler = (e: MouseEvent) => {\n\t\t\tif (!openMediaPickerRef.current) return;\n\t\t\tconst target = e.target as Element;\n\t\t\t// Only intercept clicks inside the upload placeholder area.\n\t\t\tconst inPlaceholder = target.closest(\".image-edit .placeholder\");\n\t\t\tif (!inPlaceholder) return;\n\t\t\t// Let the hidden file itself through (shouldn't receive clicks normally).\n\t\t\tif ((target as HTMLElement).matches(\"input\")) return;\n\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\n\t\t\tconst imageEdit = inPlaceholder.closest(\".image-edit\");\n\t\t\tconst linkInput = imageEdit?.querySelector(\n\t\t\t\t\".link-input-area\",\n\t\t\t) as HTMLInputElement | null;\n\n\t\t\topenMediaPickerRef.current((url: string) => {\n\t\t\t\tif (!linkInput) return;\n\t\t\t\t// Use the native setter so Vue's reactivity picks up the change.\n\t\t\t\tconst nativeSetter = Object.getOwnPropertyDescriptor(\n\t\t\t\t\tHTMLInputElement.prototype,\n\t\t\t\t\t\"value\",\n\t\t\t\t)?.set;\n\t\t\t\tnativeSetter?.call(linkInput, url);\n\t\t\t\tlinkInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n\t\t\t\tlinkInput.dispatchEvent(\n\t\t\t\t\tnew KeyboardEvent(\"keydown\", { key: \"Enter\", bubbles: true }),\n\t\t\t\t);\n\t\t\t});\n\t\t};\n\t\tcontainer.addEventListener(\"click\", interceptHandler, true);\n\n\t\t// Prepare throttled onChange once per editor instance\n\t\tthrottledOnChangeRef.current = throttle((markdown: string) => {\n\t\t\tif (onChangeRef.current) onChangeRef.current(markdown);\n\t\t}, 200);\n\n\t\tcrepe.editor\n\t\t\t.config((ctx) => {\n\t\t\t\tctx.get(listenerCtx).markdownUpdated((_, markdown) => {\n\t\t\t\t\tthrottledOnChangeRef.current?.(markdown);\n\t\t\t\t});\n\t\t\t})\n\t\t\t.use(listener);\n\n\t\tcrepe.create().then(() => {\n\t\t\tisReadyRef.current = true;\n\t\t\tsetIsReady(true);\n\t\t});\n\t\tcrepeRef.current = crepe;\n\n\t\treturn () => {\n\t\t\tcontainer.removeEventListener(\"click\", interceptHandler, true);\n\t\t\ttry {\n\t\t\t\tisReadyRef.current = false;\n\t\t\t\tthrottledOnChangeRef.current?.cancel?.();\n\t\t\t\tthrottledOnChangeRef.current = null;\n\t\t\t\tcrepe.destroy();\n\t\t\t} finally {\n\t\t\t\tcrepeRef.current = null;\n\t\t\t}\n\t\t};\n\t}, []);\n\n\tuseLayoutEffect(() => {\n\t\tif (!isReady) return;\n\t\tif (!crepeRef.current) return;\n\t\tif (typeof value !== \"string\") return;\n\n\t\tlet currentMarkdown: string | undefined;\n\t\ttry {\n\t\t\tcurrentMarkdown = crepeRef.current?.getMarkdown?.();\n\t\t} catch {\n\t\t\t// Editor may not have finished initializing its view/state; skip sync for now\n\t\t\treturn;\n\t\t}\n\n\t\tif (currentMarkdown === value) return;\n\n\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\tif (view?.hasFocus?.() === true) return;\n\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\tconst doc = parser(value);\n\t\t\tif (!doc) return;\n\n\t\t\tconst state = view.state;\n\t\t\tconst selection = state.selection;\n\t\t\tconst from = selection.from;\n\n\t\t\tlet tr = state.tr;\n\t\t\ttr = tr.replace(0, state.doc.content.size, new Slice(doc.content, 0, 0));\n\n\t\t\tconst docSize = doc.content.size;\n\t\t\tconst safeFrom = Math.max(1, Math.min(from, Math.max(1, docSize - 2)));\n\t\t\ttr = tr.setSelection(Selection.near(tr.doc.resolve(safeFrom)));\n\t\t\tview.dispatch(tr);\n\t\t});\n\t}, [value, isReady]);\n\n\t// Expose insertImage via ref so the parent can insert images programmatically\n\tuseLayoutEffect(() => {\n\t\tif (!insertImageRef) return;\n\t\tinsertImageRef.current = (url: string) => {\n\t\t\tif (!crepeRef.current || !isReadyRef.current) return;\n\t\t\ttry {\n\t\t\t\tconst currentMarkdown = crepeRef.current.getMarkdown?.() ?? \"\";\n\t\t\t\tconst imageMarkdown = `\\n\\n![](${url})\\n\\n`;\n\t\t\t\tconst newMarkdown = currentMarkdown.trimEnd() + imageMarkdown;\n\t\t\t\tcrepeRef.current.editor.action((ctx) => {\n\t\t\t\t\tconst view = ctx.get(editorViewCtx);\n\t\t\t\t\tconst parser = ctx.get(parserCtx);\n\t\t\t\t\tconst doc = parser(newMarkdown);\n\t\t\t\t\tif (!doc) return;\n\t\t\t\t\tconst state = view.state;\n\t\t\t\t\tconst tr = state.tr.replace(\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tstate.doc.content.size,\n\t\t\t\t\t\tnew Slice(doc.content, 0, 0),\n\t\t\t\t\t);\n\t\t\t\t\tview.dispatch(tr);\n\t\t\t\t});\n\t\t\t\tif (onChangeRef.current) onChangeRef.current(newMarkdown);\n\t\t\t} catch {\n\t\t\t\t// Editor may not be ready yet\n\t\t\t}\n\t\t};\n\t\treturn () => {\n\t\t\tif (insertImageRef) insertImageRef.current = null;\n\t\t};\n\t}, [insertImageRef]);\n\n\treturn (\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/forms/markdown-editor.tsx" }, { diff --git a/packages/stack/registry/btst-media.json b/packages/stack/registry/btst-media.json new file mode 100644 index 00000000..fd59b060 --- /dev/null +++ b/packages/stack/registry/btst-media.json @@ -0,0 +1,104 @@ +{ + "name": "btst-media", + "type": "registry:block", + "title": "Media Plugin Pages", + "description": "Ejectable page components for the @btst/stack media plugin. Customize the UI layer while keeping data-fetching in @btst/stack.", + "author": "BTST ", + "dependencies": [ + "@btst/stack", + "@vercel/blob" + ], + "registryDependencies": [ + "button", + "input", + "popover", + "tabs" + ], + "files": [ + { + "path": "btst/media/types.ts", + "type": "registry:lib", + "content": "export type Asset = {\n\tid: string;\n\tfilename: string;\n\toriginalName: string;\n\tmimeType: string;\n\tsize: number;\n\turl: string;\n\tfolderId?: string;\n\talt?: string;\n\tcreatedAt: Date;\n};\n\nexport type Folder = {\n\tid: string;\n\tname: string;\n\tparentId?: string;\n\tcreatedAt: Date;\n};\n\nexport interface SerializedAsset extends Omit {\n\tcreatedAt: string;\n}\n\nexport interface SerializedFolder extends Omit {\n\tcreatedAt: string;\n}\n", + "target": "src/components/btst/media/types.ts" + }, + { + "path": "btst/media/schemas.ts", + "type": "registry:lib", + "content": "import { z } from \"zod\";\n\nexport const AssetListQuerySchema = z.object({\n\tfolderId: z.string().optional(),\n\tmimeType: z.string().optional(),\n\tquery: z.string().optional(),\n\toffset: z.coerce.number().int().min(0).optional(),\n\tlimit: z.coerce.number().int().min(1).max(100).optional(),\n});\n\nexport const createAssetSchema = z.object({\n\tfilename: z.string().min(1),\n\toriginalName: z.string().min(1),\n\tmimeType: z.string().min(1),\n\t// Allow 0 for URL-registered assets where size is unknown at registration time.\n\tsize: z.number().int().min(0),\n\turl: z.httpUrl(),\n\tfolderId: z.string().optional(),\n\talt: z.string().optional(),\n});\n\nexport const updateAssetSchema = z.object({\n\talt: z.string().optional(),\n\tfolderId: z.string().nullable().optional(),\n});\n\nexport const createFolderSchema = z.object({\n\tname: z.string().min(1),\n\tparentId: z.string().optional(),\n});\n\nexport const uploadTokenRequestSchema = z.object({\n\tfilename: z.string().min(1),\n\tmimeType: z.string().min(1),\n\tsize: z.number().int().positive(),\n\tfolderId: z.string().optional(),\n});\n", + "target": "src/components/btst/media/schemas.ts" + }, + { + "path": "btst/media/client/components/media-picker/asset-card.tsx", + "type": "registry:component", + "content": "import { useDeleteAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { cn } from \"@/lib/utils\";\nimport { File, Check, Trash2 } from \"lucide-react\";\nimport { isImage, formatBytes } from \"./utils\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\n\nexport function AssetCard({\n\tasset,\n\tselected,\n\tonToggle,\n}: {\n\tasset: SerializedAsset;\n\tselected: boolean;\n\tonToggle: () => void;\n}) {\n\tconst { mutateAsync: deleteAsset } = useDeleteAsset();\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\treturn (\n\t\t (e.key === \"Enter\" || e.key === \" \") && onToggle()}\n\t\t\tclassName={cn(\n\t\t\t\t\"group relative cursor-pointer rounded-md border bg-muted/30 p-1 transition-all hover:border-ring hover:shadow-sm\",\n\t\t\t\tselected && \"border-ring ring-1 ring-ring\",\n\t\t\t)}\n\t\t>\n\t\t\t{/* Thumbnail */}\n\t\t\t
\n\t\t\t\t{isImage(asset.mimeType) ? (\n\t\t\t\t\tImageComponent ? (\n\t\t\t\t\t\t\n\t\t\t\t\t) : (\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t{/* Name + size */}\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{asset.originalName}\n\t\t\t\t

\n\t\t\t\t

\n\t\t\t\t\t{formatBytes(asset.size)}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t{/* Selection indicator */}\n\t\t\t{selected && (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t{/* Delete button (on hover) */}\n\t\t\t {\n\t\t\t\t\te.stopPropagation();\n\t\t\t\t\tif (confirm(`Delete \"${asset.originalName}\"?`)) {\n\t\t\t\t\t\tdeleteAsset(asset.id).catch(console.error);\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t\tclassName=\"absolute left-1 top-1 hidden rounded bg-destructive/80 p-0.5 text-white group-hover:flex\"\n\t\t\t>\n\t\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n", + "target": "src/components/btst/media/client/components/media-picker/asset-card.tsx" + }, + { + "path": "btst/media/client/components/media-picker/browse-tab.tsx", + "type": "registry:component", + "content": "import { useState, useRef } from \"react\";\nimport { useAssets } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Search, X, Image } from \"lucide-react\";\nimport { AssetCard } from \"./asset-card\";\nimport { matchesAccept } from \"./utils\";\n\nexport function BrowseTab({\n\tfolderId,\n\tselected,\n\tmultiple,\n\taccept,\n\tonToggle,\n}: {\n\tfolderId: string | null;\n\tselected: SerializedAsset[];\n\tmultiple: boolean;\n\taccept?: string[];\n\tonToggle: (asset: SerializedAsset) => void;\n}) {\n\tconst [search, setSearch] = useState(\"\");\n\tconst [debouncedSearch, setDebouncedSearch] = useState(\"\");\n\tconst debounceRef = useRef | null>(null);\n\n\tconst handleSearch = (v: string) => {\n\t\tsetSearch(v);\n\t\tif (debounceRef.current) clearTimeout(debounceRef.current);\n\t\tdebounceRef.current = setTimeout(() => setDebouncedSearch(v), 300);\n\t};\n\n\tconst { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =\n\t\tuseAssets({\n\t\t\tfolderId: folderId ?? undefined,\n\t\t\tquery: debouncedSearch || undefined,\n\t\t\tlimit: 40,\n\t\t});\n\n\tconst allAssets = data?.pages.flatMap((p) => p.items) ?? [];\n\tconst filtered = accept\n\t\t? allAssets.filter((a) => matchesAccept(a.mimeType, accept))\n\t\t: allAssets;\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t handleSearch(e.target.value)}\n\t\t\t\t\tplaceholder=\"Search files…\"\n\t\t\t\t\tclassName=\"h-8 pl-7 text-sm\"\n\t\t\t\t/>\n\t\t\t\t{search && (\n\t\t\t\t\t {\n\t\t\t\t\t\t\tsetSearch(\"\");\n\t\t\t\t\t\t\tsetDebouncedSearch(\"\");\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t{isLoading ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t) : filtered.length === 0 ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

No files found

\n\t\t\t\t
\n\t\t\t) : (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{filtered.map((asset) => (\n\t\t\t\t\t\t\t s.id === asset.id)}\n\t\t\t\t\t\t\t\tonToggle={() => onToggle(asset)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t
\n\t\t\t\t\t{hasNextPage && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t fetchNextPage()}\n\t\t\t\t\t\t\t\tdisabled={isFetchingNextPage}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{isFetchingNextPage ? (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\tLoad more\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n", + "target": "src/components/btst/media/client/components/media-picker/browse-tab.tsx" + }, + { + "path": "btst/media/client/components/media-picker/folder-tree.tsx", + "type": "registry:component", + "content": "import { useState } from \"react\";\nimport {\n\tuseFolders,\n\tuseCreateFolder,\n\tuseDeleteFolder,\n} from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedFolder } from \"../../../types\";\nimport { FolderPlus } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Check, Folder, Trash2, ChevronRight, FolderOpen } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport function FolderTree({\n\tselectedId,\n\tonSelect,\n}: {\n\tselectedId: string | null;\n\tonSelect: (id: string | null) => void;\n}) {\n\tconst { data: rootFoldersRaw = [] } = useFolders(null);\n\tconst rootFolders =\n\t\trootFoldersRaw as import(\"../../../types\").SerializedFolder[];\n\tconst [newFolderName, setNewFolderName] = useState(\"\");\n\tconst [isCreating, setIsCreating] = useState(false);\n\tconst { mutateAsync: createFolder } = useCreateFolder();\n\tconst { mutateAsync: deleteFolder } = useDeleteFolder();\n\n\tconst handleCreateFolder = async () => {\n\t\tconst name = newFolderName.trim();\n\t\tif (!name) return;\n\t\ttry {\n\t\t\tawait createFolder({ name, parentId: selectedId ?? undefined });\n\t\t\tsetNewFolderName(\"\");\n\t\t\tsetIsCreating(false);\n\t\t} catch (err) {\n\t\t\tconsole.error(\"[btst/media] Failed to create folder\", err);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tFolders\n\t\t\t\t\n\t\t\t\t setIsCreating((v) => !v)}\n\t\t\t\t\tclassName=\"rounded p-0.5 hover:bg-muted\"\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t{isCreating && (\n\t\t\t\t
\n\t\t\t\t\t setNewFolderName(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"Folder name\"\n\t\t\t\t\t\tclassName=\"h-6 text-xs\"\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif (e.key === \"Enter\") void handleCreateFolder();\n\t\t\t\t\t\t\tif (e.key === \"Escape\") setIsCreating(false);\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t{/* All assets (root) */}\n\t\t\t\t onSelect(null)}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\t\tselectedId === null && \"bg-muted font-medium\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tAll files\n\t\t\t\t\n\n\t\t\t\t{rootFolders.map((folder) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t
\n\n\t\t\t{selectedId && (\n\t\t\t\t
\n\t\t\t\t\t {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tconfirm(\"Delete this folder? Assets inside will be unaffected.\")\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tawait deleteFolder(selectedId);\n\t\t\t\t\t\t\t\t\tonSelect(null);\n\t\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"[btst/media] Failed to delete folder\", err);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 text-xs text-destructive hover:underline\"\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\tDelete folder\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n\nexport function FolderTreeItem({\n\tfolder,\n\tselectedId,\n\tonSelect,\n\tdepth = 0,\n}: {\n\tfolder: SerializedFolder;\n\tselectedId: string | null;\n\tonSelect: (id: string | null) => void;\n\tdepth?: number;\n}) {\n\tconst [expanded, setExpanded] = useState(false);\n\tconst { data: children = [] } = useFolders(folder.id);\n\n\treturn (\n\t\t
\n\t\t\t {\n\t\t\t\t\tonSelect(folder.id);\n\t\t\t\t\tsetExpanded((v) => !v);\n\t\t\t\t}}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\tselectedId === folder.id && \"bg-muted font-medium\",\n\t\t\t\t)}\n\t\t\t\tstyle={{ paddingLeft: `${8 + depth * 12}px` }}\n\t\t\t>\n\t\t\t\t{children.length > 0 ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{expanded ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{folder.name}\n\t\t\t\n\t\t\t{expanded &&\n\t\t\t\tchildren.map((child) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t
\n\t);\n}\n", + "target": "src/components/btst/media/client/components/media-picker/folder-tree.tsx" + }, + { + "path": "btst/media/client/components/media-picker/index.tsx", + "type": "registry:component", + "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n\n/**\n * Upload a file via the media plugin's direct upload endpoint.\n * @param file - The file to upload.\n * @param baseURL - The base URL of the server (e.g. `https://example.com`).\n * @param apiBasePath - The API base path configured for the media plugin (e.g. `/api/v2`). Defaults to `/api/data`.\n */\nexport async function uploadMediaFile(\n\tfile: File,\n\tbaseURL: string,\n\tapiBasePath: string,\n): Promise {\n\tconst formData = new FormData();\n\tformData.append(\"file\", file);\n\tconst res = await fetch(`${baseURL}${apiBasePath}/media/upload`, {\n\t\tmethod: \"POST\",\n\t\tbody: formData,\n\t});\n\tif (!res.ok) {\n\t\tconst err = await res.json().catch(() => ({ message: res.statusText }));\n\t\tthrow new Error(err.message ?? \"Upload failed\");\n\t}\n\tconst asset = (await res.json()) as SerializedAsset;\n\treturn asset;\n}\n", + "target": "src/components/btst/media/client/components/media-picker/index.tsx" + }, + { + "path": "btst/media/client/components/media-picker/upload-tab.tsx", + "type": "registry:component", + "content": "import { useState, useCallback, useRef } from \"react\";\nimport { useUploadAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Upload } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { matchesAccept } from \"./utils\";\n\nexport function UploadTab({\n\tfolderId,\n\taccept,\n\tonUploaded,\n}: {\n\tfolderId: string | null;\n\taccept?: string[];\n\tonUploaded: (asset: SerializedAsset) => void;\n}) {\n\tconst [dragging, setDragging] = useState(false);\n\tconst [uploading, setUploading] = useState(false);\n\tconst [error, setError] = useState(null);\n\tconst fileInputRef = useRef(null);\n\tconst { mutateAsync: uploadAsset } = useUploadAsset();\n\n\tconst acceptAttr = accept?.join(\",\") ?? undefined;\n\n\tconst handleFiles = useCallback(\n\t\tasync (files: FileList | File[]) => {\n\t\t\tconst fileArr = Array.from(files);\n\t\t\tif (fileArr.length === 0) return;\n\t\t\tsetError(null);\n\t\t\tsetUploading(true);\n\t\t\ttry {\n\t\t\t\tfor (const file of fileArr) {\n\t\t\t\t\tif (accept && !matchesAccept(file.type, accept)) {\n\t\t\t\t\t\tsetError(`File type ${file.type} is not accepted.`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst asset = await uploadAsset({\n\t\t\t\t\t\tfile,\n\t\t\t\t\t\tfolderId: folderId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tonUploaded(asset);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tsetError(err instanceof Error ? err.message : \"Upload failed\");\n\t\t\t} finally {\n\t\t\t\tsetUploading(false);\n\t\t\t}\n\t\t},\n\t\t[accept, folderId, uploadAsset, onUploaded],\n\t);\n\n\treturn (\n\t\t
\n\t\t\t {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(true);\n\t\t\t\t}}\n\t\t\t\tonDragLeave={() => setDragging(false)}\n\t\t\t\tonDrop={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(false);\n\t\t\t\t\tvoid handleFiles(e.dataTransfer.files);\n\t\t\t\t}}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed transition-colors\",\n\t\t\t\t\tdragging ? \"border-ring bg-ring/5\" : \"border-muted-foreground/30\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{uploading ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t

Uploading…

\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t

Drop files here

\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\tor click to browse\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tChoose files\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t\t{error &&

{error}

}\n\t\t\t e.target.files && handleFiles(e.target.files)}\n\t\t\t/>\n\t\t
\n\t);\n}\n", + "target": "src/components/btst/media/client/components/media-picker/upload-tab.tsx" + }, + { + "path": "btst/media/client/components/media-picker/url-tab.tsx", + "type": "registry:component", + "content": "import { useState } from \"react\";\nimport { useRegisterAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Check } from \"lucide-react\";\n\nexport function UrlTab({\n\tfolderId,\n\tonRegistered,\n}: {\n\tfolderId: string | null;\n\tonRegistered: (asset: SerializedAsset) => void;\n}) {\n\tconst [url, setUrl] = useState(\"\");\n\tconst [error, setError] = useState(null);\n\tconst { mutateAsync: registerAsset, isPending } = useRegisterAsset();\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\t\tconst trimmed = url.trim();\n\t\tif (!trimmed) return;\n\t\ttry {\n\t\t\tconst filename = trimmed.split(\"/\").pop() ?? \"asset\";\n\t\t\tconst asset = await registerAsset({\n\t\t\t\turl: trimmed,\n\t\t\t\tfilename,\n\t\t\t\tfolderId: folderId ?? undefined,\n\t\t\t});\n\t\t\tsetUrl(\"\");\n\t\t\tonRegistered(asset);\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"Failed to register URL\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t

\n\t\t\t\tPaste a public URL to register it as an asset without uploading a file.\n\t\t\t

\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t setUrl(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"https://example.com/image.png\"\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tdata-testid=\"media-url-input\"\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t{error &&

{error}

}\n\t\t\t
\n\t\t
\n\t);\n}\n", + "target": "src/components/btst/media/client/components/media-picker/url-tab.tsx" + }, + { + "path": "btst/media/client/components/media-picker/utils.ts", + "type": "registry:lib", + "content": "export function matchesAccept(mimeType: string, accept?: string[]) {\n\tif (!accept || accept.length === 0) return true;\n\treturn accept.some((a) => {\n\t\tif (a.endsWith(\"/*\")) return mimeType.startsWith(a.slice(0, -1));\n\t\treturn mimeType === a;\n\t});\n}\n\nexport function isImage(mimeType: string) {\n\treturn mimeType.startsWith(\"image/\");\n}\n\nexport function formatBytes(bytes: number) {\n\tif (bytes < 1024) return `${bytes} B`;\n\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n}\n", + "target": "src/components/btst/media/client/components/media-picker/utils.ts" + }, + { + "path": "btst/media/client/components/pages/library-page.internal.tsx", + "type": "registry:component", + "content": "\"use client\";\nimport { useState, useCallback, useRef, type ComponentType } from \"react\";\nimport {\n\tuseAssets,\n\tuseDeleteAsset,\n\tuseFolders,\n\tuseUploadAsset,\n\tuseCreateFolder,\n} from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset, SerializedFolder } from \"../../../types\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n\tFolder,\n\tImage,\n\tFile as FileIcon,\n\tUpload,\n\tTrash2,\n\tSearch,\n\tX,\n\tLoader2,\n\tFolderPlus,\n\tCheck,\n\tCopy,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { toast } from \"sonner\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { formatBytes } from \"../media-picker/utils\";\nimport { FolderTreeItem } from \"../media-picker/folder-tree\";\n\nfunction LibrarySidebar({\n\tselectedFolder,\n\tonSelect,\n}: {\n\tselectedFolder: string | null;\n\tonSelect: (id: string | null) => void;\n}) {\n\tconst { data: rootFoldersRaw = [] } = useFolders(null);\n\tconst rootFolders = rootFoldersRaw as SerializedFolder[];\n\tconst [newFolderName, setNewFolderName] = useState(\"\");\n\tconst [isCreating, setIsCreating] = useState(false);\n\tconst { mutateAsync: createFolder, isPending } = useCreateFolder();\n\n\tconst handleCreate = async () => {\n\t\tconst name = newFolderName.trim();\n\t\tif (!name) return;\n\t\ttry {\n\t\t\tawait createFolder({ name, parentId: selectedFolder ?? undefined });\n\t\t\tsetNewFolderName(\"\");\n\t\t\tsetIsCreating(false);\n\t\t\ttoast.success(\"Folder created\");\n\t\t} catch (err) {\n\t\t\ttoast.error(\n\t\t\t\terr instanceof Error ? err.message : \"Failed to create folder\",\n\t\t\t);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tFolders\n\t\t\t\t\n\t\t\t\t setIsCreating((v) => !v)}\n\t\t\t\t\ttitle=\"New folder\"\n\t\t\t\t\tclassName=\"rounded p-0.5 hover:bg-muted\"\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t{isCreating && (\n\t\t\t\t
\n\t\t\t\t\t setNewFolderName(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"Folder name\"\n\t\t\t\t\t\tclassName=\"h-7 text-xs\"\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif (e.key === \"Enter\") void handleCreate();\n\t\t\t\t\t\t\tif (e.key === \"Escape\") setIsCreating(false);\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t\t
\n\t\t\t\t onSelect(null)}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\t\tselectedFolder === null && \"bg-muted font-medium\",\n\t\t\t\t\t)}\n\t\t\t\t\tstyle={{ paddingLeft: \"8px\" }}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tAll files\n\t\t\t\t\n\t\t\t\t{rootFolders.map((folder) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t
\n\t\t
\n\t);\n}\n\nfunction AssetCard({\n\tasset,\n\tonDelete,\n\tImageComponent,\n\tapiBaseURL,\n}: {\n\tasset: SerializedAsset;\n\tonDelete: (id: string) => void;\n\tImageComponent?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\tapiBaseURL: string;\n}) {\n\tconst isImg = asset.mimeType.startsWith(\"image/\");\n\n\tconst copyUrl = () => {\n\t\tlet fullUrl: string;\n\t\ttry {\n\t\t\t// new URL() handles both absolute and relative URLs and encodes\n\t\t\t// special characters (spaces, non-ASCII) in the path correctly.\n\t\t\tfullUrl = new URL(asset.url, apiBaseURL).href;\n\t\t} catch {\n\t\t\tfullUrl = asset.url;\n\t\t}\n\t\tnavigator.clipboard\n\t\t\t.writeText(fullUrl)\n\t\t\t.then(() => toast.success(\"URL copied\"));\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t{isImg ? (\n\t\t\t\t\tImageComponent ? (\n\t\t\t\t\t\t\n\t\t\t\t\t) : (\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{asset.originalName}\n\t\t\t\t

\n\t\t\t\t

\n\t\t\t\t\t{asset.mimeType} · {formatBytes(asset.size)}\n\t\t\t\t

\n\t\t\t\t\n\t\t\t\t\t{asset.url}\n\t\t\t\t

\n\t\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nexport function LibraryPage() {\n\tconst overrides = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tuseRouteLifecycle({\n\t\trouteName: \"library\",\n\t\tcontext: {\n\t\t\tpath: \"/media\",\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforeLibraryPageRendered) {\n\t\t\t\treturn overrides.onBeforeLibraryPageRendered(context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [search, setSearch] = useState(\"\");\n\tconst [debouncedSearch, setDebouncedSearch] = useState(\"\");\n\tconst debounceRef = useRef | null>(null);\n\tconst [dragging, setDragging] = useState(false);\n\tconst fileInputRef = useRef(null);\n\tconst { mutateAsync: uploadAsset, isPending: isUploading } = useUploadAsset();\n\tconst { mutateAsync: deleteAsset } = useDeleteAsset();\n\tconst { Image: ImageComponent, apiBaseURL = \"\" } = overrides;\n\n\tconst handleSearch = (v: string) => {\n\t\tsetSearch(v);\n\t\tif (debounceRef.current) clearTimeout(debounceRef.current);\n\t\tdebounceRef.current = setTimeout(() => setDebouncedSearch(v), 300);\n\t};\n\n\tconst { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =\n\t\tuseAssets({\n\t\t\tfolderId: selectedFolder ?? undefined,\n\t\t\tquery: debouncedSearch || undefined,\n\t\t\tlimit: 40,\n\t\t});\n\n\tconst assets = data?.pages.flatMap((p) => p.items) ?? [];\n\n\tconst handleUpload = useCallback(\n\t\tasync (files: FileList | File[]) => {\n\t\t\tconst arr = Array.from(files);\n\t\t\tfor (const file of arr) {\n\t\t\t\ttry {\n\t\t\t\t\tawait uploadAsset({ file, folderId: selectedFolder ?? undefined });\n\t\t\t\t\ttoast.success(`Uploaded ${file.name}`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\ttoast.error(err instanceof Error ? err.message : \"Upload failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[selectedFolder, uploadAsset],\n\t);\n\n\tconst handleDelete = async (id: string) => {\n\t\tif (!confirm(\"Delete this asset?\")) return;\n\t\ttry {\n\t\t\tawait deleteAsset(id);\n\t\t\ttoast.success(\"Deleted\");\n\t\t} catch (err) {\n\t\t\ttoast.error(err instanceof Error ? err.message : \"Delete failed\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t\n\n\t\t\t {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(true);\n\t\t\t\t}}\n\t\t\t\tonDragLeave={() => setDragging(false)}\n\t\t\t\tonDrop={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(false);\n\t\t\t\t\tvoid handleUpload(e.dataTransfer.files);\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* Toolbar */}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t handleSearch(e.target.value)}\n\t\t\t\t\t\t\tplaceholder=\"Search files…\"\n\t\t\t\t\t\t\tclassName=\"h-8 pl-8\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{search && (\n\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\tsetSearch(\"\");\n\t\t\t\t\t\t\t\t\tsetDebouncedSearch(\"\");\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tclassName=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t>\n\t\t\t\t\t\t{isUploading ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tUpload\n\t\t\t\t\t\n\t\t\t\t\t e.target.files && handleUpload(e.target.files)}\n\t\t\t\t\t/>\n\t\t\t\t
\n\n\t\t\t\t{/* Drop overlay */}\n\t\t\t\t{dragging && (\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

Drop files to upload

\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t)}\n\n\t\t\t\t{/* Asset grid */}\n\t\t\t\t
\n\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t) : assets.length === 0 ? (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\tNo files yet. Drag & drop or click Upload.\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t) : (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{assets.map((asset) => (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t\t{hasNextPage && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t fetchNextPage()}\n\t\t\t\t\t\t\t\tdisabled={isFetchingNextPage}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{isFetchingNextPage && (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tLoad more\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n}\n", + "target": "src/components/btst/media/client/components/pages/library-page.internal.tsx" + }, + { + "path": "btst/media/client/components/pages/library-page.tsx", + "type": "registry:page", + "content": "\"use client\";\nimport { lazy } from \"react\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { ComposedRoute } from \"@btst/stack/client/components\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { Loader2 } from \"lucide-react\";\n\nconst LibraryPage = lazy(() =>\n\timport(\"./library-page.internal\").then((m) => ({ default: m.LibraryPage })),\n);\n\nfunction LibraryLoading() {\n\treturn (\n\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nfunction LibraryError({ error }: { error: Error }) {\n\treturn (\n\t\t
\n\t\t\t

{error.message}

\n\t\t
\n\t);\n}\n\nexport function LibraryPageComponent() {\n\tusePluginOverrides(\"media\");\n\treturn (\n\t\t null}\n\t\t\tonError={(error) => console.error(\"[btst/media] Library error:\", error)}\n\t\t/>\n\t);\n}\n", + "target": "src/components/btst/media/client/components/pages/library-page.tsx" + }, + { + "path": "btst/media/client/overrides.ts", + "type": "registry:lib", + "content": "import type { ComponentType } from \"react\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { ImageCompressionOptions } from \"./utils/image-compression\";\n\n/**\n * Upload mode — must match the storage adapter configured in mediaBackendPlugin.\n * - `\"direct\"` — local filesystem adapter, files are uploaded via `POST /media/upload`\n * - `\"s3\"` — AWS S3 / R2 / MinIO, the client fetches a presigned token then PUTs directly to S3\n * - `\"vercel-blob\"` — Vercel Blob, uses the `@vercel/blob/client` SDK for direct upload\n */\nexport type MediaUploadMode = \"direct\" | \"s3\" | \"vercel-blob\";\n\n/**\n * Overridable components and functions for the Media plugin.\n *\n * External consumers provide these when registering the media client plugin\n * via the StackProvider overrides.\n */\nexport interface MediaPluginOverrides {\n\t/**\n\t * Base URL for API calls (e.g., \"http://localhost:3000\").\n\t */\n\tapiBaseURL: string;\n\n\t/**\n\t * Path where the API is mounted (e.g., \"/api/data\").\n\t */\n\tapiBasePath: string;\n\n\t/**\n\t * React Query client — used by the MediaPicker to cache and fetch assets.\n\t */\n\tqueryClient: QueryClient;\n\n\t/**\n\t * Upload mode — must match the storageAdapter configured in mediaBackendPlugin.\n\t * @default \"direct\"\n\t */\n\tuploadMode?: MediaUploadMode;\n\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth).\n\t */\n\theaders?: HeadersInit;\n\n\t/**\n\t * Navigation function for programmatic navigation.\n\t */\n\tnavigate: (path: string) => void | Promise;\n\n\t/**\n\t * Link component for navigation within the media library page.\n\t */\n\tLink?: ComponentType & Record>;\n\n\t/**\n\t * Image component for rendering asset thumbnails and previews.\n\t *\n\t * When provided, replaces the default `` element in asset cards,\n\t * the media library grid, and the ImageInputField preview. Use this\n\t * to plug in Next.js `` for automatic optimisation.\n\t *\n\t * @example\n\t * ```tsx\n\t * Image: (props) => \n\t * ```\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\n\t/**\n\t * Client-side image compression applied before upload via the Canvas API.\n\t *\n\t * Images are scaled down to fit within `maxWidth` × `maxHeight` (preserving\n\t * aspect ratio) and re-encoded at `quality`. SVG and GIF files are always\n\t * passed through unchanged.\n\t *\n\t * Set to `false` to disable compression entirely.\n\t *\n\t * @default { maxWidth: 2048, maxHeight: 2048, quality: 0.85 }\n\t */\n\timageCompression?: ImageCompressionOptions | false;\n\n\t// ============ Lifecycle Hooks ============\n\n\t/**\n\t * Called when a media route is rendered.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: MediaRouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a media route encounters an error.\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: MediaRouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the media library page is rendered.\n\t * Return `false` to prevent rendering (e.g., redirect unauthenticated users).\n\t *\n\t * @example\n\t * ```ts\n\t * media: {\n\t * onBeforeLibraryPageRendered: (context) => !!currentUser?.isAdmin,\n\t * onRouteError: (routeName, error, context) => navigate(\"/login\"),\n\t * }\n\t * ```\n\t */\n\tonBeforeLibraryPageRendered?: (context: MediaRouteContext) => boolean;\n}\n\nexport interface MediaRouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t[key: string]: unknown;\n}\n", + "target": "src/components/btst/media/client/overrides.ts" + }, + { + "path": "btst/media/client/utils/image-compression.ts", + "type": "registry:lib", + "content": "/**\n * Canvas-based client-side image compression.\n *\n * Skips SVG and GIF (vector data / animation would be lost on a canvas round-trip).\n * All other image/* types are scaled down to fit within maxWidth × maxHeight\n * (preserving aspect ratio) and re-encoded at the configured quality.\n */\n\nexport interface ImageCompressionOptions {\n\t/**\n\t * Maximum width in pixels. Images wider than this are scaled down.\n\t * @default 2048\n\t */\n\tmaxWidth?: number;\n\n\t/**\n\t * Maximum height in pixels. Images taller than this are scaled down.\n\t * @default 2048\n\t */\n\tmaxHeight?: number;\n\n\t/**\n\t * Encoding quality (0–1). Applies to JPEG and WebP.\n\t * @default 0.85\n\t */\n\tquality?: number;\n\n\t/**\n\t * Output MIME type. Defaults to the source image's MIME type.\n\t * Set to `\"image/webp\"` for better compression at the cost of format change.\n\t */\n\toutputFormat?: string;\n}\n\nfunction loadImage(file: File): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\tconst url = URL.createObjectURL(file);\n\t\tconst img = new Image();\n\t\timg.onload = () => {\n\t\t\tURL.revokeObjectURL(url);\n\t\t\tresolve(img);\n\t\t};\n\t\timg.onerror = () => {\n\t\t\tURL.revokeObjectURL(url);\n\t\t\treject(new Error(`Failed to load image: ${file.name}`));\n\t\t};\n\t\timg.src = url;\n\t});\n}\n\nconst SKIP_TYPES = new Set([\"image/svg+xml\", \"image/gif\"]);\n\n/**\n * Compresses an image file client-side using the Canvas API.\n *\n * Returns the original file unchanged if:\n * - The file is not an image\n * - The MIME type is SVG or GIF (would lose vector data / animation)\n * - The browser does not support canvas (SSR guard)\n */\nexport async function compressImage(\n\tfile: File,\n\toptions: ImageCompressionOptions = {},\n): Promise {\n\tif (!file.type.startsWith(\"image/\") || SKIP_TYPES.has(file.type)) {\n\t\treturn file;\n\t}\n\n\t// SSR guard — canvas is only available in the browser\n\tif (typeof document === \"undefined\") return file;\n\n\tconst {\n\t\tmaxWidth = 2048,\n\t\tmaxHeight = 2048,\n\t\tquality = 0.85,\n\t\toutputFormat,\n\t} = options;\n\n\tconst img = await loadImage(file);\n\n\tlet { width, height } = img;\n\n\tconst needsResize = width > maxWidth || height > maxHeight;\n\tconst needsFormatChange =\n\t\toutputFormat !== undefined && outputFormat !== file.type;\n\n\t// Skip canvas entirely if the image is already within the limits and no\n\t// format conversion is needed — re-encoding a small image can make it larger.\n\tif (!needsResize && !needsFormatChange) return file;\n\n\t// Scale down proportionally if either dimension exceeds the max\n\tif (needsResize) {\n\t\tconst ratio = Math.min(maxWidth / width, maxHeight / height);\n\t\twidth = Math.round(width * ratio);\n\t\theight = Math.round(height * ratio);\n\t}\n\n\tconst canvas = document.createElement(\"canvas\");\n\tcanvas.width = width;\n\tcanvas.height = height;\n\n\tconst ctx = canvas.getContext(\"2d\");\n\tif (!ctx) return file;\n\n\tctx.drawImage(img, 0, 0, width, height);\n\n\tconst mimeType = outputFormat ?? file.type;\n\n\treturn new Promise((resolve, reject) => {\n\t\tcanvas.toBlob(\n\t\t\t(blob) => {\n\t\t\t\tif (!blob) {\n\t\t\t\t\treject(new Error(\"canvas.toBlob returned null\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Preserve the original filename, updating extension only if\n\t\t\t\t// the output format changed from the source.\n\t\t\t\tlet name = file.name;\n\t\t\t\tif (outputFormat && outputFormat !== file.type) {\n\t\t\t\t\tconst ext = outputFormat.split(\"/\")[1] ?? \"jpg\";\n\t\t\t\t\tname = name.replace(/\\.[^.]+$/, `.${ext}`);\n\t\t\t\t}\n\n\t\t\t\tresolve(new File([blob], name, { type: mimeType }));\n\t\t\t},\n\t\t\tmimeType,\n\t\t\tquality,\n\t\t);\n\t});\n}\n", + "target": "src/components/btst/media/client/utils/image-compression.ts" + }, + { + "path": "ui/hooks/use-route-lifecycle.ts", + "type": "registry:hook", + "content": "\"use client\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Base route context interface that plugins can extend\n */\nexport interface BaseRouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { slug: \"my-post\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Minimum interface required for route lifecycle hooks\n * Plugin overrides should implement these optional hooks\n */\nexport interface RouteLifecycleOverrides {\n\t/** Called when a route is rendered */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: TContext,\n\t) => void | Promise;\n\t/** Called when a route encounters an error */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: TContext,\n\t) => void | Promise;\n}\n\n/**\n * Hook to handle route lifecycle events\n * - Calls authorization check before render\n * - Calls onRouteRender on mount\n * - Handles errors with onRouteError\n *\n * @example\n * ```tsx\n * const overrides = usePluginOverrides(\"myPlugin\");\n *\n * useRouteLifecycle({\n * routeName: \"dashboard\",\n * context: { path: \"/dashboard\", isSSR: typeof window === \"undefined\" },\n * overrides,\n * beforeRenderHook: (overrides, context) => {\n * if (overrides.onBeforeDashboardRendered) {\n * return overrides.onBeforeDashboardRendered(context);\n * }\n * return true;\n * },\n * });\n * ```\n */\nexport function useRouteLifecycle<\n\tTContext extends BaseRouteContext,\n\tTOverrides extends RouteLifecycleOverrides,\n>({\n\trouteName,\n\tcontext,\n\toverrides,\n\tbeforeRenderHook,\n}: {\n\trouteName: string;\n\tcontext: TContext;\n\toverrides: TOverrides;\n\tbeforeRenderHook?: (overrides: TOverrides, context: TContext) => boolean;\n}) {\n\t// Authorization check - runs synchronously before render\n\tif (beforeRenderHook) {\n\t\tconst canRender = beforeRenderHook(overrides, context);\n\t\tif (!canRender) {\n\t\t\tconst error = new Error(`Unauthorized: Cannot render ${routeName}`);\n\t\t\t// Call error hook synchronously\n\t\t\tif (overrides.onRouteError) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = overrides.onRouteError(routeName, error, context);\n\t\t\t\t\tif (result instanceof Promise) {\n\t\t\t\t\t\tresult.catch(() => {}); // Ignore promise rejection\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore errors in error hook\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t// Lifecycle hook - runs on mount\n\tuseEffect(() => {\n\t\tif (overrides.onRouteRender) {\n\t\t\ttry {\n\t\t\t\tconst result = overrides.onRouteRender(routeName, context);\n\t\t\t\tif (result instanceof Promise) {\n\t\t\t\t\tresult.catch((error) => {\n\t\t\t\t\t\t// If onRouteRender throws, call onRouteError\n\t\t\t\t\t\tif (overrides.onRouteError) {\n\t\t\t\t\t\t\toverrides.onRouteError(routeName, error, context);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If onRouteRender throws, call onRouteError\n\t\t\t\tif (overrides.onRouteError) {\n\t\t\t\t\toverrides.onRouteError(routeName, error as Error, context);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [routeName, overrides, context]);\n}\n", + "target": "src/hooks/use-route-lifecycle.ts" + } + ], + "docs": "https://better-stack.ai/docs/plugins/media" +} diff --git a/packages/stack/registry/registry.json b/packages/stack/registry/registry.json index e4381a05..5da30c4e 100644 --- a/packages/stack/registry/registry.json +++ b/packages/stack/registry/registry.json @@ -219,6 +219,24 @@ "table" ], "docs": "https://better-stack.ai/docs/plugins/ui-builder" + }, + { + "name": "btst-media", + "type": "registry:block", + "title": "Media Plugin Pages", + "description": "Ejectable page components for the @btst/stack media plugin. Customize the UI layer while keeping data-fetching in @btst/stack.", + "author": "BTST ", + "dependencies": [ + "@btst/stack", + "@vercel/blob" + ], + "registryDependencies": [ + "button", + "input", + "popover", + "tabs" + ], + "docs": "https://better-stack.ai/docs/plugins/media" } ] } diff --git a/packages/stack/scripts/build-registry.ts b/packages/stack/scripts/build-registry.ts index e14d4ac0..3926013c 100644 --- a/packages/stack/scripts/build-registry.ts +++ b/packages/stack/scripts/build-registry.ts @@ -297,6 +297,19 @@ const PLUGINS: PluginConfig[] = [ // hook files (excluded). Only types.ts is needed by ejected components. pluginRootFiles: ["types.ts"], }, + { + name: "media", + title: "Media Plugin Pages", + description: + "Ejectable page components for the @btst/stack media plugin. " + + "Customize the UI layer while keeping data-fetching in @btst/stack.", + // @vercel/blob is required by @btst/stack's use-media hook even when using + // "direct" upload mode — Turbopack statically resolves dynamic imports so + // the package must be present at build time. + extraNpmDeps: ["@vercel/blob"], + extraRegistryDeps: [], + pluginRootFiles: ["types.ts", "schemas.ts"], + }, ]; // --------------------------------------------------------------------------- diff --git a/packages/stack/scripts/test-registry.sh b/packages/stack/scripts/test-registry.sh index 474ac406..dbb0832a 100755 --- a/packages/stack/scripts/test-registry.sh +++ b/packages/stack/scripts/test-registry.sh @@ -47,7 +47,7 @@ SERVER_PORT=8766 SERVER_PID="" TEST_PASSED=false -PLUGIN_NAMES=("blog" "ai-chat" "cms" "form-builder" "kanban" "comments" "ui-builder") +PLUGIN_NAMES=("ui-builder" "blog" "ai-chat" "cms" "form-builder" "kanban" "comments" "media") # --------------------------------------------------------------------------- # Cleanup @@ -333,10 +333,12 @@ import { FormListPageComponent } from "@/components/btst/form-builder/client/com import { BoardsListPageComponent } from "@/components/btst/kanban/client/components/pages/boards-list-page"; import { ModerationPageComponent } from "@/components/btst/comments/client/components/pages/moderation-page"; import { PageListPage } from "@/components/btst/ui-builder/client/components/pages/page-list-page"; +import { LibraryPageComponent } from "@/components/btst/media/client/components/pages/library-page"; // Suppress unused-import warnings while still forcing TS to resolve everything. void [HomePageComponent, ChatPageComponent, DashboardPageComponent, - FormListPageComponent, BoardsListPageComponent, ModerationPageComponent, PageListPage]; + FormListPageComponent, BoardsListPageComponent, ModerationPageComponent, PageListPage, + LibraryPageComponent]; export default function SmokeTestPage() { return
Registry smoke test — all plugin imports resolved.
; From 2e2994ef61e87a20ce023e7c27cb1429c212dc4a Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 14:35:03 -0400 Subject: [PATCH 17/29] chore: update .gitignore to include .tanstack and local media uploads directory for better file management --- examples/tanstack/.gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/tanstack/.gitignore b/examples/tanstack/.gitignore index d530986d..58e7b63d 100644 --- a/examples/tanstack/.gitignore +++ b/examples/tanstack/.gitignore @@ -1,3 +1,6 @@ .nitro .output -.tanstack \ No newline at end of file +.tanstack + +# Local media uploads (localAdapter writes to public/uploads/) +/public/uploads/** \ No newline at end of file From 976368d6a7cdd9412b19972301a3c48ca1baaca7 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 14:45:44 -0400 Subject: [PATCH 18/29] refactor: enhance error handling in LibraryPage component by using FallbackProps for improved error message display --- packages/stack/registry/btst-media.json | 2 +- packages/stack/scripts/test-registry.sh | 17 ++++++++++------- .../client/components/pages/library-page.tsx | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/stack/registry/btst-media.json b/packages/stack/registry/btst-media.json index fd59b060..1e7bf535 100644 --- a/packages/stack/registry/btst-media.json +++ b/packages/stack/registry/btst-media.json @@ -78,7 +78,7 @@ { "path": "btst/media/client/components/pages/library-page.tsx", "type": "registry:page", - "content": "\"use client\";\nimport { lazy } from \"react\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { ComposedRoute } from \"@btst/stack/client/components\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { Loader2 } from \"lucide-react\";\n\nconst LibraryPage = lazy(() =>\n\timport(\"./library-page.internal\").then((m) => ({ default: m.LibraryPage })),\n);\n\nfunction LibraryLoading() {\n\treturn (\n\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nfunction LibraryError({ error }: { error: Error }) {\n\treturn (\n\t\t
\n\t\t\t

{error.message}

\n\t\t
\n\t);\n}\n\nexport function LibraryPageComponent() {\n\tusePluginOverrides(\"media\");\n\treturn (\n\t\t null}\n\t\t\tonError={(error) => console.error(\"[btst/media] Library error:\", error)}\n\t\t/>\n\t);\n}\n", + "content": "\"use client\";\nimport { lazy } from \"react\";\nimport type { FallbackProps } from \"react-error-boundary\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { ComposedRoute } from \"@btst/stack/client/components\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { Loader2 } from \"lucide-react\";\n\nconst LibraryPage = lazy(() =>\n\timport(\"./library-page.internal\").then((m) => ({ default: m.LibraryPage })),\n);\n\nfunction LibraryLoading() {\n\treturn (\n\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nfunction LibraryError({ error }: FallbackProps) {\n\tconst message = error instanceof Error ? error.message : String(error);\n\treturn (\n\t\t
\n\t\t\t

{message}

\n\t\t
\n\t);\n}\n\nexport function LibraryPageComponent() {\n\tusePluginOverrides(\"media\");\n\treturn (\n\t\t null}\n\t\t\tonError={(error) => console.error(\"[btst/media] Library error:\", error)}\n\t\t/>\n\t);\n}\n", "target": "src/components/btst/media/client/components/pages/library-page.tsx" }, { diff --git a/packages/stack/scripts/test-registry.sh b/packages/stack/scripts/test-registry.sh index dbb0832a..359b8fe3 100755 --- a/packages/stack/scripts/test-registry.sh +++ b/packages/stack/scripts/test-registry.sh @@ -121,17 +121,20 @@ main() { npx --yes http-server "$REGISTRY_DIR" -p $SERVER_PORT -c-1 --silent & SERVER_PID=$! - # Wait for server to be ready (up to 15s), then an extra 20s for stability - for i in $(seq 1 15); do - if curl -sf "http://localhost:$SERVER_PORT/btst-blog.json" > /dev/null 2>&1; then + # Wait for server to be ready. `npx http-server` can take >15s on a cold cache + # or slow CI; use 127.0.0.1 to avoid IPv6 localhost ordering quirks. + SERVER_READY=false + for _ in $(seq 1 60); do + if curl -sf "http://127.0.0.1:$SERVER_PORT/btst-blog.json" > /dev/null 2>&1; then + SERVER_READY=true break fi sleep 1 - if [ "$i" = "15" ]; then - error "HTTP server did not become available in time" - exit 1 - fi done + if [ "$SERVER_READY" != true ]; then + error "HTTP server did not become available in time (waited 60s; check npx / port $SERVER_PORT)" + exit 1 + fi success "HTTP server running (PID: $SERVER_PID)" pause 20 diff --git a/packages/stack/src/plugins/media/client/components/pages/library-page.tsx b/packages/stack/src/plugins/media/client/components/pages/library-page.tsx index aec7a8e7..f6c424b0 100644 --- a/packages/stack/src/plugins/media/client/components/pages/library-page.tsx +++ b/packages/stack/src/plugins/media/client/components/pages/library-page.tsx @@ -1,5 +1,6 @@ "use client"; import { lazy } from "react"; +import type { FallbackProps } from "react-error-boundary"; import { usePluginOverrides } from "@btst/stack/context"; import { ComposedRoute } from "@btst/stack/client/components"; import type { MediaPluginOverrides } from "../../overrides"; @@ -17,10 +18,11 @@ function LibraryLoading() { ); } -function LibraryError({ error }: { error: Error }) { +function LibraryError({ error }: FallbackProps) { + const message = error instanceof Error ? error.message : String(error); return (
-

{error.message}

+

{message}

); } From 0995b8cfc80703d8293f2466e5f4f53da674c7d3 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 15:10:26 -0400 Subject: [PATCH 19/29] feat: update uploadImage handling across examples for consistency --- e2e/tests/smoke.media.spec.ts | 37 +++- examples/nextjs/app/pages/layout.tsx | 24 ++- .../react-router/app/routes/pages/_layout.tsx | 15 +- examples/tanstack/src/routes/pages/route.tsx | 15 +- packages/stack/registry/btst-blog.json | 2 +- packages/stack/registry/btst-cms.json | 2 +- packages/stack/registry/btst-kanban.json | 2 +- packages/stack/registry/btst-media.json | 8 +- .../src/plugins/blog/client/overrides.ts | 3 +- .../stack/src/plugins/cms/client/overrides.ts | 5 +- .../src/plugins/kanban/client/overrides.ts | 5 +- .../plugins/media/client/components/index.tsx | 1 - .../client/components/media-picker/index.tsx | 25 --- .../plugins/media/client/hooks/use-media.tsx | 146 ++------------- .../stack/src/plugins/media/client/index.ts | 2 + .../stack/src/plugins/media/client/upload.ts | 171 ++++++++++++++++++ 16 files changed, 266 insertions(+), 197 deletions(-) create mode 100644 packages/stack/src/plugins/media/client/upload.ts diff --git a/e2e/tests/smoke.media.spec.ts b/e2e/tests/smoke.media.spec.ts index 889d1801..8dfad5ab 100644 --- a/e2e/tests/smoke.media.spec.ts +++ b/e2e/tests/smoke.media.spec.ts @@ -61,6 +61,20 @@ async function selectFirstAsset(page: Page) { }); } +/** + * Blog new-post has two "Browse Media" buttons: featured image (ImageInputField) + * and the markdown editor's picker (`data-testid="image-picker-trigger"`). + * Use this so the asset is inserted into the editor, not the featured field. + */ +async function openBlogEditorMediaPicker(page: Page) { + const trigger = page.locator( + '[data-testid="image-picker-trigger"] [data-testid="open-media-picker"]', + ); + await expect(trigger).toBeVisible({ timeout: 10000 }); + await trigger.click(); + await expect(page.getByText("Media Library")).toBeVisible({ timeout: 5000 }); +} + test.describe("Media Plugin — direct upload via MediaPicker", () => { test("MediaPicker trigger is visible on blog new post page", async ({ page, @@ -167,8 +181,8 @@ test.describe("Media Plugin — direct upload via MediaPicker", () => { await page.waitForSelector(".milkdown-custom", { state: "visible" }); await page.waitForTimeout(500); - // Open the MediaPicker (trigger is adjacent to editor) - await openMediaPicker(page); + // Open the markdown editor's MediaPicker (not the featured-image picker above) + await openBlogEditorMediaPicker(page); // Upload a new image await uploadInMediaPicker(page); @@ -176,11 +190,10 @@ test.describe("Media Plugin — direct upload via MediaPicker", () => { // Select it — this inserts the image URL into the editor await selectFirstAsset(page); - // The editor should now contain an image — verify via markdown content - // (Milkdown renders images as tags inside the contenteditable) - await page.waitForTimeout(500); - const editorImages = page.locator(".milkdown-custom [contenteditable] img"); - await expect(editorImages.first()).toBeVisible({ timeout: 10000 }); + // Crepe renders `![](url)` as under `.milkdown-custom`; the image + // node is not always a descendant of `[contenteditable]` (node views). + const editorImages = page.locator(".milkdown-custom img"); + await expect(editorImages.first()).toBeVisible({ timeout: 15000 }); // The image src should be a real URL (not a placeholder) const imgSrc = await editorImages.first().getAttribute("src"); @@ -281,7 +294,15 @@ test.describe("Media Plugin — direct upload via MediaPicker", () => { }); const imagePreview = page.locator('[data-testid="image-preview"]'); await expect(imagePreview).toBeVisible({ timeout: 5000 }); - await expect(imagePreview).toHaveAttribute("src", testUrl); + // CMS uses `media.Image` (Next.js Image in the example app): the DOM `src` + // is often `/_next/image?url=...`, not the raw remote URL. + const previewSrc = await imagePreview.getAttribute("src"); + expect(previewSrc).toBeTruthy(); + expect( + previewSrc === testUrl || + previewSrc?.startsWith("/_next/image") || + previewSrc?.includes("placehold.co"), + ).toBe(true); expect(errors, `Console errors: \n${errors.join("\n")}`).toEqual([]); }); diff --git a/examples/nextjs/app/pages/layout.tsx b/examples/nextjs/app/pages/layout.tsx index 5c37c3c0..eef20f27 100644 --- a/examples/nextjs/app/pages/layout.tsx +++ b/examples/nextjs/app/pages/layout.tsx @@ -18,8 +18,8 @@ import { defaultComponentRegistry } from "@btst/stack/plugins/ui-builder/client" import type { KanbanPluginOverrides } from "@btst/stack/plugins/kanban/client" import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" import { CommentThread } from "@btst/stack/plugins/comments/client/components" -import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" -import { MediaPicker, ImageInputField, uploadMediaFile } from "@btst/stack/plugins/media/client/components" +import { uploadAsset, type MediaPluginOverrides } from "@btst/stack/plugins/media/client" +import { MediaPicker, ImageInputField } from "@btst/stack/plugins/media/client/components" import { resolveUser, searchUsers } from "@/lib/mock-users" import { Button } from "@/components/ui/button" @@ -86,12 +86,22 @@ export default function ExampleLayout({ // fresh instance to avoid stale client cache overriding hydrated data const [queryClient] = useState(() => getOrCreateQueryClient()) const baseURL = getBaseURL() + const mediaClientConfig = React.useMemo( + () => ({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + uploadMode: "direct" as const, + }), + [baseURL], + ) const uploadImage = React.useCallback( async (file: File) => { - const asset = await uploadMediaFile(file, baseURL, "/api/data") - return asset.url; - }, [baseURL]); + const asset = await uploadAsset(mediaClientConfig, { file }) + return asset.url + }, + [mediaClientConfig], + ) // For chat file attachments we embed as a data URL so OpenAI can read the // content directly — a local /uploads/... path is not reachable from OpenAI's servers. @@ -291,10 +301,8 @@ export default function ExampleLayout({ }, }, media: { - apiBaseURL: baseURL, - apiBasePath: "/api/data", + ...mediaClientConfig, queryClient, - uploadMode: "direct", navigate: (path) => router.push(path), Link: ({ href, ...props }) => , Image: NextImageWrapper, diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index 4b36cd96..32985d7c 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -10,8 +10,8 @@ import type { FormBuilderPluginOverrides } from "@btst/stack/plugins/form-builde import type { KanbanPluginOverrides } from "@btst/stack/plugins/kanban/client" import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" import { CommentThread } from "@btst/stack/plugins/comments/client/components" -import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" -import { MediaPicker, ImageInputField, uploadMediaFile } from "@btst/stack/plugins/media/client/components" +import { uploadAsset, type MediaPluginOverrides } from "@btst/stack/plugins/media/client" +import { MediaPicker, ImageInputField } from "@btst/stack/plugins/media/client/components" import { Button } from "../../components/ui/button" import { resolveUser, searchUsers } from "../../lib/mock-users" import { getOrCreateQueryClient } from "../../lib/query-client" @@ -42,9 +42,14 @@ export default function Layout() { console.log("baseURL", baseURL) const navigate = useNavigate() const [queryClient] = useState(() => getOrCreateQueryClient()) + const mediaClientConfig = { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + uploadMode: "direct" as const, + } const uploadImage = async (file: File) => { - const asset = await uploadMediaFile(file, baseURL, "/api/data") + const asset = await uploadAsset(mediaClientConfig, { file }) return asset.url } @@ -213,10 +218,8 @@ export default function Layout() { }, }, media: { - apiBaseURL: baseURL, - apiBasePath: "/api/data", + ...mediaClientConfig, queryClient, - uploadMode: "direct", navigate: (href) => navigate(href), Link: ({ href, children, className, ...props }) => ( diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index cd8f274d..a0502ce6 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -9,8 +9,8 @@ import type { FormBuilderPluginOverrides } from "@btst/stack/plugins/form-builde import type { KanbanPluginOverrides } from "@btst/stack/plugins/kanban/client" import type { CommentsPluginOverrides } from "@btst/stack/plugins/comments/client" import { CommentThread } from "@btst/stack/plugins/comments/client/components" -import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" -import { MediaPicker, ImageInputField, uploadMediaFile } from "@btst/stack/plugins/media/client/components" +import { uploadAsset, type MediaPluginOverrides } from "@btst/stack/plugins/media/client" +import { MediaPicker, ImageInputField } from "@btst/stack/plugins/media/client/components" import { Button } from "../../components/ui/button" import { resolveUser, searchUsers } from "../../lib/mock-users" import { Link, useRouter, Outlet, createFileRoute } from "@tanstack/react-router" @@ -45,9 +45,14 @@ function Layout() { const router = useRouter() const routeContext = Route.useRouteContext() const baseURL = getBaseURL() + const mediaClientConfig = { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + uploadMode: "direct" as const, + } const uploadImage = async (file: File) => { - const asset = await uploadMediaFile(file, baseURL, "/api/data") + const asset = await uploadAsset(mediaClientConfig, { file }) return asset.url } @@ -217,10 +222,8 @@ function Layout() { }, }, media: { - apiBaseURL: baseURL, - apiBasePath: "/api/data", + ...mediaClientConfig, queryClient: routeContext.queryClient, - uploadMode: "direct", navigate: (href) => router.navigate({ href }), Link: ({ href, children, className, ...props }) => ( diff --git a/packages/stack/registry/btst-blog.json b/packages/stack/registry/btst-blog.json index f1319296..e830291b 100644 --- a/packages/stack/registry/btst-blog.json +++ b/packages/stack/registry/btst-blog.json @@ -352,7 +352,7 @@ { "path": "btst/blog/client/overrides.ts", "type": "registry:lib", - "content": "import type { SerializedPost } from \"../types\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { BlogLocalization } from \"./localization\";\n\n/**\n * Props for the overridable blog featured image input component.\n */\nexport interface BlogImageInputFieldProps {\n\t/** Current image URL value */\n\tvalue: string;\n\t/** Called when the image URL changes */\n\tonChange: (value: string) => void;\n\t/** Whether the field is required */\n\tisRequired?: boolean;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { slug: \"my-post\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: any;\n}\n\n/**\n * Overridable components and functions for the Blog plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface BlogPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Post card component for displaying a post\n\t */\n\tPostCard?: ComponentType<{\n\t\tpost: SerializedPost;\n\t}>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Function used to upload an image and return its URL.\n\t */\n\tuploadImage: (file: File) => Promise;\n\t/**\n\t * Optional custom component for the featured image field.\n\t *\n\t * When provided it replaces the default file-upload input entirely.\n\t * The component receives `value` (current URL string) and `onChange` (setter).\n\t *\n\t * Typical use case: render a preview when a value is set, and a media-picker\n\t * trigger when no value is set.\n\t *\n\t * @example\n\t * ```tsx\n\t * imageInputField: ({ value, onChange }) =>\n\t * value ? (\n\t *
\n\t * \"Preview\"\n\t * Change} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t *
\n\t * ) : (\n\t * Browse media} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t * )\n\t * ```\n\t */\n\timageInputField?: ComponentType;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered adjacent to the Markdown editor and allows\n\t * users to browse and select previously uploaded assets.\n\t * Receives `onSelect(url)` — insert the chosen URL into the editor.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\t/**\n\t * Localization object for the blog plugin\n\t */\n\tlocalization?: BlogLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'posts', 'post', 'newPost')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the posts list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforePostsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug\n\t * @param context - Route context\n\t */\n\tonBeforePostPageRendered?: (slug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the new post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewPostPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the edit post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug being edited\n\t * @param context - Route context\n\t */\n\tonBeforeEditPostPageRendered?: (\n\t\tslug: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the drafts page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDraftsPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered below the blog post body.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the blog plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * blog: {\n\t * postBottomSlot: (post) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tpostBottomSlot?: (post: SerializedPost) => ReactNode;\n}\n", + "content": "import type { SerializedPost } from \"../types\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport type { BlogLocalization } from \"./localization\";\n\n/**\n * Props for the overridable blog featured image input component.\n */\nexport interface BlogImageInputFieldProps {\n\t/** Current image URL value */\n\tvalue: string;\n\t/** Called when the image URL changes */\n\tonChange: (value: string) => void;\n\t/** Whether the field is required */\n\tisRequired?: boolean;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { slug: \"my-post\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: any;\n}\n\n/**\n * Overridable components and functions for the Blog plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface BlogPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Post card component for displaying a post\n\t */\n\tPostCard?: ComponentType<{\n\t\tpost: SerializedPost;\n\t}>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Function used to upload a new image file and return its URL.\n\t * This is separate from `imagePicker`, which selects an existing asset URL.\n\t */\n\tuploadImage: (file: File) => Promise;\n\t/**\n\t * Optional custom component for the featured image field.\n\t *\n\t * When provided it replaces the default file-upload input entirely.\n\t * The component receives `value` (current URL string) and `onChange` (setter).\n\t *\n\t * Typical use case: render a preview when a value is set, and a media-picker\n\t * trigger when no value is set.\n\t *\n\t * @example\n\t * ```tsx\n\t * imageInputField: ({ value, onChange }) =>\n\t * value ? (\n\t *
\n\t * \"Preview\"\n\t * Change} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t *
\n\t * ) : (\n\t * Browse media} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t * )\n\t * ```\n\t */\n\timageInputField?: ComponentType;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered adjacent to the Markdown editor and allows\n\t * users to browse and select previously uploaded assets.\n\t * Receives `onSelect(url)` — insert the chosen URL into the editor.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\t/**\n\t * Localization object for the blog plugin\n\t */\n\tlocalization?: BlogLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'posts', 'post', 'newPost')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the posts list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforePostsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug\n\t * @param context - Route context\n\t */\n\tonBeforePostPageRendered?: (slug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the new post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewPostPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the edit post page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param slug - The post slug being edited\n\t * @param context - Route context\n\t */\n\tonBeforeEditPostPageRendered?: (\n\t\tslug: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the drafts page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDraftsPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered below the blog post body.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the blog plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * blog: {\n\t * postBottomSlot: (post) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tpostBottomSlot?: (post: SerializedPost) => ReactNode;\n}\n", "target": "src/components/btst/blog/client/overrides.ts" }, { diff --git a/packages/stack/registry/btst-cms.json b/packages/stack/registry/btst-cms.json index 94c80b2f..eae95ea4 100644 --- a/packages/stack/registry/btst-cms.json +++ b/packages/stack/registry/btst-cms.json @@ -199,7 +199,7 @@ { "path": "btst/cms/client/overrides.ts", "type": "registry:lib", - "content": "import type { ComponentType } from \"react\";\nimport type { CMSLocalization } from \"./localization\";\nimport type { AutoFormInputComponentProps } from \"@/components/ui/auto-form/types\";\n\n/**\n * Props for the overridable CMS image input field component.\n */\nexport interface CmsImageInputFieldProps {\n\t/** Current image URL value */\n\tvalue: string;\n\t/** Called when the image URL changes */\n\tonChange: (value: string) => void;\n\t/** Whether the field is required */\n\tisRequired?: boolean;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { typeSlug: \"product\", id: \"123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the CMS plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface CMSPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\n\t/**\n\t * Function used to upload an image and return its URL.\n\t * Used by the default \"file\" field component.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Optional custom component for image fields (fieldType: \"file\").\n\t *\n\t * When provided it replaces the default file-upload input entirely.\n\t * The component receives `value` (current URL string) and `onChange` (setter).\n\t *\n\t * @example\n\t * ```tsx\n\t * imageInputField: ({ value, onChange }) =>\n\t * value ? (\n\t *
\n\t * \"Preview\"\n\t * Change} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t *
\n\t * ) : (\n\t * Browse media} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t * )\n\t * ```\n\t */\n\timageInputField?: ComponentType;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered inside the default \"file\" field component as a\n\t * \"Browse media\" option, letting users select a previously uploaded asset.\n\t * Receives `onSelect(url)` — the URL is set as the field value.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\n\t/**\n\t * Custom field components for AutoForm fields.\n\t *\n\t * These map field type names to React components. Use these to:\n\t * - Override built-in field types (checkbox, date, select, radio, switch, textarea, file, number, fallback)\n\t * - Add custom field types for your content types\n\t *\n\t * The component receives AutoFormInputComponentProps which includes:\n\t * - field: react-hook-form field controller\n\t * - label: the field label\n\t * - isRequired: whether the field is required\n\t * - fieldConfigItem: the field config (description, inputProps, etc.)\n\t * - fieldProps: additional props from fieldConfig.inputProps\n\t * - zodItem: the Zod schema for this field\n\t *\n\t * @example\n\t * ```tsx\n\t * fieldComponents: {\n\t * // Override the file type with custom S3 upload\n\t * file: ({ field, label, isRequired }) => (\n\t * \n\t * ),\n\t * // Add a custom rich text editor\n\t * richText: ({ field, label }) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tfieldComponents?: Record>;\n\n\t/**\n\t * Localization object for the CMS plugin\n\t */\n\tlocalization?: CMSLocalization;\n\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'dashboard', 'contentList', 'contentEditor')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the dashboard page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDashboardRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param context - Route context\n\t */\n\tonBeforeListRendered?: (typeSlug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content editor page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param id - The content item ID (null for new items)\n\t * @param context - Route context\n\t */\n\tonBeforeEditorRendered?: (\n\t\ttypeSlug: string,\n\t\tid: string | null,\n\t\tcontext: RouteContext,\n\t) => boolean;\n}\n", + "content": "import type { ComponentType } from \"react\";\nimport type { CMSLocalization } from \"./localization\";\nimport type { AutoFormInputComponentProps } from \"@/components/ui/auto-form/types\";\n\n/**\n * Props for the overridable CMS image input field component.\n */\nexport interface CmsImageInputFieldProps {\n\t/** Current image URL value */\n\tvalue: string;\n\t/** Called when the image URL changes */\n\tonChange: (value: string) => void;\n\t/** Whether the field is required */\n\tisRequired?: boolean;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { typeSlug: \"product\", id: \"123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the CMS plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface CMSPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\n\t/**\n\t * Function used to upload a new image file and return its URL.\n\t * Used by the default \"file\" field component when not selecting an existing\n\t * asset via `imagePicker` or `imageInputField`.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Optional custom component for image fields (fieldType: \"file\").\n\t *\n\t * When provided it replaces the default file-upload input entirely.\n\t * The component receives `value` (current URL string) and `onChange` (setter).\n\t *\n\t * @example\n\t * ```tsx\n\t * imageInputField: ({ value, onChange }) =>\n\t * value ? (\n\t *
\n\t * \"Preview\"\n\t * Change} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t *
\n\t * ) : (\n\t * Browse media} accept={[\"image/*\"]}\n\t * onSelect={(assets) => onChange(assets[0].url)} />\n\t * )\n\t * ```\n\t */\n\timageInputField?: ComponentType;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it is rendered inside the default \"file\" field component as a\n\t * \"Browse media\" option, letting users select a previously uploaded asset.\n\t * Receives `onSelect(url)` — the URL is set as the field value.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\n\t/**\n\t * Custom field components for AutoForm fields.\n\t *\n\t * These map field type names to React components. Use these to:\n\t * - Override built-in field types (checkbox, date, select, radio, switch, textarea, file, number, fallback)\n\t * - Add custom field types for your content types\n\t *\n\t * The component receives AutoFormInputComponentProps which includes:\n\t * - field: react-hook-form field controller\n\t * - label: the field label\n\t * - isRequired: whether the field is required\n\t * - fieldConfigItem: the field config (description, inputProps, etc.)\n\t * - fieldProps: additional props from fieldConfig.inputProps\n\t * - zodItem: the Zod schema for this field\n\t *\n\t * @example\n\t * ```tsx\n\t * fieldComponents: {\n\t * // Override the file type with custom S3 upload\n\t * file: ({ field, label, isRequired }) => (\n\t * \n\t * ),\n\t * // Add a custom rich text editor\n\t * richText: ({ field, label }) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\tfieldComponents?: Record>;\n\n\t/**\n\t * Localization object for the CMS plugin\n\t */\n\tlocalization?: CMSLocalization;\n\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t// Lifecycle Hooks (optional)\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'dashboard', 'contentList', 'contentEditor')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the dashboard page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeDashboardRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param context - Route context\n\t */\n\tonBeforeListRendered?: (typeSlug: string, context: RouteContext) => boolean;\n\n\t/**\n\t * Called before the content editor page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param typeSlug - The content type slug\n\t * @param id - The content item ID (null for new items)\n\t * @param context - Route context\n\t */\n\tonBeforeEditorRendered?: (\n\t\ttypeSlug: string,\n\t\tid: string | null,\n\t\tcontext: RouteContext,\n\t) => boolean;\n}\n", "target": "src/components/btst/cms/client/overrides.ts" }, { diff --git a/packages/stack/registry/btst-kanban.json b/packages/stack/registry/btst-kanban.json index 04662319..fa67c9ec 100644 --- a/packages/stack/registry/btst-kanban.json +++ b/packages/stack/registry/btst-kanban.json @@ -193,7 +193,7 @@ { "path": "btst/kanban/client/overrides.ts", "type": "registry:lib", - "content": "import type { ComponentType, ReactNode } from \"react\";\nimport type { KanbanLocalization } from \"./localization\";\nimport type { SerializedTask } from \"../types\";\n\n/**\n * User information for assignee display/selection\n * Framework-agnostic - consumers map their auth system to this shape\n */\nexport interface KanbanUser {\n\tid: string;\n\tname: string;\n\tavatarUrl?: string;\n\temail?: string;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { boardId: \"abc123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the Kanban plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface KanbanPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Localization object for the kanban plugin\n\t */\n\tlocalization?: KanbanLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t/**\n\t * Function used to upload an image from the task description editor and return its URL.\n\t * Wired as the `uploader` prop of MinimalTiptapEditor — handles drag-drop image uploads.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it appears inside the image insertion dialog of the task description editor,\n\t * letting users browse and select previously uploaded assets.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\n\t// ============ User Resolution (required for assignee features) ============\n\n\t/**\n\t * Resolve user info from an assigneeId\n\t * Called when rendering task cards/forms that have an assignee\n\t * Return null for unknown users (will show fallback UI)\n\t */\n\tresolveUser: (\n\t\tuserId: string,\n\t) => Promise | KanbanUser | null;\n\n\t/**\n\t * Search/list users available for assignment\n\t * Called when user opens the assignee picker\n\t * @param query - Search query (empty string for initial load)\n\t * @param boardId - Optional board context for scoped user lists\n\t */\n\tsearchUsers: (\n\t\tquery: string,\n\t\tboardId?: string,\n\t) => Promise | KanbanUser[];\n\n\t// ============ Lifecycle Hooks (optional) ============\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'boards', 'board', 'newBoard')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the boards list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeBoardsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param boardId - The board ID\n\t * @param context - Route context\n\t */\n\tonBeforeBoardPageRendered?: (\n\t\tboardId: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the new board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewBoardPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered at the bottom of the task detail dialog.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the kanban plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * kanban: {\n\t * taskDetailBottomSlot: (task) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\ttaskDetailBottomSlot?: (task: SerializedTask) => ReactNode;\n}\n", + "content": "import type { ComponentType, ReactNode } from \"react\";\nimport type { KanbanLocalization } from \"./localization\";\nimport type { SerializedTask } from \"../types\";\n\n/**\n * User information for assignee display/selection\n * Framework-agnostic - consumers map their auth system to this shape\n */\nexport interface KanbanUser {\n\tid: string;\n\tname: string;\n\tavatarUrl?: string;\n\temail?: string;\n}\n\n/**\n * Context passed to lifecycle hooks\n */\nexport interface RouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters (e.g., { boardId: \"abc123\" }) */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t/** Additional context properties */\n\t[key: string]: unknown;\n}\n\n/**\n * Overridable components and functions for the Kanban plugin\n *\n * External consumers can provide their own implementations of these\n * to customize the behavior for their framework (Next.js, React Router, etc.)\n */\nexport interface KanbanPluginOverrides {\n\t/**\n\t * Link component for navigation\n\t */\n\tLink?: ComponentType & Record>;\n\t/**\n\t * Navigation function for programmatic navigation\n\t */\n\tnavigate: (path: string) => void | Promise;\n\t/**\n\t * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())\n\t */\n\trefresh?: () => void | Promise;\n\t/**\n\t * Image component for displaying images\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\t/**\n\t * Localization object for the kanban plugin\n\t */\n\tlocalization?: KanbanLocalization;\n\t/**\n\t * API base URL\n\t */\n\tapiBaseURL: string;\n\t/**\n\t * API base path\n\t */\n\tapiBasePath: string;\n\t/**\n\t * Whether to show the attribution\n\t */\n\tshowAttribution?: boolean;\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth)\n\t */\n\theaders?: HeadersInit;\n\n\t/**\n\t * Function used to upload a new image file from the task description editor\n\t * and return its URL. This is separate from `imagePicker`, which selects an\n\t * existing asset URL.\n\t */\n\tuploadImage?: (file: File) => Promise;\n\n\t/**\n\t * Optional trigger component for a media picker.\n\t * When provided, it appears inside the image insertion dialog of the task description editor,\n\t * letting users browse and select previously uploaded assets.\n\t *\n\t * @example\n\t * ```tsx\n\t * imagePicker: ({ onSelect }) => (\n\t * Browse media}\n\t * accept={[\"image/*\"]}\n\t * onSelect={(assets) => onSelect(assets[0].url)}\n\t * />\n\t * )\n\t * ```\n\t */\n\timagePicker?: ComponentType<{ onSelect: (url: string) => void }>;\n\n\t// ============ User Resolution (required for assignee features) ============\n\n\t/**\n\t * Resolve user info from an assigneeId\n\t * Called when rendering task cards/forms that have an assignee\n\t * Return null for unknown users (will show fallback UI)\n\t */\n\tresolveUser: (\n\t\tuserId: string,\n\t) => Promise | KanbanUser | null;\n\n\t/**\n\t * Search/list users available for assignment\n\t * Called when user opens the assignee picker\n\t * @param query - Search query (empty string for initial load)\n\t * @param boardId - Optional board context for scoped user lists\n\t */\n\tsearchUsers: (\n\t\tquery: string,\n\t\tboardId?: string,\n\t) => Promise | KanbanUser[];\n\n\t// ============ Lifecycle Hooks (optional) ============\n\n\t/**\n\t * Called when a route is rendered\n\t * @param routeName - Name of the route (e.g., 'boards', 'board', 'newBoard')\n\t * @param context - Route context with path, params, etc.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a route encounters an error\n\t * @param routeName - Name of the route\n\t * @param error - The error that occurred\n\t * @param context - Route context\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: RouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the boards list page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeBoardsPageRendered?: (context: RouteContext) => boolean;\n\n\t/**\n\t * Called before a single board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param boardId - The board ID\n\t * @param context - Route context\n\t */\n\tonBeforeBoardPageRendered?: (\n\t\tboardId: string,\n\t\tcontext: RouteContext,\n\t) => boolean;\n\n\t/**\n\t * Called before the new board page is rendered\n\t * Return false to prevent rendering (e.g., for authorization)\n\t * @param context - Route context\n\t */\n\tonBeforeNewBoardPageRendered?: (context: RouteContext) => boolean;\n\n\t// ============ Slot Overrides ============\n\n\t/**\n\t * Optional slot rendered at the bottom of the task detail dialog.\n\t * Use this to inject a comment thread or any custom content without\n\t * coupling the kanban plugin to the comments plugin.\n\t *\n\t * @example\n\t * ```tsx\n\t * kanban: {\n\t * taskDetailBottomSlot: (task) => (\n\t * \n\t * ),\n\t * }\n\t * ```\n\t */\n\ttaskDetailBottomSlot?: (task: SerializedTask) => ReactNode;\n}\n", "target": "src/components/btst/kanban/client/overrides.ts" }, { diff --git a/packages/stack/registry/btst-media.json b/packages/stack/registry/btst-media.json index 1e7bf535..30c2be12 100644 --- a/packages/stack/registry/btst-media.json +++ b/packages/stack/registry/btst-media.json @@ -48,7 +48,7 @@ { "path": "btst/media/client/components/media-picker/index.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n\n/**\n * Upload a file via the media plugin's direct upload endpoint.\n * @param file - The file to upload.\n * @param baseURL - The base URL of the server (e.g. `https://example.com`).\n * @param apiBasePath - The API base path configured for the media plugin (e.g. `/api/v2`). Defaults to `/api/data`.\n */\nexport async function uploadMediaFile(\n\tfile: File,\n\tbaseURL: string,\n\tapiBasePath: string,\n): Promise {\n\tconst formData = new FormData();\n\tformData.append(\"file\", file);\n\tconst res = await fetch(`${baseURL}${apiBasePath}/media/upload`, {\n\t\tmethod: \"POST\",\n\t\tbody: formData,\n\t});\n\tif (!res.ok) {\n\t\tconst err = await res.json().catch(() => ({ message: res.statusText }));\n\t\tthrow new Error(err.message ?? \"Upload failed\");\n\t}\n\tconst asset = (await res.json()) as SerializedAsset;\n\treturn asset;\n}\n", + "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/index.tsx" }, { @@ -87,6 +87,12 @@ "content": "import type { ComponentType } from \"react\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { ImageCompressionOptions } from \"./utils/image-compression\";\n\n/**\n * Upload mode — must match the storage adapter configured in mediaBackendPlugin.\n * - `\"direct\"` — local filesystem adapter, files are uploaded via `POST /media/upload`\n * - `\"s3\"` — AWS S3 / R2 / MinIO, the client fetches a presigned token then PUTs directly to S3\n * - `\"vercel-blob\"` — Vercel Blob, uses the `@vercel/blob/client` SDK for direct upload\n */\nexport type MediaUploadMode = \"direct\" | \"s3\" | \"vercel-blob\";\n\n/**\n * Overridable components and functions for the Media plugin.\n *\n * External consumers provide these when registering the media client plugin\n * via the StackProvider overrides.\n */\nexport interface MediaPluginOverrides {\n\t/**\n\t * Base URL for API calls (e.g., \"http://localhost:3000\").\n\t */\n\tapiBaseURL: string;\n\n\t/**\n\t * Path where the API is mounted (e.g., \"/api/data\").\n\t */\n\tapiBasePath: string;\n\n\t/**\n\t * React Query client — used by the MediaPicker to cache and fetch assets.\n\t */\n\tqueryClient: QueryClient;\n\n\t/**\n\t * Upload mode — must match the storageAdapter configured in mediaBackendPlugin.\n\t * @default \"direct\"\n\t */\n\tuploadMode?: MediaUploadMode;\n\n\t/**\n\t * Optional headers to pass with API requests (e.g., for SSR auth).\n\t */\n\theaders?: HeadersInit;\n\n\t/**\n\t * Navigation function for programmatic navigation.\n\t */\n\tnavigate: (path: string) => void | Promise;\n\n\t/**\n\t * Link component for navigation within the media library page.\n\t */\n\tLink?: ComponentType & Record>;\n\n\t/**\n\t * Image component for rendering asset thumbnails and previews.\n\t *\n\t * When provided, replaces the default `` element in asset cards,\n\t * the media library grid, and the ImageInputField preview. Use this\n\t * to plug in Next.js `` for automatic optimisation.\n\t *\n\t * @example\n\t * ```tsx\n\t * Image: (props) => \n\t * ```\n\t */\n\tImage?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\n\t/**\n\t * Client-side image compression applied before upload via the Canvas API.\n\t *\n\t * Images are scaled down to fit within `maxWidth` × `maxHeight` (preserving\n\t * aspect ratio) and re-encoded at `quality`. SVG and GIF files are always\n\t * passed through unchanged.\n\t *\n\t * Set to `false` to disable compression entirely.\n\t *\n\t * @default { maxWidth: 2048, maxHeight: 2048, quality: 0.85 }\n\t */\n\timageCompression?: ImageCompressionOptions | false;\n\n\t// ============ Lifecycle Hooks ============\n\n\t/**\n\t * Called when a media route is rendered.\n\t */\n\tonRouteRender?: (\n\t\trouteName: string,\n\t\tcontext: MediaRouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called when a media route encounters an error.\n\t */\n\tonRouteError?: (\n\t\trouteName: string,\n\t\terror: Error,\n\t\tcontext: MediaRouteContext,\n\t) => void | Promise;\n\n\t/**\n\t * Called before the media library page is rendered.\n\t * Return `false` to prevent rendering (e.g., redirect unauthenticated users).\n\t *\n\t * @example\n\t * ```ts\n\t * media: {\n\t * onBeforeLibraryPageRendered: (context) => !!currentUser?.isAdmin,\n\t * onRouteError: (routeName, error, context) => navigate(\"/login\"),\n\t * }\n\t * ```\n\t */\n\tonBeforeLibraryPageRendered?: (context: MediaRouteContext) => boolean;\n}\n\nexport interface MediaRouteContext {\n\t/** Current route path */\n\tpath: string;\n\t/** Route parameters */\n\tparams?: Record;\n\t/** Whether rendering on server (true) or client (false) */\n\tisSSR: boolean;\n\t[key: string]: unknown;\n}\n", "target": "src/components/btst/media/client/overrides.ts" }, + { + "path": "btst/media/client/upload.ts", + "type": "registry:lib", + "content": "\"use client\";\n\nimport type { SerializedAsset } from \"../types\";\nimport type { MediaPluginOverrides } from \"./overrides\";\nimport { compressImage } from \"./utils/image-compression\";\n\nexport type MediaUploadClientConfig = Pick<\n\tMediaPluginOverrides,\n\t\"apiBaseURL\" | \"apiBasePath\" | \"headers\" | \"uploadMode\" | \"imageCompression\"\n>;\n\nexport interface UploadAssetInput {\n\tfile: File;\n\tfolderId?: string;\n}\n\nconst DEFAULT_IMAGE_COMPRESSION = {\n\tmaxWidth: 2048,\n\tmaxHeight: 2048,\n\tquality: 0.85,\n} as const;\n\n/**\n * Upload an asset using the media plugin's configured storage mode.\n *\n * Use this in non-React contexts like editor `uploadImage` callbacks. React\n * components should usually prefer `useUploadAsset()`, which wraps this helper\n * and handles cache invalidation.\n */\nexport async function uploadAsset(\n\tconfig: MediaUploadClientConfig,\n\tinput: UploadAssetInput,\n): Promise {\n\tconst {\n\t\tapiBaseURL,\n\t\tapiBasePath,\n\t\theaders,\n\t\tuploadMode = \"direct\",\n\t\timageCompression,\n\t} = config;\n\tconst { file, folderId } = input;\n\n\tconst processedFile =\n\t\timageCompression === false\n\t\t\t? file\n\t\t\t: await compressImage(\n\t\t\t\t\tfile,\n\t\t\t\t\timageCompression ?? DEFAULT_IMAGE_COMPRESSION,\n\t\t\t\t);\n\n\tconst base = `${apiBaseURL}${apiBasePath}`;\n\tconst headersObj = new Headers(headers as HeadersInit | undefined);\n\n\tif (uploadMode === \"direct\") {\n\t\tconst formData = new FormData();\n\t\tformData.append(\"file\", processedFile);\n\t\tif (folderId) formData.append(\"folderId\", folderId);\n\n\t\tconst res = await fetch(`${base}/media/upload`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: headersObj,\n\t\t\tbody: formData,\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tconst err = await res.json().catch(() => ({ message: res.statusText }));\n\t\t\tthrow new Error(err.message ?? \"Upload failed\");\n\t\t}\n\t\treturn res.json();\n\t}\n\n\tif (uploadMode === \"s3\") {\n\t\tconst tokenRes = await fetch(`${base}/media/upload/token`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t...Object.fromEntries(headersObj.entries()),\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfilename: processedFile.name,\n\t\t\t\tmimeType: processedFile.type,\n\t\t\t\tsize: processedFile.size,\n\t\t\t\tfolderId,\n\t\t\t}),\n\t\t});\n\t\tif (!tokenRes.ok) {\n\t\t\tconst err = await tokenRes\n\t\t\t\t.json()\n\t\t\t\t.catch(() => ({ message: tokenRes.statusText }));\n\t\t\tthrow new Error(err.message ?? \"Failed to get upload token\");\n\t\t}\n\n\t\tconst token = (await tokenRes.json()) as {\n\t\t\ttype: \"presigned-url\";\n\t\t\tpayload: {\n\t\t\t\tuploadUrl: string;\n\t\t\t\tpublicUrl: string;\n\t\t\t\tkey: string;\n\t\t\t\tmethod: \"PUT\";\n\t\t\t\theaders: Record;\n\t\t\t};\n\t\t};\n\n\t\tconst putRes = await fetch(token.payload.uploadUrl, {\n\t\t\tmethod: \"PUT\",\n\t\t\theaders: token.payload.headers,\n\t\t\tbody: processedFile,\n\t\t});\n\t\tif (!putRes.ok) throw new Error(\"Failed to upload to S3\");\n\n\t\tconst assetRes = await fetch(`${base}/media/assets`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t...Object.fromEntries(headersObj.entries()),\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfilename: processedFile.name,\n\t\t\t\toriginalName: file.name,\n\t\t\t\tmimeType: processedFile.type,\n\t\t\t\tsize: processedFile.size,\n\t\t\t\turl: token.payload.publicUrl,\n\t\t\t\tfolderId,\n\t\t\t}),\n\t\t});\n\t\tif (!assetRes.ok) {\n\t\t\tconst err = await assetRes\n\t\t\t\t.json()\n\t\t\t\t.catch(() => ({ message: assetRes.statusText }));\n\t\t\tthrow new Error(err.message ?? \"Failed to register asset\");\n\t\t}\n\t\treturn assetRes.json();\n\t}\n\n\tif (uploadMode === \"vercel-blob\") {\n\t\t// Dynamic import keeps @vercel/blob/client optional.\n\t\tconst { upload } = await import(\"@vercel/blob/client\");\n\t\tconst blob = await upload(processedFile.name, processedFile, {\n\t\t\taccess: \"public\",\n\t\t\thandleUploadUrl: `${base}/media/upload/vercel-blob`,\n\t\t\tclientPayload: JSON.stringify({\n\t\t\t\tmimeType: processedFile.type,\n\t\t\t\tsize: processedFile.size,\n\t\t\t}),\n\t\t});\n\n\t\tconst assetRes = await fetch(`${base}/media/assets`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t...Object.fromEntries(headersObj.entries()),\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfilename: processedFile.name,\n\t\t\t\toriginalName: file.name,\n\t\t\t\tmimeType: processedFile.type,\n\t\t\t\tsize: processedFile.size,\n\t\t\t\turl: blob.url,\n\t\t\t\tfolderId,\n\t\t\t}),\n\t\t});\n\t\tif (!assetRes.ok) {\n\t\t\tconst err = await assetRes\n\t\t\t\t.json()\n\t\t\t\t.catch(() => ({ message: assetRes.statusText }));\n\t\t\tthrow new Error(err.message ?? \"Failed to register asset\");\n\t\t}\n\t\treturn assetRes.json();\n\t}\n\n\tthrow new Error(`Unknown uploadMode: ${uploadMode}`);\n}\n", + "target": "src/components/btst/media/client/upload.ts" + }, { "path": "btst/media/client/utils/image-compression.ts", "type": "registry:lib", diff --git a/packages/stack/src/plugins/blog/client/overrides.ts b/packages/stack/src/plugins/blog/client/overrides.ts index 79f6ab6e..974233ea 100644 --- a/packages/stack/src/plugins/blog/client/overrides.ts +++ b/packages/stack/src/plugins/blog/client/overrides.ts @@ -60,7 +60,8 @@ export interface BlogPluginOverrides { React.ImgHTMLAttributes & Record >; /** - * Function used to upload an image and return its URL. + * Function used to upload a new image file and return its URL. + * This is separate from `imagePicker`, which selects an existing asset URL. */ uploadImage: (file: File) => Promise; /** diff --git a/packages/stack/src/plugins/cms/client/overrides.ts b/packages/stack/src/plugins/cms/client/overrides.ts index 5c1a9267..6b1b1e49 100644 --- a/packages/stack/src/plugins/cms/client/overrides.ts +++ b/packages/stack/src/plugins/cms/client/overrides.ts @@ -58,8 +58,9 @@ export interface CMSPluginOverrides { >; /** - * Function used to upload an image and return its URL. - * Used by the default "file" field component. + * Function used to upload a new image file and return its URL. + * Used by the default "file" field component when not selecting an existing + * asset via `imagePicker` or `imageInputField`. */ uploadImage?: (file: File) => Promise; diff --git a/packages/stack/src/plugins/kanban/client/overrides.ts b/packages/stack/src/plugins/kanban/client/overrides.ts index 5e8f8502..bb6385e9 100644 --- a/packages/stack/src/plugins/kanban/client/overrides.ts +++ b/packages/stack/src/plugins/kanban/client/overrides.ts @@ -74,8 +74,9 @@ export interface KanbanPluginOverrides { headers?: HeadersInit; /** - * Function used to upload an image from the task description editor and return its URL. - * Wired as the `uploader` prop of MinimalTiptapEditor — handles drag-drop image uploads. + * Function used to upload a new image file from the task description editor + * and return its URL. This is separate from `imagePicker`, which selects an + * existing asset URL. */ uploadImage?: (file: File) => Promise; diff --git a/packages/stack/src/plugins/media/client/components/index.tsx b/packages/stack/src/plugins/media/client/components/index.tsx index 0d3caf46..75f9e0cc 100644 --- a/packages/stack/src/plugins/media/client/components/index.tsx +++ b/packages/stack/src/plugins/media/client/components/index.tsx @@ -2,6 +2,5 @@ export { MediaPicker, ImageInputField, type MediaPickerProps, - uploadMediaFile, } from "./media-picker"; export { LibraryPageComponent } from "./pages/library-page"; diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx index bd9b2f0f..04a855b3 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -326,28 +326,3 @@ export function ImageInputField({
); } - -/** - * Upload a file via the media plugin's direct upload endpoint. - * @param file - The file to upload. - * @param baseURL - The base URL of the server (e.g. `https://example.com`). - * @param apiBasePath - The API base path configured for the media plugin (e.g. `/api/v2`). Defaults to `/api/data`. - */ -export async function uploadMediaFile( - file: File, - baseURL: string, - apiBasePath: string, -): Promise { - const formData = new FormData(); - formData.append("file", file); - const res = await fetch(`${baseURL}${apiBasePath}/media/upload`, { - method: "POST", - body: formData, - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ message: res.statusText })); - throw new Error(err.message ?? "Upload failed"); - } - const asset = (await res.json()) as SerializedAsset; - return asset; -} diff --git a/packages/stack/src/plugins/media/client/hooks/use-media.tsx b/packages/stack/src/plugins/media/client/hooks/use-media.tsx index f7b11284..ab345cb0 100644 --- a/packages/stack/src/plugins/media/client/hooks/use-media.tsx +++ b/packages/stack/src/plugins/media/client/hooks/use-media.tsx @@ -12,7 +12,7 @@ import type { MediaPluginOverrides } from "../overrides"; import { createMediaQueryKeys } from "../../query-keys"; import type { AssetListParams } from "../../api/getters"; import type { SerializedAsset, SerializedFolder } from "../../types"; -import { compressImage } from "../utils/image-compression"; +import { uploadAsset } from "../upload"; function useMediaConfig() { return usePluginOverrides("media"); @@ -99,139 +99,17 @@ export function useUploadAsset() { }: { file: File; folderId?: string; - }): Promise => { - const processedFile = - imageCompression === false - ? file - : await compressImage( - file, - imageCompression ?? { - maxWidth: 2048, - maxHeight: 2048, - quality: 0.85, - }, - ); - - const base = `${apiBaseURL}${apiBasePath}`; - const headersObj = new Headers(headers as HeadersInit | undefined); - - if (uploadMode === "direct") { - const formData = new FormData(); - formData.append("file", processedFile); - if (folderId) formData.append("folderId", folderId); - const res = await fetch(`${base}/media/upload`, { - method: "POST", - headers: headersObj, - body: formData, - }); - if (!res.ok) { - const err = await res - .json() - .catch(() => ({ message: res.statusText })); - throw new Error(err.message ?? "Upload failed"); - } - return res.json(); - } - - if (uploadMode === "s3") { - const tokenRes = await fetch(`${base}/media/upload/token`, { - method: "POST", - headers: { - ...Object.fromEntries(headersObj.entries()), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - filename: processedFile.name, - mimeType: processedFile.type, - size: processedFile.size, - folderId, - }), - }); - if (!tokenRes.ok) { - const err = await tokenRes - .json() - .catch(() => ({ message: tokenRes.statusText })); - throw new Error(err.message ?? "Failed to get upload token"); - } - const token = (await tokenRes.json()) as { - type: "presigned-url"; - payload: { - uploadUrl: string; - publicUrl: string; - key: string; - method: "PUT"; - headers: Record; - }; - }; - - const putRes = await fetch(token.payload.uploadUrl, { - method: "PUT", - headers: token.payload.headers, - body: processedFile, - }); - if (!putRes.ok) throw new Error("Failed to upload to S3"); - - const assetRes = await fetch(`${base}/media/assets`, { - method: "POST", - headers: { - ...Object.fromEntries(headersObj.entries()), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - filename: processedFile.name, - originalName: file.name, - mimeType: processedFile.type, - size: processedFile.size, - url: token.payload.publicUrl, - folderId, - }), - }); - if (!assetRes.ok) { - const err = await assetRes - .json() - .catch(() => ({ message: assetRes.statusText })); - throw new Error(err.message ?? "Failed to register asset"); - } - return assetRes.json(); - } - - if (uploadMode === "vercel-blob") { - // Dynamic import keeps @vercel/blob/client optional - const { upload } = await import("@vercel/blob/client"); - const blob = await upload(processedFile.name, processedFile, { - access: "public", - handleUploadUrl: `${base}/media/upload/vercel-blob`, - clientPayload: JSON.stringify({ - mimeType: processedFile.type, - size: processedFile.size, - }), - }); - const assetRes = await fetch(`${base}/media/assets`, { - method: "POST", - headers: { - ...Object.fromEntries(headersObj.entries()), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - filename: processedFile.name, - originalName: file.name, - mimeType: processedFile.type, - size: processedFile.size, - url: blob.url, - folderId, - }), - }); - if (!assetRes.ok) { - const err = await assetRes - .json() - .catch(() => ({ message: assetRes.statusText })); - throw new Error(err.message ?? "Failed to register asset"); - } - return assetRes.json(); - } - - throw new Error(`Unknown uploadMode: ${uploadMode}`); - }, + }): Promise => + uploadAsset( + { + apiBaseURL, + apiBasePath, + headers, + uploadMode, + imageCompression, + }, + { file, folderId }, + ), onSuccess: () => { reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] }); }, diff --git a/packages/stack/src/plugins/media/client/index.ts b/packages/stack/src/plugins/media/client/index.ts index 4cb8ca40..438cabcc 100644 --- a/packages/stack/src/plugins/media/client/index.ts +++ b/packages/stack/src/plugins/media/client/index.ts @@ -1,2 +1,4 @@ export { mediaClientPlugin } from "./plugin"; export type { MediaPluginOverrides, MediaUploadMode } from "./overrides"; +export { uploadAsset } from "./upload"; +export type { MediaUploadClientConfig, UploadAssetInput } from "./upload"; diff --git a/packages/stack/src/plugins/media/client/upload.ts b/packages/stack/src/plugins/media/client/upload.ts new file mode 100644 index 00000000..89d70ec0 --- /dev/null +++ b/packages/stack/src/plugins/media/client/upload.ts @@ -0,0 +1,171 @@ +"use client"; + +import type { SerializedAsset } from "../types"; +import type { MediaPluginOverrides } from "./overrides"; +import { compressImage } from "./utils/image-compression"; + +export type MediaUploadClientConfig = Pick< + MediaPluginOverrides, + "apiBaseURL" | "apiBasePath" | "headers" | "uploadMode" | "imageCompression" +>; + +export interface UploadAssetInput { + file: File; + folderId?: string; +} + +const DEFAULT_IMAGE_COMPRESSION = { + maxWidth: 2048, + maxHeight: 2048, + quality: 0.85, +} as const; + +/** + * Upload an asset using the media plugin's configured storage mode. + * + * Use this in non-React contexts like editor `uploadImage` callbacks. React + * components should usually prefer `useUploadAsset()`, which wraps this helper + * and handles cache invalidation. + */ +export async function uploadAsset( + config: MediaUploadClientConfig, + input: UploadAssetInput, +): Promise { + const { + apiBaseURL, + apiBasePath, + headers, + uploadMode = "direct", + imageCompression, + } = config; + const { file, folderId } = input; + + const processedFile = + imageCompression === false + ? file + : await compressImage( + file, + imageCompression ?? DEFAULT_IMAGE_COMPRESSION, + ); + + const base = `${apiBaseURL}${apiBasePath}`; + const headersObj = new Headers(headers as HeadersInit | undefined); + + if (uploadMode === "direct") { + const formData = new FormData(); + formData.append("file", processedFile); + if (folderId) formData.append("folderId", folderId); + + const res = await fetch(`${base}/media/upload`, { + method: "POST", + headers: headersObj, + body: formData, + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? "Upload failed"); + } + return res.json(); + } + + if (uploadMode === "s3") { + const tokenRes = await fetch(`${base}/media/upload/token`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: processedFile.name, + mimeType: processedFile.type, + size: processedFile.size, + folderId, + }), + }); + if (!tokenRes.ok) { + const err = await tokenRes + .json() + .catch(() => ({ message: tokenRes.statusText })); + throw new Error(err.message ?? "Failed to get upload token"); + } + + const token = (await tokenRes.json()) as { + type: "presigned-url"; + payload: { + uploadUrl: string; + publicUrl: string; + key: string; + method: "PUT"; + headers: Record; + }; + }; + + const putRes = await fetch(token.payload.uploadUrl, { + method: "PUT", + headers: token.payload.headers, + body: processedFile, + }); + if (!putRes.ok) throw new Error("Failed to upload to S3"); + + const assetRes = await fetch(`${base}/media/assets`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: processedFile.name, + originalName: file.name, + mimeType: processedFile.type, + size: processedFile.size, + url: token.payload.publicUrl, + folderId, + }), + }); + if (!assetRes.ok) { + const err = await assetRes + .json() + .catch(() => ({ message: assetRes.statusText })); + throw new Error(err.message ?? "Failed to register asset"); + } + return assetRes.json(); + } + + if (uploadMode === "vercel-blob") { + // Dynamic import keeps @vercel/blob/client optional. + const { upload } = await import("@vercel/blob/client"); + const blob = await upload(processedFile.name, processedFile, { + access: "public", + handleUploadUrl: `${base}/media/upload/vercel-blob`, + clientPayload: JSON.stringify({ + mimeType: processedFile.type, + size: processedFile.size, + }), + }); + + const assetRes = await fetch(`${base}/media/assets`, { + method: "POST", + headers: { + ...Object.fromEntries(headersObj.entries()), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: processedFile.name, + originalName: file.name, + mimeType: processedFile.type, + size: processedFile.size, + url: blob.url, + folderId, + }), + }); + if (!assetRes.ok) { + const err = await assetRes + .json() + .catch(() => ({ message: assetRes.statusText })); + throw new Error(err.message ?? "Failed to register asset"); + } + return assetRes.json(); + } + + throw new Error(`Unknown uploadMode: ${uploadMode}`); +} From 569090238d47b91ac6f8d5a22d701ff68c5dee8c Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 16:06:33 -0400 Subject: [PATCH 20/29] feat: enhance AssetCard and MediaPicker components with new features including URL copying, asset previewing, and improved delete handling --- packages/stack/registry/btst-media.json | 21 +- packages/stack/registry/registry.json | 1 + .../components/media-picker/asset-card.tsx | 100 ++++-- .../media-picker/asset-preview-button.tsx | 67 ++++ .../components/media-picker/browse-tab.tsx | 27 +- .../components/media-picker/folder-tree.tsx | 4 +- .../client/components/media-picker/index.tsx | 53 ++- .../components/media-picker/upload-tab.tsx | 2 +- .../components/media-picker/url-tab.tsx | 9 +- .../pages/library-page.internal.tsx | 319 ++---------------- 10 files changed, 242 insertions(+), 361 deletions(-) create mode 100644 packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.tsx diff --git a/packages/stack/registry/btst-media.json b/packages/stack/registry/btst-media.json index 30c2be12..0272161e 100644 --- a/packages/stack/registry/btst-media.json +++ b/packages/stack/registry/btst-media.json @@ -10,6 +10,7 @@ ], "registryDependencies": [ "button", + "dialog", "input", "popover", "tabs" @@ -30,37 +31,43 @@ { "path": "btst/media/client/components/media-picker/asset-card.tsx", "type": "registry:component", - "content": "import { useDeleteAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { cn } from \"@/lib/utils\";\nimport { File, Check, Trash2 } from \"lucide-react\";\nimport { isImage, formatBytes } from \"./utils\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\n\nexport function AssetCard({\n\tasset,\n\tselected,\n\tonToggle,\n}: {\n\tasset: SerializedAsset;\n\tselected: boolean;\n\tonToggle: () => void;\n}) {\n\tconst { mutateAsync: deleteAsset } = useDeleteAsset();\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\treturn (\n\t\t (e.key === \"Enter\" || e.key === \" \") && onToggle()}\n\t\t\tclassName={cn(\n\t\t\t\t\"group relative cursor-pointer rounded-md border bg-muted/30 p-1 transition-all hover:border-ring hover:shadow-sm\",\n\t\t\t\tselected && \"border-ring ring-1 ring-ring\",\n\t\t\t)}\n\t\t>\n\t\t\t{/* Thumbnail */}\n\t\t\t
\n\t\t\t\t{isImage(asset.mimeType) ? (\n\t\t\t\t\tImageComponent ? (\n\t\t\t\t\t\t\n\t\t\t\t\t) : (\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t{/* Name + size */}\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{asset.originalName}\n\t\t\t\t

\n\t\t\t\t

\n\t\t\t\t\t{formatBytes(asset.size)}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t{/* Selection indicator */}\n\t\t\t{selected && (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t{/* Delete button (on hover) */}\n\t\t\t {\n\t\t\t\t\te.stopPropagation();\n\t\t\t\t\tif (confirm(`Delete \"${asset.originalName}\"?`)) {\n\t\t\t\t\t\tdeleteAsset(asset.id).catch(console.error);\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t\tclassName=\"absolute left-1 top-1 hidden rounded bg-destructive/80 p-0.5 text-white group-hover:flex\"\n\t\t\t>\n\t\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n", + "content": "import { useDeleteAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { cn } from \"@/lib/utils\";\nimport { File, Check, Copy, Trash2 } from \"lucide-react\";\nimport { isImage, formatBytes } from \"./utils\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { AssetPreviewButton } from \"./asset-preview-button\";\nimport { toast } from \"sonner\";\n\nexport function AssetCard({\n\tasset,\n\tonToggle,\n\tselected = false,\n\tonDelete,\n\tapiBaseURL,\n}: {\n\tasset: SerializedAsset;\n\tselected?: boolean;\n\tonToggle?: () => void;\n\tonDelete?: (id: string) => void | Promise;\n\tapiBaseURL?: string;\n}) {\n\tconst { mutateAsync: deleteAsset } = useDeleteAsset();\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\tconst imageAsset = isImage(asset.mimeType);\n\tconst selectable = typeof onToggle === \"function\";\n\n\tconst copyUrl = () => {\n\t\tlet fullUrl: string;\n\t\ttry {\n\t\t\tfullUrl = new URL(asset.url, apiBaseURL).href;\n\t\t} catch {\n\t\t\tfullUrl = asset.url;\n\t\t}\n\t\tnavigator.clipboard\n\t\t\t.writeText(fullUrl)\n\t\t\t.then(() => toast.success(\"URL copied\"));\n\t};\n\n\tconst handleDelete = () => {\n\t\tif (onDelete) {\n\t\t\treturn onDelete(asset.id);\n\t\t}\n\n\t\tif (confirm(`Delete \"${asset.originalName}\"?`)) {\n\t\t\treturn deleteAsset(asset.id).catch(console.error);\n\t\t}\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (selectable && (e.key === \"Enter\" || e.key === \" \")) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tonToggle();\n\t\t\t\t}\n\t\t\t}}\n\t\t\tclassName={cn(\n\t\t\t\t\"group relative cursor-pointer rounded-md border bg-muted/30 p-1 transition-all hover:border-ring hover:shadow-sm\",\n\t\t\t\t!selectable && \"cursor-default\",\n\t\t\t\tselected && \"border-ring ring-1 ring-ring\",\n\t\t\t)}\n\t\t>\n\t\t\t{/* Thumbnail */}\n\t\t\t
\n\t\t\t\t{imageAsset ? (\n\t\t\t\t\tImageComponent ? (\n\t\t\t\t\t\t\n\t\t\t\t\t) : (\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t{/* Name + size */}\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{asset.originalName}\n\t\t\t\t

\n\t\t\t\t

\n\t\t\t\t\t{formatBytes(asset.size)}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t{/* Selection indicator */}\n\t\t\t{selected && (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/asset-card.tsx" }, + { + "path": "btst/media/client/components/media-picker/asset-preview-button.tsx", + "type": "registry:component", + "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n\tDialog,\n\tDialogClose,\n\tDialogContent,\n\tDialogHeader,\n\tDialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Eye, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\n\nexport function AssetPreviewButton({\n\tasset,\n\tclassName,\n}: {\n\tasset: SerializedAsset;\n\tclassName: string;\n}) {\n\tconst [open, setOpen] = useState(false);\n\n\treturn (\n\t\t<>\n\t\t\t {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\tsetOpen(true);\n\t\t\t\t}}\n\t\t\t\tclassName={className}\n\t\t\t>\n\t\t\t\t\n\t\t\t\n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{asset.alt || asset.originalName}\n\t\t\t\t\t\n\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t\n\t);\n}\n", + "target": "src/components/btst/media/client/components/media-picker/asset-preview-button.tsx" + }, { "path": "btst/media/client/components/media-picker/browse-tab.tsx", "type": "registry:component", - "content": "import { useState, useRef } from \"react\";\nimport { useAssets } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Search, X, Image } from \"lucide-react\";\nimport { AssetCard } from \"./asset-card\";\nimport { matchesAccept } from \"./utils\";\n\nexport function BrowseTab({\n\tfolderId,\n\tselected,\n\tmultiple,\n\taccept,\n\tonToggle,\n}: {\n\tfolderId: string | null;\n\tselected: SerializedAsset[];\n\tmultiple: boolean;\n\taccept?: string[];\n\tonToggle: (asset: SerializedAsset) => void;\n}) {\n\tconst [search, setSearch] = useState(\"\");\n\tconst [debouncedSearch, setDebouncedSearch] = useState(\"\");\n\tconst debounceRef = useRef | null>(null);\n\n\tconst handleSearch = (v: string) => {\n\t\tsetSearch(v);\n\t\tif (debounceRef.current) clearTimeout(debounceRef.current);\n\t\tdebounceRef.current = setTimeout(() => setDebouncedSearch(v), 300);\n\t};\n\n\tconst { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =\n\t\tuseAssets({\n\t\t\tfolderId: folderId ?? undefined,\n\t\t\tquery: debouncedSearch || undefined,\n\t\t\tlimit: 40,\n\t\t});\n\n\tconst allAssets = data?.pages.flatMap((p) => p.items) ?? [];\n\tconst filtered = accept\n\t\t? allAssets.filter((a) => matchesAccept(a.mimeType, accept))\n\t\t: allAssets;\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t handleSearch(e.target.value)}\n\t\t\t\t\tplaceholder=\"Search files…\"\n\t\t\t\t\tclassName=\"h-8 pl-7 text-sm\"\n\t\t\t\t/>\n\t\t\t\t{search && (\n\t\t\t\t\t {\n\t\t\t\t\t\t\tsetSearch(\"\");\n\t\t\t\t\t\t\tsetDebouncedSearch(\"\");\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t{isLoading ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t) : filtered.length === 0 ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

No files found

\n\t\t\t\t
\n\t\t\t) : (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{filtered.map((asset) => (\n\t\t\t\t\t\t\t s.id === asset.id)}\n\t\t\t\t\t\t\t\tonToggle={() => onToggle(asset)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t
\n\t\t\t\t\t{hasNextPage && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t fetchNextPage()}\n\t\t\t\t\t\t\t\tdisabled={isFetchingNextPage}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{isFetchingNextPage ? (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\tLoad more\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n", + "content": "import { useState, useRef } from \"react\";\nimport { useAssets } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Search, X, Image } from \"lucide-react\";\nimport { AssetCard } from \"./asset-card\";\nimport { matchesAccept } from \"./utils\";\n\nexport function BrowseTab({\n\tfolderId,\n\tselected = [],\n\taccept,\n\tonToggle,\n\tonDelete,\n\tapiBaseURL,\n\temptyMessage = \"No files found\",\n}: {\n\tfolderId: string | null;\n\tselected?: SerializedAsset[];\n\taccept?: string[];\n\tonToggle?: (asset: SerializedAsset) => void;\n\tonDelete?: (id: string) => void | Promise;\n\tapiBaseURL?: string;\n\temptyMessage?: string;\n}) {\n\tconst [search, setSearch] = useState(\"\");\n\tconst [debouncedSearch, setDebouncedSearch] = useState(\"\");\n\tconst debounceRef = useRef | null>(null);\n\tconst selectable = typeof onToggle === \"function\";\n\n\tconst handleSearch = (v: string) => {\n\t\tsetSearch(v);\n\t\tif (debounceRef.current) clearTimeout(debounceRef.current);\n\t\tdebounceRef.current = setTimeout(() => setDebouncedSearch(v), 300);\n\t};\n\n\tconst { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =\n\t\tuseAssets({\n\t\t\tfolderId: folderId ?? undefined,\n\t\t\tquery: debouncedSearch || undefined,\n\t\t\tlimit: 40,\n\t\t});\n\n\tconst allAssets = data?.pages.flatMap((p) => p.items) ?? [];\n\tconst filtered = accept\n\t\t? allAssets.filter((a) => matchesAccept(a.mimeType, accept))\n\t\t: allAssets;\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t handleSearch(e.target.value)}\n\t\t\t\t\tplaceholder=\"Search files…\"\n\t\t\t\t\tclassName=\"h-8 pl-7 text-sm\"\n\t\t\t\t/>\n\t\t\t\t{search && (\n\t\t\t\t\t {\n\t\t\t\t\t\t\tsetSearch(\"\");\n\t\t\t\t\t\t\tsetDebouncedSearch(\"\");\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\n\t\t\t{isLoading ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t) : filtered.length === 0 ? (\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

{emptyMessage}

\n\t\t\t\t
\n\t\t\t) : (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{filtered.map((asset) => (\n\t\t\t\t\t\t\t s.id === asset.id)}\n\t\t\t\t\t\t\t\tonToggle={selectable ? () => onToggle(asset) : undefined}\n\t\t\t\t\t\t\t\tonDelete={onDelete}\n\t\t\t\t\t\t\t\tapiBaseURL={apiBaseURL}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t
\n\t\t\t\t\t{hasNextPage && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t fetchNextPage()}\n\t\t\t\t\t\t\t\tdisabled={isFetchingNextPage}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{isFetchingNextPage ? (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\tLoad more\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/browse-tab.tsx" }, { "path": "btst/media/client/components/media-picker/folder-tree.tsx", "type": "registry:component", - "content": "import { useState } from \"react\";\nimport {\n\tuseFolders,\n\tuseCreateFolder,\n\tuseDeleteFolder,\n} from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedFolder } from \"../../../types\";\nimport { FolderPlus } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Check, Folder, Trash2, ChevronRight, FolderOpen } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport function FolderTree({\n\tselectedId,\n\tonSelect,\n}: {\n\tselectedId: string | null;\n\tonSelect: (id: string | null) => void;\n}) {\n\tconst { data: rootFoldersRaw = [] } = useFolders(null);\n\tconst rootFolders =\n\t\trootFoldersRaw as import(\"../../../types\").SerializedFolder[];\n\tconst [newFolderName, setNewFolderName] = useState(\"\");\n\tconst [isCreating, setIsCreating] = useState(false);\n\tconst { mutateAsync: createFolder } = useCreateFolder();\n\tconst { mutateAsync: deleteFolder } = useDeleteFolder();\n\n\tconst handleCreateFolder = async () => {\n\t\tconst name = newFolderName.trim();\n\t\tif (!name) return;\n\t\ttry {\n\t\t\tawait createFolder({ name, parentId: selectedId ?? undefined });\n\t\t\tsetNewFolderName(\"\");\n\t\t\tsetIsCreating(false);\n\t\t} catch (err) {\n\t\t\tconsole.error(\"[btst/media] Failed to create folder\", err);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tFolders\n\t\t\t\t\n\t\t\t\t setIsCreating((v) => !v)}\n\t\t\t\t\tclassName=\"rounded p-0.5 hover:bg-muted\"\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t{isCreating && (\n\t\t\t\t
\n\t\t\t\t\t setNewFolderName(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"Folder name\"\n\t\t\t\t\t\tclassName=\"h-6 text-xs\"\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif (e.key === \"Enter\") void handleCreateFolder();\n\t\t\t\t\t\t\tif (e.key === \"Escape\") setIsCreating(false);\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t{/* All assets (root) */}\n\t\t\t\t onSelect(null)}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\t\tselectedId === null && \"bg-muted font-medium\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tAll files\n\t\t\t\t\n\n\t\t\t\t{rootFolders.map((folder) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t
\n\n\t\t\t{selectedId && (\n\t\t\t\t
\n\t\t\t\t\t {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tconfirm(\"Delete this folder? Assets inside will be unaffected.\")\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tawait deleteFolder(selectedId);\n\t\t\t\t\t\t\t\t\tonSelect(null);\n\t\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"[btst/media] Failed to delete folder\", err);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 text-xs text-destructive hover:underline\"\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\tDelete folder\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n\nexport function FolderTreeItem({\n\tfolder,\n\tselectedId,\n\tonSelect,\n\tdepth = 0,\n}: {\n\tfolder: SerializedFolder;\n\tselectedId: string | null;\n\tonSelect: (id: string | null) => void;\n\tdepth?: number;\n}) {\n\tconst [expanded, setExpanded] = useState(false);\n\tconst { data: children = [] } = useFolders(folder.id);\n\n\treturn (\n\t\t
\n\t\t\t {\n\t\t\t\t\tonSelect(folder.id);\n\t\t\t\t\tsetExpanded((v) => !v);\n\t\t\t\t}}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\tselectedId === folder.id && \"bg-muted font-medium\",\n\t\t\t\t)}\n\t\t\t\tstyle={{ paddingLeft: `${8 + depth * 12}px` }}\n\t\t\t>\n\t\t\t\t{children.length > 0 ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{expanded ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{folder.name}\n\t\t\t\n\t\t\t{expanded &&\n\t\t\t\tchildren.map((child) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t
\n\t);\n}\n", + "content": "import { useState } from \"react\";\nimport {\n\tuseFolders,\n\tuseCreateFolder,\n\tuseDeleteFolder,\n} from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedFolder } from \"../../../types\";\nimport { FolderPlus } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Check, Folder, Trash2, ChevronRight, FolderOpen } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport function FolderTree({\n\tselectedId,\n\tonSelect,\n}: {\n\tselectedId: string | null;\n\tonSelect: (id: string | null) => void;\n}) {\n\tconst { data: rootFoldersRaw = [] } = useFolders(null);\n\tconst rootFolders =\n\t\trootFoldersRaw as import(\"../../../types\").SerializedFolder[];\n\tconst [newFolderName, setNewFolderName] = useState(\"\");\n\tconst [isCreating, setIsCreating] = useState(false);\n\tconst { mutateAsync: createFolder } = useCreateFolder();\n\tconst { mutateAsync: deleteFolder } = useDeleteFolder();\n\n\tconst handleCreateFolder = async () => {\n\t\tconst name = newFolderName.trim();\n\t\tif (!name) return;\n\t\ttry {\n\t\t\tawait createFolder({ name, parentId: selectedId ?? undefined });\n\t\t\tsetNewFolderName(\"\");\n\t\t\tsetIsCreating(false);\n\t\t} catch (err) {\n\t\t\tconsole.error(\"[btst/media] Failed to create folder\", err);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tFolders\n\t\t\t\t\n\t\t\t\t setIsCreating((v) => !v)}\n\t\t\t\t\tclassName=\"rounded p-0.5 hover:bg-muted\"\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t{isCreating && (\n\t\t\t\t
\n\t\t\t\t\t setNewFolderName(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"Folder name\"\n\t\t\t\t\t\tclassName=\"h-6 text-xs\"\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif (e.key === \"Enter\") void handleCreateFolder();\n\t\t\t\t\t\t\tif (e.key === \"Escape\") setIsCreating(false);\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\n\t\t\t
\n\t\t\t\t{/* All assets (root) */}\n\t\t\t\t onSelect(null)}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\t\tselectedId === null && \"bg-muted font-medium\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tAll files\n\t\t\t\t\n\n\t\t\t\t{rootFolders.map((folder) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t
\n\n\t\t\t{selectedId && (\n\t\t\t\t
\n\t\t\t\t\t {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tconfirm(\"Delete this folder? Assets inside will be unaffected.\")\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tawait deleteFolder(selectedId);\n\t\t\t\t\t\t\t\t\tonSelect(null);\n\t\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"[btst/media] Failed to delete folder\", err);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 text-xs text-destructive hover:underline\"\n\t\t\t\t\t>\n\t\t\t\t\t\t\n\t\t\t\t\t\tDelete folder\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t
\n\t);\n}\n\nexport function FolderTreeItem({\n\tfolder,\n\tselectedId,\n\tonSelect,\n\tdepth = 0,\n}: {\n\tfolder: SerializedFolder;\n\tselectedId: string | null;\n\tonSelect: (id: string | null) => void;\n\tdepth?: number;\n}) {\n\tconst [expanded, setExpanded] = useState(false);\n\tconst { data: children = [] } = useFolders(folder.id);\n\n\treturn (\n\t\t
\n\t\t\t {\n\t\t\t\t\tonSelect(folder.id);\n\t\t\t\t\tsetExpanded((v) => !v);\n\t\t\t\t}}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\tselectedId === folder.id && \"bg-muted font-medium\",\n\t\t\t\t)}\n\t\t\t\tstyle={{ paddingLeft: `${8 + depth * 12}px` }}\n\t\t\t>\n\t\t\t\t{children.length > 0 ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{expanded ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t{folder.name}\n\t\t\t\n\t\t\t{expanded &&\n\t\t\t\tchildren.map((child) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/folder-tree.tsx" }, { "path": "btst/media/client/components/media-picker/index.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/index.tsx" }, { "path": "btst/media/client/components/media-picker/upload-tab.tsx", "type": "registry:component", - "content": "import { useState, useCallback, useRef } from \"react\";\nimport { useUploadAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Upload } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { matchesAccept } from \"./utils\";\n\nexport function UploadTab({\n\tfolderId,\n\taccept,\n\tonUploaded,\n}: {\n\tfolderId: string | null;\n\taccept?: string[];\n\tonUploaded: (asset: SerializedAsset) => void;\n}) {\n\tconst [dragging, setDragging] = useState(false);\n\tconst [uploading, setUploading] = useState(false);\n\tconst [error, setError] = useState(null);\n\tconst fileInputRef = useRef(null);\n\tconst { mutateAsync: uploadAsset } = useUploadAsset();\n\n\tconst acceptAttr = accept?.join(\",\") ?? undefined;\n\n\tconst handleFiles = useCallback(\n\t\tasync (files: FileList | File[]) => {\n\t\t\tconst fileArr = Array.from(files);\n\t\t\tif (fileArr.length === 0) return;\n\t\t\tsetError(null);\n\t\t\tsetUploading(true);\n\t\t\ttry {\n\t\t\t\tfor (const file of fileArr) {\n\t\t\t\t\tif (accept && !matchesAccept(file.type, accept)) {\n\t\t\t\t\t\tsetError(`File type ${file.type} is not accepted.`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst asset = await uploadAsset({\n\t\t\t\t\t\tfile,\n\t\t\t\t\t\tfolderId: folderId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tonUploaded(asset);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tsetError(err instanceof Error ? err.message : \"Upload failed\");\n\t\t\t} finally {\n\t\t\t\tsetUploading(false);\n\t\t\t}\n\t\t},\n\t\t[accept, folderId, uploadAsset, onUploaded],\n\t);\n\n\treturn (\n\t\t
\n\t\t\t {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(true);\n\t\t\t\t}}\n\t\t\t\tonDragLeave={() => setDragging(false)}\n\t\t\t\tonDrop={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(false);\n\t\t\t\t\tvoid handleFiles(e.dataTransfer.files);\n\t\t\t\t}}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed transition-colors\",\n\t\t\t\t\tdragging ? \"border-ring bg-ring/5\" : \"border-muted-foreground/30\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{uploading ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t

Uploading…

\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t

Drop files here

\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\tor click to browse\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tChoose files\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t\t{error &&

{error}

}\n\t\t\t e.target.files && handleFiles(e.target.files)}\n\t\t\t/>\n\t\t
\n\t);\n}\n", + "content": "import { useState, useCallback, useRef } from \"react\";\nimport { useUploadAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Upload } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { matchesAccept } from \"./utils\";\n\nexport function UploadTab({\n\tfolderId,\n\taccept,\n\tonUploaded,\n}: {\n\tfolderId: string | null;\n\taccept?: string[];\n\tonUploaded: (asset: SerializedAsset) => void;\n}) {\n\tconst [dragging, setDragging] = useState(false);\n\tconst [uploading, setUploading] = useState(false);\n\tconst [error, setError] = useState(null);\n\tconst fileInputRef = useRef(null);\n\tconst { mutateAsync: uploadAsset } = useUploadAsset();\n\n\tconst acceptAttr = accept?.join(\",\") ?? undefined;\n\n\tconst handleFiles = useCallback(\n\t\tasync (files: FileList | File[]) => {\n\t\t\tconst fileArr = Array.from(files);\n\t\t\tif (fileArr.length === 0) return;\n\t\t\tsetError(null);\n\t\t\tsetUploading(true);\n\t\t\ttry {\n\t\t\t\tfor (const file of fileArr) {\n\t\t\t\t\tif (accept && !matchesAccept(file.type, accept)) {\n\t\t\t\t\t\tsetError(`File type ${file.type} is not accepted.`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst asset = await uploadAsset({\n\t\t\t\t\t\tfile,\n\t\t\t\t\t\tfolderId: folderId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tonUploaded(asset);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tsetError(err instanceof Error ? err.message : \"Upload failed\");\n\t\t\t} finally {\n\t\t\t\tsetUploading(false);\n\t\t\t}\n\t\t},\n\t\t[accept, folderId, uploadAsset, onUploaded],\n\t);\n\n\treturn (\n\t\t
\n\t\t\t {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(true);\n\t\t\t\t}}\n\t\t\t\tonDragLeave={() => setDragging(false)}\n\t\t\t\tonDrop={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(false);\n\t\t\t\t\tvoid handleFiles(e.dataTransfer.files);\n\t\t\t\t}}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed px-4 py-6 text-center transition-colors sm:px-6\",\n\t\t\t\t\tdragging ? \"border-ring bg-ring/5\" : \"border-muted-foreground/30\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{uploading ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t

Uploading…

\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t

Drop files here

\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\tor click to browse\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tChoose files\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t\t{error &&

{error}

}\n\t\t\t e.target.files && handleFiles(e.target.files)}\n\t\t\t/>\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/upload-tab.tsx" }, { "path": "btst/media/client/components/media-picker/url-tab.tsx", "type": "registry:component", - "content": "import { useState } from \"react\";\nimport { useRegisterAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Check } from \"lucide-react\";\n\nexport function UrlTab({\n\tfolderId,\n\tonRegistered,\n}: {\n\tfolderId: string | null;\n\tonRegistered: (asset: SerializedAsset) => void;\n}) {\n\tconst [url, setUrl] = useState(\"\");\n\tconst [error, setError] = useState(null);\n\tconst { mutateAsync: registerAsset, isPending } = useRegisterAsset();\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\t\tconst trimmed = url.trim();\n\t\tif (!trimmed) return;\n\t\ttry {\n\t\t\tconst filename = trimmed.split(\"/\").pop() ?? \"asset\";\n\t\t\tconst asset = await registerAsset({\n\t\t\t\turl: trimmed,\n\t\t\t\tfilename,\n\t\t\t\tfolderId: folderId ?? undefined,\n\t\t\t});\n\t\t\tsetUrl(\"\");\n\t\t\tonRegistered(asset);\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"Failed to register URL\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t

\n\t\t\t\tPaste a public URL to register it as an asset without uploading a file.\n\t\t\t

\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t setUrl(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"https://example.com/image.png\"\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tdata-testid=\"media-url-input\"\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t{error &&

{error}

}\n\t\t\t
\n\t\t
\n\t);\n}\n", + "content": "import { useState } from \"react\";\nimport { useRegisterAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, Check } from \"lucide-react\";\n\nexport function UrlTab({\n\tfolderId,\n\tonRegistered,\n}: {\n\tfolderId: string | null;\n\tonRegistered: (asset: SerializedAsset) => void;\n}) {\n\tconst [url, setUrl] = useState(\"\");\n\tconst [error, setError] = useState(null);\n\tconst { mutateAsync: registerAsset, isPending } = useRegisterAsset();\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault();\n\t\tsetError(null);\n\t\tconst trimmed = url.trim();\n\t\tif (!trimmed) return;\n\t\ttry {\n\t\t\tconst filename = trimmed.split(\"/\").pop() ?? \"asset\";\n\t\t\tconst asset = await registerAsset({\n\t\t\t\turl: trimmed,\n\t\t\t\tfilename,\n\t\t\t\tfolderId: folderId ?? undefined,\n\t\t\t});\n\t\t\tsetUrl(\"\");\n\t\t\tonRegistered(asset);\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : \"Failed to register URL\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t

\n\t\t\t\tPaste a public URL to register it as an asset without uploading a file.\n\t\t\t

\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t setUrl(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"https://example.com/image.png\"\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tdata-testid=\"media-url-input\"\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\t\t{isPending ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tUse URL\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t{error &&

{error}

}\n\t\t\t
\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/url-tab.tsx" }, { @@ -72,7 +79,7 @@ { "path": "btst/media/client/components/pages/library-page.internal.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { useState, useCallback, useRef, type ComponentType } from \"react\";\nimport {\n\tuseAssets,\n\tuseDeleteAsset,\n\tuseFolders,\n\tuseUploadAsset,\n\tuseCreateFolder,\n} from \"@btst/stack/plugins/media/client/hooks\";\nimport type { SerializedAsset, SerializedFolder } from \"../../../types\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n\tFolder,\n\tImage,\n\tFile as FileIcon,\n\tUpload,\n\tTrash2,\n\tSearch,\n\tX,\n\tLoader2,\n\tFolderPlus,\n\tCheck,\n\tCopy,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { toast } from \"sonner\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { formatBytes } from \"../media-picker/utils\";\nimport { FolderTreeItem } from \"../media-picker/folder-tree\";\n\nfunction LibrarySidebar({\n\tselectedFolder,\n\tonSelect,\n}: {\n\tselectedFolder: string | null;\n\tonSelect: (id: string | null) => void;\n}) {\n\tconst { data: rootFoldersRaw = [] } = useFolders(null);\n\tconst rootFolders = rootFoldersRaw as SerializedFolder[];\n\tconst [newFolderName, setNewFolderName] = useState(\"\");\n\tconst [isCreating, setIsCreating] = useState(false);\n\tconst { mutateAsync: createFolder, isPending } = useCreateFolder();\n\n\tconst handleCreate = async () => {\n\t\tconst name = newFolderName.trim();\n\t\tif (!name) return;\n\t\ttry {\n\t\t\tawait createFolder({ name, parentId: selectedFolder ?? undefined });\n\t\t\tsetNewFolderName(\"\");\n\t\t\tsetIsCreating(false);\n\t\t\ttoast.success(\"Folder created\");\n\t\t} catch (err) {\n\t\t\ttoast.error(\n\t\t\t\terr instanceof Error ? err.message : \"Failed to create folder\",\n\t\t\t);\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tFolders\n\t\t\t\t\n\t\t\t\t setIsCreating((v) => !v)}\n\t\t\t\t\ttitle=\"New folder\"\n\t\t\t\t\tclassName=\"rounded p-0.5 hover:bg-muted\"\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t{isCreating && (\n\t\t\t\t
\n\t\t\t\t\t setNewFolderName(e.target.value)}\n\t\t\t\t\t\tplaceholder=\"Folder name\"\n\t\t\t\t\t\tclassName=\"h-7 text-xs\"\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif (e.key === \"Enter\") void handleCreate();\n\t\t\t\t\t\t\tif (e.key === \"Escape\") setIsCreating(false);\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t\t
\n\t\t\t\t onSelect(null)}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm hover:bg-muted\",\n\t\t\t\t\t\tselectedFolder === null && \"bg-muted font-medium\",\n\t\t\t\t\t)}\n\t\t\t\t\tstyle={{ paddingLeft: \"8px\" }}\n\t\t\t\t>\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\tAll files\n\t\t\t\t\n\t\t\t\t{rootFolders.map((folder) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t
\n\t\t
\n\t);\n}\n\nfunction AssetCard({\n\tasset,\n\tonDelete,\n\tImageComponent,\n\tapiBaseURL,\n}: {\n\tasset: SerializedAsset;\n\tonDelete: (id: string) => void;\n\tImageComponent?: ComponentType<\n\t\tReact.ImgHTMLAttributes & Record\n\t>;\n\tapiBaseURL: string;\n}) {\n\tconst isImg = asset.mimeType.startsWith(\"image/\");\n\n\tconst copyUrl = () => {\n\t\tlet fullUrl: string;\n\t\ttry {\n\t\t\t// new URL() handles both absolute and relative URLs and encodes\n\t\t\t// special characters (spaces, non-ASCII) in the path correctly.\n\t\t\tfullUrl = new URL(asset.url, apiBaseURL).href;\n\t\t} catch {\n\t\t\tfullUrl = asset.url;\n\t\t}\n\t\tnavigator.clipboard\n\t\t\t.writeText(fullUrl)\n\t\t\t.then(() => toast.success(\"URL copied\"));\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t{isImg ? (\n\t\t\t\t\tImageComponent ? (\n\t\t\t\t\t\t\n\t\t\t\t\t) : (\n\t\t\t\t\t\t\n\t\t\t\t\t)\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{asset.originalName}\n\t\t\t\t

\n\t\t\t\t

\n\t\t\t\t\t{asset.mimeType} · {formatBytes(asset.size)}\n\t\t\t\t

\n\t\t\t\t\n\t\t\t\t\t{asset.url}\n\t\t\t\t

\n\t\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n\nexport function LibraryPage() {\n\tconst overrides = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tuseRouteLifecycle({\n\t\trouteName: \"library\",\n\t\tcontext: {\n\t\t\tpath: \"/media\",\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforeLibraryPageRendered) {\n\t\t\t\treturn overrides.onBeforeLibraryPageRendered(context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [search, setSearch] = useState(\"\");\n\tconst [debouncedSearch, setDebouncedSearch] = useState(\"\");\n\tconst debounceRef = useRef | null>(null);\n\tconst [dragging, setDragging] = useState(false);\n\tconst fileInputRef = useRef(null);\n\tconst { mutateAsync: uploadAsset, isPending: isUploading } = useUploadAsset();\n\tconst { mutateAsync: deleteAsset } = useDeleteAsset();\n\tconst { Image: ImageComponent, apiBaseURL = \"\" } = overrides;\n\n\tconst handleSearch = (v: string) => {\n\t\tsetSearch(v);\n\t\tif (debounceRef.current) clearTimeout(debounceRef.current);\n\t\tdebounceRef.current = setTimeout(() => setDebouncedSearch(v), 300);\n\t};\n\n\tconst { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =\n\t\tuseAssets({\n\t\t\tfolderId: selectedFolder ?? undefined,\n\t\t\tquery: debouncedSearch || undefined,\n\t\t\tlimit: 40,\n\t\t});\n\n\tconst assets = data?.pages.flatMap((p) => p.items) ?? [];\n\n\tconst handleUpload = useCallback(\n\t\tasync (files: FileList | File[]) => {\n\t\t\tconst arr = Array.from(files);\n\t\t\tfor (const file of arr) {\n\t\t\t\ttry {\n\t\t\t\t\tawait uploadAsset({ file, folderId: selectedFolder ?? undefined });\n\t\t\t\t\ttoast.success(`Uploaded ${file.name}`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\ttoast.error(err instanceof Error ? err.message : \"Upload failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[selectedFolder, uploadAsset],\n\t);\n\n\tconst handleDelete = async (id: string) => {\n\t\tif (!confirm(\"Delete this asset?\")) return;\n\t\ttry {\n\t\t\tawait deleteAsset(id);\n\t\t\ttoast.success(\"Deleted\");\n\t\t} catch (err) {\n\t\t\ttoast.error(err instanceof Error ? err.message : \"Delete failed\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t\n\n\t\t\t {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(true);\n\t\t\t\t}}\n\t\t\t\tonDragLeave={() => setDragging(false)}\n\t\t\t\tonDrop={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(false);\n\t\t\t\t\tvoid handleUpload(e.dataTransfer.files);\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* Toolbar */}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t handleSearch(e.target.value)}\n\t\t\t\t\t\t\tplaceholder=\"Search files…\"\n\t\t\t\t\t\t\tclassName=\"h-8 pl-8\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{search && (\n\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\tsetSearch(\"\");\n\t\t\t\t\t\t\t\t\tsetDebouncedSearch(\"\");\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tclassName=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t>\n\t\t\t\t\t\t{isUploading ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tUpload\n\t\t\t\t\t\n\t\t\t\t\t e.target.files && handleUpload(e.target.files)}\n\t\t\t\t\t/>\n\t\t\t\t
\n\n\t\t\t\t{/* Drop overlay */}\n\t\t\t\t{dragging && (\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

Drop files to upload

\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t)}\n\n\t\t\t\t{/* Asset grid */}\n\t\t\t\t
\n\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t) : assets.length === 0 ? (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\tNo files yet. Drag & drop or click Upload.\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t) : (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{assets.map((asset) => (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t\t{hasNextPage && (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t fetchNextPage()}\n\t\t\t\t\t\t\t\tdisabled={isFetchingNextPage}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{isFetchingNextPage && (\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tLoad more\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\nimport { useState, useCallback, useRef } from \"react\";\nimport { useDeleteAsset, useUploadAsset } from \"@btst/stack/plugins/media/client/hooks\";\nimport { Button } from \"@/components/ui/button\";\nimport { Upload, Loader2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { toast } from \"sonner\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { BrowseTab } from \"../media-picker/browse-tab\";\nimport { FolderTree } from \"../media-picker/folder-tree\";\n\nexport function LibraryPage() {\n\tconst overrides = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tuseRouteLifecycle({\n\t\trouteName: \"library\",\n\t\tcontext: {\n\t\t\tpath: \"/media\",\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforeLibraryPageRendered) {\n\t\t\t\treturn overrides.onBeforeLibraryPageRendered(context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [dragging, setDragging] = useState(false);\n\tconst fileInputRef = useRef(null);\n\tconst { mutateAsync: uploadAsset, isPending: isUploading } = useUploadAsset();\n\tconst { mutateAsync: deleteAsset } = useDeleteAsset();\n\tconst { apiBaseURL = \"\" } = overrides;\n\n\tconst handleUpload = useCallback(\n\t\tasync (files: FileList | File[]) => {\n\t\t\tconst arr = Array.from(files);\n\t\t\tfor (const file of arr) {\n\t\t\t\ttry {\n\t\t\t\t\tawait uploadAsset({ file, folderId: selectedFolder ?? undefined });\n\t\t\t\t\ttoast.success(`Uploaded ${file.name}`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\ttoast.error(err instanceof Error ? err.message : \"Upload failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[selectedFolder, uploadAsset],\n\t);\n\n\tconst handleDelete = async (id: string) => {\n\t\tif (!confirm(\"Delete this asset?\")) return;\n\t\ttry {\n\t\t\tawait deleteAsset(id);\n\t\t\ttoast.success(\"Deleted\");\n\t\t} catch (err) {\n\t\t\ttoast.error(err instanceof Error ? err.message : \"Delete failed\");\n\t\t}\n\t};\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t
\n\n\t\t\t {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(true);\n\t\t\t\t}}\n\t\t\t\tonDragLeave={() => setDragging(false)}\n\t\t\t\tonDrop={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tsetDragging(false);\n\t\t\t\t\tvoid handleUpload(e.dataTransfer.files);\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* Toolbar */}\n\t\t\t\t
\n\t\t\t\t\t fileInputRef.current?.click()}\n\t\t\t\t\t\tdisabled={isUploading}\n\t\t\t\t\t\tclassName=\"w-full sm:w-auto\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{isUploading ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tUpload\n\t\t\t\t\t\n\t\t\t\t\t e.target.files && handleUpload(e.target.files)}\n\t\t\t\t\t/>\n\t\t\t\t
\n\n\t\t\t\t{/* Drop overlay */}\n\t\t\t\t{dragging && (\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

Drop files to upload

\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t)}\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/pages/library-page.internal.tsx" }, { diff --git a/packages/stack/registry/registry.json b/packages/stack/registry/registry.json index 5da30c4e..f02b772b 100644 --- a/packages/stack/registry/registry.json +++ b/packages/stack/registry/registry.json @@ -232,6 +232,7 @@ ], "registryDependencies": [ "button", + "dialog", "input", "popover", "tabs" diff --git a/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx b/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx index 35e42216..957a88ae 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/asset-card.tsx @@ -1,41 +1,77 @@ import { useDeleteAsset } from "../../hooks/use-media"; import type { SerializedAsset } from "../../../types"; import { cn } from "@workspace/ui/lib/utils"; -import { File, Check, Trash2 } from "lucide-react"; +import { File, Check, Copy, Trash2 } from "lucide-react"; import { isImage, formatBytes } from "./utils"; import { usePluginOverrides } from "@btst/stack/context"; import type { MediaPluginOverrides } from "../../overrides"; +import { AssetPreviewButton } from "./asset-preview-button"; +import { toast } from "sonner"; export function AssetCard({ asset, - selected, onToggle, + selected = false, + onDelete, + apiBaseURL, }: { asset: SerializedAsset; - selected: boolean; - onToggle: () => void; + selected?: boolean; + onToggle?: () => void; + onDelete?: (id: string) => void | Promise; + apiBaseURL?: string; }) { const { mutateAsync: deleteAsset } = useDeleteAsset(); const { Image: ImageComponent } = usePluginOverrides< MediaPluginOverrides, Partial >("media", {}); + const imageAsset = isImage(asset.mimeType); + const selectable = typeof onToggle === "function"; + + const copyUrl = () => { + let fullUrl: string; + try { + fullUrl = new URL(asset.url, apiBaseURL).href; + } catch { + fullUrl = asset.url; + } + navigator.clipboard + .writeText(fullUrl) + .then(() => toast.success("URL copied")); + }; + + const handleDelete = () => { + if (onDelete) { + return onDelete(asset.id); + } + + if (confirm(`Delete "${asset.originalName}"?`)) { + return deleteAsset(asset.id).catch(console.error); + } + }; return (
(e.key === "Enter" || e.key === " ") && onToggle()} + onKeyDown={(e) => { + if (selectable && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + onToggle(); + } + }} className={cn( "group relative cursor-pointer rounded-md border bg-muted/30 p-1 transition-all hover:border-ring hover:shadow-sm", + !selectable && "cursor-default", selected && "border-ring ring-1 ring-ring", )} > {/* Thumbnail */} -
- {isImage(asset.mimeType) ? ( +
+ {imageAsset ? ( ImageComponent ? ( )} - {/* Delete button (on hover) */} - +
+ {apiBaseURL ? ( + + ) : null} + {imageAsset ? ( + + ) : null} + +
); } diff --git a/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.tsx b/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.tsx new file mode 100644 index 00000000..7421203b --- /dev/null +++ b/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useState } from "react"; +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogTitle, +} from "@workspace/ui/components/dialog"; +import { Eye, X } from "lucide-react"; +import type { SerializedAsset } from "../../../types"; + +export function AssetPreviewButton({ + asset, + className, +}: { + asset: SerializedAsset; + className: string; +}) { + const [open, setOpen] = useState(false); + + return ( + <> + + + + + + {asset.alt || asset.originalName} + + + + + + +
+
+ {asset.alt +
+
+
+
+ + ); +} diff --git a/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx b/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx index 3d3915e9..f3812d18 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.tsx @@ -9,20 +9,25 @@ import { matchesAccept } from "./utils"; export function BrowseTab({ folderId, - selected, - multiple, + selected = [], accept, onToggle, + onDelete, + apiBaseURL, + emptyMessage = "No files found", }: { folderId: string | null; - selected: SerializedAsset[]; - multiple: boolean; + selected?: SerializedAsset[]; accept?: string[]; - onToggle: (asset: SerializedAsset) => void; + onToggle?: (asset: SerializedAsset) => void; + onDelete?: (id: string) => void | Promise; + apiBaseURL?: string; + emptyMessage?: string; }) { const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const debounceRef = useRef | null>(null); + const selectable = typeof onToggle === "function"; const handleSearch = (v: string) => { setSearch(v); @@ -43,7 +48,7 @@ export function BrowseTab({ : allAssets; return ( -
+
-

No files found

+

{emptyMessage}

) : ( -
-
+
+
{filtered.map((asset) => ( s.id === asset.id)} - onToggle={() => onToggle(asset)} + onToggle={selectable ? () => onToggle(asset) : undefined} + onDelete={onDelete} + apiBaseURL={apiBaseURL} /> ))}
diff --git a/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx b/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx index 22766e9c..bbeb1ff8 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.tsx @@ -38,7 +38,7 @@ export function FolderTree({ }; return ( -
+
Folders @@ -76,7 +76,7 @@ export function FolderTree({
)} -
+
{/* All assets (root) */} @@ -226,6 +244,7 @@ export function MediaPicker({ data-testid="media-select-button" onClick={handleConfirm} disabled={selectedAssets.length === 0} + className="flex-1 sm:flex-none" > {multiple ? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : ""}` diff --git a/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx b/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx index cbf526f9..031f3ef6 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.tsx @@ -64,7 +64,7 @@ export function UploadTab({ void handleFiles(e.dataTransfer.files); }} className={cn( - "flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed transition-colors", + "flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed px-4 py-6 text-center transition-colors sm:px-6", dragging ? "border-ring bg-ring/5" : "border-muted-foreground/30", )} > diff --git a/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx b/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx index 0e12bcf3..3238b518 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/url-tab.tsx @@ -41,7 +41,7 @@ export function UrlTab({ Paste a public URL to register it as an asset without uploading a file.

-
+
- -
- {isCreating && ( -
- setNewFolderName(e.target.value)} - placeholder="Folder name" - className="h-7 text-xs" - onKeyDown={(e) => { - if (e.key === "Enter") void handleCreate(); - if (e.key === "Escape") setIsCreating(false); - }} - /> - -
- )} -
- - {rootFolders.map((folder) => ( - - ))} -
-
- ); -} - -function AssetCard({ - asset, - onDelete, - ImageComponent, - apiBaseURL, -}: { - asset: SerializedAsset; - onDelete: (id: string) => void; - ImageComponent?: ComponentType< - React.ImgHTMLAttributes & Record - >; - apiBaseURL: string; -}) { - const isImg = asset.mimeType.startsWith("image/"); - - const copyUrl = () => { - let fullUrl: string; - try { - // new URL() handles both absolute and relative URLs and encodes - // special characters (spaces, non-ASCII) in the path correctly. - fullUrl = new URL(asset.url, apiBaseURL).href; - } catch { - fullUrl = asset.url; - } - navigator.clipboard - .writeText(fullUrl) - .then(() => toast.success("URL copied")); - }; - - return ( -
-
- {isImg ? ( - ImageComponent ? ( - - ) : ( - {asset.alt - ) - ) : ( - - )} -
-
-

- {asset.originalName} -

-

- {asset.mimeType} · {formatBytes(asset.size)} -

-

- {asset.url} -

-
-
- - -
-
- ); -} +import { BrowseTab } from "../media-picker/browse-tab"; +import { FolderTree } from "../media-picker/folder-tree"; export function LibraryPage() { const overrides = usePluginOverrides< @@ -239,29 +33,11 @@ export function LibraryPage() { }); const [selectedFolder, setSelectedFolder] = useState(null); - const [search, setSearch] = useState(""); - const [debouncedSearch, setDebouncedSearch] = useState(""); - const debounceRef = useRef | null>(null); const [dragging, setDragging] = useState(false); const fileInputRef = useRef(null); const { mutateAsync: uploadAsset, isPending: isUploading } = useUploadAsset(); const { mutateAsync: deleteAsset } = useDeleteAsset(); - const { Image: ImageComponent, apiBaseURL = "" } = overrides; - - const handleSearch = (v: string) => { - setSearch(v); - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => setDebouncedSearch(v), 300); - }; - - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = - useAssets({ - folderId: selectedFolder ?? undefined, - query: debouncedSearch || undefined, - limit: 40, - }); - - const assets = data?.pages.flatMap((p) => p.items) ?? []; + const { apiBaseURL = "" } = overrides; const handleUpload = useCallback( async (files: FileList | File[]) => { @@ -289,15 +65,14 @@ export function LibraryPage() { }; return ( -
- +
+
+ +
{ @@ -312,32 +87,12 @@ export function LibraryPage() { }} > {/* Toolbar */} -
-
- - handleSearch(e.target.value)} - placeholder="Search files…" - className="h-8 pl-8" - /> - {search && ( - - )} -
+
)} - {/* Asset grid */} -
- {isLoading ? ( -
- -
- ) : assets.length === 0 ? ( -
- -

- No files yet. Drag & drop or click Upload. -

-
- ) : ( -
- {assets.map((asset) => ( - - ))} -
- )} - {hasNextPage && ( -
- -
- )} +
+
From 5f7de12b788371eb8d96e8bd4cdae542ab289ab4 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 16:06:44 -0400 Subject: [PATCH 21/29] refactor: optimize media client configuration and upload functions using useMemo and useCallback for improved performance --- .../react-router/app/routes/pages/_layout.tsx | 36 +++++++++++-------- examples/tanstack/src/routes/pages/route.tsx | 35 ++++++++++-------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index 32985d7c..fe87a14e 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -1,5 +1,5 @@ // app/routes/__root.tsx -import { useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Outlet, Link, useNavigate } from "react-router"; import { StackProvider } from "@btst/stack/context" import type { BlogPluginOverrides } from "@btst/stack/plugins/blog/client" @@ -42,26 +42,32 @@ export default function Layout() { console.log("baseURL", baseURL) const navigate = useNavigate() const [queryClient] = useState(() => getOrCreateQueryClient()) - const mediaClientConfig = { - apiBaseURL: baseURL, - apiBasePath: "/api/data", - uploadMode: "direct" as const, - } + const mediaClientConfig = useMemo( + () => ({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + uploadMode: "direct" as const, + }), + [baseURL], + ) - const uploadImage = async (file: File) => { + const uploadImage = useCallback(async (file: File) => { const asset = await uploadAsset(mediaClientConfig, { file }) return asset.url - } + }, [mediaClientConfig]) // For chat file attachments we embed as a data URL so OpenAI can read the // content directly — a local /uploads/... path is not reachable from OpenAI's servers. - const uploadFileForChat = (file: File): Promise => - new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = (e) => resolve(e.target?.result as string) - reader.onerror = () => reject(new Error("Failed to read file")) - reader.readAsDataURL(file) - }) + const uploadFileForChat = useCallback( + (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = () => reject(new Error("Failed to read file")) + reader.readAsDataURL(file) + }), + [], + ) return ( diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index a0502ce6..cf2b108f 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -1,6 +1,7 @@ import { StackProvider } from "@btst/stack/context" import { QueryClientProvider } from "@tanstack/react-query" import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { useCallback, useMemo } from "react" import type { BlogPluginOverrides } from "@btst/stack/plugins/blog/client" import type { AiChatPluginOverrides } from "@btst/stack/plugins/ai-chat/client" import { ChatLayout } from "@btst/stack/plugins/ai-chat/client" @@ -45,26 +46,32 @@ function Layout() { const router = useRouter() const routeContext = Route.useRouteContext() const baseURL = getBaseURL() - const mediaClientConfig = { - apiBaseURL: baseURL, - apiBasePath: "/api/data", - uploadMode: "direct" as const, - } + const mediaClientConfig = useMemo( + () => ({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + uploadMode: "direct" as const, + }), + [baseURL], + ) - const uploadImage = async (file: File) => { + const uploadImage = useCallback(async (file: File) => { const asset = await uploadAsset(mediaClientConfig, { file }) return asset.url - } + }, [mediaClientConfig]) // For chat file attachments we embed as a data URL so OpenAI can read the // content directly — a local /uploads/... path is not reachable from OpenAI's servers. - const uploadFileForChat = (file: File): Promise => - new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = (e) => resolve(e.target?.result as string) - reader.onerror = () => reject(new Error("Failed to read file")) - reader.readAsDataURL(file) - }) + const uploadFileForChat = useCallback( + (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = () => reject(new Error("Failed to read file")) + reader.readAsDataURL(file) + }), + [], + ) return ( From e085781fd9fff27fc0f100ad094c7bccefd2ff5f Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 16:26:42 -0400 Subject: [PATCH 22/29] fix: update ImagePicker component to safely handle asset URL selection with optional chaining and fallback --- examples/nextjs/app/pages/layout.tsx | 2 +- examples/react-router/app/routes/pages/_layout.tsx | 2 +- examples/tanstack/src/routes/pages/route.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/nextjs/app/pages/layout.tsx b/examples/nextjs/app/pages/layout.tsx index eef20f27..bab6ac57 100644 --- a/examples/nextjs/app/pages/layout.tsx +++ b/examples/nextjs/app/pages/layout.tsx @@ -341,7 +341,7 @@ const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { } accept={["image/*"]} - onSelect={(assets) => onSelect(assets[0].url)} + onSelect={(assets) => onSelect(assets[0]?.url ?? "")} /> ) } diff --git a/examples/react-router/app/routes/pages/_layout.tsx b/examples/react-router/app/routes/pages/_layout.tsx index fe87a14e..fb826c63 100644 --- a/examples/react-router/app/routes/pages/_layout.tsx +++ b/examples/react-router/app/routes/pages/_layout.tsx @@ -267,7 +267,7 @@ const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { } accept={["image/*"]} - onSelect={(assets) => onSelect(assets[0].url)} + onSelect={(assets) => onSelect(assets[0]?.url ?? "")} /> ) } diff --git a/examples/tanstack/src/routes/pages/route.tsx b/examples/tanstack/src/routes/pages/route.tsx index cf2b108f..d715bb7a 100644 --- a/examples/tanstack/src/routes/pages/route.tsx +++ b/examples/tanstack/src/routes/pages/route.tsx @@ -272,7 +272,7 @@ const ImagePicker = ({ onSelect }: { onSelect: (url: string) => void }) => { } accept={["image/*"]} - onSelect={(assets) => onSelect(assets[0].url)} + onSelect={(assets) => onSelect(assets[0]?.url ?? "")} /> ) } From cc975356c251b340d431e1d8cadc7caee4a5dee0 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 16:55:24 -0400 Subject: [PATCH 23/29] refactor: improve media picker tab interactions and adjust dimensions for better usability --- e2e/tests/smoke.media.spec.ts | 14 +++++++++++--- .../media/client/components/media-picker/index.tsx | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/e2e/tests/smoke.media.spec.ts b/e2e/tests/smoke.media.spec.ts index 8dfad5ab..711e3d2c 100644 --- a/e2e/tests/smoke.media.spec.ts +++ b/e2e/tests/smoke.media.spec.ts @@ -26,8 +26,11 @@ async function openMediaPicker(page: Page) { // Helper: upload a file inside the open MediaPicker (Upload tab) async function uploadInMediaPicker(page: Page) { - // Switch to Upload tab - await page.getByRole("tab", { name: /upload/i }).click(); + // The blog editor opens the picker lower on the page in Next.js, so the + // tab strip can render partially offscreen at the configured viewport. + const uploadTab = page.getByRole("tab", { name: /upload/i }).last(); + await uploadTab.scrollIntoViewIfNeeded(); + await uploadTab.click(); // Find the hidden file input inside the upload tab const fileInput = page.locator('[data-testid="media-upload-input"]').first(); @@ -39,7 +42,9 @@ async function uploadInMediaPicker(page: Page) { }); // Wait for upload to complete — a thumbnail should appear in the Browse tab - await page.getByRole("tab", { name: /browse/i }).click(); + const browseTab = page.getByRole("tab", { name: /browse/i }).last(); + await browseTab.scrollIntoViewIfNeeded(); + await browseTab.click(); // The uploaded asset should appear in the grid await expect( page.locator('[data-testid="media-asset-item"]').first(), @@ -71,6 +76,9 @@ async function openBlogEditorMediaPicker(page: Page) { '[data-testid="image-picker-trigger"] [data-testid="open-media-picker"]', ); await expect(trigger).toBeVisible({ timeout: 10000 }); + await trigger.evaluate((element) => + element.scrollIntoView({ block: "center", inline: "nearest" }), + ); await trigger.click(); await expect(page.getByText("Media Library")).toBeVisible({ timeout: 5000 }); } diff --git a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx index ae9319f4..55ec9b11 100644 --- a/packages/stack/src/plugins/media/client/components/media-picker/index.tsx +++ b/packages/stack/src/plugins/media/client/components/media-picker/index.tsx @@ -122,13 +122,13 @@ export function MediaPicker({ > {trigger}
From ec6482a1682db203eb551450003dadab7df88ac7 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 16:55:52 -0400 Subject: [PATCH 24/29] chore: bump version to 2.9.1 in package.json for @btst/stack --- packages/stack/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/package.json b/packages/stack/package.json index 1bd0dab9..b0d6ac8f 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -1,6 +1,6 @@ { "name": "@btst/stack", - "version": "2.8.1", + "version": "2.9.1", "description": "A composable, plugin-based library for building full-stack applications.", "repository": { "type": "git", From c29b6a040f29f6a6fbe887e4a34c0117fd29014b Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 16:56:48 -0400 Subject: [PATCH 25/29] feat: add Media plugin documentation and demo assets for enhanced media library integration --- docs/assets/media-demo-1.png | Bin 0 -> 106045 bytes docs/content/docs/meta.json | 1 + docs/content/docs/plugins/index.mdx | 14 +- docs/content/docs/plugins/media.mdx | 613 ++++++++++++++++++++++++++++ 4 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 docs/assets/media-demo-1.png create mode 100644 docs/content/docs/plugins/media.mdx diff --git a/docs/assets/media-demo-1.png b/docs/assets/media-demo-1.png new file mode 100644 index 0000000000000000000000000000000000000000..cce759c17faf30ae05003376a48f09d49ea6597d GIT binary patch literal 106045 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

>2zaC# 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

#t)<1-H?^}eh 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 l+D6f?8b)78n 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 description="Project management with boards, columns, tasks, drag-and-drop, and priority levels." /> + } + description="Threaded comments with moderation, likes, replies, and embeddable comment threads." + /> + } + description="Media library with uploads, folders, picker UI, URL registration, and reusable image inputs." + /> + + Media Plugin Demo - Media library picker + +

+ +The Media plugin gives you a built-in media library with folders, uploads, URL-based asset registration, and reusable picker components that can be embedded anywhere in your app. It works well as a standalone `/media` library route and as shared infrastructure for other plugins such as Blog, CMS, and Kanban. + +## Installation + + +Ensure you followed the general [framework installation guide](/installation) first. + + +Follow these steps to add the Media plugin to your BTST setup. + +### 1. Add Plugin to Backend API + +Import and register the media backend plugin in your `stack.ts` file: + +```ts title="lib/stack.ts" +import { stack } from "@btst/stack" +import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api" + +const { handler, dbSchema } = stack({ + basePath: "/api/data", + plugins: { + media: mediaBackendPlugin({ + storageAdapter: localAdapter(), + maxFileSizeBytes: 10 * 1024 * 1024, + allowedMimeTypes: ["image/*", "application/pdf"], + }), + }, + adapter: (db) => createPrismaAdapter(prisma, db, { + provider: "postgresql", + }), +}) + +export { handler, dbSchema } +``` + +The `mediaBackendPlugin()` requires a `storageAdapter`. BTST currently ships with three modes: + +- `localAdapter()` for local filesystem uploads and self-hosted setups +- `s3Adapter()` for S3-compatible object storage using presigned uploads +- `vercelBlobAdapter()` for direct uploads to Vercel Blob + + +Pick the backend storage adapter first, then make the client-side `uploadMode` match it. A mismatch between the two is the most common Media plugin integration mistake. + + +### 2. Add Plugin to Client + +Register the media client plugin in your `stack-client.tsx` file: + +```tsx title="lib/stack-client.tsx" +import { createStackClient } from "@btst/stack/client" +import { mediaClientPlugin } from "@btst/stack/plugins/media/client" +import { QueryClient } from "@tanstack/react-query" + +const getBaseURL = () => + process.env.BASE_URL || "http://localhost:3000" + +export const getStackClient = (queryClient: QueryClient) => { + const baseURL = getBaseURL() + + return createStackClient({ + plugins: { + media: mediaClientPlugin({ + apiBaseURL: baseURL, + apiBasePath: "/api/data", + siteBaseURL: baseURL, + siteBasePath: "/pages", + queryClient, + }), + }, + }) +} +``` + +**Required configuration:** +- `apiBaseURL`: Base URL for server-side API requests +- `apiBasePath`: Path where your BTST API is mounted +- `siteBaseURL`: Base URL of your site for route metadata +- `siteBasePath`: Path where your BTST pages are mounted +- `queryClient`: React Query client used for prefetching and caching + + +The media client plugin registers the `/media` page route and prefetches the initial asset grid and folder tree during SSR. It expects the same API base settings you use elsewhere in your BTST client setup. + + +### 3. Import Plugin CSS + +Add the media plugin CSS to your global stylesheet: + +```css title="app/globals.css" +@import "@btst/stack/plugins/media/css"; +``` + +This includes the built-in media library UI, picker layout, folder tree, upload states, and image previews. + +### 4. Add Context Overrides + +Configure framework-specific overrides in your `StackProvider`: + + + + ```tsx title="app/pages/layout.tsx" + import { StackProvider } from "@btst/stack/context" + import { uploadAsset, type MediaPluginOverrides } from "@btst/stack/plugins/media/client" + import Link from "next/link" + import Image from "next/image" + import { useRouter } from "next/navigation" + import { QueryClient } from "@tanstack/react-query" + + const getBaseURL = () => + typeof window !== "undefined" + ? process.env.NEXT_PUBLIC_BASE_URL || window.location.origin + : process.env.BASE_URL || "http://localhost:3000" + + type PluginOverrides = { + media: MediaPluginOverrides + } + + export default function Layout({ children }) { + const router = useRouter() + const queryClient = new QueryClient() + const baseURL = getBaseURL() + + return ( + + basePath="/pages" + overrides={{ + media: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + queryClient, + uploadMode: "direct", + navigate: (path) => router.push(path), + Link: ({ href, ...props }) => , + Image: (props) => , + }, + }} + > + {children} + + ) + } + ``` + + + + ```tsx title="app/routes/pages/_layout.tsx" + import { Outlet, Link, useNavigate } from "react-router" + import { StackProvider } from "@btst/stack/context" + import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" + import { QueryClient } from "@tanstack/react-query" + + const getBaseURL = () => + typeof window !== "undefined" + ? import.meta.env.VITE_BASE_URL || window.location.origin + : process.env.BASE_URL || "http://localhost:5173" + + type PluginOverrides = { + media: MediaPluginOverrides + } + + export default function Layout() { + const navigate = useNavigate() + const queryClient = new QueryClient() + const baseURL = getBaseURL() + + return ( + + basePath="/pages" + overrides={{ + media: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + queryClient, + uploadMode: "direct", + navigate: (href) => navigate(href), + Link: ({ href, children, className, ...props }) => ( + + {children} + + ), + }, + }} + > + + + ) + } + ``` + + + + ```tsx title="src/routes/pages/route.tsx" + import { StackProvider } from "@btst/stack/context" + import type { MediaPluginOverrides } from "@btst/stack/plugins/media/client" + import { Link, Outlet, useRouter } from "@tanstack/react-router" + import { QueryClient } from "@tanstack/react-query" + + const getBaseURL = () => + typeof window !== "undefined" + ? import.meta.env.VITE_BASE_URL || window.location.origin + : process.env.BASE_URL || "http://localhost:3000" + + type PluginOverrides = { + media: MediaPluginOverrides + } + + function Layout() { + const router = useRouter() + const queryClient = new QueryClient() + const baseURL = getBaseURL() + + return ( + + basePath="/pages" + overrides={{ + media: { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + queryClient, + uploadMode: "direct", + navigate: (href) => router.navigate({ href }), + Link: ({ href, children, className, ...props }) => ( + + {children} + + ), + }, + }} + > + + + ) + } + ``` + + + +**Required overrides:** +- `apiBaseURL` +- `apiBasePath` +- `queryClient` +- `navigate` + +**Optional overrides:** +- `uploadMode`: Must match your backend storage adapter +- `Link`: Custom framework-aware link component +- `Image`: Custom image renderer such as Next.js `Image` +- `headers`: Additional request headers +- `imageCompression`: Compression settings or `false` to disable compression +- `onRouteRender`, `onRouteError`, `onBeforeLibraryPageRendered`: Route lifecycle hooks + +### 5. Generate and Apply Database Changes + +The Media plugin adds database tables for assets and folders. Generate and apply your migrations: + +```bash +npx @btst/cli generate +npx @btst/cli migrate +``` + +For more details on the CLI and all available options, see the [CLI documentation](/cli). + +## Congratulations, You're Done! + +Your media plugin is now configured and ready to use. Here is a quick reference of what you get out of the box: + +**Routes** + +| Route | Description | +| --- | --- | +| `/pages/media` | Full media library UI with folders, uploads, URL tab, and asset browsing | + +**Core API endpoints** + +| Method | Endpoint | Purpose | +| --- | --- | --- | +| `GET` | `/media/assets` | List assets with filtering and pagination | +| `POST` | `/media/assets` | Register an existing uploaded asset URL | +| `PATCH` | `/media/assets/:id` | Update asset metadata | +| `DELETE` | `/media/assets/:id` | Delete an asset | +| `GET` | `/media/folders` | List folders | +| `POST` | `/media/folders` | Create a folder | +| `DELETE` | `/media/folders/:id` | Delete a folder | +| `POST` | `/media/upload` | Direct upload endpoint for local storage | +| `POST` | `/media/upload/token` | Presigned upload token endpoint for S3-compatible storage | +| `POST` | `/media/upload/vercel-blob` | Upload handler for Vercel Blob | + +**Reusable UI pieces** + +| Export | Purpose | +| --- | --- | +| `MediaPicker` | Embed the full media browser in your own forms and editors | +| `ImageInputField` | Drop-in image field with preview, change, and remove actions | +| `uploadAsset()` | Imperative upload helper for editors and non-React callbacks | + +## Common Patterns + +### Imperative uploads for editor callbacks + +When you need to upload an image outside React hooks, use `uploadAsset()`: + +```tsx title="app/pages/layout.tsx" +import { uploadAsset } from "@btst/stack/plugins/media/client" + +const mediaClientConfig = { + apiBaseURL: baseURL, + apiBasePath: "/api/data", + uploadMode: "direct" as const, +} + +const uploadImage = async (file: File) => { + const asset = await uploadAsset(mediaClientConfig, { file }) + return asset.url +} +``` + +This is the same pattern used in the example apps to connect Blog, CMS, and Kanban image uploads to the shared Media plugin. + +### Embedding the picker in your own UI + +Use `MediaPicker` when you want a compact "browse media" flow inside a custom form: + +```tsx title="components/image-picker.tsx" +import { MediaPicker } from "@btst/stack/plugins/media/client/components" +import { Button } from "@/components/ui/button" + +export function ImagePicker({ onSelect }: { onSelect: (url: string) => void }) { + return ( + Browse media} + accept={["image/*"]} + onSelect={(assets) => onSelect(assets[0]?.url ?? "")} + /> + ) +} +``` + +### Using the built-in image field + +Use `ImageInputField` when you want a simple image preview and replacement flow without building your own wrapper: + +```tsx title="components/product-image-field.tsx" +import { ImageInputField } from "@btst/stack/plugins/media/client/components" + +export function ProductImageField({ + value, + onChange, +}: { + value: string + onChange: (value: string) => void +}) { + return +} +``` + +## API Reference + +### Backend (`@btst/stack/plugins/media/api`) + +#### mediaBackendPlugin + + + +#### MediaBackendConfig + +Choose your storage adapter and optional upload constraints: + + + +#### MediaBackendHooks + +Customize backend behavior with optional lifecycle hooks for uploads, listing, folder management, and deletes: + + + +**Example usage:** + +```ts +import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api" + +mediaBackendPlugin({ + storageAdapter: localAdapter(), + hooks: { + onBeforeUpload: async (_meta, context) => { + const session = await getSession(context.headers as Headers) + if (!session?.user?.isAdmin) throw new Error("Admin access required") + }, + onBeforeDelete: async (asset) => { + if (asset.mimeType.startsWith("image/")) return + throw new Error("Only image deletion is allowed here") + }, + }, +}) +``` + +#### MediaApiContext + + + +#### StorageAdapter + + + +#### DirectStorageAdapter + + + +#### S3StorageAdapter + + + +#### VercelBlobStorageAdapter + + + +### Client (`@btst/stack/plugins/media/client`) + +#### mediaClientPlugin + + + +#### MediaClientConfig + +The client plugin accepts the required route, site, and React Query configuration: + + + +#### MediaClientHooks + +These hooks run around the media library SSR loader: + + + +#### MediaLoaderContext + + + +#### MediaPluginOverrides + +Configure framework-specific overrides and route lifecycle hooks: + + + +#### MediaUploadMode + + + +#### MediaRouteContext + + + +#### uploadAsset + + + +#### MediaUploadClientConfig + + + +#### UploadAssetInput + + + +### Components (`@btst/stack/plugins/media/client/components`) + +#### MediaPicker + +The full popover-based media browser with Browse, Upload, and URL tabs: + + + +#### MediaPickerProps + + + +#### ImageInputField + +Use the built-in image preview field when you only need single-image selection: + + + +### Hooks (`@btst/stack/plugins/media/client/hooks`) + +The Media plugin exposes React Query-powered hooks for reading and mutating assets and folders: + +#### useAssets + + + +#### useFolders + + + +#### useUploadAsset + + + +#### useRegisterAsset + + + +#### useDeleteAsset + + + +#### useCreateFolder + + + +#### useDeleteFolder + + + +## Server-side Data Access + +Like other BTST plugins, the Media plugin supports two server-side access patterns: + +1. Use `stack().api.media.*` when you already have a configured stack instance. +2. Import getters and mutations directly from `@btst/stack/plugins/media/api` when you want lower-level access with an adapter. + +**Available getters via `stack().api.media`:** + +| Function | Description | +| --- | --- | +| `listAssets(params?)` | List assets with pagination, search, MIME filtering, and folder filtering | +| `getAssetById(id)` | Fetch a single asset by ID | +| `listFolders(params?)` | List folders, optionally scoped to a parent folder | +| `getFolderById(id)` | Fetch a single folder by ID | + +**Available direct mutations:** + +| Function | Description | +| --- | --- | +| `createAsset(adapter, input)` | Register an asset record | +| `updateAsset(adapter, id, input)` | Update an existing asset | +| `deleteAsset(adapter, id)` | Delete an asset record | +| `createFolder(adapter, input)` | Create a folder | +| `deleteFolder(adapter, id)` | Delete a folder | + + +Authorization hooks are not called when you use `stack().api.media.*` or direct getter and mutation imports. Enforce access control at the call site. + + +### `AssetListParams` + + + +### `AssetListResult` + + + +### `FolderListParams` + + + +### `CreateAssetInput` + + + +### `UpdateAssetInput` + + + +### `CreateFolderInput` + + + +## Types + +#### Asset + + + +#### Folder + + + +#### SerializedAsset + + + +#### SerializedFolder + + + +## Static Site Generation (SSG) + +The Media plugin does not currently support build-time SSG prefetching. Its client loader warns when no server is available at build time, and the library route is intended to run against a live API. + +Use the Media plugin for authenticated dashboards, admin tools, and editor workflows rather than static public pages. From cc74a9c3bb3180fcb16f19882f2a05abfb385a38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:11:57 +0000 Subject: [PATCH 26/29] chore: update shadcn registry [skip ci] --- packages/stack/registry/btst-media.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/registry/btst-media.json b/packages/stack/registry/btst-media.json index 0272161e..1fd23145 100644 --- a/packages/stack/registry/btst-media.json +++ b/packages/stack/registry/btst-media.json @@ -55,7 +55,7 @@ { "path": "btst/media/client/components/media-picker/index.tsx", "type": "registry:component", - "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\nimport { useState, type ReactNode } from \"react\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tTabs,\n\tTabsContent,\n\tTabsList,\n\tTabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Image, Upload, Link, X } from \"lucide-react\";\nimport type { SerializedAsset } from \"../../../types\";\nimport { FolderTree } from \"./folder-tree\";\nimport { BrowseTab } from \"./browse-tab\";\nimport { UploadTab } from \"./upload-tab\";\nimport { UrlTab } from \"./url-tab\";\nimport type { MediaPluginOverrides } from \"../../overrides\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\n\nexport interface MediaPickerProps {\n\t/**\n\t * Element that triggers opening the picker. Required.\n\t */\n\ttrigger: ReactNode;\n\t/**\n\t * Called when the user confirms their selection.\n\t */\n\tonSelect: (assets: SerializedAsset[]) => void;\n\t/**\n\t * Allow multiple selection.\n\t * @default false\n\t */\n\tmultiple?: boolean;\n\t/**\n\t * Filter displayed assets by MIME type prefix (e.g. \"image/\").\n\t */\n\taccept?: string[];\n}\n\n/**\n * MediaPicker — a Popover-based media browser.\n *\n * Reads API config from the `media` plugin overrides context (set up in StackProvider).\n * Must be rendered inside a `StackProvider` that includes media overrides.\n *\n * @example\n * ```tsx\n * Browse media}\n * accept={[\"image/*\"]}\n * onSelect={(assets) => form.setValue(\"image\", assets[0].url)}\n * />\n * ```\n */\nexport function MediaPicker({\n\ttrigger,\n\tonSelect,\n\tmultiple = false,\n\taccept,\n}: MediaPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [selectedFolder, setSelectedFolder] = useState(null);\n\tconst [selectedAssets, setSelectedAssets] = useState([]);\n\tconst [activeTab, setActiveTab] = useState<\"browse\" | \"upload\" | \"url\">(\n\t\t\"browse\",\n\t);\n\n\tconst handleClose = () => {\n\t\tsetOpen(false);\n\t\tsetSelectedAssets([]);\n\t};\n\n\tconst handleConfirm = () => {\n\t\tif (selectedAssets.length === 0) return;\n\t\t// Copy selection before clearing; defer onSelect so the popover has time\n\t\t// to start its close animation before any parent state updates that might\n\t\t// unmount this component (e.g. CMSFileUpload hiding when previewUrl is set).\n\t\tconst toSelect = [...selectedAssets];\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect(toSelect), 0);\n\t};\n\n\tconst handleToggleAsset = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) =>\n\t\t\t\tprev.some((a) => a.id === asset.id)\n\t\t\t\t\t? prev.filter((a) => a.id !== asset.id)\n\t\t\t\t\t: [...prev, asset],\n\t\t\t);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t}\n\t};\n\n\tconst handleUploaded = (asset: SerializedAsset) => {\n\t\tif (multiple) {\n\t\t\tsetSelectedAssets((prev) => [...prev, asset]);\n\t\t} else {\n\t\t\tsetSelectedAssets([asset]);\n\t\t\tsetActiveTab(\"browse\");\n\t\t}\n\t};\n\n\tconst handleUrlRegistered = (asset: SerializedAsset) => {\n\t\t// Close the popover first, then notify parent — same deferral as handleConfirm.\n\t\tconst toSelect = asset;\n\t\thandleClose();\n\t\tsetTimeout(() => onSelect([toSelect]), 0);\n\t};\n\n\treturn (\n\t\t {\n\t\t\t\tif (!v) handleClose();\n\t\t\t\telse setOpen(true);\n\t\t\t}}\n\t\t>\n\t\t\t{trigger}\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/* Header */}\n\t\t\t\t\t
\n\t\t\t\t\t\tMedia Library\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Body */}\n\t\t\t\t\t
\n\t\t\t\t\t\t{/* Folder sidebar */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t{/* Main panel */}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t setActiveTab(v as any)}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col min-h-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tBrowse\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tUpload\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tURL\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/* Footer */}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedAssets.length > 0\n\t\t\t\t\t\t\t\t? `${selectedAssets.length} selected`\n\t\t\t\t\t\t\t\t: \"Click a file to select it\"}\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{multiple\n\t\t\t\t\t\t\t\t\t? `Select ${selectedAssets.length > 0 ? `(${selectedAssets.length})` : \"\"}`\n\t\t\t\t\t\t\t\t\t: \"Select\"}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n\n/**\n * ImageInputField — displays an image preview with change/remove actions, or a\n * \"Browse Media\" button that opens the full MediaPicker popover (Browse / Upload / URL tabs).\n *\n * Upload mode, folder selection, and multi-mode cloud support are all handled inside\n * the MediaPicker's UploadTab — this component is purely a thin wrapper.\n */\nexport function ImageInputField({\n\tvalue,\n\tonChange,\n}: {\n\tvalue: string;\n\tonChange: (v: string) => void;\n}) {\n\tconst { Image: ImageComponent } = usePluginOverrides<\n\t\tMediaPluginOverrides,\n\t\tPartial\n\t>(\"media\", {});\n\n\tif (value) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t{ImageComponent ? (\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t\tChange Image\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t\t\t/>\n\t\t\t\t\t onChange(\"\")}\n\t\t\t\t\t>\n\t\t\t\t\t\tRemove\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\t\tBrowse Media\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\taccept={[\"image/*\"]}\n\t\t\t\tonSelect={(assets) => onChange(assets[0]?.url ?? \"\")}\n\t\t\t/>\n\t\t
\n\t);\n}\n", "target": "src/components/btst/media/client/components/media-picker/index.tsx" }, { From 1b1bfbb06df8fe0cad883342439cf968df610e4f Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 17:22:18 -0400 Subject: [PATCH 27/29] docs: update README to include Media plugin details and installation instructions --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d3d6c09e..9c7af8c6 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Enable the features you need and keep building your product. | **Form Builder** | Dynamic form builder with drag-and-drop editor, submissions, and validation | | **UI Builder** | Visual drag-and-drop page builder with component registry and public rendering | | **Kanban** | Project management with boards, columns, tasks, drag-and-drop, and priority levels | +| **Media** | Media library with uploads, folders, picker UI, URL registration, and reusable image inputs | | **OpenAPI** | Auto-generated API documentation with interactive Scalar UI | | **Route Docs** | Auto-generated client route documentation with interactive navigation | | **Better Auth UI** | Beautiful shadcn/ui authentication components for better-auth | @@ -121,8 +122,8 @@ Supports Prisma, Drizzle, MongoDB and Kysely SQL dialects. Each plugin's UI layer is available as a [shadcn registry](https://ui.shadcn.com/docs/registry) block. Use it to **eject and fully customize** the page components while keeping all data-fetching and API logic from `@btst/stack`: ```bash -# Install a single plugin's UI -npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-blog.json +# Install a single plugin's UI (for example, Media) +npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json # Or install the full collection npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/registry.json From 1d36169e87e77ddc6263c19de1ea6692c0705a68 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 17:23:04 -0400 Subject: [PATCH 28/29] feat: add Comments and Media plugin support to shadcn registry documentation, including installation instructions and usage examples --- docs/content/docs/plugins/media.mdx | 50 +++++++++++++++ docs/content/docs/shadcn-registry.mdx | 63 ++++++++++++++++++- .../stack/src/plugins/media/client/plugin.tsx | 23 +++++-- 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/plugins/media.mdx b/docs/content/docs/plugins/media.mdx index 8a6f793e..4b1ced72 100644 --- a/docs/content/docs/plugins/media.mdx +++ b/docs/content/docs/plugins/media.mdx @@ -611,3 +611,53 @@ Authorization hooks are not called when you use `stack().api.media.*` or direct The Media plugin does not currently support build-time SSG prefetching. Its client loader warns when no server is available at build time, and the library route is intended to run against a live API. Use the Media plugin for authenticated dashboards, admin tools, and editor workflows rather than static public pages. + +## Shadcn Registry + +The Media plugin UI layer is distributed as a [shadcn registry](https://ui.shadcn.com/docs/registry) block. Use the registry to **eject and fully customize** the page components while keeping all data-fetching and API logic from `@btst/stack`. + + +The registry installs only the view layer. Hooks and data-fetching continue to come from `@btst/stack/plugins/media/client/hooks`. + + + + + ```bash + npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json + ``` + + + ```bash + pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json + ``` + + + ```bash + bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json + ``` + + + +This copies the media page components into `src/components/btst/media/client/` in your project. All relative imports remain valid and you can edit the files freely while the plugin's data layer stays intact. + +### Using ejected components + +After installing, wire your custom components into the plugin via the `pageComponents` option in your client plugin config: + +```tsx title="lib/stack-client.tsx" +import { mediaClientPlugin } from "@btst/stack/plugins/media/client" +import { LibraryPageComponent } from "@/components/btst/media/client/components/pages/library-page" + +mediaClientPlugin({ + apiBaseURL: "...", + apiBasePath: "/api/data", + siteBaseURL: "...", + siteBasePath: "/pages", + queryClient, + pageComponents: { + library: LibraryPageComponent, // replaces the media library page + }, +}) +``` + +The ejected library page still relies on your `media` `StackProvider` overrides for API configuration, navigation, upload mode, and hooks. diff --git a/docs/content/docs/shadcn-registry.mdx b/docs/content/docs/shadcn-registry.mdx index a23bc2aa..3b228e57 100644 --- a/docs/content/docs/shadcn-registry.mdx +++ b/docs/content/docs/shadcn-registry.mdx @@ -6,7 +6,7 @@ description: Eject and fully customize plugin UI components using the shadcn reg import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; -import { BookOpen, Bot, Database, FileText, Layout, Columns3 } from "lucide-react"; +import { BookOpen, Bot, Database, FileText, Layout, Columns3, MessageSquare, ImageIcon } from "lucide-react"; Every BTST plugin ships its page components as a [shadcn v4 registry](https://ui.shadcn.com/docs/registry) block. This lets you **eject the entire view layer** into your own codebase and customize it freely — while all data-fetching, API logic, hooks, and routing stay untouched inside `@btst/stack`. @@ -32,6 +32,8 @@ Pick the plugin you want to customize: } description="Form list, editor, submissions pages" /> } description="Page list, page builder editor" /> } description="Boards list, board detail page" /> + } description="Moderation pages, user comments pages, and reusable thread UI" /> + } description="Media library page and reusable picker UI" /> Or install a single plugin's UI directly: @@ -56,6 +58,12 @@ Or install a single plugin's UI directly: # Kanban npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-kanban.json + + # Comments + npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-comments.json + + # Media + npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json ``` @@ -77,6 +85,12 @@ Or install a single plugin's UI directly: # Kanban pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-kanban.json + + # Comments + pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-comments.json + + # Media + pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json ``` @@ -98,13 +112,19 @@ Or install a single plugin's UI directly: # Kanban bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-kanban.json + + # Comments + bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-comments.json + + # Media + bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json ``` ## Wire up ejected components -After the install, import your ejected components and pass them to the client plugin via `pageComponents`. Any key you omit falls back to the built-in default, so you only need to override the pages you actually want to change. +After the install, most plugins let you import your ejected components and pass them to the client plugin via `pageComponents`. Any key you omit falls back to the built-in default, so you only need to override the pages you actually want to change. ### Blog @@ -221,8 +241,46 @@ kanbanClientPlugin({ }) ``` +### Comments + +Comments is a little different: the ejected UI is typically rendered directly in your app rather than passed through a `pageComponents` option. + +```tsx title="app/comments/moderation/page.tsx" +import { ModerationPageComponent } from "@/components/btst/comments/client/components/pages/moderation-page" + +export default function CommentsModerationPage() { + return +} +``` + +Keep `commentsClientPlugin()` registered and your `comments` overrides configured in `StackProvider`. The ejected page components will continue to use the shared hooks from `@btst/stack/plugins/comments/client/hooks`. + +### Media + +Media uses a single `pageComponents.library` override for the media library page. + +```tsx title="lib/stack-client.tsx" +import { mediaClientPlugin } from "@btst/stack/plugins/media/client" +import { LibraryPageComponent } from "@/components/btst/media/client/components/pages/library-page" + +mediaClientPlugin({ + apiBaseURL: "...", + apiBasePath: "/api/data", + siteBaseURL: "...", + siteBasePath: "/pages", + queryClient, + pageComponents: { + library: LibraryPageComponent, // media library page + }, +}) +``` + +Keep your `media` overrides configured in `StackProvider` so the ejected component can resolve API config, upload mode, and hooks correctly. + ## Available `pageComponents` keys +The table below covers the plugins that currently support `pageComponents` overrides directly. Comments still use the direct-import pattern shown above. + | Plugin | Key | Props | Description | |---|---|---|---| | Blog | `posts` | — | Published posts list | @@ -247,6 +305,7 @@ kanbanClientPlugin({ | Kanban | `boards` | — | Boards list | | Kanban | `newBoard` | — | New board | | Kanban | `board` | `{ boardId: string }` | Board detail | +| Media | `library` | — | Media library page | ## What the registry installs diff --git a/packages/stack/src/plugins/media/client/plugin.tsx b/packages/stack/src/plugins/media/client/plugin.tsx index 5d729a36..df1fb7aa 100644 --- a/packages/stack/src/plugins/media/client/plugin.tsx +++ b/packages/stack/src/plugins/media/client/plugin.tsx @@ -4,6 +4,7 @@ import { isConnectionError, } from "@btst/stack/plugins/client"; import { createRoute } from "@btst/yar"; +import type { ComponentType } from "react"; import type { QueryClient } from "@tanstack/react-query"; import { LibraryPageComponent } from "./components/pages/library-page"; import { createMediaQueryKeys } from "../query-keys"; @@ -46,6 +47,15 @@ export interface MediaClientConfig { headers?: HeadersInit; /** Optional lifecycle hooks for the media client plugin */ hooks?: MediaClientHooks; + /** + * Optional page component overrides. + * Replace any plugin page with a custom React component. + * The built-in component is used as the fallback when not provided. + */ + pageComponents?: { + /** Replaces the media library page */ + library?: ComponentType; + }; } /** @@ -72,11 +82,14 @@ export const mediaClientPlugin = (config: MediaClientConfig) => name: "media", routes: () => ({ - library: createRoute("/media", () => ({ - PageComponent: LibraryPageComponent, - loader: createMediaLibraryLoader(config), - meta: createMediaLibraryMeta(config), - })), + library: createRoute("/media", () => { + const CustomLibrary = config.pageComponents?.library; + return { + PageComponent: CustomLibrary ?? LibraryPageComponent, + loader: createMediaLibraryLoader(config), + meta: createMediaLibraryMeta(config), + }; + }), }), }); From 453a25e583255800877d17902d38e72be0a0a855 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Fri, 20 Mar 2026 17:27:26 -0400 Subject: [PATCH 29/29] docs: clarify routing for plugin pages in shadcn registry documentation, emphasizing the use of pageComponents and direct-import patterns --- AGENTS.md | 1 + CONTRIBUTING.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ff44cac0..b3e4ae36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -748,6 +748,7 @@ Plugin UI pages are distributed as a shadcn v4 registry so consumers can eject a ### Key design rules - **Hooks are excluded** from the registry. Components import hooks from `@btst/stack/plugins/{name}/client/hooks`. Only the view layer is ejectable. +- **Routable plugin pages should be wired back in via `pageComponents`** on the client plugin config when the plugin supports page overrides. If a plugin intentionally does not support `pageComponents`, document the direct-import rendering pattern clearly in the plugin docs and shared shadcn registry guide. - **`@workspace/ui` imports are rewritten**: standard shadcn components → `registryDependencies`; custom components (`page-wrapper`, `empty`, etc.) → embedded as `registry:component` files from `packages/ui/src/`; multi-file components (`auto-form`, `minimal-tiptap`, `ui-builder`) → external registry URL in `registryDependencies`. - **Directory structure is preserved**: source files land at `src/components/btst/{name}/client/{relative}` so all relative imports remain valid with no rewriting. - **`EXTERNAL_REGISTRY_COMPONENTS`** in `build-registry.ts` maps directory-based workspace/ui components to their external registry URLs. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 595bc5fe..4aa09ce2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -792,6 +792,8 @@ npx shadcn@latest add "https://raw.githubusercontent.com/better-stack-ai/better- Files are installed into `src/components/btst/{plugin}/client/` with all relative imports preserved. Data-fetching hooks remain in `@btst/stack`. +When a plugin exposes `pageComponents` on its client config, wire the ejected routable pages back in through that option. If a plugin intentionally does not support `pageComponents`, document the direct-import rendering pattern clearly in the plugin docs and the shared shadcn registry guide. + ### Rebuild the registry locally ```bash

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