@murumets-ee/media 0.11.0 → 0.12.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.
Files changed (58) hide show
  1. package/dist/admin.d.mts +4 -2
  2. package/dist/admin.d.mts.map +1 -1
  3. package/dist/admin.mjs +1 -1
  4. package/dist/{client-DlqLgXKE.mjs → client-BNiqNAEm.mjs} +2 -2
  5. package/dist/{client-DlqLgXKE.mjs.map → client-BNiqNAEm.mjs.map} +1 -1
  6. package/dist/client.d.mts +1 -1
  7. package/dist/client.mjs +1 -1
  8. package/dist/client.mjs.map +1 -1
  9. package/dist/image-styles-settings-DdTdlRmk.mjs +2 -0
  10. package/dist/image-styles-settings-DdTdlRmk.mjs.map +1 -0
  11. package/dist/image-styles-settings-DfZrDSVW.mjs +2 -0
  12. package/dist/image-styles-settings-DfZrDSVW.mjs.map +1 -0
  13. package/dist/image-styles-settings.d.mts +1 -1
  14. package/dist/image-styles-settings.d.mts.map +1 -1
  15. package/dist/image-styles-settings.mjs +1 -2
  16. package/dist/image-styles.d.mts +47 -14
  17. package/dist/image-styles.d.mts.map +1 -1
  18. package/dist/image-styles.mjs +1 -1
  19. package/dist/image-styles.mjs.map +1 -1
  20. package/dist/index.d.mts +3 -3
  21. package/dist/index.mjs +1 -1
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/picker.d.mts +18 -18
  24. package/dist/picker.mjs.map +1 -1
  25. package/dist/plugin-BTpBdM10.mjs +2 -0
  26. package/dist/plugin-BTpBdM10.mjs.map +1 -0
  27. package/dist/plugin.d.mts +1 -1
  28. package/dist/plugin.d.mts.map +1 -1
  29. package/dist/plugin.mjs +1 -1
  30. package/dist/plugin.mjs.map +1 -1
  31. package/dist/processing.d.mts +1 -1
  32. package/dist/processing.mjs +1 -1
  33. package/dist/query-client.d.mts +1 -1
  34. package/dist/query-client.mjs.map +1 -1
  35. package/dist/ref.d.mts.map +1 -1
  36. package/dist/ref.mjs +1 -1
  37. package/dist/ref.mjs.map +1 -1
  38. package/dist/{regenerate-variants-Dm3KCvDF.mjs → regenerate-variants-BUJ8zDIg.mjs} +2 -2
  39. package/dist/{regenerate-variants-Dm3KCvDF.mjs.map → regenerate-variants-BUJ8zDIg.mjs.map} +1 -1
  40. package/dist/{resolve-image-styles-ChzDiAJz.mjs → resolve-image-styles-4j9mMtPn.mjs} +2 -2
  41. package/dist/{resolve-image-styles-ChzDiAJz.mjs.map → resolve-image-styles-4j9mMtPn.mjs.map} +1 -1
  42. package/dist/{resolve-image-styles-BI3pvJBZ.mjs → resolve-image-styles-PSaPMMRO.mjs} +1 -1
  43. package/dist/{resolve-image-styles-BI3pvJBZ.mjs.map → resolve-image-styles-PSaPMMRO.mjs.map} +1 -1
  44. package/dist/routes-DjgvKCWm.mjs +2 -0
  45. package/dist/routes-DjgvKCWm.mjs.map +1 -0
  46. package/dist/{types-BMW3aeEB.d.mts → types-D2w-_pmL.d.mts} +1 -1
  47. package/dist/{types-BMW3aeEB.d.mts.map → types-D2w-_pmL.d.mts.map} +1 -1
  48. package/dist/{variant-key-gVMhzKyv.mjs → variant-key-BnmVwEjR.mjs} +1 -1
  49. package/dist/{variant-key-gVMhzKyv.mjs.map → variant-key-BnmVwEjR.mjs.map} +1 -1
  50. package/dist/variant-key-CFr3fR-n.mjs.map +1 -1
  51. package/package.json +12 -12
  52. package/dist/image-styles-settings-2CQrMr0T.mjs +0 -2
  53. package/dist/image-styles-settings-2CQrMr0T.mjs.map +0 -1
  54. package/dist/image-styles-settings.mjs.map +0 -1
  55. package/dist/plugin-B6vv7QGO.mjs +0 -2
  56. package/dist/plugin-B6vv7QGO.mjs.map +0 -1
  57. package/dist/routes-DzG8k0oP.mjs +0 -2
  58. package/dist/routes-DzG8k0oP.mjs.map +0 -1
@@ -34,10 +34,6 @@ interface ImageStylesManagerLabels {
34
34
  deleteConfirmDescription?: string;
35
35
  systemBadge?: string;
36
36
  noStyles?: string;
37
- regenerate?: string;
38
- regenerateDescription?: string;
39
- regenerating?: string;
40
- saving?: string;
41
37
  px?: string;
42
38
  }
