@murumets-ee/media 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{admin.d.ts → admin.d.mts} +4 -25
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +2 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/client-DNTvmvYS.mjs +2 -0
- package/dist/client-DNTvmvYS.mjs.map +1 -0
- package/dist/client.d.mts +95 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +2 -0
- package/dist/client.mjs.map +1 -0
- package/dist/entity-D5P2l05s.mjs +2 -0
- package/dist/entity-D5P2l05s.mjs.map +1 -0
- package/dist/entity-DZFku8b7.mjs +2 -0
- package/dist/entity-DZFku8b7.mjs.map +1 -0
- package/dist/image-styles-settings-5zpGEfKG.mjs +2 -0
- package/dist/image-styles-settings-5zpGEfKG.mjs.map +1 -0
- package/dist/image-styles-settings.d.mts +24 -0
- package/dist/image-styles-settings.d.mts.map +1 -0
- package/dist/image-styles-settings.mjs +2 -0
- package/dist/image-styles-settings.mjs.map +1 -0
- package/dist/image-styles.d.mts +71 -0
- package/dist/image-styles.d.mts.map +1 -0
- package/dist/image-styles.mjs +3 -0
- package/dist/image-styles.mjs.map +1 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/picker.d.mts +228 -0
- package/dist/picker.d.mts.map +1 -0
- package/dist/picker.mjs +3 -0
- package/dist/picker.mjs.map +1 -0
- package/dist/plugin-DV7lvImm.mjs +2 -0
- package/dist/plugin-DV7lvImm.mjs.map +1 -0
- package/dist/plugin.d.mts +20 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/query-client.d.mts +28 -0
- package/dist/query-client.d.mts.map +1 -0
- package/dist/query-client.mjs +2 -0
- package/dist/query-client.mjs.map +1 -0
- package/dist/{ref.d.ts → ref.d.mts} +26 -24
- package/dist/ref.d.mts.map +1 -0
- package/dist/ref.mjs +2 -0
- package/dist/ref.mjs.map +1 -0
- package/dist/regenerate-variants-DY7D4Ky3.mjs +2 -0
- package/dist/regenerate-variants-DY7D4Ky3.mjs.map +1 -0
- package/dist/types-BV_pOm23.d.mts +103 -0
- package/dist/types-BV_pOm23.d.mts.map +1 -0
- package/dist/usage-D7Bn7Vvv.mjs +2 -0
- package/dist/usage-D7Bn7Vvv.mjs.map +1 -0
- package/dist/usage.d.mts +21 -0
- package/dist/usage.d.mts.map +1 -0
- package/dist/usage.mjs +2 -0
- package/dist/usage.mjs.map +1 -0
- package/dist/variant-key-DZUYURS5.mjs +2 -0
- package/dist/variant-key-DZUYURS5.mjs.map +1 -0
- package/package.json +29 -29
- package/dist/admin.js +0 -1
- package/dist/chunk-JGE5BDIT.js +0 -1
- package/dist/chunk-L6BDKI76.js +0 -1
- package/dist/chunk-QTUXM53A.js +0 -1
- package/dist/chunk-YAHM4C5J.js +0 -1
- package/dist/client-37WG2Y6P.js +0 -1
- package/dist/client.d.ts +0 -102
- package/dist/client.js +0 -1
- package/dist/entity-QOBW3TFU.js +0 -1
- package/dist/image-styles-settings-AB5WFEFF.js +0 -1
- package/dist/image-styles-settings.d.ts +0 -9
- package/dist/image-styles-settings.js +0 -1
- package/dist/image-styles.d.ts +0 -62
- package/dist/image-styles.js +0 -2
- package/dist/index.d.ts +0 -131
- package/dist/index.js +0 -1
- package/dist/picker.d.ts +0 -183
- package/dist/picker.js +0 -2
- package/dist/plugin-CAV5BZMF.js +0 -1
- package/dist/plugin.d.ts +0 -35
- package/dist/plugin.js +0 -1
- package/dist/query-client.d.ts +0 -36
- package/dist/query-client.js +0 -1
- package/dist/ref.js +0 -1
- package/dist/regenerate-variants-IUDIIWXU.js +0 -1
- package/dist/types-ChlTxvlq.d.ts +0 -101
- package/dist/usage-E5RZMWE4.js +0 -1
- package/dist/usage.d.ts +0 -28
- package/dist/usage.js +0 -1
|
@@ -0,0 +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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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { a as MediaRecord, c as MediaUploadResult, i as MediaPluginConfig, n as MediaListOptions, o as MediaType, r as MediaListResult, s as MediaUploadOptions, t as ImageStyle } from "./types-BV_pOm23.mjs";
|
|
2
|
+
import { defaultImageStyles, imageStylesSettings } from "./image-styles-settings.mjs";
|
|
3
|
+
import * as _$_murumets_ee_entity0 from "@murumets-ee/entity";
|
|
4
|
+
//#region src/enrich.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Enrich entity list items by resolving media field UUIDs to variant URLs.
|
|
7
|
+
*
|
|
8
|
+
* Mutates items in place — injects `${fieldName}Url` for each media field.
|
|
9
|
+
*
|
|
10
|
+
* @param entity - Entity definition (or any object with allFields containing type info)
|
|
11
|
+
* @param items - Array of entity records to enrich
|
|
12
|
+
* @param styleName - Image style to resolve (default: 'thumbnail')
|
|
13
|
+
*/
|
|
14
|
+
declare function enrichWithMediaUrls(entity: {
|
|
15
|
+
allFields: Record<string, {
|
|
16
|
+
type: string;
|
|
17
|
+
}>;
|
|
18
|
+
}, items: Record<string, unknown>[], styleName?: string): Promise<void>;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/entity.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* Pre-defined Media entity.
|
|
23
|
+
*
|
|
24
|
+
* Auto-registered by the media() plugin — users do NOT add this to their entities array.
|
|
25
|
+
* Connected to toolkit_files via the fileKey field (stores the StorageClient file key).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { media } from '@murumets-ee/media/plugin'
|
|
30
|
+
*
|
|
31
|
+
* export default defineConfig({
|
|
32
|
+
* plugins: [storage(), media()],
|
|
33
|
+
* // Media entity is auto-added — no need to list it here
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare const Media: _$_murumets_ee_entity0.Entity<{
|
|
38
|
+
id: _$_murumets_ee_entity0.IdField;
|
|
39
|
+
} & _$_murumets_ee_entity0.AuditableFields & {
|
|
40
|
+
title: _$_murumets_ee_entity0.TextField & {
|
|
41
|
+
readonly translatable: true;
|
|
42
|
+
};
|
|
43
|
+
alt: _$_murumets_ee_entity0.TextField & {
|
|
44
|
+
readonly translatable: true;
|
|
45
|
+
};
|
|
46
|
+
description: _$_murumets_ee_entity0.TextField & {
|
|
47
|
+
readonly translatable: true;
|
|
48
|
+
}; /** Key into toolkit_files table (e.g., 'uploads/2026/02/uuid/photo.jpg') */
|
|
49
|
+
fileKey: _$_murumets_ee_entity0.TextField & {
|
|
50
|
+
readonly required: true;
|
|
51
|
+
readonly indexed: true;
|
|
52
|
+
};
|
|
53
|
+
filename: _$_murumets_ee_entity0.TextField & {
|
|
54
|
+
readonly required: true;
|
|
55
|
+
};
|
|
56
|
+
mimeType: _$_murumets_ee_entity0.TextField & {
|
|
57
|
+
readonly required: true;
|
|
58
|
+
readonly indexed: true;
|
|
59
|
+
};
|
|
60
|
+
size: _$_murumets_ee_entity0.NumberField & {
|
|
61
|
+
readonly required: true;
|
|
62
|
+
readonly integer: true;
|
|
63
|
+
};
|
|
64
|
+
width: _$_murumets_ee_entity0.NumberField & {
|
|
65
|
+
readonly integer: true;
|
|
66
|
+
};
|
|
67
|
+
height: _$_murumets_ee_entity0.NumberField & {
|
|
68
|
+
readonly integer: true;
|
|
69
|
+
};
|
|
70
|
+
mediaType: _$_murumets_ee_entity0.SelectField & {
|
|
71
|
+
options: readonly ["image", "video", "audio", "document", "other"];
|
|
72
|
+
} & {
|
|
73
|
+
readonly options: readonly ["image", "video", "audio", "document", "other"];
|
|
74
|
+
readonly required: true;
|
|
75
|
+
readonly indexed: true;
|
|
76
|
+
};
|
|
77
|
+
}>;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/picker/types.d.ts
|
|
80
|
+
/** Lightweight media item for the picker (no server-only deps) */
|
|
81
|
+
interface MediaPickerItem {
|
|
82
|
+
id: string;
|
|
83
|
+
title: string | null;
|
|
84
|
+
alt: string | null;
|
|
85
|
+
filename: string;
|
|
86
|
+
mimeType: string;
|
|
87
|
+
size: number;
|
|
88
|
+
width: number | null;
|
|
89
|
+
height: number | null;
|
|
90
|
+
mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
91
|
+
url: string;
|
|
92
|
+
}
|
|
93
|
+
/** Paginated result from fetchMedia callback */
|
|
94
|
+
interface MediaPickerListResult {
|
|
95
|
+
items: MediaPickerItem[];
|
|
96
|
+
total: number;
|
|
97
|
+
}
|
|
98
|
+
/** Callbacks the consumer provides — framework-agnostic */
|
|
99
|
+
interface MediaPickerCallbacks {
|
|
100
|
+
/** Fetch media items with filtering and pagination */
|
|
101
|
+
fetchMedia: (options: {
|
|
102
|
+
search?: string;
|
|
103
|
+
mediaType?: string;
|
|
104
|
+
limit: number;
|
|
105
|
+
offset: number;
|
|
106
|
+
}) => Promise<MediaPickerListResult>;
|
|
107
|
+
/** Upload a file and return the created media item */
|
|
108
|
+
uploadMedia: (file: File) => Promise<MediaPickerItem>;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { type ImageStyle, Media, type MediaListOptions, type MediaListResult, type MediaPickerCallbacks, type MediaPickerItem, type MediaPickerListResult, type MediaPluginConfig, type MediaRecord, type MediaType, type MediaUploadOptions, type MediaUploadResult, defaultImageStyles, enrichWithMediaUrls, imageStylesSettings };
|
|
112
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/enrich.ts","../src/entity.ts","../src/picker/types.ts"],"mappings":";;;;;;;;;;;;;iBA8BsB,mBAAA,CACpB,MAAA;EAAU,SAAA,EAAW,MAAA;IAAiB,IAAA;EAAA;AAAA,GACtC,KAAA,EAAO,MAAA,qBACP,SAAA,YACC,OAAA;;;;;;;;;;AAJH;;;;;;;;;cCXa,KAAA,yBAAK,MAAA;MAoChB,sBAAA,CAAA,OAAA;AAAA;;;;;;;;;KApCW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UChBI,eAAA;EACf,EAAA;EACA,KAAA;EACA,GAAA;EACA,QAAA;EACA,QAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,SAAA;EACA,GAAA;AAAA;;UAIe,qBAAA;EACf,KAAA,EAAO,eAAA;EACP,KAAA;AAAA;;UAIe,oBAAA;EFUf;EERA,UAAA,GAAa,OAAA;IACX,MAAA;IACA,SAAA;IACA,KAAA;IACA,MAAA;EAAA,MACI,OAAA,CAAQ,qBAAA;EDXH;ECcX,WAAA,GAAc,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,eAAA;AAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./entity-D5P2l05s.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};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
|
|
2
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/picker/types.d.ts
|
|
6
|
+
/** Lightweight media item for the picker (no server-only deps) */
|
|
7
|
+
interface MediaPickerItem {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string | null;
|
|
10
|
+
alt: string | null;
|
|
11
|
+
filename: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
size: number;
|
|
14
|
+
width: number | null;
|
|
15
|
+
height: number | null;
|
|
16
|
+
mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
17
|
+
url: string;
|
|
18
|
+
}
|
|
19
|
+
/** Paginated result from fetchMedia callback */
|
|
20
|
+
interface MediaPickerListResult {
|
|
21
|
+
items: MediaPickerItem[];
|
|
22
|
+
total: number;
|
|
23
|
+
}
|
|
24
|
+
/** Callbacks the consumer provides — framework-agnostic */
|
|
25
|
+
interface MediaPickerCallbacks {
|
|
26
|
+
/** Fetch media items with filtering and pagination */
|
|
27
|
+
fetchMedia: (options: {
|
|
28
|
+
search?: string;
|
|
29
|
+
mediaType?: string;
|
|
30
|
+
limit: number;
|
|
31
|
+
offset: number;
|
|
32
|
+
}) => Promise<MediaPickerListResult>;
|
|
33
|
+
/** Upload a file and return the created media item */
|
|
34
|
+
uploadMedia: (file: File) => Promise<MediaPickerItem>;
|
|
35
|
+
}
|
|
36
|
+
/** Selection mode */
|
|
37
|
+
type MediaPickerMode = 'single' | 'multiple';
|
|
38
|
+
/** Per-element class overrides for MediaPicker */
|
|
39
|
+
interface MediaPickerClassNames {
|
|
40
|
+
overlay?: string;
|
|
41
|
+
content?: string;
|
|
42
|
+
header?: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
toolbar?: string;
|
|
45
|
+
searchInput?: string;
|
|
46
|
+
filterTabs?: string;
|
|
47
|
+
grid?: string;
|
|
48
|
+
card?: string;
|
|
49
|
+
cardSelected?: string;
|
|
50
|
+
cardImage?: string;
|
|
51
|
+
cardLabel?: string;
|
|
52
|
+
uploadZone?: string;
|
|
53
|
+
uploadZoneActive?: string;
|
|
54
|
+
footer?: string;
|
|
55
|
+
confirmButton?: string;
|
|
56
|
+
cancelButton?: string;
|
|
57
|
+
loading?: string;
|
|
58
|
+
empty?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Props for the MediaPicker dialog */
|
|
61
|
+
interface MediaPickerProps {
|
|
62
|
+
/** Whether the dialog is open */
|
|
63
|
+
open: boolean;
|
|
64
|
+
/** Called when open state changes */
|
|
65
|
+
onOpenChange: (open: boolean) => void;
|
|
66
|
+
/** Called when user confirms selection */
|
|
67
|
+
onSelect: (items: MediaPickerItem[]) => void;
|
|
68
|
+
/** Selection mode (default: 'single') */
|
|
69
|
+
mode?: MediaPickerMode;
|
|
70
|
+
/** MIME patterns for upload restriction (e.g., ['image/*', 'video/*']) — passed to file input */
|
|
71
|
+
accept?: string[];
|
|
72
|
+
/** Filter browse results to a specific media classification (e.g., 'image', 'video') */
|
|
73
|
+
mediaType?: string;
|
|
74
|
+
/** Maximum items selectable in multi mode */
|
|
75
|
+
maxSelect?: number;
|
|
76
|
+
/** Currently selected item IDs (for pre-selection) */
|
|
77
|
+
selectedIds?: string[];
|
|
78
|
+
/** Dialog title (default: 'Select Media') */
|
|
79
|
+
title?: string;
|
|
80
|
+
/** Dialog description for screen readers (optional) */
|
|
81
|
+
description?: string;
|
|
82
|
+
/** Per-element class overrides */
|
|
83
|
+
classNames?: MediaPickerClassNames;
|
|
84
|
+
/** Additional className for the dialog content */
|
|
85
|
+
className?: string;
|
|
86
|
+
/** Children rendered in dialog footer (extra actions) */
|
|
87
|
+
children?: ReactNode;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/picker/admin-callbacks.d.ts
|
|
91
|
+
/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */
|
|
92
|
+
type AdminMediaCallbacks = MediaPickerCallbacks & {
|
|
93
|
+
getMediaUrl: (id: string) => Promise<string | null>;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Create media callbacks that talk to the admin API.
|
|
97
|
+
*
|
|
98
|
+
* @param apiBasePath - Base path for admin API (default: '/api/admin')
|
|
99
|
+
* @returns Callbacks for MediaPickerProvider + BlockEditor media prop
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* import { createAdminMediaCallbacks } from '@murumets-ee/media/picker'
|
|
104
|
+
*
|
|
105
|
+
* const media = createAdminMediaCallbacks()
|
|
106
|
+
* // Use with BlockEditor:
|
|
107
|
+
* <BlockEditor media={media} ... />
|
|
108
|
+
* // Use with MediaPickerProvider:
|
|
109
|
+
* <MediaPickerProvider fetchMedia={media.fetchMedia} uploadMedia={media.uploadMedia}>
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function createAdminMediaCallbacks(apiBasePath?: string): AdminMediaCallbacks;
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/picker/media-card.d.ts
|
|
115
|
+
interface MediaCardProps {
|
|
116
|
+
item: MediaPickerItem;
|
|
117
|
+
isSelected: boolean;
|
|
118
|
+
onToggle: () => void;
|
|
119
|
+
classNames?: MediaPickerClassNames;
|
|
120
|
+
}
|
|
121
|
+
declare function MediaCard({
|
|
122
|
+
item,
|
|
123
|
+
isSelected,
|
|
124
|
+
onToggle,
|
|
125
|
+
classNames
|
|
126
|
+
}: MediaCardProps): _$react_jsx_runtime0.JSX.Element;
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/picker/media-grid.d.ts
|
|
129
|
+
interface MediaGridProps {
|
|
130
|
+
items: MediaPickerItem[];
|
|
131
|
+
selected: Set<string>;
|
|
132
|
+
onToggle: (item: MediaPickerItem) => void;
|
|
133
|
+
isLoading: boolean;
|
|
134
|
+
total: number;
|
|
135
|
+
offset: number;
|
|
136
|
+
limit: number;
|
|
137
|
+
onPageChange: (offset: number) => void;
|
|
138
|
+
classNames?: MediaPickerClassNames;
|
|
139
|
+
}
|
|
140
|
+
declare function MediaGrid({
|
|
141
|
+
items,
|
|
142
|
+
selected,
|
|
143
|
+
onToggle,
|
|
144
|
+
isLoading,
|
|
145
|
+
total,
|
|
146
|
+
offset,
|
|
147
|
+
limit,
|
|
148
|
+
onPageChange,
|
|
149
|
+
classNames
|
|
150
|
+
}: MediaGridProps): _$react_jsx_runtime0.JSX.Element;
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/picker/media-picker.d.ts
|
|
153
|
+
declare function MediaPicker({
|
|
154
|
+
open,
|
|
155
|
+
onOpenChange,
|
|
156
|
+
onSelect,
|
|
157
|
+
mode,
|
|
158
|
+
accept,
|
|
159
|
+
mediaType,
|
|
160
|
+
maxSelect,
|
|
161
|
+
selectedIds,
|
|
162
|
+
title,
|
|
163
|
+
description,
|
|
164
|
+
classNames,
|
|
165
|
+
className,
|
|
166
|
+
children
|
|
167
|
+
}: MediaPickerProps): _$react_jsx_runtime0.JSX.Element;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/picker/provider.d.ts
|
|
170
|
+
interface MediaPickerProviderProps extends MediaPickerCallbacks {
|
|
171
|
+
children: ReactNode;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Provides media picker callbacks to all picker instances below.
|
|
175
|
+
* Wrap your admin layout with this provider.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```tsx
|
|
179
|
+
* <MediaPickerProvider
|
|
180
|
+
* fetchMedia={fetchMediaAction}
|
|
181
|
+
* uploadMedia={uploadMediaAction}
|
|
182
|
+
* >
|
|
183
|
+
* <AdminShell>...</AdminShell>
|
|
184
|
+
* </MediaPickerProvider>
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
declare function MediaPickerProvider({
|
|
188
|
+
children,
|
|
189
|
+
fetchMedia,
|
|
190
|
+
uploadMedia
|
|
191
|
+
}: MediaPickerProviderProps): _$react_jsx_runtime0.JSX.Element;
|
|
192
|
+
declare function useMediaPicker(): MediaPickerCallbacks;
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/picker/search-bar.d.ts
|
|
195
|
+
interface SearchBarProps {
|
|
196
|
+
value: string;
|
|
197
|
+
onChange: (value: string) => void;
|
|
198
|
+
mediaType: string | undefined;
|
|
199
|
+
onMediaTypeChange: (type: string | undefined) => void;
|
|
200
|
+
/** When true, the media type filter is locked (no tabs shown) */
|
|
201
|
+
locked?: boolean;
|
|
202
|
+
classNames?: MediaPickerClassNames;
|
|
203
|
+
}
|
|
204
|
+
declare function SearchBar({
|
|
205
|
+
value,
|
|
206
|
+
onChange,
|
|
207
|
+
mediaType,
|
|
208
|
+
onMediaTypeChange,
|
|
209
|
+
locked,
|
|
210
|
+
classNames
|
|
211
|
+
}: SearchBarProps): _$react_jsx_runtime0.JSX.Element;
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/picker/upload-zone.d.ts
|
|
214
|
+
interface UploadZoneProps {
|
|
215
|
+
onUpload: (file: File) => Promise<void>;
|
|
216
|
+
isUploading: boolean;
|
|
217
|
+
accept?: string[];
|
|
218
|
+
classNames?: MediaPickerClassNames;
|
|
219
|
+
}
|
|
220
|
+
declare function UploadZone({
|
|
221
|
+
onUpload,
|
|
222
|
+
isUploading,
|
|
223
|
+
accept,
|
|
224
|
+
classNames
|
|
225
|
+
}: UploadZoneProps): _$react_jsx_runtime0.JSX.Element;
|
|
226
|
+
//#endregion
|
|
227
|
+
export { type AdminMediaCallbacks, MediaCard, MediaGrid, MediaPicker, type MediaPickerCallbacks, type MediaPickerClassNames, type MediaPickerItem, type MediaPickerListResult, type MediaPickerMode, type MediaPickerProps, MediaPickerProvider, SearchBar, UploadZone, createAdminMediaCallbacks, useMediaPicker };
|
|
228
|
+
//# sourceMappingURL=picker.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/types.ts","../src/picker/admin-callbacks.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":"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"}
|
package/dist/picker.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{Check as e,File as t,FileText as n,Film as r,Music as i,Search as a,Upload as o,X as s}from"lucide-react";import{clsx as c}from"clsx";import{twMerge as l}from"tailwind-merge";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import*as p from"@radix-ui/react-dialog";import*as m from"@radix-ui/react-visually-hidden";import{createContext as h,useCallback as g,useContext as _,useEffect as v,useMemo as y,useRef as b,useState as x}from"react";function S(e=`/api/admin`){let t=`${e}/media`;return{fetchMedia:async e=>{let n=new URLSearchParams;e.search&&n.set(`search`,e.search),e.mediaType&&n.set(`mediaType`,e.mediaType),n.set(`limit`,String(e.limit)),n.set(`offset`,String(e.offset));let r=`${t}?${n.toString()}`,i=await fetch(r);if(!i.ok)throw Error(`Failed to fetch media: ${i.status}`);return i.json()},uploadMedia:async e=>{let n=new FormData;n.append(`file`,e);let r=await fetch(t,{method:`POST`,body:n});if(!r.ok)throw Error(`Upload failed: ${r.status}`);return r.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function C(...e){return l(c(e))}const w={video:r,audio:i,document:n,other:t};function T({item:n,isSelected:r,onToggle:i,classNames:a}){let o=n.mediaType===`image`,s=o?null:w[n.mediaType]??t;return f(`button`,{type:`button`,onClick:i,className:C(`group relative aspect-square overflow-hidden rounded-lg border-2 transition-all`,`focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950`,r?`border-blue-500 ring-2 ring-blue-500/20`:`border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700`,a?.card,r&&a?.cardSelected),children:[o?d(`img`,{src:n.url,alt:n.alt??n.filename,className:C(`h-full w-full object-cover`,a?.cardImage),loading:`lazy`}):d(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:s&&d(s,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),r&&d(`div`,{className:`absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white`,children:d(e,{className:`h-3 w-3`})}),d(`div`,{className:C(`absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5`,`opacity-0 transition-opacity group-hover:opacity-100`,a?.cardLabel),children:d(`p`,{className:`truncate text-xs text-white`,children:n.title??n.filename})})]})}function E({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>d(`div`,{className:C(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return d(`div`,{className:C(`py-12 text-center text-sm text-zinc-500 dark:text-zinc-400`,c?.empty),children:`No media found. Upload a file to get started.`});let l=Math.ceil(i/o),p=Math.floor(a/o)+1;return f(u,{children:[d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>d(T,{item:e,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&f(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[d(`button`,{type:`button`,disabled:p<=1,onClick:()=>s(a-o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Prev`}),f(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[p,` / `,l]}),d(`button`,{type:`button`,disabled:p>=l,onClick:()=>s(a+o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Next`})]})]})}const D=h(null);function O({children:e,fetchMedia:t,uploadMedia:n}){return d(D,{value:y(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function k(){let e=_(D);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const A=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function j({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:o}){let s=!i;return f(`div`,{className:`flex items-center gap-3`,children:[f(`div`,{className:`relative flex-1`,children:[d(a,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),d(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:C(`w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm`,`placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500`,`dark:border-zinc-700 dark:placeholder:text-zinc-500`,o?.searchInput)})]}),s&&d(`div`,{className:C(`flex gap-1`,o?.filterTabs),children:A.map(e=>d(`button`,{type:`button`,onClick:()=>r(e.value),className:C(`rounded-md px-3 py-1.5 text-xs font-medium transition-colors`,n===e.value?`bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900`:`text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800`),children:e.label},e.label))})]})}function M({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=b(null),[a,s]=x(!1),c=g(async t=>{t.preventDefault(),s(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),l=g(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return f(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),s(!0)},onDragLeave:()=>s(!1),onDrop:c,onClick:()=>i.current?.click(),className:C(`mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors`,a?`border-blue-500 bg-blue-50 dark:bg-blue-950/20`:`border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600`,r?.uploadZone,a&&r?.uploadZoneActive),children:[d(o,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),d(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),d(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:l,className:`hidden`})]})}function N({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:c=[],title:l=`Select Media`,description:u,classNames:h,className:_,children:y}){let{fetchMedia:S,uploadMedia:w}=k(),[T,D]=x([]),[O,A]=x(0),[N,P]=x(()=>new Set(c)),[F,I]=x(``),[L,R]=x(a),[z,B]=x(!1),[V,H]=x(!1),[U,W]=x(0),G=g(async()=>{B(!0);try{let e=await S({search:F||void 0,mediaType:L,limit:24,offset:U});D(e.items),A(e.total)}catch{}finally{B(!1)}},[S,F,L,U]);v(()=>{e&&G()},[e,G]);let K=b(e),q=b(c);q.current=c,v(()=>{K.current&&!e&&(I(``),W(0),P(new Set(q.current))),K.current=e},[e]);let J=g(e=>{if(r===`single`){n([e]),t(!1);return}P(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,n,t]),Y=g(()=>{n(T.filter(e=>N.has(e.id))),t(!1)},[T,N,n,t]),X=g(async e=>{H(!0);try{let i=await w(e);if(r===`single`){n([i]),t(!1);return}D(e=>[i,...e]),A(e=>e+1),P(e=>new Set([...e,i.id]))}finally{H(!1)}},[w,r,n,t]);return d(p.Root,{open:e,onOpenChange:t,children:f(p.Portal,{children:[d(p.Overlay,{className:C(`fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,h?.overlay)}),f(p.Content,{...!u&&{"aria-describedby":void 0},className:C(`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2`,`flex max-h-[85vh] w-[90vw] max-w-4xl flex-col`,`rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950`,h?.content,_),children:[f(`div`,{className:C(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,h?.header),children:[d(p.Title,{className:C(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,h?.title),children:l}),u&&d(m.Root,{asChild:!0,children:d(p.Description,{children:u})}),f(p.Close,{className:`rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600`,children:[d(s,{className:`h-4 w-4`}),d(m.Root,{children:`Close`})]})]}),d(`div`,{className:C(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,h?.toolbar),children:d(j,{value:F,onChange:I,mediaType:L,onMediaTypeChange:R,locked:!!a,classNames:h})}),f(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[d(M,{onUpload:X,isUploading:V,accept:i,classNames:h}),d(E,{items:T,selected:N,onToggle:J,isLoading:z,total:O,offset:U,limit:24,onPageChange:W,classNames:h})]}),f(`div`,{className:C(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,h?.footer),children:[d(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${O.toString()} item${O===1?``:`s`} — click to select`:N.size>0?`${N.size.toString()} selected`:`${O.toString()} item${O===1?``:`s`}`}),f(`div`,{className:`flex gap-2`,children:[y,d(p.Close,{asChild:!0,children:d(`button`,{type:`button`,className:C(`rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50`,`dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900`,h?.cancelButton),children:`Cancel`})}),r!==`single`&&d(`button`,{type:`button`,onClick:Y,disabled:N.size===0,className:C(`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500`,`disabled:cursor-not-allowed disabled:opacity-50`,h?.confirmButton),children:`Select (${N.size.toString()})`})]})]})]})]})})}export{T as MediaCard,E as MediaGrid,N as MediaPicker,O as MediaPickerProvider,j as SearchBar,M as UploadZone,S as createAdminMediaCallbacks,k as useMediaPicker};
|
|
3
|
+
//# sourceMappingURL=picker.mjs.map
|
|
@@ -0,0 +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 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(`Failed to fetch media: ${res.status}`)\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(`Upload failed: ${res.status}`)\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\n // Fetch media on open / filter change\n const loadMedia = useCallback(async () => {\n setIsLoading(true)\n try {\n const result = await fetchMedia({\n search: search || undefined,\n mediaType: mediaTypeFilter,\n limit: ITEMS_PER_PAGE,\n offset,\n })\n setItems(result.items)\n setTotal(result.total)\n } catch {\n // silently fail — UI shows empty state\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 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 } 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 <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 </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,QAE/B,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,0BAA0B,EAAI,SAAS,CACpE,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,kBAAkB,EAAI,SAAS,CAC5D,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,CCxEH,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,CAGjC,EAAY,EAAY,SAAY,CACxC,EAAa,GAAK,CAClB,GAAI,CACF,IAAM,EAAS,MAAM,EAAW,CAC9B,OAAQ,GAAU,IAAA,GAClB,UAAW,EACX,MAAO,GACP,SACD,CAAC,CACF,EAAS,EAAO,MAAM,CACtB,EAAS,EAAO,MAAM,MAChB,SAEE,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,EAAgB,MAAkB,CAEtC,EADsB,EAAM,OAAQ,GAAS,EAAS,IAAI,EAAK,GAAG,CAAC,CAC5C,CACvB,EAAa,GAAM,EAClB,CAAC,EAAO,EAAU,EAAU,EAAa,CAAC,CAGvC,EAAe,EACnB,KAAO,IAAe,CACpB,EAAe,GAAK,CACpB,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,QAC9C,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,CACE,EAAC,EAAD,CACE,SAAU,EACG,cACL,SACI,aACZ,CAAA,CACF,EAAC,EAAD,CACS,QACG,WACV,SAAU,EACC,YACJ,QACC,SACR,MAAO,GACP,aAAc,EACF,aACZ,CAAA,CACE,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,EACT,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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-DV7lvImm.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Media plugin — auto-registers the Media entity and validates storage dependency.\n *\n * @example\n * ```typescript\n * import { media } from '@murumets-ee/media/plugin'\n *\n * export default defineConfig({\n * plugins: [\n * storage(),\n * media({ maxUploadSize: 10 * 1024 * 1024 }),\n * ],\n * })\n * ```\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport { Media } from './entity.js'\nimport type { MediaPluginConfig } from './types.js'\n\nlet _mediaConfig: Required<MediaPluginConfig> | null = null\n\n/**\n * Get the resolved media plugin configuration.\n * Throws if plugin not initialized.\n */\nexport function getMediaConfig(): Required<MediaPluginConfig> {\n if (!_mediaConfig) {\n throw new Error('@murumets-ee/media plugin not initialized. Add media() to your plugins array.')\n }\n return _mediaConfig\n}\n\n/**\n * Media plugin factory.\n *\n * - Auto-registers the Media entity (no need to add to entities array)\n * - Validates that @murumets-ee/storage plugin is registered\n * - Stores resolved configuration\n */\nexport function media(config?: MediaPluginConfig): Plugin {\n const resolvedConfig: Required<MediaPluginConfig> = {\n acceptedTypes: config?.acceptedTypes ?? ['image/*', 'video/*', 'audio/*', 'application/pdf'],\n maxUploadSize: config?.maxUploadSize ?? 50 * 1024 * 1024,\n defaultVisibility: config?.defaultVisibility ?? 'public',\n imageStyles: config?.imageStyles ?? {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n }\n\n return {\n name: '@murumets-ee/media',\n entities: [Media],\n init: async (app) => {\n // Validate storage plugin is loaded (must be registered before media)\n if (!app.plugins.has('@murumets-ee/storage')) {\n throw new Error(\n '@murumets-ee/media requires @murumets-ee/storage plugin. ' +\n 'Add storage() before media() in your plugins array.',\n )\n }\n\n // Validate settings plugin is loaded\n if (!app.plugins.has('@murumets-ee/settings')) {\n throw new Error(\n '@murumets-ee/media requires @murumets-ee/settings plugin. ' +\n 'Add settings() before media() in your plugins array.',\n )\n }\n\n _mediaConfig = resolvedConfig\n\n // Seed image styles to settings DB on first boot\n try {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('./image-styles-settings.js')\n const client = createSettingsClient(imageStylesSettings, { app })\n const hasStored = await client.has('imageStyles')\n if (!hasStored) {\n await client.set('imageStyles', resolvedConfig.imageStyles)\n app.logger.info(\n { styles: Object.keys(resolvedConfig.imageStyles) },\n 'Seeded default image styles to settings DB',\n )\n }\n } catch (seedErr) {\n app.logger.warn(\n { error: seedErr },\n 'Failed to seed image styles to settings DB (non-fatal)',\n )\n }\n\n app.logger.info(\n {\n acceptedTypes: resolvedConfig.acceptedTypes,\n maxUploadSize: resolvedConfig.maxUploadSize,\n defaultVisibility: resolvedConfig.defaultVisibility,\n },\n 'Media plugin initialized',\n )\n },\n }\n}\n"],"mappings":"8BA0BA,SAAgB,GAA8C,CAE1D,MAAU,MAAM,gFAAgF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { i as MediaPluginConfig } from "./types-BV_pOm23.mjs";
|
|
2
|
+
import { Plugin } from "@murumets-ee/core";
|
|
3
|
+
|
|
4
|
+
//#region src/plugin.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Get the resolved media plugin configuration.
|
|
7
|
+
* Throws if plugin not initialized.
|
|
8
|
+
*/
|
|
9
|
+
declare function getMediaConfig(): Required<MediaPluginConfig>;
|
|
10
|
+
/**
|
|
11
|
+
* Media plugin factory.
|
|
12
|
+
*
|
|
13
|
+
* - Auto-registers the Media entity (no need to add to entities array)
|
|
14
|
+
* - Validates that @murumets-ee/storage plugin is registered
|
|
15
|
+
* - Stores resolved configuration
|
|
16
|
+
*/
|
|
17
|
+
declare function media(config?: MediaPluginConfig): Plugin;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { getMediaConfig, media };
|
|
20
|
+
//# sourceMappingURL=plugin.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;;iBA0BgB,cAAA,CAAA,GAAkB,QAAA,CAAS,iBAAA;;;;;;;;iBAc3B,KAAA,CAAM,MAAA,GAAS,iBAAA,GAAoB,MAAA"}
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./entity-D5P2l05s.mjs";let t=null;function n(){if(!t)throw Error(`@murumets-ee/media plugin not initialized. Add media() to your plugins array.`);return t}function r(n){let r={acceptedTypes:n?.acceptedTypes??[`image/*`,`video/*`,`audio/*`,`application/pdf`],maxUploadSize:n?.maxUploadSize??50*1024*1024,defaultVisibility:n?.defaultVisibility??`public`,imageStyles:n?.imageStyles??{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}}};return{name:`@murumets-ee/media`,entities:[e],init:async e=>{if(!e.plugins.has(`@murumets-ee/storage`))throw Error(`@murumets-ee/media requires @murumets-ee/storage plugin. Add storage() before media() in your plugins array.`);if(!e.plugins.has(`@murumets-ee/settings`))throw Error(`@murumets-ee/media requires @murumets-ee/settings plugin. Add settings() before media() in your plugins array.`);t=r;try{let{createSettingsClient:t}=await import(`@murumets-ee/settings`),{imageStylesSettings:n}=await import(`./image-styles-settings.mjs`),i=t(n,{app:e});await i.has(`imageStyles`)||(await i.set(`imageStyles`,r.imageStyles),e.logger.info({styles:Object.keys(r.imageStyles)},`Seeded default image styles to settings DB`))}catch(t){e.logger.warn({error:t},`Failed to seed image styles to settings DB (non-fatal)`)}e.logger.info({acceptedTypes:r.acceptedTypes,maxUploadSize:r.maxUploadSize,defaultVisibility:r.defaultVisibility},`Media plugin initialized`)}}}export{n as getMediaConfig,r as media};
|
|
2
|
+
//# sourceMappingURL=plugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Media plugin — auto-registers the Media entity and validates storage dependency.\n *\n * @example\n * ```typescript\n * import { media } from '@murumets-ee/media/plugin'\n *\n * export default defineConfig({\n * plugins: [\n * storage(),\n * media({ maxUploadSize: 10 * 1024 * 1024 }),\n * ],\n * })\n * ```\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport { Media } from './entity.js'\nimport type { MediaPluginConfig } from './types.js'\n\nlet _mediaConfig: Required<MediaPluginConfig> | null = null\n\n/**\n * Get the resolved media plugin configuration.\n * Throws if plugin not initialized.\n */\nexport function getMediaConfig(): Required<MediaPluginConfig> {\n if (!_mediaConfig) {\n throw new Error('@murumets-ee/media plugin not initialized. Add media() to your plugins array.')\n }\n return _mediaConfig\n}\n\n/**\n * Media plugin factory.\n *\n * - Auto-registers the Media entity (no need to add to entities array)\n * - Validates that @murumets-ee/storage plugin is registered\n * - Stores resolved configuration\n */\nexport function media(config?: MediaPluginConfig): Plugin {\n const resolvedConfig: Required<MediaPluginConfig> = {\n acceptedTypes: config?.acceptedTypes ?? ['image/*', 'video/*', 'audio/*', 'application/pdf'],\n maxUploadSize: config?.maxUploadSize ?? 50 * 1024 * 1024,\n defaultVisibility: config?.defaultVisibility ?? 'public',\n imageStyles: config?.imageStyles ?? {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n }\n\n return {\n name: '@murumets-ee/media',\n entities: [Media],\n init: async (app) => {\n // Validate storage plugin is loaded (must be registered before media)\n if (!app.plugins.has('@murumets-ee/storage')) {\n throw new Error(\n '@murumets-ee/media requires @murumets-ee/storage plugin. ' +\n 'Add storage() before media() in your plugins array.',\n )\n }\n\n // Validate settings plugin is loaded\n if (!app.plugins.has('@murumets-ee/settings')) {\n throw new Error(\n '@murumets-ee/media requires @murumets-ee/settings plugin. ' +\n 'Add settings() before media() in your plugins array.',\n )\n }\n\n _mediaConfig = resolvedConfig\n\n // Seed image styles to settings DB on first boot\n try {\n const { createSettingsClient } = await import('@murumets-ee/settings')\n const { imageStylesSettings } = await import('./image-styles-settings.js')\n const client = createSettingsClient(imageStylesSettings, { app })\n const hasStored = await client.has('imageStyles')\n if (!hasStored) {\n await client.set('imageStyles', resolvedConfig.imageStyles)\n app.logger.info(\n { styles: Object.keys(resolvedConfig.imageStyles) },\n 'Seeded default image styles to settings DB',\n )\n }\n } catch (seedErr) {\n app.logger.warn(\n { error: seedErr },\n 'Failed to seed image styles to settings DB (non-fatal)',\n )\n }\n\n app.logger.info(\n {\n acceptedTypes: resolvedConfig.acceptedTypes,\n maxUploadSize: resolvedConfig.maxUploadSize,\n defaultVisibility: resolvedConfig.defaultVisibility,\n },\n 'Media plugin initialized',\n )\n },\n }\n}\n"],"mappings":"0CAoBA,IAAI,EAAmD,KAMvD,SAAgB,GAA8C,CAC5D,GAAI,CAAC,EACH,MAAU,MAAM,gFAAgF,CAElG,OAAO,EAUT,SAAgB,EAAM,EAAoC,CACxD,IAAM,EAA8C,CAClD,cAAe,GAAQ,eAAiB,CAAC,UAAW,UAAW,UAAW,kBAAkB,CAC5F,cAAe,GAAQ,eAAiB,GAAK,KAAO,KACpD,kBAAmB,GAAQ,mBAAqB,SAChD,YAAa,GAAQ,aAAe,CAClC,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,GAAI,CAClF,CACF,CAED,MAAO,CACL,KAAM,qBACN,SAAU,CAAC,EAAM,CACjB,KAAM,KAAO,IAAQ,CAEnB,GAAI,CAAC,EAAI,QAAQ,IAAI,uBAAuB,CAC1C,MAAU,MACR,+GAED,CAIH,GAAI,CAAC,EAAI,QAAQ,IAAI,wBAAwB,CAC3C,MAAU,MACR,iHAED,CAGH,EAAe,EAGf,GAAI,CACF,GAAM,CAAE,wBAAyB,MAAM,OAAO,yBACxC,CAAE,uBAAwB,MAAM,OAAO,+BACvC,EAAS,EAAqB,EAAqB,CAAE,MAAK,CAAC,CAC/C,MAAM,EAAO,IAAI,cAAc,GAE/C,MAAM,EAAO,IAAI,cAAe,EAAe,YAAY,CAC3D,EAAI,OAAO,KACT,CAAE,OAAQ,OAAO,KAAK,EAAe,YAAY,CAAE,CACnD,6CACD,QAEI,EAAS,CAChB,EAAI,OAAO,KACT,CAAE,MAAO,EAAS,CAClB,yDACD,CAGH,EAAI,OAAO,KACT,CACE,cAAe,EAAe,cAC9B,cAAe,EAAe,cAC9B,kBAAmB,EAAe,kBACnC,CACD,2BACD,EAEJ"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { a as MediaRecord } from "./types-BV_pOm23.mjs";
|
|
2
|
+
import { Logger } from "@murumets-ee/core";
|
|
3
|
+
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
|
+
import { CountOptions, FindByIdOptions, FindManyOptions } from "@murumets-ee/entity/query";
|
|
5
|
+
|
|
6
|
+
//#region src/query-client.d.ts
|
|
7
|
+
interface MediaQueryClientConfig {
|
|
8
|
+
db: PostgresJsDatabase;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
}
|
|
11
|
+
declare class MediaQueryClient {
|
|
12
|
+
private db;
|
|
13
|
+
private logger?;
|
|
14
|
+
private query;
|
|
15
|
+
constructor(config: MediaQueryClientConfig);
|
|
16
|
+
private getQuery;
|
|
17
|
+
findById(id: string, options?: FindByIdOptions): Promise<MediaRecord | null>;
|
|
18
|
+
findMany(options?: FindManyOptions): Promise<MediaRecord[]>;
|
|
19
|
+
count(options?: CountOptions): Promise<number>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Factory — creates a MediaQueryClient.
|
|
23
|
+
* Must be called after createApp().
|
|
24
|
+
*/
|
|
25
|
+
declare function createMediaQueryClient(): Promise<MediaQueryClient>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { MediaQueryClient, MediaQueryClientConfig, createMediaQueryClient };
|
|
28
|
+
//# sourceMappingURL=query-client.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-client.d.mts","names":[],"sources":["../src/query-client.ts"],"mappings":";;;;;;UAoBiB,sBAAA;EACf,EAAA,EAAI,kBAAA;EACJ,MAAA,GAAS,MAAA;AAAA;AAAA,cAGE,gBAAA;EAAA,QACH,EAAA;EAAA,QACA,MAAA;EAAA,QACA,KAAA;cAEI,MAAA,EAAQ,sBAAA;EAAA,QAKN,QAAA;EAYR,QAAA,CAAS,EAAA,UAAY,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,WAAA;EAMzD,QAAA,CAAS,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,WAAA;EAM7C,KAAA,CAAM,OAAA,GAAU,YAAA,GAAe,OAAA;AAAA;;;;;iBAUjB,sBAAA,CAAA,GAA0B,OAAA,CAAQ,gBAAA"}
|