@murumets-ee/media 0.66.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/picker.d.mts.map +1 -1
- package/dist/picker.mjs +1 -1
- package/dist/picker.mjs.map +1 -1
- package/package.json +8 -8
package/dist/picker.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/types.ts","../src/picker/admin-callbacks.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/media-type-icon.ts","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":";;;;;;UAGiB,eAAA;EACf,EAAA;EACA,KAAA;EACA,GAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,GAAA;AAAA;;UAIe,qBAAA;EACf,KAAA,EAAO,eAAe;EACtB,KAAA;EANG;AAAA;AAIL;;;EAQE,UAAA;EAPA;EASA,MAAA;AAAA;;UAIe,oBAAA;EAJT;EAMN,UAAA,GAAa,OAAA;IACX,MAAA;IACA,SAAA;IACA,KAAA;IACA,MAAA;EAAA,MACI,OAAA,CAAQ,qBAAA;EAGM;EAApB,WAAA,GAAc,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,eAAA;AAAA;;KAI3B,eAAA;;UAGK,qBAAA;EACf,OAAA;EACA,OAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA;EACA,WAAA;EACA,UAAA;EACA,IAAA;EACA,IAAA;EACA,YAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,gBAAA;EACA,MAAA;EACA,aAAA;EACA,YAAA;EACA,OAAA;EACA,KAAA;AAAA;;UAIe,gBAAA;EAtBf;EAwBA,IAAA;EAtBA;EAwBA,YAAA,GAAe,IAAA;EAtBf;EAwBA,QAAA,GAAW,KAAA,EAAO,eAAA;EAtBlB;EAwBA,IAAA,GAAO,eAAA;EAtBP;EAwBA,MAAA;EAtBA;EAwBA,SAAA;EAtBA;EAwBA,SAAA;EAtBA;EAwBA,WAAA;EAtBA;EAwBA,KAAA;EAtBA;EAwBA,WAAA;EAxBK;EA0BL,UAAA,GAAa,qBAAA;EAtBkB;EAwB/B,SAAA;EAlBkB;EAoBlB,QAAA,GAAW,SAAA;AAAA;;;;KCpFD,mBAAA,GAAsB,oBAAA;EAChC,WAAA,GAAc,EAAA,aAAe,OAAO;AAAA;;;;;;ADDjC;AAIL;;;;;;;;;;AAUQ;iBCOQ,yBAAA,CAA0B,WAAA,YAA6B,mBAAmB;;;UC7BhF,cAAA;EACR,IAAA,EAAM,eAAA;EACN,UAAA;EACA,QAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAGpB,SAAA,CAAA;EAAY,IAAA;EAAM,UAAA;EAAY,QAAA;EAAU;AAAA,GAAc,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCR1E,cAAA;EACR,KAAA,EAAO,eAAA;EACP,QAAA,EAAU,GAAA;EACV,QAAA,GAAW,IAAA,EAAM,eAAA;EACjB,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;EACf,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;
|
|
1
|
+
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/types.ts","../src/picker/admin-callbacks.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/media-type-icon.ts","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":";;;;;;UAGiB,eAAA;EACf,EAAA;EACA,KAAA;EACA,GAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,GAAA;AAAA;;UAIe,qBAAA;EACf,KAAA,EAAO,eAAe;EACtB,KAAA;EANG;AAAA;AAIL;;;EAQE,UAAA;EAPA;EASA,MAAA;AAAA;;UAIe,oBAAA;EAJT;EAMN,UAAA,GAAa,OAAA;IACX,MAAA;IACA,SAAA;IACA,KAAA;IACA,MAAA;EAAA,MACI,OAAA,CAAQ,qBAAA;EAGM;EAApB,WAAA,GAAc,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,eAAA;AAAA;;KAI3B,eAAA;;UAGK,qBAAA;EACf,OAAA;EACA,OAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA;EACA,WAAA;EACA,UAAA;EACA,IAAA;EACA,IAAA;EACA,YAAA;EACA,SAAA;EACA,SAAA;EACA,UAAA;EACA,gBAAA;EACA,MAAA;EACA,aAAA;EACA,YAAA;EACA,OAAA;EACA,KAAA;AAAA;;UAIe,gBAAA;EAtBf;EAwBA,IAAA;EAtBA;EAwBA,YAAA,GAAe,IAAA;EAtBf;EAwBA,QAAA,GAAW,KAAA,EAAO,eAAA;EAtBlB;EAwBA,IAAA,GAAO,eAAA;EAtBP;EAwBA,MAAA;EAtBA;EAwBA,SAAA;EAtBA;EAwBA,SAAA;EAtBA;EAwBA,WAAA;EAtBA;EAwBA,KAAA;EAtBA;EAwBA,WAAA;EAxBK;EA0BL,UAAA,GAAa,qBAAA;EAtBkB;EAwB/B,SAAA;EAlBkB;EAoBlB,QAAA,GAAW,SAAA;AAAA;;;;KCpFD,mBAAA,GAAsB,oBAAA;EAChC,WAAA,GAAc,EAAA,aAAe,OAAO;AAAA;;;;;;ADDjC;AAIL;;;;;;;;;;AAUQ;iBCOQ,yBAAA,CAA0B,WAAA,YAA6B,mBAAmB;;;UC7BhF,cAAA;EACR,IAAA,EAAM,eAAA;EACN,UAAA;EACA,QAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAGpB,SAAA,CAAA;EAAY,IAAA;EAAM,UAAA;EAAY,QAAA;EAAU;AAAA,GAAc,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCR1E,cAAA;EACR,KAAA,EAAO,eAAA;EACP,QAAA,EAAU,GAAA;EACV,QAAA,GAAW,IAAA,EAAM,eAAA;EACjB,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;EACf,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;iBCsCD,WAAA,CAAA;EACd,IAAA;EACA,YAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,WAAA;EACA,UAAA;EACA,SAAA;EACA;AAAA,GACC,gBAAA,+BAAgB,GAAA,CAAA,OAAA;;;;;;AJ3EnB;;;;;;;;;;;;;KKeK,iBAAA;;ALLA;AAIL;;;;;;;cKYa,0BAAA,EAA4B,MAAA,CAAO,iBAAA,EAAmB,UAAA;;iBAQnD,aAAA,CAAc,SAAA,YAAqB,iBAAA,GAAoB,UAAU;;;UChChE,wBAAA,SAAiC,oBAAoB;EACpE,QAAA,EAAU,SAAA;AAAA;;;;;;;;;;;;;;;iBAiBI,mBAAA,CAAA;EACd,QAAA;EACA,UAAA;EACA;AAAA,GACC,wBAAA,+BAAwB,GAAA,CAAA,OAAA;AAAA,iBASX,cAAA,CAAA,GAAkB,oBAAoB;;;UChC5C,cAAA;EACR,KAAA;EACA,QAAA,GAAW,KAAA;EACX,SAAA;EACA,iBAAA,GAAoB,IAAA;;EAEpB,MAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAWpB,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,SAAA;EACA,iBAAA;EACA,MAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCxBP,eAAA;EACR,QAAA,GAAW,IAAA,EAAM,IAAA,KAAS,OAAA;EAC1B,WAAA;EACA,MAAA;EACA,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,UAAA,CAAA;EAAa,QAAA;EAAU,WAAA;EAAa,MAAA;EAAQ;AAAA,GAAc,eAAA,+BAAe,GAAA,CAAA,OAAA"}
|
package/dist/picker.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import{Check as e,File as t,FileText as n,Film as r,Music as i,Search as a,Upload as o,X as s}from"lucide-react";import{clsx as c}from"clsx";import{twMerge as l}from"tailwind-merge";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import*as p from"@radix-ui/react-dialog";import*as m from"@radix-ui/react-visually-hidden";import{createContext as h,useCallback as g,useContext as _,useEffect as v,useMemo as y,useRef as b,useState as x}from"react";function S(e=`/api/admin`){let t=`${e}/media`,n=async e=>{try{let t=await e.clone().json();if(typeof t.error==`string`&&t.error.length>0)return t.error}catch{}try{let t=await e.text();if(t)return t}catch{}return`HTTP ${String(e.status)}`};return{fetchMedia:async e=>{let r=new URLSearchParams;e.search&&r.set(`search`,e.search),e.mediaType&&r.set(`mediaType`,e.mediaType),r.set(`limit`,String(e.limit)),r.set(`offset`,String(e.offset));let i=`${t}?${r.toString()}`,a=await fetch(i);if(!a.ok)throw Error(await n(a));return a.json()},uploadMedia:async e=>{let r=new FormData;r.append(`file`,e),r.append(`visibility`,`public`);let i=await fetch(t,{method:`POST`,body:r});if(!i.ok)throw Error(await n(i));return i.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function C(...e){return l(c(e))}const w={video:r,audio:i,document:n,other:t};function T(e){return e===`image`?null:w[e]}function E({item:t,isSelected:n,onToggle:r,classNames:i}){let a=t.mediaType===`image`,o=T(t.mediaType);return f(`button`,{type:`button`,onClick:r,className:C(`group relative aspect-square overflow-hidden rounded-lg border-2 transition-all`,`focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950`,n?`border-blue-500 ring-2 ring-blue-500/20`:`border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700`,i?.card,n&&i?.cardSelected),children:[a?d(`img`,{src:t.url,alt:t.alt??t.filename,className:C(`h-full w-full object-cover`,i?.cardImage),loading:`lazy`}):d(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:o&&d(o,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),n&&d(`div`,{className:`absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white`,children:d(e,{className:`h-3 w-3`})}),d(`div`,{className:C(`absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5`,`opacity-0 transition-opacity group-hover:opacity-100`,i?.cardLabel),children:d(`p`,{className:`truncate text-xs text-white`,children:t.title??t.filename})})]})}function D({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>d(`div`,{className:C(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return d(`div`,{className:C(`py-12 text-center text-sm text-zinc-500 dark:text-zinc-400`,c?.empty),children:`No media found. Upload a file to get started.`});let l=Math.ceil(i/o),p=Math.floor(a/o)+1;return f(u,{children:[d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>d(E,{item:e,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&f(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[d(`button`,{type:`button`,disabled:p<=1,onClick:()=>s(a-o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Prev`}),f(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[p,` / `,l]}),d(`button`,{type:`button`,disabled:p>=l,onClick:()=>s(a+o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Next`})]})]})}const O=h(null);function k({children:e,fetchMedia:t,uploadMedia:n}){return d(O,{value:y(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function A(){let e=_(O);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const j=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function M({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:o}){let s=!i;return f(`div`,{className:`flex items-center gap-3`,children:[f(`div`,{className:`relative flex-1`,children:[d(a,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),d(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:C(`w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm`,`placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500`,`dark:border-zinc-700 dark:placeholder:text-zinc-500`,o?.searchInput)})]}),s&&d(`div`,{className:C(`flex gap-1`,o?.filterTabs),children:j.map(e=>d(`button`,{type:`button`,onClick:()=>r(e.value),className:C(`rounded-md px-3 py-1.5 text-xs font-medium transition-colors`,n===e.value?`bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900`:`text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800`),children:e.label},e.label))})]})}function N({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=b(null),[a,s]=x(!1),c=g(async t=>{t.preventDefault(),s(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),l=g(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return f(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),s(!0)},onDragLeave:()=>s(!1),onDrop:c,onClick:()=>i.current?.click(),className:C(`mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors`,a?`border-blue-500 bg-blue-50 dark:bg-blue-950/20`:`border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600`,r?.uploadZone,a&&r?.uploadZoneActive),children:[d(o,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),d(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),d(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:l,className:`hidden`})]})}function P({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:c=[],title:l=`Select Media`,description:h,classNames:_,className:y,children:S}){let{fetchMedia:w,uploadMedia:T}=A(),[E,O]=x([]),[k,j]=x(0),[P,F]=x(()=>new Set(c)),[I,L]=x(``),[R,z]=x(a),[B,V]=x(!1),[H,U]=x(!1),[W,G]=x(0),[K,q]=x(null),[J,Y]=x(null),X=g(async()=>{V(!0),q(null);try{let e=await w({search:I||void 0,mediaType:R,limit:24,offset:W});e.configured===!1?(Y(e.reason??`Media storage is not configured on this server.`),O([]),j(0)):(Y(null),O(e.items),j(e.total))}catch(e){q(e instanceof Error&&e.message?`Failed to load media: ${e.message}`:`Failed to load media.`)}finally{V(!1)}},[w,I,R,W]);v(()=>{e&&X()},[e,X]);let Z=b(e),Q=b(c);Q.current=c,v(()=>{Z.current&&!e&&(L(``),G(0),F(new Set(Q.current))),Z.current=e},[e]);let $=g(e=>{if(r===`single`){n([e]),t(!1);return}F(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,n,t]),ee=g(()=>{n(E.filter(e=>P.has(e.id))),t(!1)},[E,P,n,t]),te=g(async e=>{U(!0),q(null);try{let i=await T(e);if(r===`single`){n([i]),t(!1);return}O(e=>[i,...e]),j(e=>e+1),F(e=>new Set([...e,i.id]))}catch(e){q(e instanceof Error&&e.message?`Upload failed: ${e.message}`:`Upload failed. Please try again.`)}finally{U(!1)}},[T,r,n,t]);return d(p.Root,{open:e,onOpenChange:t,children:f(p.Portal,{children:[d(p.Overlay,{className:C(`fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,_?.overlay)}),f(p.Content,{...!h&&{"aria-describedby":void 0},className:C(`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2`,`flex max-h-[85vh] w-[90vw] max-w-4xl flex-col`,`rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950`,_?.content,y),children:[f(`div`,{className:C(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,_?.header),children:[d(p.Title,{className:C(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,_?.title),children:l}),h&&d(m.Root,{asChild:!0,children:d(p.Description,{children:h})}),f(p.Close,{className:`rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600`,children:[d(s,{className:`h-4 w-4`}),d(m.Root,{children:`Close`})]})]}),d(`div`,{className:C(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,_?.toolbar),children:d(M,{value:I,onChange:L,mediaType:R,onMediaTypeChange:z,locked:!!a,classNames:_})}),f(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[K&&d(`div`,{role:`alert`,className:`mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400`,children:K}),J?f(`div`,{role:`alert`,className:`rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30`,children:[d(`p`,{className:`text-sm font-medium text-amber-900 dark:text-amber-200`,children:`Media storage is not configured`}),d(`p`,{className:`mt-1 text-xs text-amber-800 dark:text-amber-300`,children:J}),d(`p`,{className:`mt-3 text-xs text-amber-700 dark:text-amber-400`,children:`Set the required environment variables and restart the app to enable uploads.`})]}):f(u,{children:[d(N,{onUpload:te,isUploading:H,accept:i,classNames:_}),d(D,{items:E,selected:P,onToggle:$,isLoading:B,total:k,offset:W,limit:24,onPageChange:G,classNames:_})]})]}),f(`div`,{className:C(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,_?.footer),children:[d(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${k.toString()} item${k===1?``:`s`} — click to select`:P.size>0?`${P.size.toString()} selected`:`${k.toString()} item${k===1?``:`s`}`}),f(`div`,{className:`flex gap-2`,children:[S,d(p.Close,{asChild:!0,children:d(`button`,{type:`button`,className:C(`rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50`,`dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900`,_?.cancelButton),children:`Cancel`})}),r!==`single`&&d(`button`,{type:`button`,onClick:ee,disabled:P.size===0,className:C(`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500`,`disabled:cursor-not-allowed disabled:opacity-50`,_?.confirmButton),children:`Select (${P.size.toString()})`})]})]})]})]})})}export{E as MediaCard,D as MediaGrid,P as MediaPicker,k as MediaPickerProvider,w as NON_IMAGE_MEDIA_TYPE_ICONS,M as SearchBar,N as UploadZone,S as createAdminMediaCallbacks,T as mediaTypeIcon,A as useMediaPicker};
|
|
2
|
+
import{Check as e,File as t,FileText as n,Film as r,Music as i,Search as a,Upload as o,X as s}from"lucide-react";import{clsx as c}from"clsx";import{twMerge as l}from"tailwind-merge";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import*as p from"@radix-ui/react-dialog";import*as m from"@radix-ui/react-visually-hidden";import{createContext as h,useCallback as g,useContext as _,useEffect as v,useMemo as y,useRef as b,useState as x}from"react";function S(e=`/api/admin`){let t=`${e}/media`,n=async e=>{try{let t=await e.clone().json();if(typeof t.error==`string`&&t.error.length>0)return t.error}catch{}try{let t=await e.text();if(t)return t}catch{}return`HTTP ${String(e.status)}`};return{fetchMedia:async e=>{let r=new URLSearchParams;e.search&&r.set(`search`,e.search),e.mediaType&&r.set(`mediaType`,e.mediaType),r.set(`limit`,String(e.limit)),r.set(`offset`,String(e.offset));let i=`${t}?${r.toString()}`,a=await fetch(i);if(!a.ok)throw Error(await n(a));return a.json()},uploadMedia:async e=>{let r=new FormData;r.append(`file`,e),r.append(`visibility`,`public`);let i=await fetch(t,{method:`POST`,body:r});if(!i.ok)throw Error(await n(i));return i.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function C(...e){return l(c(e))}const w={video:r,audio:i,document:n,other:t};function T(e){return e===`image`?null:w[e]}function E({item:t,isSelected:n,onToggle:r,classNames:i}){let a=t.mediaType===`image`,o=T(t.mediaType);return f(`button`,{type:`button`,onClick:r,className:C(`group relative aspect-square overflow-hidden rounded-lg border-2 transition-all`,`focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950`,n?`border-blue-500 ring-2 ring-blue-500/20`:`border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700`,i?.card,n&&i?.cardSelected),children:[a?d(`img`,{src:t.url,alt:t.alt??t.filename,className:C(`h-full w-full object-cover`,i?.cardImage),loading:`lazy`}):d(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:o&&d(o,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),n&&d(`div`,{className:`absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white`,children:d(e,{className:`h-3 w-3`})}),d(`div`,{className:C(`absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5`,`opacity-0 transition-opacity group-hover:opacity-100`,i?.cardLabel),children:d(`p`,{className:`truncate text-xs text-white`,children:t.title??t.filename})})]})}function D({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>d(`div`,{className:C(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return d(`div`,{className:C(`py-12 text-center text-sm text-zinc-500 dark:text-zinc-400`,c?.empty),children:`No media found. Upload a file to get started.`});let l=Math.ceil(i/o),p=Math.floor(a/o)+1;return f(u,{children:[d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>d(E,{item:e,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&f(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[d(`button`,{type:`button`,disabled:p<=1,onClick:()=>s(a-o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Prev`}),f(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[p,` / `,l]}),d(`button`,{type:`button`,disabled:p>=l,onClick:()=>s(a+o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Next`})]})]})}const O=h(null);function k({children:e,fetchMedia:t,uploadMedia:n}){return d(O,{value:y(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function A(){let e=_(O);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const j=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function M({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:o}){let s=!i;return f(`div`,{className:`flex items-center gap-3`,children:[f(`div`,{className:`relative flex-1`,children:[d(a,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),d(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:C(`w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm`,`placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500`,`dark:border-zinc-700 dark:placeholder:text-zinc-500`,o?.searchInput)})]}),s&&d(`div`,{className:C(`flex gap-1`,o?.filterTabs),children:j.map(e=>d(`button`,{type:`button`,onClick:()=>r(e.value),className:C(`rounded-md px-3 py-1.5 text-xs font-medium transition-colors`,n===e.value?`bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900`:`text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800`),children:e.label},e.label))})]})}function N({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=b(null),[a,s]=x(!1),c=g(async t=>{t.preventDefault(),s(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),l=g(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return f(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),s(!0)},onDragLeave:()=>s(!1),onDrop:c,onClick:()=>i.current?.click(),className:C(`mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors`,a?`border-blue-500 bg-blue-50 dark:bg-blue-950/20`:`border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600`,r?.uploadZone,a&&r?.uploadZoneActive),children:[d(o,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),d(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),d(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:l,className:`hidden`})]})}const P=new Set([`image`,`video`,`audio`]);function F(e){return e!==void 0&&P.has(e)?[`${e}/*`]:void 0}const I=`file`;function ee(e,t){return e===void 0||t.type===``?!0:e===I?!t.type.startsWith(`image/`)&&!t.type.startsWith(`video/`):P.has(e)?t.type.startsWith(`${e}/`):!0}function L(e,t){if(e===void 0||t.mediaType===e)return null;let n=t.title??t.filename;return e===I?t.mediaType!==`image`&&t.mediaType!==`video`?null:`This field takes a document or other file — “${n}” is ${R(t.mediaType)}.`:`This field takes ${e} files — “${n}” is ${t.mediaType===`other`?`not one`:R(t.mediaType)}.`}function R(e){return/^[aeiou]/.test(e)?`an ${e}`:`a ${e}`}function z({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:c=[],title:l=`Select Media`,description:h,classNames:_,className:y,children:S}){let{fetchMedia:w,uploadMedia:T}=A(),[E,O]=x([]),[k,j]=x(0),[P,R]=x(()=>new Set(c)),[z,B]=x(``),[te,ne]=x(void 0),V=a??te,H=a===I?void 0:V,re=i??F(a),[ie,U]=x(!1),[ae,W]=x(!1),[G,K]=x(0),[q,J]=x(null),[Y,X]=x(null),Z=g(async()=>{U(!0),J(null);try{let e=await w({search:z||void 0,mediaType:H,limit:24,offset:G});e.configured===!1?(X(e.reason??`Media storage is not configured on this server.`),O([]),j(0)):(X(null),O(e.items),j(e.total))}catch(e){J(e instanceof Error&&e.message?`Failed to load media: ${e.message}`:`Failed to load media.`)}finally{U(!1)}},[w,z,H,G]);v(()=>{e&&Z()},[e,Z]);let Q=b(e),$=b(c);$.current=c,v(()=>{Q.current&&!e&&(B(``),K(0),R(new Set($.current))),Q.current=e},[e]);let oe=g(e=>{let i=L(a,e);if(i!==null){J(i);return}if(r===`single`){n([e]),t(!1);return}R(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,a,n,t]),se=g(()=>{n(E.filter(e=>P.has(e.id))),t(!1)},[E,P,n,t]),ce=g(async e=>{if(J(null),!ee(a,e)){J(a===I?`This field takes a document or other file — “${e.name}” is an image or a video.`:`This field takes ${a} files — “${e.name}” is not one.`);return}W(!0);try{let i=await T(e),o=L(a,i);if(o!==null){J(o);return}if(r===`single`){n([i]),t(!1);return}O(e=>[i,...e]),j(e=>e+1),R(e=>new Set([...e,i.id]))}catch(e){J(e instanceof Error&&e.message?`Upload failed: ${e.message}`:`Upload failed. Please try again.`)}finally{W(!1)}},[T,r,a,n,t]);return d(p.Root,{open:e,onOpenChange:t,children:f(p.Portal,{children:[d(p.Overlay,{className:C(`fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,_?.overlay)}),f(p.Content,{...!h&&{"aria-describedby":void 0},className:C(`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2`,`flex max-h-[85vh] w-[90vw] max-w-4xl flex-col`,`rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950`,_?.content,y),children:[f(`div`,{className:C(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,_?.header),children:[d(p.Title,{className:C(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,_?.title),children:l}),h&&d(m.Root,{asChild:!0,children:d(p.Description,{children:h})}),f(p.Close,{className:`rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600`,children:[d(s,{className:`h-4 w-4`}),d(m.Root,{children:`Close`})]})]}),d(`div`,{className:C(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,_?.toolbar),children:d(M,{value:z,onChange:B,mediaType:V,onMediaTypeChange:ne,locked:!!a,classNames:_})}),f(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[q&&d(`div`,{role:`alert`,className:`mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400`,children:q}),Y?f(`div`,{role:`alert`,className:`rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30`,children:[d(`p`,{className:`text-sm font-medium text-amber-900 dark:text-amber-200`,children:`Media storage is not configured`}),d(`p`,{className:`mt-1 text-xs text-amber-800 dark:text-amber-300`,children:Y}),d(`p`,{className:`mt-3 text-xs text-amber-700 dark:text-amber-400`,children:`Set the required environment variables and restart the app to enable uploads.`})]}):f(u,{children:[d(N,{onUpload:ce,isUploading:ae,accept:re,classNames:_}),d(D,{items:E,selected:P,onToggle:oe,isLoading:ie,total:k,offset:G,limit:24,onPageChange:K,classNames:_})]})]}),f(`div`,{className:C(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,_?.footer),children:[d(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${k.toString()} item${k===1?``:`s`} — click to select`:P.size>0?`${P.size.toString()} selected`:`${k.toString()} item${k===1?``:`s`}`}),f(`div`,{className:`flex gap-2`,children:[S,d(p.Close,{asChild:!0,children:d(`button`,{type:`button`,className:C(`rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50`,`dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900`,_?.cancelButton),children:`Cancel`})}),r!==`single`&&d(`button`,{type:`button`,onClick:se,disabled:P.size===0,className:C(`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500`,`disabled:cursor-not-allowed disabled:opacity-50`,_?.confirmButton),children:`Select (${P.size.toString()})`})]})]})]})]})})}export{E as MediaCard,D as MediaGrid,z as MediaPicker,k as MediaPickerProvider,w as NON_IMAGE_MEDIA_TYPE_ICONS,M as SearchBar,N as UploadZone,S as createAdminMediaCallbacks,T as mediaTypeIcon,A as useMediaPicker};
|
|
3
3
|
//# sourceMappingURL=picker.mjs.map
|
package/dist/picker.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.mjs","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/lib/cn.ts","../src/picker/media-type-icon.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx","../src/picker/media-picker.tsx"],"sourcesContent":["/**\n * Pre-built media callbacks that talk to the admin API.\n *\n * Eliminates boilerplate in every project — just call:\n * const media = createAdminMediaCallbacks('/api/admin')\n *\n * Returns callbacks compatible with both MediaPickerProvider and\n * BlockEditor's `media` prop (with getMediaUrl for thumbnail previews).\n */\n\nimport type { MediaPickerCallbacks, MediaPickerItem, MediaPickerListResult } from './types'\n\n/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */\nexport type AdminMediaCallbacks = MediaPickerCallbacks & {\n getMediaUrl: (id: string) => Promise<string | null>\n}\n\n/**\n * Create media callbacks that talk to the admin API.\n *\n * @param apiBasePath - Base path for admin API (default: '/api/admin')\n * @returns Callbacks for MediaPickerProvider + BlockEditor media prop\n *\n * @example\n * ```tsx\n * import { createAdminMediaCallbacks } from '@murumets-ee/media/picker'\n *\n * const media = createAdminMediaCallbacks()\n * // Use with BlockEditor:\n * <BlockEditor media={media} ... />\n * // Use with MediaPickerProvider:\n * <MediaPickerProvider fetchMedia={media.fetchMedia} uploadMedia={media.uploadMedia}>\n * ```\n */\nexport function createAdminMediaCallbacks(apiBasePath = '/api/admin'): AdminMediaCallbacks {\n const baseUrl = `${apiBasePath}/media`\n\n // Admin API error responses are `{ error: string }`. Parse the body so\n // the real cause (e.g. \"Missing required environment variable:\n // STORAGE_ENDPOINT\") reaches the UI instead of just the status code.\n const readErrorMessage = async (res: Response): Promise<string> => {\n try {\n const body = (await res.clone().json()) as { error?: unknown }\n if (typeof body.error === 'string' && body.error.length > 0) return body.error\n } catch {\n // fall through — response wasn't JSON\n }\n try {\n const text = await res.text()\n if (text) return text\n } catch {\n // ignore\n }\n return `HTTP ${String(res.status)}`\n }\n\n return {\n fetchMedia: async (options: {\n search?: string | undefined\n mediaType?: string | undefined\n limit: number\n offset: number\n }): Promise<MediaPickerListResult> => {\n const params = new URLSearchParams()\n if (options.search) params.set('search', options.search)\n if (options.mediaType) params.set('mediaType', options.mediaType)\n params.set('limit', String(options.limit))\n params.set('offset', String(options.offset))\n\n const url = `${baseUrl}?${params.toString()}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerListResult>\n },\n\n uploadMedia: async (file: File): Promise<MediaPickerItem> => {\n const formData = new FormData()\n formData.append('file', file)\n // The media LIBRARY is publishable web assets, so it opts in explicitly.\n // Without this the upload route's deny-by-default leaves the asset\n // private, `getPublicUrls` returns nothing for it, and every image on the\n // public site resolves to `null` behind a 200 and one warn log (F030).\n // Sent per-upload rather than by flipping a global default, because the\n // same route also receives rent's inspection photos, which must not be\n // world-readable.\n formData.append('visibility', 'public')\n\n const res = await fetch(baseUrl, { method: 'POST', body: formData })\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerItem>\n },\n\n getMediaUrl: async (id: string): Promise<string | null> => {\n try {\n const res = await fetch(`${baseUrl}/${id}`)\n if (!res.ok) return null\n const item = (await res.json()) as MediaPickerItem\n return item.url ?? null\n } catch {\n return null\n }\n },\n }\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import { File, FileText, Film, type LucideIcon, Music } from 'lucide-react'\n\n/**\n * The non-image members of `@murumets-ee/media`'s `MediaType`, copied rather\n * than imported.\n *\n * This module ships from the `./picker` tsdown entry — a build SEPARATE from\n * the root `index.ts` entry that already exports `MediaType` by name. Naming\n * it here too would duplicate its declaration across two `.d.mts` files\n * (`verify:dts`, #767): each array entry in `tsdown.config.ts` builds\n * independently and cannot share a declaration chunk, so a type named in two\n * entries' public APIs is emitted twice, and TypeScript can no longer treat\n * the two copies as identical if either ever gains an invariant member.\n *\n * Kept honest by `media-type-icon.test.ts`, which imports the real\n * `MediaType` (a type-only import, erased before it ever reaches a `.d.mts`)\n * and asserts this literal still matches it exactly.\n */\ntype NonImageMediaType = 'video' | 'audio' | 'document' | 'other'\n\n/**\n * The typed-icon fallback for every media type that is not `'image'`.\n *\n * The ONE rule three admin surfaces branch on (F115): the media list, the\n * picker's `MediaCard` and the media edit form all show a real `<img>` for\n * `mediaType === 'image'` and one of these icons otherwise. Extracted from\n * `MediaCard`, which held the only copy, so a fourth surface reaches for this\n * instead of writing a fourth `Record<MediaType, LucideIcon>`.\n */\nexport const NON_IMAGE_MEDIA_TYPE_ICONS: Record<NonImageMediaType, LucideIcon> = {\n video: Film,\n audio: Music,\n document: FileText,\n other: File,\n}\n\n/** The icon for a non-image media type, or `null` for `'image'` (render the asset itself). */\nexport function mediaTypeIcon(mediaType: 'image' | NonImageMediaType): LucideIcon | null {\n return mediaType === 'image' ? null : NON_IMAGE_MEDIA_TYPE_ICONS[mediaType]\n}\n","import { Check } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport { mediaTypeIcon } from './media-type-icon'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaCardProps {\n item: MediaPickerItem\n isSelected: boolean\n onToggle: () => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaCard({ item, isSelected, onToggle, classNames }: MediaCardProps) {\n const isImage = item.mediaType === 'image'\n const Icon = mediaTypeIcon(item.mediaType)\n\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n className={cn(\n 'group relative aspect-square overflow-hidden rounded-lg border-2 transition-all',\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950',\n isSelected\n ? 'border-blue-500 ring-2 ring-blue-500/20'\n : 'border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700',\n classNames?.card,\n isSelected && classNames?.cardSelected,\n )}\n >\n {/* Thumbnail */}\n {isImage ? (\n <img\n src={item.url}\n alt={item.alt ?? item.filename}\n className={cn('h-full w-full object-cover', classNames?.cardImage)}\n loading=\"lazy\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800\">\n {Icon && <Icon className=\"h-8 w-8 text-zinc-400 dark:text-zinc-500\" />}\n </div>\n )}\n\n {/* Selection checkmark */}\n {isSelected && (\n <div className=\"absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white\">\n <Check className=\"h-3 w-3\" />\n </div>\n )}\n\n {/* Filename on hover */}\n <div\n className={cn(\n 'absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5',\n 'opacity-0 transition-opacity group-hover:opacity-100',\n classNames?.cardLabel,\n )}\n >\n <p className=\"truncate text-xs text-white\">{item.title ?? item.filename}</p>\n </div>\n </button>\n )\n}\n","import { cn } from '../lib/cn'\nimport { MediaCard } from './media-card'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaGridProps {\n items: MediaPickerItem[]\n selected: Set<string>\n onToggle: (item: MediaPickerItem) => void\n isLoading: boolean\n total: number\n offset: number\n limit: number\n onPageChange: (offset: number) => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaGrid({\n items,\n selected,\n onToggle,\n isLoading,\n total,\n offset,\n limit,\n onPageChange,\n classNames,\n}: MediaGridProps) {\n if (isLoading) {\n return (\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {Array.from({ length: 12 }).map((_, i) => (\n <div\n key={`skeleton-${i.toString()}`}\n className={cn(\n 'aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800',\n classNames?.loading,\n )}\n />\n ))}\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <div\n className={cn(\n 'py-12 text-center text-sm text-zinc-500 dark:text-zinc-400',\n classNames?.empty,\n )}\n >\n No media found. Upload a file to get started.\n </div>\n )\n }\n\n const totalPages = Math.ceil(total / limit)\n const currentPage = Math.floor(offset / limit) + 1\n\n return (\n <>\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {items.map((item) => (\n <MediaCard\n key={item.id}\n item={item}\n isSelected={selected.has(item.id)}\n onToggle={() => onToggle(item)}\n classNames={classNames}\n />\n ))}\n </div>\n {totalPages > 1 && (\n <div className=\"mt-4 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={currentPage <= 1}\n onClick={() => onPageChange(offset - limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Prev\n </button>\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {currentPage} / {totalPages}\n </span>\n <button\n type=\"button\"\n disabled={currentPage >= totalPages}\n onClick={() => onPageChange(offset + limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Next\n </button>\n </div>\n )}\n </>\n )\n}\n","import { createContext, type ReactNode, useContext, useMemo } from 'react'\nimport type { MediaPickerCallbacks } from './types'\n\nconst MediaPickerContext = createContext<MediaPickerCallbacks | null>(null)\n\nexport interface MediaPickerProviderProps extends MediaPickerCallbacks {\n children: ReactNode\n}\n\n/**\n * Provides media picker callbacks to all picker instances below.\n * Wrap your admin layout with this provider.\n *\n * @example\n * ```tsx\n * <MediaPickerProvider\n * fetchMedia={fetchMediaAction}\n * uploadMedia={uploadMediaAction}\n * >\n * <AdminShell>...</AdminShell>\n * </MediaPickerProvider>\n * ```\n */\nexport function MediaPickerProvider({\n children,\n fetchMedia,\n uploadMedia,\n}: MediaPickerProviderProps) {\n const value = useMemo<MediaPickerCallbacks>(\n () => ({ fetchMedia, uploadMedia }),\n [fetchMedia, uploadMedia],\n )\n\n return <MediaPickerContext value={value}>{children}</MediaPickerContext>\n}\n\nexport function useMediaPicker(): MediaPickerCallbacks {\n const ctx = useContext(MediaPickerContext)\n if (!ctx) {\n throw new Error(\n 'useMediaPicker must be used within <MediaPickerProvider>. ' +\n 'Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.',\n )\n }\n return ctx\n}\n","import { Search } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface SearchBarProps {\n value: string\n onChange: (value: string) => void\n mediaType: string | undefined\n onMediaTypeChange: (type: string | undefined) => void\n /** When true, the media type filter is locked (no tabs shown) */\n locked?: boolean | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nconst FILTER_OPTIONS = [\n { value: undefined, label: 'All' },\n { value: 'image', label: 'Images' },\n { value: 'video', label: 'Videos' },\n { value: 'audio', label: 'Audio' },\n { value: 'document', label: 'Docs' },\n] as const\n\nexport function SearchBar({\n value,\n onChange,\n mediaType,\n onMediaTypeChange,\n locked,\n classNames,\n}: SearchBarProps) {\n // If locked (caller pre-set the mediaType), hide filter tabs\n const showFilters = !locked\n\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400\" />\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder=\"Search media...\"\n className={cn(\n 'w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm',\n 'placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',\n 'dark:border-zinc-700 dark:placeholder:text-zinc-500',\n classNames?.searchInput,\n )}\n />\n </div>\n {showFilters && (\n <div className={cn('flex gap-1', classNames?.filterTabs)}>\n {FILTER_OPTIONS.map((opt) => (\n <button\n key={opt.label}\n type=\"button\"\n onClick={() => onMediaTypeChange(opt.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-xs font-medium transition-colors',\n mediaType === opt.value\n ? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'\n : 'text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800',\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n )}\n </div>\n )\n}\n","import { Upload } from 'lucide-react'\nimport { type DragEvent, useCallback, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface UploadZoneProps {\n onUpload: (file: File) => Promise<void>\n isUploading: boolean\n accept?: string[] | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function UploadZone({ onUpload, isUploading, accept, classNames }: UploadZoneProps) {\n const inputRef = useRef<HTMLInputElement>(null)\n const [isDragging, setIsDragging] = useState(false)\n\n const handleDrop = useCallback(\n async (e: DragEvent) => {\n e.preventDefault()\n setIsDragging(false)\n const file = e.dataTransfer.files[0]\n if (file) {\n await onUpload(file)\n }\n },\n [onUpload],\n )\n\n const handleFileSelect = useCallback(\n async (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0]\n if (file) {\n await onUpload(file)\n e.target.value = ''\n }\n },\n [onUpload],\n )\n\n return (\n <button\n type=\"button\"\n onDragOver={(e) => {\n e.preventDefault()\n setIsDragging(true)\n }}\n onDragLeave={() => setIsDragging(false)}\n onDrop={handleDrop}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors',\n isDragging\n ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'\n : 'border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600',\n classNames?.uploadZone,\n isDragging && classNames?.uploadZoneActive,\n )}\n >\n <Upload className=\"mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500\" />\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {isUploading ? 'Uploading...' : 'Drop a file here or click to upload'}\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept?.join(',')}\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n </button>\n )\n}\n","import * as DialogPrimitive from '@radix-ui/react-dialog'\nimport * as VisuallyHidden from '@radix-ui/react-visually-hidden'\nimport { X } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport { MediaGrid } from './media-grid'\nimport { useMediaPicker } from './provider'\nimport { SearchBar } from './search-bar'\nimport type { MediaPickerItem, MediaPickerProps } from './types'\nimport { UploadZone } from './upload-zone'\n\nconst ITEMS_PER_PAGE = 24\n\nexport function MediaPicker({\n open,\n onOpenChange,\n onSelect,\n mode = 'single',\n accept,\n mediaType,\n maxSelect,\n selectedIds = [],\n title = 'Select Media',\n description,\n classNames,\n className,\n children,\n}: MediaPickerProps) {\n const { fetchMedia, uploadMedia } = useMediaPicker()\n\n // State\n const [items, setItems] = useState<MediaPickerItem[]>([])\n const [total, setTotal] = useState(0)\n const [selected, setSelected] = useState<Set<string>>(() => new Set(selectedIds))\n const [search, setSearch] = useState('')\n const [mediaTypeFilter, setMediaTypeFilter] = useState<string | undefined>(mediaType)\n const [isLoading, setIsLoading] = useState(false)\n const [isUploading, setIsUploading] = useState(false)\n const [offset, setOffset] = useState(0)\n const [error, setError] = useState<string | null>(null)\n // When the server reports storage isn't configured, we render a\n // banner and hide the upload zone + grid instead of failing silently\n // or showing an empty state the admin can't act on.\n const [storageDisabled, setStorageDisabled] = useState<string | null>(null)\n\n // Fetch media on open / filter change\n const loadMedia = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n try {\n const result = await fetchMedia({\n search: search || undefined,\n mediaType: mediaTypeFilter,\n limit: ITEMS_PER_PAGE,\n offset,\n })\n if (result.configured === false) {\n setStorageDisabled(result.reason ?? 'Media storage is not configured on this server.')\n setItems([])\n setTotal(0)\n } else {\n setStorageDisabled(null)\n setItems(result.items)\n setTotal(result.total)\n }\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Failed to load media: ${err.message}`\n : 'Failed to load media.',\n )\n } finally {\n setIsLoading(false)\n }\n }, [fetchMedia, search, mediaTypeFilter, offset])\n\n useEffect(() => {\n if (open) {\n loadMedia()\n }\n }, [open, loadMedia])\n\n // Reset state when dialog closes (open transitions true → false)\n const prevOpen = useRef(open)\n const selectedIdsRef = useRef(selectedIds)\n selectedIdsRef.current = selectedIds\n useEffect(() => {\n if (prevOpen.current && !open) {\n setSearch('')\n setOffset(0)\n setSelected(new Set(selectedIdsRef.current))\n }\n prevOpen.current = open\n }, [open])\n\n // Selection\n const handleToggle = useCallback(\n (item: MediaPickerItem) => {\n // Single mode: auto-confirm on click — no separate \"Select\" step needed\n if (mode === 'single') {\n onSelect([item])\n onOpenChange(false)\n return\n }\n\n // Multi mode: toggle selection in the set\n setSelected((prev) => {\n const next = new Set(prev)\n if (next.has(item.id)) {\n next.delete(item.id)\n } else {\n if (maxSelect && next.size >= maxSelect) return prev\n next.add(item.id)\n }\n return next\n })\n },\n [mode, maxSelect, onSelect, onOpenChange],\n )\n\n const handleConfirm = useCallback(() => {\n const selectedItems = items.filter((item) => selected.has(item.id))\n onSelect(selectedItems)\n onOpenChange(false)\n }, [items, selected, onSelect, onOpenChange])\n\n // Upload\n const handleUpload = useCallback(\n async (file: File) => {\n setIsUploading(true)\n setError(null)\n try {\n const uploaded = await uploadMedia(file)\n\n // Single mode: auto-confirm the just-uploaded file\n if (mode === 'single') {\n onSelect([uploaded])\n onOpenChange(false)\n return\n }\n\n // Multi mode: add to grid and select\n setItems((prev) => [uploaded, ...prev])\n setTotal((prev) => prev + 1)\n setSelected((prev) => new Set([...prev, uploaded.id]))\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Upload failed: ${err.message}`\n : 'Upload failed. Please try again.',\n )\n } finally {\n setIsUploading(false)\n }\n },\n [uploadMedia, mode, onSelect, onOpenChange],\n )\n\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n classNames?.overlay,\n )}\n />\n <DialogPrimitive.Content\n {...(!description && { 'aria-describedby': undefined })}\n className={cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'flex max-h-[85vh] w-[90vw] max-w-4xl flex-col',\n 'rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950',\n classNames?.content,\n className,\n )}\n >\n {/* Header */}\n <div\n className={cn(\n 'flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.header,\n )}\n >\n <DialogPrimitive.Title\n className={cn(\n 'text-lg font-semibold text-zinc-900 dark:text-zinc-50',\n classNames?.title,\n )}\n >\n {title}\n </DialogPrimitive.Title>\n {description && (\n <VisuallyHidden.Root asChild>\n <DialogPrimitive.Description>{description}</DialogPrimitive.Description>\n </VisuallyHidden.Root>\n )}\n <DialogPrimitive.Close className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600\">\n <X className=\"h-4 w-4\" />\n <VisuallyHidden.Root>Close</VisuallyHidden.Root>\n </DialogPrimitive.Close>\n </div>\n\n {/* Toolbar */}\n <div\n className={cn(\n 'border-b border-zinc-200 px-6 py-3 dark:border-zinc-800',\n classNames?.toolbar,\n )}\n >\n <SearchBar\n value={search}\n onChange={setSearch}\n mediaType={mediaTypeFilter}\n onMediaTypeChange={setMediaTypeFilter}\n locked={!!mediaType}\n classNames={classNames}\n />\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-y-auto px-6 py-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400\"\n >\n {error}\n </div>\n )}\n {storageDisabled ? (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30\"\n >\n <p className=\"text-sm font-medium text-amber-900 dark:text-amber-200\">\n Media storage is not configured\n </p>\n <p className=\"mt-1 text-xs text-amber-800 dark:text-amber-300\">\n {storageDisabled}\n </p>\n <p className=\"mt-3 text-xs text-amber-700 dark:text-amber-400\">\n Set the required environment variables and restart the app to enable\n uploads.\n </p>\n </div>\n ) : (\n <>\n <UploadZone\n onUpload={handleUpload}\n isUploading={isUploading}\n accept={accept}\n classNames={classNames}\n />\n <MediaGrid\n items={items}\n selected={selected}\n onToggle={handleToggle}\n isLoading={isLoading}\n total={total}\n offset={offset}\n limit={ITEMS_PER_PAGE}\n onPageChange={setOffset}\n classNames={classNames}\n />\n </>\n )}\n </div>\n\n {/* Footer — multi mode shows confirm/cancel, single mode shows item count only */}\n <div\n className={cn(\n 'flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.footer,\n )}\n >\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {mode === 'single'\n ? `${total.toString()} item${total !== 1 ? 's' : ''} — click to select`\n : selected.size > 0\n ? `${selected.size.toString()} selected`\n : `${total.toString()} item${total !== 1 ? 's' : ''}`}\n </span>\n <div className=\"flex gap-2\">\n {children}\n <DialogPrimitive.Close asChild>\n <button\n type=\"button\"\n className={cn(\n 'rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50',\n 'dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900',\n classNames?.cancelButton,\n )}\n >\n Cancel\n </button>\n </DialogPrimitive.Close>\n {mode !== 'single' && (\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={selected.size === 0}\n className={cn(\n 'rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n classNames?.confirmButton,\n )}\n >\n {`Select (${selected.size.toString()})`}\n </button>\n )}\n </div>\n </div>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"picker.mjs","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/lib/cn.ts","../src/picker/media-type-icon.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx","../src/picker/media-picker.tsx"],"sourcesContent":["/**\n * Pre-built media callbacks that talk to the admin API.\n *\n * Eliminates boilerplate in every project — just call:\n * const media = createAdminMediaCallbacks('/api/admin')\n *\n * Returns callbacks compatible with both MediaPickerProvider and\n * BlockEditor's `media` prop (with getMediaUrl for thumbnail previews).\n */\n\nimport type { MediaPickerCallbacks, MediaPickerItem, MediaPickerListResult } from './types'\n\n/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */\nexport type AdminMediaCallbacks = MediaPickerCallbacks & {\n getMediaUrl: (id: string) => Promise<string | null>\n}\n\n/**\n * Create media callbacks that talk to the admin API.\n *\n * @param apiBasePath - Base path for admin API (default: '/api/admin')\n * @returns Callbacks for MediaPickerProvider + BlockEditor media prop\n *\n * @example\n * ```tsx\n * import { createAdminMediaCallbacks } from '@murumets-ee/media/picker'\n *\n * const media = createAdminMediaCallbacks()\n * // Use with BlockEditor:\n * <BlockEditor media={media} ... />\n * // Use with MediaPickerProvider:\n * <MediaPickerProvider fetchMedia={media.fetchMedia} uploadMedia={media.uploadMedia}>\n * ```\n */\nexport function createAdminMediaCallbacks(apiBasePath = '/api/admin'): AdminMediaCallbacks {\n const baseUrl = `${apiBasePath}/media`\n\n // Admin API error responses are `{ error: string }`. Parse the body so\n // the real cause (e.g. \"Missing required environment variable:\n // STORAGE_ENDPOINT\") reaches the UI instead of just the status code.\n const readErrorMessage = async (res: Response): Promise<string> => {\n try {\n const body = (await res.clone().json()) as { error?: unknown }\n if (typeof body.error === 'string' && body.error.length > 0) return body.error\n } catch {\n // fall through — response wasn't JSON\n }\n try {\n const text = await res.text()\n if (text) return text\n } catch {\n // ignore\n }\n return `HTTP ${String(res.status)}`\n }\n\n return {\n fetchMedia: async (options: {\n search?: string | undefined\n mediaType?: string | undefined\n limit: number\n offset: number\n }): Promise<MediaPickerListResult> => {\n const params = new URLSearchParams()\n if (options.search) params.set('search', options.search)\n if (options.mediaType) params.set('mediaType', options.mediaType)\n params.set('limit', String(options.limit))\n params.set('offset', String(options.offset))\n\n const url = `${baseUrl}?${params.toString()}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerListResult>\n },\n\n uploadMedia: async (file: File): Promise<MediaPickerItem> => {\n const formData = new FormData()\n formData.append('file', file)\n // The media LIBRARY is publishable web assets, so it opts in explicitly.\n // Without this the upload route's deny-by-default leaves the asset\n // private, `getPublicUrls` returns nothing for it, and every image on the\n // public site resolves to `null` behind a 200 and one warn log (F030).\n // Sent per-upload rather than by flipping a global default, because the\n // same route also receives rent's inspection photos, which must not be\n // world-readable.\n formData.append('visibility', 'public')\n\n const res = await fetch(baseUrl, { method: 'POST', body: formData })\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerItem>\n },\n\n getMediaUrl: async (id: string): Promise<string | null> => {\n try {\n const res = await fetch(`${baseUrl}/${id}`)\n if (!res.ok) return null\n const item = (await res.json()) as MediaPickerItem\n return item.url ?? null\n } catch {\n return null\n }\n },\n }\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import { File, FileText, Film, type LucideIcon, Music } from 'lucide-react'\n\n/**\n * The non-image members of `@murumets-ee/media`'s `MediaType`, copied rather\n * than imported.\n *\n * This module ships from the `./picker` tsdown entry — a build SEPARATE from\n * the root `index.ts` entry that already exports `MediaType` by name. Naming\n * it here too would duplicate its declaration across two `.d.mts` files\n * (`verify:dts`, #767): each array entry in `tsdown.config.ts` builds\n * independently and cannot share a declaration chunk, so a type named in two\n * entries' public APIs is emitted twice, and TypeScript can no longer treat\n * the two copies as identical if either ever gains an invariant member.\n *\n * Kept honest by `media-type-icon.test.ts`, which imports the real\n * `MediaType` (a type-only import, erased before it ever reaches a `.d.mts`)\n * and asserts this literal still matches it exactly.\n */\ntype NonImageMediaType = 'video' | 'audio' | 'document' | 'other'\n\n/**\n * The typed-icon fallback for every media type that is not `'image'`.\n *\n * The ONE rule three admin surfaces branch on (F115): the media list, the\n * picker's `MediaCard` and the media edit form all show a real `<img>` for\n * `mediaType === 'image'` and one of these icons otherwise. Extracted from\n * `MediaCard`, which held the only copy, so a fourth surface reaches for this\n * instead of writing a fourth `Record<MediaType, LucideIcon>`.\n */\nexport const NON_IMAGE_MEDIA_TYPE_ICONS: Record<NonImageMediaType, LucideIcon> = {\n video: Film,\n audio: Music,\n document: FileText,\n other: File,\n}\n\n/** The icon for a non-image media type, or `null` for `'image'` (render the asset itself). */\nexport function mediaTypeIcon(mediaType: 'image' | NonImageMediaType): LucideIcon | null {\n return mediaType === 'image' ? null : NON_IMAGE_MEDIA_TYPE_ICONS[mediaType]\n}\n","import { Check } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport { mediaTypeIcon } from './media-type-icon'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaCardProps {\n item: MediaPickerItem\n isSelected: boolean\n onToggle: () => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaCard({ item, isSelected, onToggle, classNames }: MediaCardProps) {\n const isImage = item.mediaType === 'image'\n const Icon = mediaTypeIcon(item.mediaType)\n\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n className={cn(\n 'group relative aspect-square overflow-hidden rounded-lg border-2 transition-all',\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950',\n isSelected\n ? 'border-blue-500 ring-2 ring-blue-500/20'\n : 'border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700',\n classNames?.card,\n isSelected && classNames?.cardSelected,\n )}\n >\n {/* Thumbnail */}\n {isImage ? (\n <img\n src={item.url}\n alt={item.alt ?? item.filename}\n className={cn('h-full w-full object-cover', classNames?.cardImage)}\n loading=\"lazy\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800\">\n {Icon && <Icon className=\"h-8 w-8 text-zinc-400 dark:text-zinc-500\" />}\n </div>\n )}\n\n {/* Selection checkmark */}\n {isSelected && (\n <div className=\"absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white\">\n <Check className=\"h-3 w-3\" />\n </div>\n )}\n\n {/* Filename on hover */}\n <div\n className={cn(\n 'absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5',\n 'opacity-0 transition-opacity group-hover:opacity-100',\n classNames?.cardLabel,\n )}\n >\n <p className=\"truncate text-xs text-white\">{item.title ?? item.filename}</p>\n </div>\n </button>\n )\n}\n","import { cn } from '../lib/cn'\nimport { MediaCard } from './media-card'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaGridProps {\n items: MediaPickerItem[]\n selected: Set<string>\n onToggle: (item: MediaPickerItem) => void\n isLoading: boolean\n total: number\n offset: number\n limit: number\n onPageChange: (offset: number) => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaGrid({\n items,\n selected,\n onToggle,\n isLoading,\n total,\n offset,\n limit,\n onPageChange,\n classNames,\n}: MediaGridProps) {\n if (isLoading) {\n return (\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {Array.from({ length: 12 }).map((_, i) => (\n <div\n key={`skeleton-${i.toString()}`}\n className={cn(\n 'aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800',\n classNames?.loading,\n )}\n />\n ))}\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <div\n className={cn(\n 'py-12 text-center text-sm text-zinc-500 dark:text-zinc-400',\n classNames?.empty,\n )}\n >\n No media found. Upload a file to get started.\n </div>\n )\n }\n\n const totalPages = Math.ceil(total / limit)\n const currentPage = Math.floor(offset / limit) + 1\n\n return (\n <>\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {items.map((item) => (\n <MediaCard\n key={item.id}\n item={item}\n isSelected={selected.has(item.id)}\n onToggle={() => onToggle(item)}\n classNames={classNames}\n />\n ))}\n </div>\n {totalPages > 1 && (\n <div className=\"mt-4 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={currentPage <= 1}\n onClick={() => onPageChange(offset - limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Prev\n </button>\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {currentPage} / {totalPages}\n </span>\n <button\n type=\"button\"\n disabled={currentPage >= totalPages}\n onClick={() => onPageChange(offset + limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Next\n </button>\n </div>\n )}\n </>\n )\n}\n","import { createContext, type ReactNode, useContext, useMemo } from 'react'\nimport type { MediaPickerCallbacks } from './types'\n\nconst MediaPickerContext = createContext<MediaPickerCallbacks | null>(null)\n\nexport interface MediaPickerProviderProps extends MediaPickerCallbacks {\n children: ReactNode\n}\n\n/**\n * Provides media picker callbacks to all picker instances below.\n * Wrap your admin layout with this provider.\n *\n * @example\n * ```tsx\n * <MediaPickerProvider\n * fetchMedia={fetchMediaAction}\n * uploadMedia={uploadMediaAction}\n * >\n * <AdminShell>...</AdminShell>\n * </MediaPickerProvider>\n * ```\n */\nexport function MediaPickerProvider({\n children,\n fetchMedia,\n uploadMedia,\n}: MediaPickerProviderProps) {\n const value = useMemo<MediaPickerCallbacks>(\n () => ({ fetchMedia, uploadMedia }),\n [fetchMedia, uploadMedia],\n )\n\n return <MediaPickerContext value={value}>{children}</MediaPickerContext>\n}\n\nexport function useMediaPicker(): MediaPickerCallbacks {\n const ctx = useContext(MediaPickerContext)\n if (!ctx) {\n throw new Error(\n 'useMediaPicker must be used within <MediaPickerProvider>. ' +\n 'Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.',\n )\n }\n return ctx\n}\n","import { Search } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface SearchBarProps {\n value: string\n onChange: (value: string) => void\n mediaType: string | undefined\n onMediaTypeChange: (type: string | undefined) => void\n /** When true, the media type filter is locked (no tabs shown) */\n locked?: boolean | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nconst FILTER_OPTIONS = [\n { value: undefined, label: 'All' },\n { value: 'image', label: 'Images' },\n { value: 'video', label: 'Videos' },\n { value: 'audio', label: 'Audio' },\n { value: 'document', label: 'Docs' },\n] as const\n\nexport function SearchBar({\n value,\n onChange,\n mediaType,\n onMediaTypeChange,\n locked,\n classNames,\n}: SearchBarProps) {\n // If locked (caller pre-set the mediaType), hide filter tabs\n const showFilters = !locked\n\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400\" />\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder=\"Search media...\"\n className={cn(\n 'w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm',\n 'placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',\n 'dark:border-zinc-700 dark:placeholder:text-zinc-500',\n classNames?.searchInput,\n )}\n />\n </div>\n {showFilters && (\n <div className={cn('flex gap-1', classNames?.filterTabs)}>\n {FILTER_OPTIONS.map((opt) => (\n <button\n key={opt.label}\n type=\"button\"\n onClick={() => onMediaTypeChange(opt.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-xs font-medium transition-colors',\n mediaType === opt.value\n ? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'\n : 'text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800',\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n )}\n </div>\n )\n}\n","import { Upload } from 'lucide-react'\nimport { type DragEvent, useCallback, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface UploadZoneProps {\n onUpload: (file: File) => Promise<void>\n isUploading: boolean\n accept?: string[] | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function UploadZone({ onUpload, isUploading, accept, classNames }: UploadZoneProps) {\n const inputRef = useRef<HTMLInputElement>(null)\n const [isDragging, setIsDragging] = useState(false)\n\n const handleDrop = useCallback(\n async (e: DragEvent) => {\n e.preventDefault()\n setIsDragging(false)\n const file = e.dataTransfer.files[0]\n if (file) {\n await onUpload(file)\n }\n },\n [onUpload],\n )\n\n const handleFileSelect = useCallback(\n async (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0]\n if (file) {\n await onUpload(file)\n e.target.value = ''\n }\n },\n [onUpload],\n )\n\n return (\n <button\n type=\"button\"\n onDragOver={(e) => {\n e.preventDefault()\n setIsDragging(true)\n }}\n onDragLeave={() => setIsDragging(false)}\n onDrop={handleDrop}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors',\n isDragging\n ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'\n : 'border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600',\n classNames?.uploadZone,\n isDragging && classNames?.uploadZoneActive,\n )}\n >\n <Upload className=\"mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500\" />\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {isUploading ? 'Uploading...' : 'Drop a file here or click to upload'}\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept?.join(',')}\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n </button>\n )\n}\n","import * as DialogPrimitive from '@radix-ui/react-dialog'\nimport * as VisuallyHidden from '@radix-ui/react-visually-hidden'\nimport { X } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport { MediaGrid } from './media-grid'\nimport { useMediaPicker } from './provider'\nimport { SearchBar } from './search-bar'\nimport type { MediaPickerItem, MediaPickerProps } from './types'\nimport { UploadZone } from './upload-zone'\n\nconst ITEMS_PER_PAGE = 24\n\n/** Media types whose files share one MIME top-level type (`image/*`, …). */\nconst MIME_FAMILIES = new Set(['image', 'video', 'audio'])\n\n/** The file-input patterns a locked media type implies, if it implies any. */\nfunction acceptFor(mediaType: string | undefined): string[] | undefined {\n return mediaType !== undefined && MIME_FAMILIES.has(mediaType) ? [`${mediaType}/*`] : undefined\n}\n\n/**\n * `'file'` is the residual kind — NOT an image and NOT a video — matching the\n * server's `mediaTypeMatchesKind`. It is not a list classification the media\n * API knows, so it never goes out as a browse filter.\n */\nconst RESIDUAL_KIND = 'file'\n\n/**\n * Whether a file the author chose may go into a field locked to `mediaType`,\n * judged before upload from the browser's own guess. An EMPTY `file.type` (the\n * browser could not tell — a HEIC or MKV on some platforms) passes: the server\n * sniffs the real type from the content, and the post-upload check still\n * guards the pick.\n */\nfunction fileMatches(mediaType: string | undefined, file: File): boolean {\n if (mediaType === undefined || file.type === '') return true\n if (mediaType === RESIDUAL_KIND) {\n return !file.type.startsWith('image/') && !file.type.startsWith('video/')\n }\n if (!MIME_FAMILIES.has(mediaType)) return true\n return file.type.startsWith(`${mediaType}/`)\n}\n\n/**\n * Why `item` cannot go into a field locked to `mediaType`, or `null` when it\n * can. The browse list is already filtered server-side; this is the guard for\n * what the list does not cover — an upload the server classified differently.\n */\nfunction typeMismatch(mediaType: string | undefined, item: MediaPickerItem): string | null {\n if (mediaType === undefined || item.mediaType === mediaType) return null\n const name = item.title ?? item.filename\n if (mediaType === RESIDUAL_KIND) {\n if (item.mediaType !== 'image' && item.mediaType !== 'video') return null\n return `This field takes a document or other file — “${name}” is ${withArticle(item.mediaType)}.`\n }\n return `This field takes ${mediaType} files — “${name}” is ${item.mediaType === 'other' ? 'not one' : withArticle(item.mediaType)}.`\n}\n\n/** \"an image\", \"an audio file\"-style reading: `an` before a vowel sound, `a` otherwise. */\nfunction withArticle(mediaType: MediaPickerItem['mediaType']): string {\n return /^[aeiou]/.test(mediaType) ? `an ${mediaType}` : `a ${mediaType}`\n}\n\nexport function MediaPicker({\n open,\n onOpenChange,\n onSelect,\n mode = 'single',\n accept,\n mediaType,\n maxSelect,\n selectedIds = [],\n title = 'Select Media',\n description,\n classNames,\n className,\n children,\n}: MediaPickerProps) {\n const { fetchMedia, uploadMedia } = useMediaPicker()\n\n // State\n const [items, setItems] = useState<MediaPickerItem[]>([])\n const [total, setTotal] = useState(0)\n const [selected, setSelected] = useState<Set<string>>(() => new Set(selectedIds))\n const [search, setSearch] = useState('')\n // The browse filter the author picks from the tabs. A caller's `mediaType`\n // LOCKS the filter and wins over it, derived per render rather than copied\n // into state: the dialog stays mounted between opens, so a copy taken at\n // first mount (when a host's picker had no type yet) kept listing every\n // file forever — a field asking for images was offered videos, and the\n // video landed in the image field (editor F116, reopened in PR59).\n const [mediaTypeFilter, setMediaTypeFilter] = useState<string | undefined>(undefined)\n const effectiveMediaType = mediaType ?? mediaTypeFilter\n // What the list request filters by. The residual `'file'` kind has no list\n // classification, so it browses everything and refuses images/videos on pick.\n const listMediaType = mediaType === RESIDUAL_KIND ? undefined : effectiveMediaType\n // A locked type also restricts what the file input offers, unless the caller\n // passed its own patterns.\n const effectiveAccept = accept ?? acceptFor(mediaType)\n const [isLoading, setIsLoading] = useState(false)\n const [isUploading, setIsUploading] = useState(false)\n const [offset, setOffset] = useState(0)\n const [error, setError] = useState<string | null>(null)\n // When the server reports storage isn't configured, we render a\n // banner and hide the upload zone + grid instead of failing silently\n // or showing an empty state the admin can't act on.\n const [storageDisabled, setStorageDisabled] = useState<string | null>(null)\n\n // Fetch media on open / filter change\n const loadMedia = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n try {\n const result = await fetchMedia({\n search: search || undefined,\n mediaType: listMediaType,\n limit: ITEMS_PER_PAGE,\n offset,\n })\n if (result.configured === false) {\n setStorageDisabled(result.reason ?? 'Media storage is not configured on this server.')\n setItems([])\n setTotal(0)\n } else {\n setStorageDisabled(null)\n setItems(result.items)\n setTotal(result.total)\n }\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Failed to load media: ${err.message}`\n : 'Failed to load media.',\n )\n } finally {\n setIsLoading(false)\n }\n }, [fetchMedia, search, listMediaType, offset])\n\n useEffect(() => {\n if (open) {\n loadMedia()\n }\n }, [open, loadMedia])\n\n // Reset state when dialog closes (open transitions true → false)\n const prevOpen = useRef(open)\n const selectedIdsRef = useRef(selectedIds)\n selectedIdsRef.current = selectedIds\n useEffect(() => {\n if (prevOpen.current && !open) {\n setSearch('')\n setOffset(0)\n setSelected(new Set(selectedIdsRef.current))\n }\n prevOpen.current = open\n }, [open])\n\n // Selection\n const handleToggle = useCallback(\n (item: MediaPickerItem) => {\n const mismatch = typeMismatch(mediaType, item)\n if (mismatch !== null) {\n setError(mismatch)\n return\n }\n // Single mode: auto-confirm on click — no separate \"Select\" step needed\n if (mode === 'single') {\n onSelect([item])\n onOpenChange(false)\n return\n }\n\n // Multi mode: toggle selection in the set\n setSelected((prev) => {\n const next = new Set(prev)\n if (next.has(item.id)) {\n next.delete(item.id)\n } else {\n if (maxSelect && next.size >= maxSelect) return prev\n next.add(item.id)\n }\n return next\n })\n },\n [mode, maxSelect, mediaType, onSelect, onOpenChange],\n )\n\n const handleConfirm = useCallback(() => {\n const selectedItems = items.filter((item) => selected.has(item.id))\n onSelect(selectedItems)\n onOpenChange(false)\n }, [items, selected, onSelect, onOpenChange])\n\n // Upload\n const handleUpload = useCallback(\n async (file: File) => {\n setError(null)\n // Refused before the upload: a file the field cannot hold would otherwise\n // land in the library AND be auto-selected into the field.\n if (!fileMatches(mediaType, file)) {\n setError(\n mediaType === RESIDUAL_KIND\n ? `This field takes a document or other file — “${file.name}” is an image or a video.`\n : `This field takes ${mediaType} files — “${file.name}” is not one.`,\n )\n return\n }\n setIsUploading(true)\n try {\n const uploaded = await uploadMedia(file)\n // The server classifies the stored file; trust that over the browser's\n // guess from the extension.\n const mismatch = typeMismatch(mediaType, uploaded)\n if (mismatch !== null) {\n setError(mismatch)\n return\n }\n\n // Single mode: auto-confirm the just-uploaded file\n if (mode === 'single') {\n onSelect([uploaded])\n onOpenChange(false)\n return\n }\n\n // Multi mode: add to grid and select\n setItems((prev) => [uploaded, ...prev])\n setTotal((prev) => prev + 1)\n setSelected((prev) => new Set([...prev, uploaded.id]))\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Upload failed: ${err.message}`\n : 'Upload failed. Please try again.',\n )\n } finally {\n setIsUploading(false)\n }\n },\n [uploadMedia, mode, mediaType, onSelect, onOpenChange],\n )\n\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n classNames?.overlay,\n )}\n />\n <DialogPrimitive.Content\n {...(!description && { 'aria-describedby': undefined })}\n className={cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'flex max-h-[85vh] w-[90vw] max-w-4xl flex-col',\n 'rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950',\n classNames?.content,\n className,\n )}\n >\n {/* Header */}\n <div\n className={cn(\n 'flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.header,\n )}\n >\n <DialogPrimitive.Title\n className={cn(\n 'text-lg font-semibold text-zinc-900 dark:text-zinc-50',\n classNames?.title,\n )}\n >\n {title}\n </DialogPrimitive.Title>\n {description && (\n <VisuallyHidden.Root asChild>\n <DialogPrimitive.Description>{description}</DialogPrimitive.Description>\n </VisuallyHidden.Root>\n )}\n <DialogPrimitive.Close className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600\">\n <X className=\"h-4 w-4\" />\n <VisuallyHidden.Root>Close</VisuallyHidden.Root>\n </DialogPrimitive.Close>\n </div>\n\n {/* Toolbar */}\n <div\n className={cn(\n 'border-b border-zinc-200 px-6 py-3 dark:border-zinc-800',\n classNames?.toolbar,\n )}\n >\n <SearchBar\n value={search}\n onChange={setSearch}\n mediaType={effectiveMediaType}\n onMediaTypeChange={setMediaTypeFilter}\n locked={!!mediaType}\n classNames={classNames}\n />\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-y-auto px-6 py-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400\"\n >\n {error}\n </div>\n )}\n {storageDisabled ? (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30\"\n >\n <p className=\"text-sm font-medium text-amber-900 dark:text-amber-200\">\n Media storage is not configured\n </p>\n <p className=\"mt-1 text-xs text-amber-800 dark:text-amber-300\">{storageDisabled}</p>\n <p className=\"mt-3 text-xs text-amber-700 dark:text-amber-400\">\n Set the required environment variables and restart the app to enable uploads.\n </p>\n </div>\n ) : (\n <>\n <UploadZone\n onUpload={handleUpload}\n isUploading={isUploading}\n accept={effectiveAccept}\n classNames={classNames}\n />\n <MediaGrid\n items={items}\n selected={selected}\n onToggle={handleToggle}\n isLoading={isLoading}\n total={total}\n offset={offset}\n limit={ITEMS_PER_PAGE}\n onPageChange={setOffset}\n classNames={classNames}\n />\n </>\n )}\n </div>\n\n {/* Footer — multi mode shows confirm/cancel, single mode shows item count only */}\n <div\n className={cn(\n 'flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.footer,\n )}\n >\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {mode === 'single'\n ? `${total.toString()} item${total !== 1 ? 's' : ''} — click to select`\n : selected.size > 0\n ? `${selected.size.toString()} selected`\n : `${total.toString()} item${total !== 1 ? 's' : ''}`}\n </span>\n <div className=\"flex gap-2\">\n {children}\n <DialogPrimitive.Close asChild>\n <button\n type=\"button\"\n className={cn(\n 'rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50',\n 'dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900',\n classNames?.cancelButton,\n )}\n >\n Cancel\n </button>\n </DialogPrimitive.Close>\n {mode !== 'single' && (\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={selected.size === 0}\n className={cn(\n 'rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n classNames?.confirmButton,\n )}\n >\n {`Select (${selected.size.toString()})`}\n </button>\n )}\n </div>\n </div>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
"sharp": "^0.34.5",
|
|
66
66
|
"tailwind-merge": "^2.6.0",
|
|
67
67
|
"zod": "^3.24.1",
|
|
68
|
-
"@murumets-ee/core": "0.
|
|
69
|
-
"@murumets-ee/db": "0.
|
|
70
|
-
"@murumets-ee/
|
|
71
|
-
"@murumets-ee/
|
|
72
|
-
"@murumets-ee/
|
|
73
|
-
"@murumets-ee/storage": "0.
|
|
74
|
-
"@murumets-ee/ui": "0.
|
|
68
|
+
"@murumets-ee/core": "0.67.0",
|
|
69
|
+
"@murumets-ee/db": "0.67.0",
|
|
70
|
+
"@murumets-ee/entity": "0.67.0",
|
|
71
|
+
"@murumets-ee/logging": "0.67.0",
|
|
72
|
+
"@murumets-ee/settings": "0.67.0",
|
|
73
|
+
"@murumets-ee/storage": "0.67.0",
|
|
74
|
+
"@murumets-ee/ui": "0.67.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"lucide-react": ">=0.400.0 <1",
|