43
39
  interface ImageStylesManagerClassNames {
@@ -46,26 +42,63 @@ interface ImageStylesManagerClassNames {
46
42
  table?: string;
47
43
  dialog?: string;
48
44
  actions?: string;
49
- regenerateSection?: string;
50
45
  }
46
+ /**
47
+ * Controlled props for the ImageStylesManager — matches the
48
+ * `SettingFieldRendererProps<Record<string, ImageStyle>>` contract from
49
+ * `@murumets-ee/admin-ui/settings-form` so it can drop into a json
50
+ * setting's `renderer` slot.
51
+ */
51
52
  interface ImageStylesManagerProps {
52
- /** Current image styles from server (required, server-first pattern) */
53
- initialStyles: Record<string, ImageStyle>;
54
- /** API base path for media admin routes (default: '/api/admin/media') */
55
- apiBasePath?: string;
56
- /** i18n labels with English defaults */
53
+ /** Current image styles (controlled). */
54
+ value: Record<string, ImageStyle> | null | undefined;
55
+ /** Commit changes to the parent form. */
56
+ onChange: (value: Record<string, ImageStyle>) => void;
57
+ /** Server- or schema-side error message. */
58
+ error?: string | undefined;
59
+ /** i18n labels with English defaults. */
57
60
  labels?: ImageStylesManagerLabels;
58
- /** Per-element class overrides */
61
+ /** Per-element class overrides. */
59
62
  classNames?: ImageStylesManagerClassNames;
60
63
  }
64
+ interface RegenerateVariantsActionLabels {
65
+ regenerate?: string;
66
+ regenerating?: string;
67
+ }
68
+ interface RegenerateVariantsActionProps {
69
+ /** Admin API base path (e.g. `/api/admin`). */
70
+ apiBasePath: string;
71
+ /** i18n labels with English defaults. */
72
+ labels?: RegenerateVariantsActionLabels;
73
+ }
61
74
  //#endregion
62
75
  //#region src/image-styles/image-styles-manager.d.ts
76
+ /**
77
+ * Controlled image-styles editor. Designed to slot into the settings-form
78
+ * renderer pattern (`renderer: 'media.imageStyles'`). Holds dialog state
79
+ * internally; commits through `onChange` so the parent settings form's
80
+ * Save button persists the change.
81
+ */
63
82
  declare function ImageStylesManager({
64
- initialStyles,
65
- apiBasePath,
83
+ value,
84
+ onChange,
85
+ error,
66
86
  labels: userLabels,
67
87
  classNames
68
88
  }: ImageStylesManagerProps): _$react_jsx_runtime0.JSX.Element;
69
89
  //#endregion
70
- export { ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps };
90
+ //#region src/image-styles/regenerate-variants-action.d.ts
91
+ /**
92
+ * Action button for the `media.imageStyles` settings page header.
93
+ * Calls `POST /api/admin/media/regenerate-variants` and surfaces the
94
+ * result inline. Owns its own request lifecycle — independent from the
95
+ * settings form's save flow because regeneration is an explicit, opt-in
96
+ * job (not a "save side effect").
97
+ */
98
+ declare function RegenerateVariantsAction({
99
+ apiBasePath,
100
+ labels: userLabels
101
+ }: RegenerateVariantsActionProps): _$react_jsx_runtime0.JSX.Element;
102
+ //#endregion
103
+ export { ImageStylesManager, type ImageStylesManagerClassNames, type ImageStylesManagerLabels, type ImageStylesManagerProps, RegenerateVariantsAction, type RegenerateVariantsActionLabels, type RegenerateVariantsActionProps };
71
104
  //# sourceMappingURL=image-styles.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx"],"mappings":";;;;UA2EiB,UAAA;ECrCA;EDuCf,KAAA;ECrCA;EDuCA,MAAA;ECrCS;EDuCT,GAAA;ECrCa;EDuCb,MAAA;ECvCyC;EDyCzC,OAAA;AAAA;;;UCnFe,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,UAAA;EACA,qBAAA;EACA,YAAA;EACA,MAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;EACA,iBAAA;AAAA;AAAA,UAGe,uBAAA;EAtBf;EAwBA,aAAA,EAAe,MAAA,SAAe,UAAA;EAtB9B;EAwBA,WAAA;EAtBA;EAwBA,MAAA,GAAS,wBAAA;EAtBT;EAwBA,UAAA,GAAa,4BAAA;AAAA;;;iBCmBC,kBAAA,CAAA;EACd,aAAA;EACA,WAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,GAAuB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"image-styles.d.mts","names":[],"sources":["../src/types.ts","../src/image-styles/types.ts","../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"mappings":";;;;UA2EiB,UAAA;EClCG;EDoClB,KAAA;ECpCW;EDsCX,MAAA;EClCA;EDoCA,GAAA;EClCA;EDoCA,MAAA;ECpCyC;EDsCzC,OAAA;AAAA;;;UCnFe,wBAAA;EACf,KAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,GAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EACA,QAAA;EACA,EAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;UASe,uBAAA;EAtBf;EAwBA,KAAA,EAAO,MAAA,SAAe,UAAA;EAtBtB;EAwBA,QAAA,GAAW,KAAA,EAAO,MAAA,SAAe,UAAA;EAtBjC;EAwBA,KAAA;EAvBE;EAyBF,MAAA,GAAS,wBAAA;EAtBM;EAwBf,UAAA,GAAa,4BAAA;AAAA;AAAA,UAGE,8BAAA;EACf,UAAA;EACA,YAAA;AAAA;AAAA,UAGe,6BAAA;EA3Bf;EA6BA,WAAA;EA7BO;EA+BP,MAAA,GAAS,8BAAA;AAAA;;;;;;;ADgBX;;iBEjBgB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,KAAA;EACA,MAAA,EAAQ,UAAA;EACR;AAAA,GACC,uBAAA,GAAuB,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;AFW1B;;;iBG9CgB,wBAAA,CAAA;EACd,WAAA;EACA,MAAA,EAAQ;AAAA,GACP,6BAAA,GAA6B,oBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- import{Badge as e,Button as t,Dialog as n,DialogContent as r,DialogDescription as i,DialogFooter as a,DialogHeader as o,DialogTitle as ee,Input as s,Label as c,Select as l,Table as te,TableBody as ne,TableCell as u,TableHead as d,TableHeader as re,TableRow as f,cn as p}from"@murumets-ee/ui";import{Loader2 as m,Pencil as ie,Plus as ae,RefreshCw as oe,Trash2 as se}from"lucide-react";import{useCallback as h,useState as g}from"react";import{Fragment as _,jsx as v,jsxs as y}from"react/jsx-runtime";const ce=[`cover`,`contain`,`inside`,`outside`,`fill`],le=[`webp`,`jpeg`,`png`,`avif`],ue={title:`Image Styles`,description:`Configure image processing presets. Variants are generated on upload for each style. Use "Regenerate" to update existing images.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,regenerate:`Regenerate All Variants`,regenerateDescription:`Reprocess all images with the current styles. This may take a while for large libraries.`,regenerating:`Regenerating...`,saving:`Saving...`,px:`px`};function b({initialStyles:b,apiBasePath:x=`/api/admin/media`,labels:de,classNames:S}){let C={...ue,...de},[w,fe]=g(b),[T,E]=g(!1),[D,O]=g(null),[pe,k]=g(!1),[A,j]=g(null),[M,N]=g(``),[P,F]=g(``),[I,L]=g(``),[R,z]=g(`cover`),[B,V]=g(`webp`),[H,U]=g(`80`),[W,G]=g(null),[me,K]=g(!1),[q,J]=g(null),[Y,X]=g(!1),[Z,he]=g(null),Q=h(async e=>{E(!0),O(null);try{let t=await fetch(`${x}/settings`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({imageStyles:e})});if(!t.ok){let e=await t.json();throw Error(e.error??`Save failed (${t.status})`)}fe(e)}catch(e){throw O(e instanceof Error?e.message:`Save failed`),e}finally{E(!1)}},[x]),ge=h(()=>{j(null),N(``),F(``),L(``),z(`cover`),V(`webp`),U(`80`),G(null),k(!0)},[]),_e=h(e=>{let t=w[e];t&&(j(e),N(e),F(t.width?.toString()??``),L(t.height?.toString()??``),z(t.fit??`cover`),V(t.format??`webp`),U((t.quality??80).toString()),G(null),k(!0))},[w]),ve=h(async()=>{G(null);let e=M.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){G(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==A&&w[e]){G(`A style named "${e}" already exists`);return}let t=P?Number.parseInt(P,10):void 0,n=I?Number.parseInt(I,10):void 0;if(!t&&!n){G(`At least width or height is required`);return}if(t!==void 0&&(Number.isNaN(t)||t<=0)){G(`Width must be a positive number`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){G(`Height must be a positive number`);return}let r=Number.parseInt(H,10);if(Number.isNaN(r)||r<1||r>100){G(`Quality must be 1-100`);return}let i={...t?{width:t}:{},...n?{height:n}:{},fit:R,format:B,quality:r},a={...w};A&&A!==e&&delete a[A],a[e]=i;try{await Q(a),k(!1)}catch{}},[w,A,M,P,I,R,B,H,Q]),ye=h(e=>{J(e),K(!0)},[]),be=h(async()=>{if(!q)return;let e={...w};delete e[q];try{await Q(e),K(!1),J(null)}catch{}},[q,w,Q]),xe=h(async()=>{X(!0),he(null);try{let e=await fetch(`${x}/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!e.ok){let t=await e.json();throw Error(t.error??`Regeneration failed (${e.status})`)}he(await e.json())}catch(e){O(e instanceof Error?e.message:`Regeneration failed`)}finally{X(!1)}},[x]),$=Object.entries(w).sort(([e],[t])=>e.localeCompare(t));return y(`div`,{className:p(`space-y-6`,S?.root),children:[y(`div`,{className:p(`flex items-start justify-between`,S?.header),children:[y(`div`,{children:[v(`h2`,{className:`text-2xl font-semibold tracking-tight`,children:C.title}),v(`p`,{className:`text-sm text-muted-foreground mt-1`,children:C.description})]}),y(t,{onClick:ge,size:`sm`,children:[v(ae,{className:`h-4 w-4 mr-1`}),C.addStyle]})]}),D&&v(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:D}),$.length===0?v(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:C.noStyles}):v(`div`,{className:p(`rounded-md border`,S?.table),children:y(te,{children:[v(re,{children:y(f,{children:[v(d,{children:C.styleName}),v(d,{className:`text-center`,children:C.width}),v(d,{className:`text-center`,children:C.height}),v(d,{className:`text-center`,children:C.fit}),v(d,{className:`text-center`,children:C.format}),v(d,{className:`text-center`,children:C.quality}),v(d,{className:`w-24`})]})}),v(ne,{children:$.map(([n,r])=>y(f,{children:[y(u,{className:`font-mono text-sm`,children:[n,n===`thumbnail`&&v(e,{variant:`secondary`,className:`ml-2 text-xs`,children:C.systemBadge})]}),v(u,{className:`text-center tabular-nums`,children:r.width?`${r.width}${C.px}`:`—`}),v(u,{className:`text-center tabular-nums`,children:r.height?`${r.height}${C.px}`:`—`}),v(u,{className:`text-center text-sm text-muted-foreground`,children:r.fit??`cover`}),v(u,{className:`text-center text-sm text-muted-foreground`,children:r.format??`webp`}),v(u,{className:`text-center tabular-nums`,children:r.quality??80}),v(u,{children:y(`div`,{className:p(`flex items-center gap-1 justify-end`,S?.actions),children:[v(t,{variant:`ghost`,size:`sm`,onClick:()=>_e(n),title:C.editStyle,children:v(ie,{className:`h-3.5 w-3.5`})}),v(t,{variant:`ghost`,size:`sm`,onClick:()=>ye(n),title:C.delete,className:`text-destructive hover:text-destructive`,children:v(se,{className:`h-3.5 w-3.5`})})]})})]},n))})]})}),y(`div`,{className:p(`rounded-md border p-4 space-y-3`,S?.regenerateSection),children:[y(`div`,{className:`flex items-center justify-between`,children:[y(`div`,{children:[v(`h3`,{className:`text-sm font-medium`,children:C.regenerate}),v(`p`,{className:`text-xs text-muted-foreground mt-0.5`,children:C.regenerateDescription})]}),v(t,{variant:`outline`,size:`sm`,onClick:xe,disabled:Y||$.length===0,children:Y?y(_,{children:[v(m,{className:`h-4 w-4 mr-1 animate-spin`}),C.regenerating]}):y(_,{children:[v(oe,{className:`h-4 w-4 mr-1`}),C.regenerate]})})]}),Z&&y(`div`,{className:`text-sm text-muted-foreground bg-muted/50 rounded px-3 py-2`,children:[`Processed `,Z.processed,` of `,Z.total,` images`,Z.skipped>0&&`, ${Z.skipped} skipped`,Z.errors>0&&y(`span`,{className:`text-destructive`,children:[`, `,Z.errors,` errors`]})]})]}),v(n,{open:pe,onOpenChange:k,children:y(r,{className:S?.dialog,children:[y(o,{children:[v(ee,{children:A?C.editStyle:C.addStyle}),v(i,{children:A?`Editing "${A}" image style.`:`Create a new image processing preset.`})]}),y(`div`,{className:`space-y-4 py-2`,children:[W&&v(`div`,{className:`text-sm text-destructive`,children:W}),y(`div`,{className:`space-y-2`,children:[v(c,{htmlFor:`style-name`,children:C.styleName}),v(s,{id:`style-name`,value:M,onChange:e=>N(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!A})]}),y(`div`,{className:`grid grid-cols-2 gap-4`,children:[y(`div`,{className:`space-y-2`,children:[y(c,{htmlFor:`style-width`,children:[C.width,` (`,C.px,`)`]}),v(s,{id:`style-width`,type:`number`,value:P,onChange:e=>F(e.target.value),placeholder:`e.g. 200`,min:1})]}),y(`div`,{className:`space-y-2`,children:[y(c,{htmlFor:`style-height`,children:[C.height,` (`,C.px,`)`]}),v(s,{id:`style-height`,type:`number`,value:I,onChange:e=>L(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),y(`div`,{className:`grid grid-cols-2 gap-4`,children:[y(`div`,{className:`space-y-2`,children:[v(c,{htmlFor:`style-fit`,children:C.fit}),v(l,{id:`style-fit`,value:R,onChange:e=>z(e.target.value),children:ce.map(e=>v(`option`,{value:e,children:e},e))})]}),y(`div`,{className:`space-y-2`,children:[v(c,{htmlFor:`style-format`,children:C.format}),v(l,{id:`style-format`,value:B,onChange:e=>V(e.target.value),children:le.map(e=>v(`option`,{value:e,children:e},e))})]})]}),y(`div`,{className:`space-y-2`,children:[y(c,{htmlFor:`style-quality`,children:[C.quality,` (1-100)`]}),v(s,{id:`style-quality`,type:`number`,value:H,onChange:e=>U(e.target.value),min:1,max:100})]})]}),y(a,{children:[v(t,{variant:`outline`,onClick:()=>k(!1),children:C.cancel}),v(t,{onClick:ve,disabled:T,children:T?y(_,{children:[v(m,{className:`h-4 w-4 mr-1 animate-spin`}),C.saving]}):C.save})]})]})}),v(n,{open:me,onOpenChange:K,children:y(r,{children:[y(o,{children:[v(ee,{children:C.deleteConfirmTitle}),y(i,{children:[C.deleteConfirmDescription,q&&v(`span`,{className:`block mt-2 font-mono text-foreground`,children:q})]})]}),y(a,{children:[v(t,{variant:`outline`,onClick:()=>K(!1),children:C.cancel}),y(t,{variant:`destructive`,onClick:be,disabled:T,children:[T?v(m,{className:`h-4 w-4 mr-1 animate-spin`}):null,C.delete]})]})]})})]})}export{b as ImageStylesManager};
2
+ import{Badge as e,Button as t,Dialog as n,DialogContent as r,DialogDescription as i,DialogFooter as a,DialogHeader as o,DialogTitle as s,Input as c,Label as l,Select as u,Table as d,TableBody as f,TableCell as p,TableHead as m,TableHeader as ee,TableRow as h,cn as g}from"@murumets-ee/ui";import{Loader2 as _,Pencil as v,Plus as y,RefreshCw as b,Trash2 as x}from"lucide-react";import{useCallback as S,useState as C}from"react";import{Fragment as w,jsx as T,jsxs as E}from"react/jsx-runtime";const te=[`cover`,`contain`,`inside`,`outside`,`fill`],ne=[`webp`,`jpeg`,`png`,`avif`],re={title:`Image Styles`,description:`Configure image processing presets. Variants are generated on upload for each style. Save the form to apply changes.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,px:`px`};function D({value:_,onChange:b,error:w,labels:D,classNames:O}){let k={...re,...D},A=_??{},[ie,j]=C(!1),[M,N]=C(null),[P,F]=C(``),[I,L]=C(``),[R,z]=C(``),[B,V]=C(`cover`),[H,U]=C(`webp`),[W,G]=C(`80`),[K,q]=C(null),[J,Y]=C(!1),[X,Z]=C(null),ae=S(()=>{N(null),F(``),L(``),z(``),V(`cover`),U(`webp`),G(`80`),q(null),j(!0)},[]),Q=S(e=>{let t=A[e];t&&(N(e),F(e),L(t.width?.toString()??``),z(t.height?.toString()??``),V(t.fit??`cover`),U(t.format??`webp`),G((t.quality??80).toString()),q(null),j(!0))},[A]),oe=S(()=>{q(null);let e=P.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){q(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==M&&A[e]){q(`A style named "${e}" already exists`);return}let t=I?Number.parseInt(I,10):void 0,n=R?Number.parseInt(R,10):void 0;if(!t&&!n){q(`At least width or height is required`);return}if(t!==void 0&&(Number.isNaN(t)||t<=0)){q(`Width must be a positive number`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){q(`Height must be a positive number`);return}let r=Number.parseInt(W,10);if(Number.isNaN(r)||r<1||r>100){q(`Quality must be 1-100`);return}let i=B,a=H,o={...t?{width:t}:{},...n?{height:n}:{},fit:i,format:a,quality:r},s={...A};M&&M!==e&&delete s[M],s[e]=o,b(s),j(!1)},[A,M,P,I,R,B,H,W,b]),se=S(e=>{Z(e),Y(!0)},[]),ce=S(()=>{if(!X)return;let e={...A};delete e[X],b(e),Y(!1),Z(null)},[X,A,b]),$=Object.entries(A).sort(([e],[t])=>e.localeCompare(t));return E(`div`,{className:g(`space-y-4`,O?.root),children:[E(`div`,{className:g(`flex items-start justify-between`,O?.header),children:[T(`p`,{className:`text-sm text-muted-foreground`,children:k.description}),E(t,{onClick:ae,size:`sm`,type:`button`,children:[T(y,{className:`h-4 w-4 mr-1`}),k.addStyle]})]}),w&&T(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:w}),$.length===0?T(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:k.noStyles}):T(`div`,{className:g(`rounded-md border`,O?.table),children:E(d,{children:[T(ee,{children:E(h,{children:[T(m,{children:k.styleName}),T(m,{className:`text-center`,children:k.width}),T(m,{className:`text-center`,children:k.height}),T(m,{className:`text-center`,children:k.fit}),T(m,{className:`text-center`,children:k.format}),T(m,{className:`text-center`,children:k.quality}),T(m,{className:`w-24`})]})}),T(f,{children:$.map(([n,r])=>E(h,{children:[E(p,{className:`font-mono text-sm`,children:[n,n===`thumbnail`&&T(e,{variant:`secondary`,className:`ml-2 text-xs`,children:k.systemBadge})]}),T(p,{className:`text-center tabular-nums`,children:r.width?`${r.width}${k.px}`:`—`}),T(p,{className:`text-center tabular-nums`,children:r.height?`${r.height}${k.px}`:`—`}),T(p,{className:`text-center text-sm text-muted-foreground`,children:r.fit??`cover`}),T(p,{className:`text-center text-sm text-muted-foreground`,children:r.format??`webp`}),T(p,{className:`text-center tabular-nums`,children:r.quality??80}),T(p,{children:E(`div`,{className:g(`flex items-center gap-1 justify-end`,O?.actions),children:[T(t,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>Q(n),title:k.editStyle,children:T(v,{className:`h-3.5 w-3.5`})}),T(t,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>se(n),title:k.delete,className:`text-destructive hover:text-destructive`,children:T(x,{className:`h-3.5 w-3.5`})})]})})]},n))})]})}),T(n,{open:ie,onOpenChange:j,children:E(r,{className:O?.dialog,children:[E(o,{children:[T(s,{children:M?k.editStyle:k.addStyle}),T(i,{children:M?`Editing "${M}" image style.`:`Create a new image processing preset.`})]}),E(`div`,{className:`space-y-4 py-2`,children:[K&&T(`div`,{className:`text-sm text-destructive`,children:K}),E(`div`,{className:`space-y-2`,children:[T(l,{htmlFor:`style-name`,children:k.styleName}),T(c,{id:`style-name`,value:P,onChange:e=>F(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!M})]}),E(`div`,{className:`grid grid-cols-2 gap-4`,children:[E(`div`,{className:`space-y-2`,children:[E(l,{htmlFor:`style-width`,children:[k.width,` (`,k.px,`)`]}),T(c,{id:`style-width`,type:`number`,value:I,onChange:e=>L(e.target.value),placeholder:`e.g. 200`,min:1})]}),E(`div`,{className:`space-y-2`,children:[E(l,{htmlFor:`style-height`,children:[k.height,` (`,k.px,`)`]}),T(c,{id:`style-height`,type:`number`,value:R,onChange:e=>z(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),E(`div`,{className:`grid grid-cols-2 gap-4`,children:[E(`div`,{className:`space-y-2`,children:[T(l,{htmlFor:`style-fit`,children:k.fit}),T(u,{id:`style-fit`,value:B,onChange:e=>V(e.target.value),children:te.map(e=>T(`option`,{value:e,children:e},e))})]}),E(`div`,{className:`space-y-2`,children:[T(l,{htmlFor:`style-format`,children:k.format}),T(u,{id:`style-format`,value:H,onChange:e=>U(e.target.value),children:ne.map(e=>T(`option`,{value:e,children:e},e))})]})]}),E(`div`,{className:`space-y-2`,children:[E(l,{htmlFor:`style-quality`,children:[k.quality,` (1-100)`]}),T(c,{id:`style-quality`,type:`number`,value:W,onChange:e=>G(e.target.value),min:1,max:100})]})]}),E(a,{children:[T(t,{variant:`outline`,type:`button`,onClick:()=>j(!1),children:k.cancel}),T(t,{type:`button`,onClick:oe,children:k.save})]})]})}),T(n,{open:J,onOpenChange:Y,children:E(r,{children:[E(o,{children:[T(s,{children:k.deleteConfirmTitle}),E(i,{children:[k.deleteConfirmDescription,X&&T(`span`,{className:`block mt-2 font-mono text-foreground`,children:X})]})]}),E(a,{children:[T(t,{variant:`outline`,type:`button`,onClick:()=>Y(!1),children:k.cancel}),T(t,{variant:`destructive`,type:`button`,onClick:ce,children:k.delete})]})]})})]})}const O={regenerate:`Regenerate All Variants`,regenerating:`Regenerating...`};function k({apiBasePath:e,labels:n}){let r={...O,...n},[i,a]=C(!1),[o,s]=C(null),[c,l]=C(null),u=S(async()=>{a(!0),s(null),l(null);try{let t=await fetch(`${e}/media/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.error??`Regeneration failed (${t.status})`)}s(await t.json())}catch(e){l(e instanceof Error?e.message:`Regeneration failed`)}finally{a(!1)}},[e]);return E(`div`,{className:`flex items-center gap-3`,children:[o&&E(`span`,{className:`text-xs text-muted-foreground`,children:[`Processed `,o.processed,` of `,o.total,o.skipped>0&&`, ${o.skipped} skipped`,o.errors>0&&E(`span`,{className:`text-destructive`,children:[`, `,o.errors,` errors`]})]}),c&&T(`span`,{className:`text-xs text-destructive`,children:c}),T(t,{variant:`outline`,size:`sm`,type:`button`,onClick:u,disabled:i,children:i?E(w,{children:[T(_,{className:`h-4 w-4 mr-1 animate-spin`}),r.regenerating]}):E(w,{children:[T(b,{className:`h-4 w-4 mr-1`}),r.regenerate]})})]})}export{D as ImageStylesManager,k as RegenerateVariantsAction};
3
3
  //# sourceMappingURL=image-styles.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles.mjs","names":[],"sources":["../src/image-styles/image-styles-manager.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Select,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type { ImageStyle } from '../types.js'\nimport type { ImageStylesManagerProps } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst FIT_OPTIONS = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst FORMAT_OPTIONS = ['webp', 'jpeg', 'png', 'avif'] as const\n\nconst DEFAULT_LABELS = {\n title: 'Image Styles',\n description:\n 'Configure image processing presets. Variants are generated on upload for each style. Use \"Regenerate\" to update existing images.',\n addStyle: 'Add Style',\n editStyle: 'Edit Style',\n styleName: 'Style Name',\n width: 'Width',\n height: 'Height',\n fit: 'Fit',\n format: 'Format',\n quality: 'Quality',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n deleteConfirmTitle: 'Delete Image Style',\n deleteConfirmDescription: 'Are you sure? This will not remove existing variant files.',\n systemBadge: 'system',\n noStyles: 'No image styles configured.',\n regenerate: 'Regenerate All Variants',\n regenerateDescription:\n 'Reprocess all images with the current styles. This may take a while for large libraries.',\n regenerating: 'Regenerating...',\n saving: 'Saving...',\n px: 'px',\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function ImageStylesManager({\n initialStyles,\n apiBasePath = '/api/admin/media',\n labels: userLabels,\n classNames,\n}: ImageStylesManagerProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n\n // ---- State ----\n const [styles, setStyles] = useState<Record<string, ImageStyle>>(initialStyles)\n const [isSaving, setIsSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n\n // Edit/Add dialog\n const [isDialogOpen, setIsDialogOpen] = useState(false)\n const [editingKey, setEditingKey] = useState<string | null>(null)\n const [formName, setFormName] = useState('')\n const [formWidth, setFormWidth] = useState('')\n const [formHeight, setFormHeight] = useState('')\n const [formFit, setFormFit] = useState<string>('cover')\n const [formFormat, setFormFormat] = useState<string>('webp')\n const [formQuality, setFormQuality] = useState('80')\n const [formError, setFormError] = useState<string | null>(null)\n\n // Delete dialog\n const [isDeleteOpen, setIsDeleteOpen] = useState(false)\n const [deleteKey, setDeleteKey] = useState<string | null>(null)\n\n // Regeneration\n const [isRegenerating, setIsRegenerating] = useState(false)\n const [regenerateResult, setRegenerateResult] = useState<{\n total: number\n processed: number\n skipped: number\n errors: number\n } | null>(null)\n\n // ---- API helpers ----\n\n const saveStyles = useCallback(\n async (newStyles: Record<string, ImageStyle>) => {\n setIsSaving(true)\n setSaveError(null)\n try {\n const res = await fetch(`${apiBasePath}/settings`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ imageStyles: newStyles }),\n })\n if (!res.ok) {\n const data = (await res.json()) as { error?: string }\n throw new Error(data.error ?? `Save failed (${res.status})`)\n }\n setStyles(newStyles)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Save failed'\n setSaveError(message)\n throw err\n } finally {\n setIsSaving(false)\n }\n },\n [apiBasePath],\n )\n\n // ---- Add / Edit ----\n\n const openAddDialog = useCallback(() => {\n setEditingKey(null)\n setFormName('')\n setFormWidth('')\n setFormHeight('')\n setFormFit('cover')\n setFormFormat('webp')\n setFormQuality('80')\n setFormError(null)\n setIsDialogOpen(true)\n }, [])\n\n const openEditDialog = useCallback(\n (key: string) => {\n const style = styles[key]\n if (!style) return\n setEditingKey(key)\n setFormName(key)\n setFormWidth(style.width?.toString() ?? '')\n setFormHeight(style.height?.toString() ?? '')\n setFormFit(style.fit ?? 'cover')\n setFormFormat(style.format ?? 'webp')\n setFormQuality((style.quality ?? 80).toString())\n setFormError(null)\n setIsDialogOpen(true)\n },\n [styles],\n )\n\n const handleSaveStyle = useCallback(async () => {\n setFormError(null)\n\n // Validate name\n const name = formName.trim().toLowerCase()\n if (!name || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n setFormError('Name must be lowercase alphanumeric (a-z, 0-9, -, _)')\n return\n }\n // Check for duplicate name (only if adding or renaming)\n if (name !== editingKey && styles[name]) {\n setFormError(`A style named \"${name}\" already exists`)\n return\n }\n\n const w = formWidth ? Number.parseInt(formWidth, 10) : undefined\n const h = formHeight ? Number.parseInt(formHeight, 10) : undefined\n if (!w && !h) {\n setFormError('At least width or height is required')\n return\n }\n if (w !== undefined && (Number.isNaN(w) || w <= 0)) {\n setFormError('Width must be a positive number')\n return\n }\n if (h !== undefined && (Number.isNaN(h) || h <= 0)) {\n setFormError('Height must be a positive number')\n return\n }\n const q = Number.parseInt(formQuality, 10)\n if (Number.isNaN(q) || q < 1 || q > 100) {\n setFormError('Quality must be 1-100')\n return\n }\n\n const style: ImageStyle = {\n ...(w ? { width: w } : {}),\n ...(h ? { height: h } : {}),\n fit: formFit as ImageStyle['fit'],\n format: formFormat as ImageStyle['format'],\n quality: q,\n }\n\n const newStyles = { ...styles }\n // If renaming, remove old key\n if (editingKey && editingKey !== name) {\n delete newStyles[editingKey]\n }\n newStyles[name] = style\n\n try {\n await saveStyles(newStyles)\n setIsDialogOpen(false)\n } catch {\n // Error already set by saveStyles\n }\n }, [\n styles,\n editingKey,\n formName,\n formWidth,\n formHeight,\n formFit,\n formFormat,\n formQuality,\n saveStyles,\n ])\n\n // ---- Delete ----\n\n const openDeleteDialog = useCallback((key: string) => {\n setDeleteKey(key)\n setIsDeleteOpen(true)\n }, [])\n\n const handleDelete = useCallback(async () => {\n if (!deleteKey) return\n const newStyles = { ...styles }\n delete newStyles[deleteKey]\n try {\n await saveStyles(newStyles)\n setIsDeleteOpen(false)\n setDeleteKey(null)\n } catch {\n // Error already set by saveStyles\n }\n }, [deleteKey, styles, saveStyles])\n\n // ---- Regenerate ----\n\n const handleRegenerate = useCallback(async () => {\n setIsRegenerating(true)\n setRegenerateResult(null)\n try {\n const res = await fetch(`${apiBasePath}/regenerate-variants`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n })\n if (!res.ok) {\n const data = (await res.json()) as { error?: string }\n throw new Error(data.error ?? `Regeneration failed (${res.status})`)\n }\n const result = (await res.json()) as {\n total: number\n processed: number\n skipped: number\n errors: number\n }\n setRegenerateResult(result)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Regeneration failed'\n setSaveError(message)\n } finally {\n setIsRegenerating(false)\n }\n }, [apiBasePath])\n\n // ---- Render ----\n\n const sortedEntries = Object.entries(styles).sort(([a], [b]) => a.localeCompare(b))\n\n return (\n <div className={cn('space-y-6', classNames?.root)}>\n {/* Header */}\n <div className={cn('flex items-start justify-between', classNames?.header)}>\n <div>\n <h2 className=\"text-2xl font-semibold tracking-tight\">{labels.title}</h2>\n <p className=\"text-sm text-muted-foreground mt-1\">{labels.description}</p>\n </div>\n <Button onClick={openAddDialog} size=\"sm\">\n <Plus className=\"h-4 w-4 mr-1\" />\n {labels.addStyle}\n </Button>\n </div>\n\n {/* Error banner */}\n {saveError && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {saveError}\n </div>\n )}\n\n {/* Styles table */}\n {sortedEntries.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">{labels.noStyles}</p>\n ) : (\n <div className={cn('rounded-md border', classNames?.table)}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{labels.styleName}</TableHead>\n <TableHead className=\"text-center\">{labels.width}</TableHead>\n <TableHead className=\"text-center\">{labels.height}</TableHead>\n <TableHead className=\"text-center\">{labels.fit}</TableHead>\n <TableHead className=\"text-center\">{labels.format}</TableHead>\n <TableHead className=\"text-center\">{labels.quality}</TableHead>\n <TableHead className=\"w-24\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {sortedEntries.map(([name, style]) => (\n <TableRow key={name}>\n <TableCell className=\"font-mono text-sm\">\n {name}\n {name === 'thumbnail' && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n {labels.systemBadge}\n </Badge>\n )}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.width ? `${style.width}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.height ? `${style.height}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.fit ?? 'cover'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.format ?? 'webp'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">{style.quality ?? 80}</TableCell>\n <TableCell>\n <div className={cn('flex items-center gap-1 justify-end', classNames?.actions)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => openEditDialog(name)}\n title={labels.editStyle}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => openDeleteDialog(name)}\n title={labels.delete}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n\n {/* Regeneration section */}\n <div className={cn('rounded-md border p-4 space-y-3', classNames?.regenerateSection)}>\n <div className=\"flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-medium\">{labels.regenerate}</h3>\n <p className=\"text-xs text-muted-foreground mt-0.5\">{labels.regenerateDescription}</p>\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleRegenerate}\n disabled={isRegenerating || sortedEntries.length === 0}\n >\n {isRegenerating ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {labels.regenerating}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {labels.regenerate}\n </>\n )}\n </Button>\n </div>\n {regenerateResult && (\n <div className=\"text-sm text-muted-foreground bg-muted/50 rounded px-3 py-2\">\n Processed {regenerateResult.processed} of {regenerateResult.total} images\n {regenerateResult.skipped > 0 && `, ${regenerateResult.skipped} skipped`}\n {regenerateResult.errors > 0 && (\n <span className=\"text-destructive\">, {regenerateResult.errors} errors</span>\n )}\n </div>\n )}\n </div>\n\n {/* Add / Edit Dialog */}\n <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n <DialogContent className={classNames?.dialog}>\n <DialogHeader>\n <DialogTitle>{editingKey ? labels.editStyle : labels.addStyle}</DialogTitle>\n <DialogDescription>\n {editingKey\n ? `Editing \"${editingKey}\" image style.`\n : 'Create a new image processing preset.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 py-2\">\n {formError && <div className=\"text-sm text-destructive\">{formError}</div>}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-name\">{labels.styleName}</Label>\n <Input\n id=\"style-name\"\n value={formName}\n onChange={(e) => setFormName(e.target.value)}\n placeholder=\"e.g. thumbnail, medium, large\"\n disabled={!!editingKey}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-width\">\n {labels.width} ({labels.px})\n </Label>\n <Input\n id=\"style-width\"\n type=\"number\"\n value={formWidth}\n onChange={(e) => setFormWidth(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-height\">\n {labels.height} ({labels.px})\n </Label>\n <Input\n id=\"style-height\"\n type=\"number\"\n value={formHeight}\n onChange={(e) => setFormHeight(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-fit\">{labels.fit}</Label>\n <Select id=\"style-fit\" value={formFit} onChange={(e) => setFormFit(e.target.value)}>\n {FIT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-format\">{labels.format}</Label>\n <Select\n id=\"style-format\"\n value={formFormat}\n onChange={(e) => setFormFormat(e.target.value)}\n >\n {FORMAT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-quality\">{labels.quality} (1-100)</Label>\n <Input\n id=\"style-quality\"\n type=\"number\"\n value={formQuality}\n onChange={(e) => setFormQuality(e.target.value)}\n min={1}\n max={100}\n />\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setIsDialogOpen(false)}>\n {labels.cancel}\n </Button>\n <Button onClick={handleSaveStyle} disabled={isSaving}>\n {isSaving ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {labels.saving}\n </>\n ) : (\n labels.save\n )}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {/* Delete Confirmation Dialog */}\n <Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{labels.deleteConfirmTitle}</DialogTitle>\n <DialogDescription>\n {labels.deleteConfirmDescription}\n {deleteKey && (\n <span className=\"block mt-2 font-mono text-foreground\">{deleteKey}</span>\n )}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setIsDeleteOpen(false)}>\n {labels.cancel}\n </Button>\n <Button variant=\"destructive\" onClick={handleDelete} disabled={isSaving}>\n {isSaving ? <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" /> : null}\n {labels.delete}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-styles.mjs","names":["DEFAULT_LABELS"],"sources":["../src/image-styles/image-styles-manager.tsx","../src/image-styles/regenerate-variants-action.tsx"],"sourcesContent":["'use client'\n\nimport {\n Badge,\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Select,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type { ImageStyle } from '../types.js'\nimport type { ImageStylesManagerProps } from './types.js'\n\nconst FIT_OPTIONS = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst FORMAT_OPTIONS = ['webp', 'jpeg', 'png', 'avif'] as const\n\nconst DEFAULT_LABELS = {\n title: 'Image Styles',\n description:\n 'Configure image processing presets. Variants are generated on upload for each style. Save the form to apply changes.',\n addStyle: 'Add Style',\n editStyle: 'Edit Style',\n styleName: 'Style Name',\n width: 'Width',\n height: 'Height',\n fit: 'Fit',\n format: 'Format',\n quality: 'Quality',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n deleteConfirmTitle: 'Delete Image Style',\n deleteConfirmDescription: 'Are you sure? This will not remove existing variant files.',\n systemBadge: 'system',\n noStyles: 'No image styles configured.',\n px: 'px',\n}\n\n/**\n * Controlled image-styles editor. Designed to slot into the settings-form\n * renderer pattern (`renderer: 'media.imageStyles'`). Holds dialog state\n * internally; commits through `onChange` so the parent settings form's\n * Save button persists the change.\n */\nexport function ImageStylesManager({\n value,\n onChange,\n error,\n labels: userLabels,\n classNames,\n}: ImageStylesManagerProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const styles = value ?? {}\n\n // Add / edit dialog\n const [isDialogOpen, setIsDialogOpen] = useState(false)\n const [editingKey, setEditingKey] = useState<string | null>(null)\n const [formName, setFormName] = useState('')\n const [formWidth, setFormWidth] = useState('')\n const [formHeight, setFormHeight] = useState('')\n const [formFit, setFormFit] = useState<string>('cover')\n const [formFormat, setFormFormat] = useState<string>('webp')\n const [formQuality, setFormQuality] = useState('80')\n const [formError, setFormError] = useState<string | null>(null)\n\n // Delete dialog\n const [isDeleteOpen, setIsDeleteOpen] = useState(false)\n const [deleteKey, setDeleteKey] = useState<string | null>(null)\n\n const openAddDialog = useCallback(() => {\n setEditingKey(null)\n setFormName('')\n setFormWidth('')\n setFormHeight('')\n setFormFit('cover')\n setFormFormat('webp')\n setFormQuality('80')\n setFormError(null)\n setIsDialogOpen(true)\n }, [])\n\n const openEditDialog = useCallback(\n (key: string) => {\n const style = styles[key]\n if (!style) return\n setEditingKey(key)\n setFormName(key)\n setFormWidth(style.width?.toString() ?? '')\n setFormHeight(style.height?.toString() ?? '')\n setFormFit(style.fit ?? 'cover')\n setFormFormat(style.format ?? 'webp')\n setFormQuality((style.quality ?? 80).toString())\n setFormError(null)\n setIsDialogOpen(true)\n },\n [styles],\n )\n\n const handleSaveStyle = useCallback(() => {\n setFormError(null)\n\n const name = formName.trim().toLowerCase()\n if (!name || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n setFormError('Name must be lowercase alphanumeric (a-z, 0-9, -, _)')\n return\n }\n if (name !== editingKey && styles[name]) {\n setFormError(`A style named \"${name}\" already exists`)\n return\n }\n\n const w = formWidth ? Number.parseInt(formWidth, 10) : undefined\n const h = formHeight ? Number.parseInt(formHeight, 10) : undefined\n if (!w && !h) {\n setFormError('At least width or height is required')\n return\n }\n if (w !== undefined && (Number.isNaN(w) || w <= 0)) {\n setFormError('Width must be a positive number')\n return\n }\n if (h !== undefined && (Number.isNaN(h) || h <= 0)) {\n setFormError('Height must be a positive number')\n return\n }\n const q = Number.parseInt(formQuality, 10)\n if (Number.isNaN(q) || q < 1 || q > 100) {\n setFormError('Quality must be 1-100')\n return\n }\n\n const fit = formFit as NonNullable<ImageStyle['fit']>\n const format = formFormat as NonNullable<ImageStyle['format']>\n const style: ImageStyle = {\n ...(w ? { width: w } : {}),\n ...(h ? { height: h } : {}),\n fit,\n format,\n quality: q,\n }\n\n const next = { ...styles }\n if (editingKey && editingKey !== name) {\n delete next[editingKey]\n }\n next[name] = style\n onChange(next)\n setIsDialogOpen(false)\n }, [\n styles,\n editingKey,\n formName,\n formWidth,\n formHeight,\n formFit,\n formFormat,\n formQuality,\n onChange,\n ])\n\n const openDeleteDialog = useCallback((key: string) => {\n setDeleteKey(key)\n setIsDeleteOpen(true)\n }, [])\n\n const handleDelete = useCallback(() => {\n if (!deleteKey) return\n const next = { ...styles }\n delete next[deleteKey]\n onChange(next)\n setIsDeleteOpen(false)\n setDeleteKey(null)\n }, [deleteKey, styles, onChange])\n\n const sortedEntries = Object.entries(styles).sort(([a], [b]) => a.localeCompare(b))\n\n return (\n <div className={cn('space-y-4', classNames?.root)}>\n <div className={cn('flex items-start justify-between', classNames?.header)}>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n <Button onClick={openAddDialog} size=\"sm\" type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n {labels.addStyle}\n </Button>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {sortedEntries.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">{labels.noStyles}</p>\n ) : (\n <div className={cn('rounded-md border', classNames?.table)}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{labels.styleName}</TableHead>\n <TableHead className=\"text-center\">{labels.width}</TableHead>\n <TableHead className=\"text-center\">{labels.height}</TableHead>\n <TableHead className=\"text-center\">{labels.fit}</TableHead>\n <TableHead className=\"text-center\">{labels.format}</TableHead>\n <TableHead className=\"text-center\">{labels.quality}</TableHead>\n <TableHead className=\"w-24\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {sortedEntries.map(([name, style]) => (\n <TableRow key={name}>\n <TableCell className=\"font-mono text-sm\">\n {name}\n {name === 'thumbnail' && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n {labels.systemBadge}\n </Badge>\n )}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.width ? `${style.width}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.height ? `${style.height}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.fit ?? 'cover'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.format ?? 'webp'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">{style.quality ?? 80}</TableCell>\n <TableCell>\n <div className={cn('flex items-center gap-1 justify-end', classNames?.actions)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openEditDialog(name)}\n title={labels.editStyle}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openDeleteDialog(name)}\n title={labels.delete}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n\n <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n <DialogContent className={classNames?.dialog}>\n <DialogHeader>\n <DialogTitle>{editingKey ? labels.editStyle : labels.addStyle}</DialogTitle>\n <DialogDescription>\n {editingKey\n ? `Editing \"${editingKey}\" image style.`\n : 'Create a new image processing preset.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 py-2\">\n {formError && <div className=\"text-sm text-destructive\">{formError}</div>}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-name\">{labels.styleName}</Label>\n <Input\n id=\"style-name\"\n value={formName}\n onChange={(e) => setFormName(e.target.value)}\n placeholder=\"e.g. thumbnail, medium, large\"\n disabled={!!editingKey}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-width\">\n {labels.width} ({labels.px})\n </Label>\n <Input\n id=\"style-width\"\n type=\"number\"\n value={formWidth}\n onChange={(e) => setFormWidth(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-height\">\n {labels.height} ({labels.px})\n </Label>\n <Input\n id=\"style-height\"\n type=\"number\"\n value={formHeight}\n onChange={(e) => setFormHeight(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-fit\">{labels.fit}</Label>\n <Select id=\"style-fit\" value={formFit} onChange={(e) => setFormFit(e.target.value)}>\n {FIT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-format\">{labels.format}</Label>\n <Select\n id=\"style-format\"\n value={formFormat}\n onChange={(e) => setFormFormat(e.target.value)}\n >\n {FORMAT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-quality\">{labels.quality} (1-100)</Label>\n <Input\n id=\"style-quality\"\n type=\"number\"\n value={formQuality}\n onChange={(e) => setFormQuality(e.target.value)}\n min={1}\n max={100}\n />\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDialogOpen(false)}>\n {labels.cancel}\n </Button>\n <Button type=\"button\" onClick={handleSaveStyle}>\n {labels.save}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{labels.deleteConfirmTitle}</DialogTitle>\n <DialogDescription>\n {labels.deleteConfirmDescription}\n {deleteKey && (\n <span className=\"block mt-2 font-mono text-foreground\">{deleteKey}</span>\n )}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDeleteOpen(false)}>\n {labels.cancel}\n </Button>\n <Button variant=\"destructive\" type=\"button\" onClick={handleDelete}>\n {labels.delete}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n","'use client'\n\nimport { Button } from '@murumets-ee/ui'\nimport { Loader2, RefreshCw } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type {\n RegenerateVariantsActionLabels,\n RegenerateVariantsActionProps,\n} from './types.js'\n\nconst DEFAULT_LABELS: Required<RegenerateVariantsActionLabels> = {\n regenerate: 'Regenerate All Variants',\n regenerating: 'Regenerating...',\n}\n\ninterface RegenerateResult {\n total: number\n processed: number\n skipped: number\n errors: number\n}\n\n/**\n * Action button for the `media.imageStyles` settings page header.\n * Calls `POST /api/admin/media/regenerate-variants` and surfaces the\n * result inline. Owns its own request lifecycle — independent from the\n * settings form's save flow because regeneration is an explicit, opt-in\n * job (not a \"save side effect\").\n */\nexport function RegenerateVariantsAction({\n apiBasePath,\n labels: userLabels,\n}: RegenerateVariantsActionProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const [isRunning, setIsRunning] = useState(false)\n const [result, setResult] = useState<RegenerateResult | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const handleClick = useCallback(async () => {\n setIsRunning(true)\n setResult(null)\n setError(null)\n try {\n const res = await fetch(`${apiBasePath}/media/regenerate-variants`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n })\n if (!res.ok) {\n const data = (await res.json().catch(() => ({}))) as { error?: string }\n throw new Error(data.error ?? `Regeneration failed (${res.status})`)\n }\n const data = (await res.json()) as RegenerateResult\n setResult(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Regeneration failed')\n } finally {\n setIsRunning(false)\n }\n }, [apiBasePath])\n\n return (\n <div className=\"flex items-center gap-3\">\n {result && (\n <span className=\"text-xs text-muted-foreground\">\n Processed {result.processed} of {result.total}\n {result.skipped > 0 && `, ${result.skipped} skipped`}\n {result.errors > 0 && (\n <span className=\"text-destructive\">, {result.errors} errors</span>\n )}\n </span>\n )}\n {error && <span className=\"text-xs text-destructive\">{error}</span>}\n <Button variant=\"outline\" size=\"sm\" type=\"button\" onClick={handleClick} disabled={isRunning}>\n {isRunning ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {labels.regenerating}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {labels.regenerate}\n </>\n )}\n </Button>\n </div>\n )\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MediaRecord, c as MediaUploadResult, i as MediaPluginConfig, l as Media, n as MediaListOptions, o as MediaType, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-BMW3aeEB.mjs";
1
+ import { a as MediaRecord, c as MediaUploadResult, i as MediaPluginConfig, l as Media, n as MediaListOptions, o as MediaType, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-D2w-_pmL.mjs";
2
2
  import { defaultImageStyles, imageStylesSettings } from "./image-styles-settings.mjs";
3
3
  //#region src/enrich.d.ts
4
4
  /**
@@ -47,8 +47,8 @@ interface MediaPickerListResult {
47
47
  interface MediaPickerCallbacks {
48
48
  /** Fetch media items with filtering and pagination */
49
49
  fetchMedia: (options: {
50
- search?: string;
51
- mediaType?: string;
50
+ search?: string | undefined;
51
+ mediaType?: string | undefined;
52
52
  limit: number;
53
53
  offset: number;
54
54
  }) => Promise<MediaPickerListResult>;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./entity-TVTU7wS3.mjs";import{defaultImageStyles as t,imageStylesSettings as n}from"./image-styles-settings.mjs";import"server-only";async function r(e,t,n=`thumbnail`){let r=Object.entries(e.allFields).filter(([,e])=>e.type===`media`).map(([e])=>e);if(r.length===0)return;let i=new Set;for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&n.length>0&&i.add(n)}if(i.size===0)return;let{getMediaClient:a}=await import(`./client.mjs`),o=await(await a()).getVariantUrls([...i],n);for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&o.has(n)&&(e[`${t}Url`]=o.get(n))}}export{e as Media,t as defaultImageStyles,r as enrichWithMediaUrls,n as imageStylesSettings};
1
+ import{t as e}from"./entity-TVTU7wS3.mjs";import{n as t,t as n}from"./image-styles-settings-DdTdlRmk.mjs";import"server-only";async function r(e,t,n=`thumbnail`){let r=Object.entries(e.allFields).filter(([,e])=>e.type===`media`).map(([e])=>e);if(r.length===0)return;let i=new Set;for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&n.length>0&&i.add(n)}if(i.size===0)return;let{getMediaClient:a}=await import(`./client.mjs`),o=await(await a()).getVariantUrls([...i],n);for(let e of t)for(let t of r){let n=e[t];typeof n==`string`&&o.has(n)&&(e[`${t}Url`]=o.get(n))}}export{e as Media,n as defaultImageStyles,r as enrichWithMediaUrls,t as imageStylesSettings};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/enrich.ts"],"sourcesContent":["/**\n * Server-side utility to enrich entity list items with resolved media URLs.\n *\n * For entities with `field.media()` columns, this scans items for media UUIDs,\n * batch-resolves them via MediaClient, and injects `${fieldName}Url` into each item.\n *\n * Convention: a media field named `coverImage` (UUID) gets enriched with\n * `coverImageUrl` (resolved URL string).\n *\n * @example\n * ```typescript\n * import { enrichWithMediaUrls } from '@murumets-ee/media'\n *\n * const items = await adminClient.findMany({ limit: 20 })\n * await enrichWithMediaUrls(Article, items)\n * // items[0].coverImageUrl → 'https://cdn.example.com/uploads/.../thumbnail_photo.webp'\n * ```\n */\n\nimport 'server-only'\n\n/**\n * Enrich entity list items by resolving media field UUIDs to variant URLs.\n *\n * Mutates items in place — injects `${fieldName}Url` for each media field.\n *\n * @param entity - Entity definition (or any object with allFields containing type info)\n * @param items - Array of entity records to enrich\n * @param styleName - Image style to resolve (default: 'thumbnail')\n */\nexport async function enrichWithMediaUrls(\n entity: { allFields: Record<string, { type: string }> },\n items: Record<string, unknown>[],\n styleName = 'thumbnail',\n): Promise<void> {\n // 1. Find media-type fields in entity definition\n const mediaFields = Object.entries(entity.allFields)\n .filter(([, config]) => config.type === 'media')\n .map(([name]) => name)\n\n if (mediaFields.length === 0) return\n\n // 2. Collect unique media UUIDs across all items\n const mediaIds = new Set<string>()\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && val.length > 0) {\n mediaIds.add(val)\n }\n }\n }\n\n if (mediaIds.size === 0) return\n\n // 3. Batch resolve via MediaClient\n const { getMediaClient } = await import('./client.js')\n const client = await getMediaClient()\n const urlMap = await client.getVariantUrls([...mediaIds], styleName)\n\n // 4. Inject ${fieldName}Url into each item\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && urlMap.has(val)) {\n item[`${f}Url`] = urlMap.get(val)\n }\n }\n }\n}\n"],"mappings":"wJA8BA,eAAsB,EACpB,EACA,EACA,EAAY,YACG,CAEf,IAAM,EAAc,OAAO,QAAQ,EAAO,UAAU,CACjD,QAAQ,EAAG,KAAY,EAAO,OAAS,QAAQ,CAC/C,KAAK,CAAC,KAAU,EAAK,CAExB,GAAI,EAAY,SAAW,EAAG,OAG9B,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAI,OAAS,GAC1C,EAAS,IAAI,EAAI,CAKvB,GAAI,EAAS,OAAS,EAAG,OAGzB,GAAM,CAAE,kBAAmB,MAAM,OAAO,gBAElC,EAAS,MADA,MAAM,GAAgB,EACT,eAAe,CAAC,GAAG,EAAS,CAAE,EAAU,CAGpE,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAO,IAAI,EAAI,GAC5C,EAAK,GAAG,EAAE,MAAQ,EAAO,IAAI,EAAI"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/enrich.ts"],"sourcesContent":["/**\n * Server-side utility to enrich entity list items with resolved media URLs.\n *\n * For entities with `field.media()` columns, this scans items for media UUIDs,\n * batch-resolves them via MediaClient, and injects `${fieldName}Url` into each item.\n *\n * Convention: a media field named `coverImage` (UUID) gets enriched with\n * `coverImageUrl` (resolved URL string).\n *\n * @example\n * ```typescript\n * import { enrichWithMediaUrls } from '@murumets-ee/media'\n *\n * const items = await adminClient.findMany({ limit: 20 })\n * await enrichWithMediaUrls(Article, items)\n * // items[0].coverImageUrl → 'https://cdn.example.com/uploads/.../thumbnail_photo.webp'\n * ```\n */\n\nimport 'server-only'\n\n/**\n * Enrich entity list items by resolving media field UUIDs to variant URLs.\n *\n * Mutates items in place — injects `${fieldName}Url` for each media field.\n *\n * @param entity - Entity definition (or any object with allFields containing type info)\n * @param items - Array of entity records to enrich\n * @param styleName - Image style to resolve (default: 'thumbnail')\n */\nexport async function enrichWithMediaUrls(\n entity: { allFields: Record<string, { type: string }> },\n items: Record<string, unknown>[],\n styleName = 'thumbnail',\n): Promise<void> {\n // 1. Find media-type fields in entity definition\n const mediaFields = Object.entries(entity.allFields)\n .filter(([, config]) => config.type === 'media')\n .map(([name]) => name)\n\n if (mediaFields.length === 0) return\n\n // 2. Collect unique media UUIDs across all items\n const mediaIds = new Set<string>()\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && val.length > 0) {\n mediaIds.add(val)\n }\n }\n }\n\n if (mediaIds.size === 0) return\n\n // 3. Batch resolve via MediaClient\n const { getMediaClient } = await import('./client.js')\n const client = await getMediaClient()\n const urlMap = await client.getVariantUrls([...mediaIds], styleName)\n\n // 4. Inject ${fieldName}Url into each item\n for (const item of items) {\n for (const f of mediaFields) {\n const val = item[f]\n if (typeof val === 'string' && urlMap.has(val)) {\n item[`${f}Url`] = urlMap.get(val)\n }\n }\n }\n}\n"],"mappings":"8HA8BA,eAAsB,EACpB,EACA,EACA,EAAY,YACG,CAEf,IAAM,EAAc,OAAO,QAAQ,EAAO,UAAU,CACjD,QAAQ,EAAG,KAAY,EAAO,OAAS,QAAQ,CAC/C,KAAK,CAAC,KAAU,EAAK,CAExB,GAAI,EAAY,SAAW,EAAG,OAG9B,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAI,OAAS,GAC1C,EAAS,IAAI,EAAI,CAKvB,GAAI,EAAS,OAAS,EAAG,OAGzB,GAAM,CAAE,kBAAmB,MAAM,OAAO,gBAElC,EAAS,MAAM,MADA,GAAgB,EACT,eAAe,CAAC,GAAG,EAAS,CAAE,EAAU,CAGpE,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAO,IAAI,EAAI,GAC5C,EAAK,GAAG,EAAE,MAAQ,EAAO,IAAI,EAAI"}
package/dist/picker.d.mts CHANGED
@@ -33,8 +33,8 @@ interface MediaPickerListResult {
33
33
  interface MediaPickerCallbacks {
34
34
  /** Fetch media items with filtering and pagination */
35
35
  fetchMedia: (options: {
36
- search?: string;
37
- mediaType?: string;
36
+ search?: string | undefined;
37
+ mediaType?: string | undefined;
38
38
  limit: number;
39
39
  offset: number;
40
40
  }) => Promise<MediaPickerListResult>;
@@ -74,25 +74,25 @@ interface MediaPickerProps {
74
74
  /** Called when user confirms selection */
75
75
  onSelect: (items: MediaPickerItem[]) => void;
76
76
  /** Selection mode (default: 'single') */
77
- mode?: MediaPickerMode;
77
+ mode?: MediaPickerMode | undefined;
78
78
  /** MIME patterns for upload restriction (e.g., ['image/*', 'video/*']) — passed to file input */
79
- accept?: string[];
79
+ accept?: string[] | undefined;
80
80
  /** Filter browse results to a specific media classification (e.g., 'image', 'video') */
81
- mediaType?: string;
81
+ mediaType?: string | undefined;
82
82
  /** Maximum items selectable in multi mode */
83
- maxSelect?: number;
83
+ maxSelect?: number | undefined;
84
84
  /** Currently selected item IDs (for pre-selection) */
85
- selectedIds?: string[];
85
+ selectedIds?: string[] | undefined;
86
86
  /** Dialog title (default: 'Select Media') */
87
- title?: string;
87
+ title?: string | undefined;
88
88
  /** Dialog description for screen readers (optional) */
89
- description?: string;
89
+ description?: string | undefined;
90
90
  /** Per-element class overrides */
91
- classNames?: MediaPickerClassNames;
91
+ classNames?: MediaPickerClassNames | undefined;
92
92
  /** Additional className for the dialog content */
93
- className?: string;
93
+ className?: string | undefined;
94
94
  /** Children rendered in dialog footer (extra actions) */
95
- children?: ReactNode;
95
+ children?: ReactNode | undefined;
96
96
  }
97
97
  //#endregion
98
98
  //#region src/picker/admin-callbacks.d.ts
@@ -124,7 +124,7 @@ interface MediaCardProps {
124
124
  item: MediaPickerItem;
125
125
  isSelected: boolean;
126
126
  onToggle: () => void;
127
- classNames?: MediaPickerClassNames;
127
+ classNames?: MediaPickerClassNames | undefined;
128
128
  }
129
129
  declare function MediaCard({
130
130
  item,
@@ -143,7 +143,7 @@ interface MediaGridProps {
143
143
  offset: number;
144
144
  limit: number;
145
145
  onPageChange: (offset: number) => void;
146
- classNames?: MediaPickerClassNames;
146
+ classNames?: MediaPickerClassNames | undefined;
147
147
  }
148
148
  declare function MediaGrid({
149
149
  items,
@@ -206,8 +206,8 @@ interface SearchBarProps {
206
206
  mediaType: string | undefined;
207
207
  onMediaTypeChange: (type: string | undefined) => void;
208
208
  /** When true, the media type filter is locked (no tabs shown) */
209
- locked?: boolean;
210
- classNames?: MediaPickerClassNames;
209
+ locked?: boolean | undefined;
210
+ classNames?: MediaPickerClassNames | undefined;
211
211
  }
212
212
  declare function SearchBar({
213
213
  value,
@@ -222,8 +222,8 @@ declare function SearchBar({
222
222
  interface UploadZoneProps {
223
223
  onUpload: (file: File) => Promise<void>;
224
224
  isUploading: boolean;
225
- accept?: string[];
226
- classNames?: MediaPickerClassNames;
225
+ accept?: string[] | undefined;
226
+ classNames?: MediaPickerClassNames | undefined;
227
227
  }
228
228
  declare function UploadZone({
229
229
  onUpload,
@@ -1 +1 @@
1
- {"version":3,"file":"picker.mjs","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/lib/cn.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\n mediaType?: string\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\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 { Check, File, FileText, Film, Music } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaCardProps {\n item: MediaPickerItem\n isSelected: boolean\n onToggle: () => void\n classNames?: MediaPickerClassNames\n}\n\nconst typeIcons = {\n video: Film,\n audio: Music,\n document: FileText,\n other: File,\n} as const\n\nexport function MediaCard({ item, isSelected, onToggle, classNames }: MediaCardProps) {\n const isImage = item.mediaType === 'image'\n const Icon = !isImage ? (typeIcons[item.mediaType as keyof typeof typeIcons] ?? File) : null\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\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\n classNames?: MediaPickerClassNames\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[]\n classNames?: MediaPickerClassNames\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":";8cAkCA,SAAgB,EAA0B,EAAc,aAAmC,CACzF,IAAM,EAAU,GAAG,EAAY,QAKzB,EAAmB,KAAO,IAAmC,CACjE,GAAI,CACF,IAAM,EAAQ,MAAM,EAAI,OAAO,CAAC,MAAM,CACtC,GAAI,OAAO,EAAK,OAAU,UAAY,EAAK,MAAM,OAAS,EAAG,OAAO,EAAK,WACnE,EAGR,GAAI,CACF,IAAM,EAAO,MAAM,EAAI,MAAM,CAC7B,GAAI,EAAM,OAAO,OACX,EAGR,MAAO,QAAQ,OAAO,EAAI,OAAO,IAGnC,MAAO,CACL,WAAY,KAAO,IAKmB,CACpC,IAAM,EAAS,IAAI,gBACf,EAAQ,QAAQ,EAAO,IAAI,SAAU,EAAQ,OAAO,CACpD,EAAQ,WAAW,EAAO,IAAI,YAAa,EAAQ,UAAU,CACjE,EAAO,IAAI,QAAS,OAAO,EAAQ,MAAM,CAAC,CAC1C,EAAO,IAAI,SAAU,OAAO,EAAQ,OAAO,CAAC,CAE5C,IAAM,EAAM,GAAG,EAAQ,GAAG,EAAO,UAAU,GACrC,EAAM,MAAM,MAAM,EAAI,CAC5B,GAAI,CAAC,EAAI,GAAI,MAAU,MAAM,MAAM,EAAiB,EAAI,CAAC,CACzD,OAAO,EAAI,MAAM,EAGnB,YAAa,KAAO,IAAyC,CAC3D,IAAM,EAAW,IAAI,SACrB,EAAS,OAAO,OAAQ,EAAK,CAE7B,IAAM,EAAM,MAAM,MAAM,EAAS,CAAE,OAAQ,OAAQ,KAAM,EAAU,CAAC,CACpE,GAAI,CAAC,EAAI,GAAI,MAAU,MAAM,MAAM,EAAiB,EAAI,CAAC,CACzD,OAAO,EAAI,MAAM,EAGnB,YAAa,KAAO,IAAuC,CACzD,GAAI,CACF,IAAM,EAAM,MAAM,MAAM,GAAG,EAAQ,GAAG,IAAK,CAG3C,OAFK,EAAI,IACK,MAAM,EAAI,MAAM,EAClB,KAAO,KAFC,UAGd,CACN,OAAO,OAGZ,CC3FH,SAAgB,EAAG,GAAG,EAAsB,CAC1C,OAAO,EAAQ,EAAK,EAAO,CAAC,CCO9B,MAAM,EAAY,CAChB,MAAO,EACP,MAAO,EACP,SAAU,EACV,MAAO,EACR,CAED,SAAgB,EAAU,CAAE,OAAM,aAAY,WAAU,cAA8B,CACpF,IAAM,EAAU,EAAK,YAAc,QAC7B,EAAQ,EAA0E,KAA/D,EAAU,EAAK,YAAwC,EAEhF,OACE,EAAC,SAAD,CACE,KAAK,SACL,QAAS,EACT,UAAW,EACT,kFACA,0GACA,EACI,0CACA,wFACJ,GAAY,KACZ,GAAc,GAAY,aAC3B,UAXH,CAcG,EACC,EAAC,MAAD,CACE,IAAK,EAAK,IACV,IAAK,EAAK,KAAO,EAAK,SACtB,UAAW,EAAG,6BAA8B,GAAY,UAAU,CAClE,QAAQ,OACR,CAAA,CAEF,EAAC,MAAD,CAAK,UAAU,uFACZ,GAAQ,EAAC,EAAD,CAAM,UAAU,2CAA6C,CAAA,CAClE,CAAA,CAIP,GACC,EAAC,MAAD,CAAK,UAAU,mHACb,EAAC,EAAD,CAAO,UAAU,UAAY,CAAA,CACzB,CAAA,CAIR,EAAC,MAAD,CACE,UAAW,EACT,wFACA,uDACA,GAAY,UACb,UAED,EAAC,IAAD,CAAG,UAAU,uCAA+B,EAAK,OAAS,EAAK,SAAa,CAAA,CACxE,CAAA,CACC,GCnDb,SAAgB,EAAU,CACxB,QACA,WACA,WACA,YACA,QACA,SACA,QACA,eACA,cACiB,CACjB,GAAI,EACF,OACE,EAAC,MAAD,CAAK,UAAW,EAAG,wCAAyC,GAAY,KAAK,UAC1E,MAAM,KAAK,CAAE,OAAQ,GAAI,CAAC,CAAC,KAAK,EAAG,IAClC,EAAC,MAAD,CAEE,UAAW,EACT,sEACA,GAAY,QACb,CACD,CALK,YAAY,EAAE,UAAU,GAK7B,CACF,CACE,CAAA,CAIV,GAAI,EAAM,SAAW,EACnB,OACE,EAAC,MAAD,CACE,UAAW,EACT,6DACA,GAAY,MACb,UACF,gDAEK,CAAA,CAIV,IAAM,EAAa,KAAK,KAAK,EAAQ,EAAM,CACrC,EAAc,KAAK,MAAM,EAAS,EAAM,CAAG,EAEjD,OACE,EAAA,EAAA,CAAA,SAAA,CACE,EAAC,MAAD,CAAK,UAAW,EAAG,wCAAyC,GAAY,KAAK,UAC1E,EAAM,IAAK,GACV,EAAC,EAAD,CAEQ,OACN,WAAY,EAAS,IAAI,EAAK,GAAG,CACjC,aAAgB,EAAS,EAAK,CAClB,aACZ,CALK,EAAK,GAKV,CACF,CACE,CAAA,CACL,EAAa,GACZ,EAAC,MAAD,CAAK,UAAU,uDAAf,CACE,EAAC,SAAD,CACE,KAAK,SACL,SAAU,GAAe,EACzB,YAAe,EAAa,EAAS,EAAM,CAC3C,UAAU,wGACX,OAEQ,CAAA,CACT,EAAC,OAAD,CAAM,UAAU,oDAAhB,CACG,EAAY,MAAI,EACZ,GACP,EAAC,SAAD,CACE,KAAK,SACL,SAAU,GAAe,EACzB,YAAe,EAAa,EAAS,EAAM,CAC3C,UAAU,wGACX,OAEQ,CAAA,CACL,GAEP,CAAA,CAAA,CC5FP,MAAM,EAAqB,EAA2C,KAAK,CAoB3E,SAAgB,EAAoB,CAClC,WACA,aACA,eAC2B,CAM3B,OAAO,EAAC,EAAD,CAAoB,MALb,OACL,CAAE,aAAY,cAAa,EAClC,CAAC,EAAY,EAAY,CAC1B,CAEyC,WAA8B,CAAA,CAG1E,SAAgB,GAAuC,CACrD,IAAM,EAAM,EAAW,EAAmB,CAC1C,GAAI,CAAC,EACH,MAAU,MACR,kJAED,CAEH,OAAO,EC9BT,MAAM,EAAiB,CACrB,CAAE,MAAO,IAAA,GAAW,MAAO,MAAO,CAClC,CAAE,MAAO,QAAS,MAAO,SAAU,CACnC,CAAE,MAAO,QAAS,MAAO,SAAU,CACnC,CAAE,MAAO,QAAS,MAAO,QAAS,CAClC,CAAE,MAAO,WAAY,MAAO,OAAQ,CACrC,CAED,SAAgB,EAAU,CACxB,QACA,WACA,YACA,oBACA,SACA,cACiB,CAEjB,IAAM,EAAc,CAAC,EAErB,OACE,EAAC,MAAD,CAAK,UAAU,mCAAf,CACE,EAAC,MAAD,CAAK,UAAU,2BAAf,CACE,EAAC,EAAD,CAAQ,UAAU,iEAAmE,CAAA,CACrF,EAAC,QAAD,CACE,KAAK,OACE,QACP,SAAW,GAAM,EAAS,EAAE,OAAO,MAAM,CACzC,YAAY,kBACZ,UAAW,EACT,kFACA,sGACA,sDACA,GAAY,YACb,CACD,CAAA,CACE,GACL,GACC,EAAC,MAAD,CAAK,UAAW,EAAG,aAAc,GAAY,WAAW,UACrD,EAAe,IAAK,GACnB,EAAC,SAAD,CAEE,KAAK,SACL,YAAe,EAAkB,EAAI,MAAM,CAC3C,UAAW,EACT,+DACA,IAAc,EAAI,MACd,6DACA,4EACL,UAEA,EAAI,MACE,CAXF,EAAI,MAWF,CACT,CACE,CAAA,CAEJ,GCzDV,SAAgB,EAAW,CAAE,WAAU,cAAa,SAAQ,cAA+B,CACzF,IAAM,EAAW,EAAyB,KAAK,CACzC,CAAC,EAAY,GAAiB,EAAS,GAAM,CAE7C,EAAa,EACjB,KAAO,IAAiB,CACtB,EAAE,gBAAgB,CAClB,EAAc,GAAM,CACpB,IAAM,EAAO,EAAE,aAAa,MAAM,GAC9B,GACF,MAAM,EAAS,EAAK,EAGxB,CAAC,EAAS,CACX,CAEK,EAAmB,EACvB,KAAO,IAA2C,CAChD,IAAM,EAAO,EAAE,OAAO,QAAQ,GAC1B,IACF,MAAM,EAAS,EAAK,CACpB,EAAE,OAAO,MAAQ,KAGrB,CAAC,EAAS,CACX,CAED,OACE,EAAC,SAAD,CACE,KAAK,SACL,WAAa,GAAM,CACjB,EAAE,gBAAgB,CAClB,EAAc,GAAK,EAErB,gBAAmB,EAAc,GAAM,CACvC,OAAQ,EACR,YAAe,EAAS,SAAS,OAAO,CACxC,UAAW,EACT,8HACA,EACI,iDACA,wFACJ,GAAY,WACZ,GAAc,GAAY,iBAC3B,UAhBH,CAkBE,EAAC,EAAD,CAAQ,UAAU,gDAAkD,CAAA,CACpE,EAAC,IAAD,CAAG,UAAU,oDACV,EAAc,eAAiB,sCAC9B,CAAA,CACJ,EAAC,QAAD,CACE,IAAK,EACL,KAAK,OACL,OAAQ,GAAQ,KAAK,IAAI,CACzB,SAAU,EACV,UAAU,SACV,CAAA,CACK,GCxDb,SAAgB,EAAY,CAC1B,OACA,eACA,WACA,OAAO,SACP,SACA,YACA,YACA,cAAc,EAAE,CAChB,QAAQ,eACR,cACA,aACA,YACA,YACmB,CACnB,GAAM,CAAE,aAAY,eAAgB,GAAgB,CAG9C,CAAC,EAAO,GAAY,EAA4B,EAAE,CAAC,CACnD,CAAC,EAAO,GAAY,EAAS,EAAE,CAC/B,CAAC,EAAU,GAAe,MAA4B,IAAI,IAAI,EAAY,CAAC,CAC3E,CAAC,EAAQ,GAAa,EAAS,GAAG,CAClC,CAAC,EAAiB,GAAsB,EAA6B,EAAU,CAC/E,CAAC,EAAW,GAAgB,EAAS,GAAM,CAC3C,CAAC,EAAa,GAAkB,EAAS,GAAM,CAC/C,CAAC,EAAQ,GAAa,EAAS,EAAE,CACjC,CAAC,EAAO,GAAY,EAAwB,KAAK,CAIjD,CAAC,EAAiB,GAAsB,EAAwB,KAAK,CAGrE,EAAY,EAAY,SAAY,CACxC,EAAa,GAAK,CAClB,EAAS,KAAK,CACd,GAAI,CACF,IAAM,EAAS,MAAM,EAAW,CAC9B,OAAQ,GAAU,IAAA,GAClB,UAAW,EACX,MAAO,GACP,SACD,CAAC,CACE,EAAO,aAAe,IACxB,EAAmB,EAAO,QAAU,kDAAkD,CACtF,EAAS,EAAE,CAAC,CACZ,EAAS,EAAE,GAEX,EAAmB,KAAK,CACxB,EAAS,EAAO,MAAM,CACtB,EAAS,EAAO,MAAM,QAEjB,EAAK,CACZ,EACE,aAAe,OAAS,EAAI,QACxB,yBAAyB,EAAI,UAC7B,wBACL,QACO,CACR,EAAa,GAAM,GAEpB,CAAC,EAAY,EAAQ,EAAiB,EAAO,CAAC,CAEjD,MAAgB,CACV,GACF,GAAW,EAEZ,CAAC,EAAM,EAAU,CAAC,CAGrB,IAAM,EAAW,EAAO,EAAK,CACvB,EAAiB,EAAO,EAAY,CAC1C,EAAe,QAAU,EACzB,MAAgB,CACV,EAAS,SAAW,CAAC,IACvB,EAAU,GAAG,CACb,EAAU,EAAE,CACZ,EAAY,IAAI,IAAI,EAAe,QAAQ,CAAC,EAE9C,EAAS,QAAU,GAClB,CAAC,EAAK,CAAC,CAGV,IAAM,EAAe,EAClB,GAA0B,CAEzB,GAAI,IAAS,SAAU,CACrB,EAAS,CAAC,EAAK,CAAC,CAChB,EAAa,GAAM,CACnB,OAIF,EAAa,GAAS,CACpB,IAAM,EAAO,IAAI,IAAI,EAAK,CAC1B,GAAI,EAAK,IAAI,EAAK,GAAG,CACnB,EAAK,OAAO,EAAK,GAAG,KACf,CACL,GAAI,GAAa,EAAK,MAAQ,EAAW,OAAO,EAChD,EAAK,IAAI,EAAK,GAAG,CAEnB,OAAO,GACP,EAEJ,CAAC,EAAM,EAAW,EAAU,EAAa,CAC1C,CAEK,GAAgB,MAAkB,CAEtC,EADsB,EAAM,OAAQ,GAAS,EAAS,IAAI,EAAK,GAAG,CAAC,CAC5C,CACvB,EAAa,GAAM,EAClB,CAAC,EAAO,EAAU,EAAU,EAAa,CAAC,CAGvC,GAAe,EACnB,KAAO,IAAe,CACpB,EAAe,GAAK,CACpB,EAAS,KAAK,CACd,GAAI,CACF,IAAM,EAAW,MAAM,EAAY,EAAK,CAGxC,GAAI,IAAS,SAAU,CACrB,EAAS,CAAC,EAAS,CAAC,CACpB,EAAa,GAAM,CACnB,OAIF,EAAU,GAAS,CAAC,EAAU,GAAG,EAAK,CAAC,CACvC,EAAU,GAAS,EAAO,EAAE,CAC5B,EAAa,GAAS,IAAI,IAAI,CAAC,GAAG,EAAM,EAAS,GAAG,CAAC,CAAC,OAC/C,EAAK,CACZ,EACE,aAAe,OAAS,EAAI,QACxB,kBAAkB,EAAI,UACtB,mCACL,QACO,CACR,EAAe,GAAM,GAGzB,CAAC,EAAa,EAAM,EAAU,EAAa,CAC5C,CAED,OACE,EAAC,EAAgB,KAAjB,CAA4B,OAAoB,wBAC9C,EAAC,EAAgB,OAAjB,CAAA,SAAA,CACE,EAAC,EAAgB,QAAjB,CACE,UAAW,EACT,yJACA,GAAY,QACb,CACD,CAAA,CACF,EAAC,EAAgB,QAAjB,CACE,GAAK,CAAC,GAAe,CAAE,mBAAoB,IAAA,GAAW,CACtD,UAAW,EACT,gEACA,gDACA,8FACA,GAAY,QACZ,EACD,UARH,CAWE,EAAC,MAAD,CACE,UAAW,EACT,4FACA,GAAY,OACb,UAJH,CAME,EAAC,EAAgB,MAAjB,CACE,UAAW,EACT,wDACA,GAAY,MACb,UAEA,EACqB,CAAA,CACvB,GACC,EAAC,EAAe,KAAhB,CAAqB,QAAA,YACnB,EAAC,EAAgB,YAAjB,CAAA,SAA8B,EAA0C,CAAA,CACpD,CAAA,CAExB,EAAC,EAAgB,MAAjB,CAAuB,UAAU,mJAAjC,CACE,EAAC,EAAD,CAAG,UAAU,UAAY,CAAA,CACzB,EAAC,EAAe,KAAhB,CAAA,SAAqB,QAA2B,CAAA,CAC1B,GACpB,GAGN,EAAC,MAAD,CACE,UAAW,EACT,0DACA,GAAY,QACb,UAED,EAAC,EAAD,CACE,MAAO,EACP,SAAU,EACV,UAAW,EACX,kBAAmB,EACnB,OAAQ,CAAC,CAAC,EACE,aACZ,CAAA,CACE,CAAA,CAGN,EAAC,MAAD,CAAK,UAAU,4CAAf,CACG,GACC,EAAC,MAAD,CACE,KAAK,QACL,UAAU,mJAET,EACG,CAAA,CAEP,EACC,EAAC,MAAD,CACE,KAAK,QACL,UAAU,2HAFZ,CAIE,EAAC,IAAD,CAAG,UAAU,kEAAyD,kCAElE,CAAA,CACJ,EAAC,IAAD,CAAG,UAAU,2DACV,EACC,CAAA,CACJ,EAAC,IAAD,CAAG,UAAU,2DAAkD,gFAG3D,CAAA,CACA,GAEN,EAAA,EAAA,CAAA,SAAA,CACE,EAAC,EAAD,CACE,SAAU,GACG,cACL,SACI,aACZ,CAAA,CACF,EAAC,EAAD,CACS,QACG,WACV,SAAU,EACC,YACJ,QACC,SACR,MAAO,GACP,aAAc,EACF,aACZ,CAAA,CACD,CAAA,CAAA,CAED,GAGN,EAAC,MAAD,CACE,UAAW,EACT,4FACA,GAAY,OACb,UAJH,CAME,EAAC,OAAD,CAAM,UAAU,oDACb,IAAS,SACN,GAAG,EAAM,UAAU,CAAC,OAAO,IAAU,EAAU,GAAN,IAAS,oBAClD,EAAS,KAAO,EACd,GAAG,EAAS,KAAK,UAAU,CAAC,WAC5B,GAAG,EAAM,UAAU,CAAC,OAAO,IAAU,EAAU,GAAN,MAC1C,CAAA,CACP,EAAC,MAAD,CAAK,UAAU,sBAAf,CACG,EACD,EAAC,EAAgB,MAAjB,CAAuB,QAAA,YACrB,EAAC,SAAD,CACE,KAAK,SACL,UAAW,EACT,iGACA,iEACA,GAAY,aACb,UACF,SAEQ,CAAA,CACa,CAAA,CACvB,IAAS,UACR,EAAC,SAAD,CACE,KAAK,SACL,QAAS,GACT,SAAU,EAAS,OAAS,EAC5B,UAAW,EACT,oFACA,kDACA,GAAY,cACb,UAEA,WAAW,EAAS,KAAK,UAAU,CAAC,GAC9B,CAAA,CAEP,GACF,GACkB,GACH,CAAA,CAAA,CACJ,CAAA"}
1
+ {"version":3,"file":"picker.mjs","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/lib/cn.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\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 { Check, File, FileText, Film, Music } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaCardProps {\n item: MediaPickerItem\n isSelected: boolean\n onToggle: () => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nconst typeIcons = {\n video: Film,\n audio: Music,\n document: FileText,\n other: File,\n} as const\n\nexport function MediaCard({ item, isSelected, onToggle, classNames }: MediaCardProps) {\n const isImage = item.mediaType === 'image'\n const Icon = !isImage ? (typeIcons[item.mediaType as keyof typeof typeIcons] ?? File) : null\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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"}
@@ -0,0 +1,2 @@
1
+ import"./routes-DjgvKCWm.mjs";import"./entity-B7zgx4yx.mjs";import"./image-styles-settings-DfZrDSVW.mjs";import"@murumets-ee/media/image-styles";function e(){throw Error(`@murumets-ee/media plugin not initialized. Add media() to your plugins array.`)}export{e as getMediaConfig};
2
+ //# sourceMappingURL=plugin-BTpBdM10.mjs.map