@murumets-ee/media 0.71.0 → 0.72.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.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/backfill-D2Ubblh3.mjs +2 -0
- package/dist/backfill-D2Ubblh3.mjs.map +1 -0
- package/dist/backfill-trigger-BFL1L7_j.mjs +2 -0
- package/dist/backfill-trigger-BFL1L7_j.mjs.map +1 -0
- package/dist/backlog-D-H5VUQA.mjs +2 -0
- package/dist/backlog-D-H5VUQA.mjs.map +1 -0
- package/dist/client.d.mts +55 -7
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/crop-editor.d.mts +60 -0
- package/dist/crop-editor.d.mts.map +1 -0
- package/dist/crop-editor.mjs +2 -0
- package/dist/crop-editor.mjs.map +1 -0
- package/dist/crop-rect-B-AJ2pWm.mjs +2 -0
- package/dist/crop-rect-B-AJ2pWm.mjs.map +1 -0
- package/dist/crop-rect-C-SbjoxK.d.mts +59 -0
- package/dist/crop-rect-C-SbjoxK.d.mts.map +1 -0
- package/dist/crop.d.mts +2 -1
- package/dist/crop.mjs +1 -1
- package/dist/definitions-LJpgrdxd.mjs +2 -0
- package/dist/definitions-LJpgrdxd.mjs.map +1 -0
- package/dist/deps-CzHEhyJK.mjs +2 -0
- package/dist/deps-CzHEhyJK.mjs.map +1 -0
- package/dist/derive-media-type-C38ZA_Cl.mjs +2 -0
- package/dist/derive-media-type-C38ZA_Cl.mjs.map +1 -0
- package/dist/en-A8YzaZ51.mjs +2 -0
- package/dist/en-A8YzaZ51.mjs.map +1 -0
- package/dist/entity-CsDdjKz6.mjs +2 -0
- package/dist/entity-CsDdjKz6.mjs.map +1 -0
- package/dist/et-CXvu8U31.mjs +2 -0
- package/dist/et-CXvu8U31.mjs.map +1 -0
- package/dist/generate-variants-zyPjaGcy.mjs +2 -0
- package/dist/generate-variants-zyPjaGcy.mjs.map +1 -0
- package/dist/i18n.mjs +1 -1
- package/dist/i18n.mjs.map +1 -1
- package/dist/image-styles-settings.d.mts +8 -1
- package/dist/image-styles-settings.d.mts.map +1 -1
- package/dist/image-styles-settings.mjs +1 -1
- package/dist/image-styles-settings.mjs.map +1 -1
- package/dist/image-styles.d.mts +35 -21
- package/dist/image-styles.d.mts.map +1 -1
- package/dist/image-styles.mjs +1 -2
- package/dist/image-styles.mjs.map +1 -1
- package/dist/index.d.mts +16 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/media-config-DmcTxuDM.mjs +2 -0
- package/dist/media-config-DmcTxuDM.mjs.map +1 -0
- package/dist/owned-delete-Cn_KFFjS.mjs +2 -0
- package/dist/owned-delete-Cn_KFFjS.mjs.map +1 -0
- package/dist/picker.d.mts +1 -94
- package/dist/picker.d.mts.map +1 -1
- package/dist/picker.mjs +1 -2
- package/dist/picker.mjs.map +1 -1
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/process-image-Deedtkh4.mjs +2 -0
- package/dist/process-image-Deedtkh4.mjs.map +1 -0
- package/dist/processing.d.mts +201 -7
- package/dist/processing.d.mts.map +1 -1
- package/dist/processing.mjs +1 -1
- package/dist/public-resolver.d.mts +1 -1
- package/dist/public-resolver.mjs +1 -1
- package/dist/public-resolver.mjs.map +1 -1
- package/dist/query-client.d.mts +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/regenerate-variants-Cp3sNHkT.mjs +2 -0
- package/dist/regenerate-variants-Cp3sNHkT.mjs.map +1 -0
- package/dist/register-BSGkELj0.mjs +2 -0
- package/dist/register-BSGkELj0.mjs.map +1 -0
- package/dist/resolve-image-styles-pf7Nd3Zf.mjs +2 -0
- package/dist/resolve-image-styles-pf7Nd3Zf.mjs.map +1 -0
- package/dist/routes-BJ23Cp0g.mjs +2 -0
- package/dist/routes-BJ23Cp0g.mjs.map +1 -0
- package/dist/ru-Dj-ax8s_.mjs +2 -0
- package/dist/ru-Dj-ax8s_.mjs.map +1 -0
- package/dist/schedule-DREjg2ji.mjs +2 -0
- package/dist/schedule-DREjg2ji.mjs.map +1 -0
- package/dist/shapes-40dHtTb-.d.mts +90 -0
- package/dist/shapes-40dHtTb-.d.mts.map +1 -0
- package/dist/shapes-BlKW-0C1.mjs +2 -0
- package/dist/shapes-BlKW-0C1.mjs.map +1 -0
- package/dist/slot-CY4isqmN.mjs +2 -0
- package/dist/slot-CY4isqmN.mjs.map +1 -0
- package/dist/{types-CnORVkLZ.d.mts → types-CgkJF5dc.d.mts} +85 -5
- package/dist/types-CgkJF5dc.d.mts.map +1 -0
- package/dist/types-DwzblZfW.d.mts +97 -0
- package/dist/types-DwzblZfW.d.mts.map +1 -0
- package/dist/variant-key-Cki4n-42.mjs +2 -0
- package/dist/variant-key-Cki4n-42.mjs.map +1 -0
- package/dist/variant-plan-DV8hsMdz.mjs +2 -0
- package/dist/variant-plan-DV8hsMdz.mjs.map +1 -0
- package/package.json +21 -8
- package/dist/client-CSOH7Zun.mjs +0 -2
- package/dist/client-CSOH7Zun.mjs.map +0 -1
- package/dist/en-0YnQ_Qz-.mjs +0 -2
- package/dist/en-0YnQ_Qz-.mjs.map +0 -1
- package/dist/entity-CNxAERYG.mjs +0 -2
- package/dist/entity-CNxAERYG.mjs.map +0 -1
- package/dist/entity-DKFCdQJT.mjs +0 -2
- package/dist/entity-DKFCdQJT.mjs.map +0 -1
- package/dist/et-BIxGYWVK.mjs +0 -2
- package/dist/et-BIxGYWVK.mjs.map +0 -1
- package/dist/image-styles-settings-DEnu-V2E.mjs +0 -2
- package/dist/image-styles-settings-DEnu-V2E.mjs.map +0 -1
- package/dist/plugin-B_ZtfiAc.mjs +0 -2
- package/dist/plugin-B_ZtfiAc.mjs.map +0 -1
- package/dist/process-image-WByEHTXQ.mjs +0 -2
- package/dist/process-image-WByEHTXQ.mjs.map +0 -1
- package/dist/regenerate-variants-2Lmo5kf8.mjs +0 -2
- package/dist/regenerate-variants-2Lmo5kf8.mjs.map +0 -1
- package/dist/regenerate-variants-B9AXXN6N.mjs +0 -2
- package/dist/regenerate-variants-B9AXXN6N.mjs.map +0 -1
- package/dist/resolve-image-styles-Cakg1Qpp.mjs +0 -2
- package/dist/resolve-image-styles-Cakg1Qpp.mjs.map +0 -1
- package/dist/resolve-image-styles-iN9JbZYf.mjs +0 -2
- package/dist/resolve-image-styles-iN9JbZYf.mjs.map +0 -1
- package/dist/routes-C7Fx-055.mjs +0 -2
- package/dist/routes-C7Fx-055.mjs.map +0 -1
- package/dist/ru-UUTBfsMF.mjs +0 -2
- package/dist/ru-UUTBfsMF.mjs.map +0 -1
- package/dist/types-CnORVkLZ.d.mts.map +0 -1
- package/dist/usage-CVAqkS6h.mjs +0 -2
- package/dist/usage-CVAqkS6h.mjs.map +0 -1
- package/dist/variant-key-JBTJXPL1.mjs +0 -2
- package/dist/variant-key-JBTJXPL1.mjs.map +0 -1
- package/dist/variant-key-RKke2RfS.mjs +0 -2
- package/dist/variant-key-RKke2RfS.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
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":";2eA2BA,MAAM,GAAc,CAAC,QAAS,UAAW,SAAU,UAAW,MAAM,EAC9D,GAAiB,CAAC,OAAQ,OAAQ,MAAO,MAAM,EAE/CA,GAAiB,CACrB,MAAO,eACP,YACE,uHACF,SAAU,YACV,UAAW,aACX,UAAW,aACX,MAAO,QACP,OAAQ,SACR,IAAK,MACL,OAAQ,SACR,QAAS,UACT,KAAM,OACN,OAAQ,SACR,OAAQ,SACR,mBAAoB,qBACpB,yBAA0B,6DAC1B,YAAa,SACb,SAAU,8BACV,GAAI,IACN,EAQA,SAAgB,EAAmB,CACjC,QACA,WACA,QACA,OAAQ,EACR,cAC0B,CAC1B,IAAM,EAAS,CAAE,GAAGA,GAAgB,GAAG,CAAW,EAC5C,EAAS,GAAS,CAAC,EAGnB,CAAC,GAAc,GAAmB,EAAS,EAAK,EAChD,CAAC,EAAY,GAAiB,EAAwB,IAAI,EAC1D,CAAC,EAAU,GAAe,EAAS,EAAE,EACrC,CAAC,EAAW,GAAgB,EAAS,EAAE,EACvC,CAAC,EAAY,GAAiB,EAAS,EAAE,EACzC,CAAC,EAAS,GAAc,EAAiB,OAAO,EAChD,CAAC,EAAY,GAAiB,EAAiB,MAAM,EACrD,CAAC,EAAa,GAAkB,EAAS,IAAI,EAC7C,CAAC,EAAW,GAAgB,EAAwB,IAAI,EAGxD,CAAC,EAAc,GAAmB,EAAS,EAAK,EAChD,CAAC,EAAW,GAAgB,EAAwB,IAAI,EAExD,GAAgB,MAAkB,CACtC,EAAc,IAAI,EAClB,EAAY,EAAE,EACd,EAAa,EAAE,EACf,EAAc,EAAE,EAChB,EAAW,OAAO,EAClB,EAAc,MAAM,EACpB,EAAe,IAAI,EACnB,EAAa,IAAI,EACjB,EAAgB,EAAI,CACtB,EAAG,CAAC,CAAC,EAEC,EAAiB,EACpB,GAAgB,CACf,IAAM,EAAQ,EAAO,GAChB,IACL,EAAc,CAAG,EACjB,EAAY,CAAG,EACf,EAAa,EAAM,OAAO,SAAS,GAAK,EAAE,EAC1C,EAAc,EAAM,QAAQ,SAAS,GAAK,EAAE,EAC5C,EAAW,EAAM,KAAO,OAAO,EAC/B,EAAc,EAAM,QAAU,MAAM,EACpC,GAAgB,EAAM,SAAW,GAAA,CAAI,SAAS,CAAC,EAC/C,EAAa,IAAI,EACjB,EAAgB,EAAI,EACtB,EACA,CAAC,CAAM,CACT,EAEM,GAAkB,MAAkB,CACxC,EAAa,IAAI,EAEjB,IAAM,EAAO,EAAS,KAAK,CAAC,CAAC,YAAY,EACzC,GAAI,CAAC,GAAQ,CAAC,qBAAqB,KAAK,CAAI,EAAG,CAC7C,EAAa,sDAAsD,EACnE,MACF,CACA,GAAI,IAAS,GAAc,EAAO,GAAO,CACvC,EAAa,kBAAkB,EAAK,iBAAiB,EACrD,MACF,CAEA,IAAM,EAAI,EAAY,OAAO,SAAS,EAAW,EAAE,EAAI,IAAA,GACjD,EAAI,EAAa,OAAO,SAAS,EAAY,EAAE,EAAI,IAAA,GACzD,GAAI,CAAC,GAAK,CAAC,EAAG,CACZ,EAAa,sCAAsC,EACnD,MACF,CACA,GAAI,IAAM,IAAA,KAAc,OAAO,MAAM,CAAC,GAAK,GAAK,GAAI,CAClD,EAAa,iCAAiC,EAC9C,MACF,CACA,GAAI,IAAM,IAAA,KAAc,OAAO,MAAM,CAAC,GAAK,GAAK,GAAI,CAClD,EAAa,kCAAkC,EAC/C,MACF,CACA,IAAM,EAAI,OAAO,SAAS,EAAa,EAAE,EACzC,GAAI,OAAO,MAAM,CAAC,GAAK,EAAI,GAAK,EAAI,IAAK,CACvC,EAAa,uBAAuB,EACpC,MACF,CAEA,IAAM,EAAM,EACN,EAAS,EACT,EAAoB,CACxB,GAAI,EAAI,CAAE,MAAO,CAAE,EAAI,CAAC,EACxB,GAAI,EAAI,CAAE,OAAQ,CAAE,EAAI,CAAC,EACzB,MACA,SACA,QAAS,CACX,EAEM,EAAO,CAAE,GAAG,CAAO,EACrB,GAAc,IAAe,GAC/B,OAAO,EAAK,GAEd,EAAK,GAAQ,EACb,EAAS,CAAI,EACb,EAAgB,EAAK,CACvB,EAAG,CACD,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,CACF,CAAC,EAEK,GAAmB,EAAa,GAAgB,CACpD,EAAa,CAAG,EAChB,EAAgB,EAAI,CACtB,EAAG,CAAC,CAAC,EAEC,GAAe,MAAkB,CACrC,GAAI,CAAC,EAAW,OAChB,IAAM,EAAO,CAAE,GAAG,CAAO,EACzB,OAAO,EAAK,GACZ,EAAS,CAAI,EACb,EAAgB,EAAK,EACrB,EAAa,IAAI,CACnB,EAAG,CAAC,EAAW,EAAQ,CAAQ,CAAC,EAE1B,EAAgB,OAAO,QAAQ,CAAM,CAAC,CAAC,MAAM,CAAC,GAAI,CAAC,KAAO,EAAE,cAAc,CAAC,CAAC,EAElF,OACE,EAAC,MAAD,CAAK,UAAW,EAAG,YAAa,GAAY,IAAI,WAAhD,CACE,EAAC,MAAD,CAAK,UAAW,EAAG,mCAAoC,GAAY,MAAM,WAAzE,CACE,EAAC,IAAD,CAAG,UAAU,yCAAiC,EAAO,WAAe,CAAA,EACpE,EAAC,EAAD,CAAQ,QAAS,GAAe,KAAK,KAAK,KAAK,kBAA/C,CACE,EAAC,EAAD,CAAM,UAAU,cAAgB,CAAA,EAC/B,EAAO,QACF,GACL,IAEJ,GACC,EAAC,MAAD,CAAK,UAAU,wGACZ,CACE,CAAA,EAGN,EAAc,SAAW,EACxB,EAAC,IAAD,CAAG,UAAU,0DAAkD,EAAO,QAAY,CAAA,EAElF,EAAC,MAAD,CAAK,UAAW,EAAG,oBAAqB,GAAY,KAAK,WACvD,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,GAAD,CAAA,SACE,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAA,SAAY,EAAO,SAAqB,CAAA,EACxC,EAAC,EAAD,CAAW,UAAU,uBAAe,EAAO,KAAiB,CAAA,EAC5D,EAAC,EAAD,CAAW,UAAU,uBAAe,EAAO,MAAkB,CAAA,EAC7D,EAAC,EAAD,CAAW,UAAU,uBAAe,EAAO,GAAe,CAAA,EAC1D,EAAC,EAAD,CAAW,UAAU,uBAAe,EAAO,MAAkB,CAAA,EAC7D,EAAC,EAAD,CAAW,UAAU,uBAAe,EAAO,OAAmB,CAAA,EAC9D,EAAC,EAAD,CAAW,UAAU,MAAQ,CAAA,CACrB,CAAA,CAAA,CACC,CAAA,EACb,EAAC,EAAD,CAAA,SACG,EAAc,KAAK,CAAC,EAAM,KACzB,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAW,UAAU,6BAArB,CACG,EACA,IAAS,aACR,EAAC,EAAD,CAAO,QAAQ,YAAY,UAAU,wBAClC,EAAO,WACH,CAAA,CAEA,IACX,EAAC,EAAD,CAAW,UAAU,oCAClB,EAAM,MAAQ,GAAG,EAAM,QAAQ,EAAO,KAAO,GACrC,CAAA,EACX,EAAC,EAAD,CAAW,UAAU,oCAClB,EAAM,OAAS,GAAG,EAAM,SAAS,EAAO,KAAO,GACvC,CAAA,EACX,EAAC,EAAD,CAAW,UAAU,qDAClB,EAAM,KAAO,OACL,CAAA,EACX,EAAC,EAAD,CAAW,UAAU,qDAClB,EAAM,QAAU,MACR,CAAA,EACX,EAAC,EAAD,CAAW,UAAU,oCAA4B,EAAM,SAAW,EAAc,CAAA,EAChF,EAAC,EAAD,CAAA,SACE,EAAC,MAAD,CAAK,UAAW,EAAG,sCAAuC,GAAY,OAAO,WAA7E,CACE,EAAC,EAAD,CACE,QAAQ,QACR,KAAK,KACL,KAAK,SACL,YAAe,EAAe,CAAI,EAClC,MAAO,EAAO,mBAEd,EAAC,EAAD,CAAQ,UAAU,aAAe,CAAA,CAC3B,CAAA,EACR,EAAC,EAAD,CACE,QAAQ,QACR,KAAK,KACL,KAAK,SACL,YAAe,GAAiB,CAAI,EACpC,MAAO,EAAO,OACd,UAAU,mDAEV,EAAC,EAAD,CAAQ,UAAU,aAAe,CAAA,CAC3B,CAAA,CACL,GACI,CAAA,CACH,CAAA,EA7CK,CA6CL,CACX,CACQ,CAAA,CACN,CAAA,CAAA,CACJ,CAAA,EAGP,EAAC,EAAD,CAAQ,KAAM,GAAc,aAAc,WACxC,EAAC,EAAD,CAAe,UAAW,GAAY,gBAAtC,CACE,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAA,SAAc,EAAa,EAAO,UAAY,EAAO,QAAsB,CAAA,EAC3E,EAAC,EAAD,CAAA,SACG,EACG,YAAY,EAAW,gBACvB,uCACa,CAAA,CACP,CAAA,CAAA,EAEd,EAAC,MAAD,CAAK,UAAU,0BAAf,CACG,GAAa,EAAC,MAAD,CAAK,UAAU,oCAA4B,CAAe,CAAA,EAExE,EAAC,MAAD,CAAK,UAAU,qBAAf,CACE,EAAC,EAAD,CAAO,QAAQ,sBAAc,EAAO,SAAiB,CAAA,EACrD,EAAC,EAAD,CACE,GAAG,aACH,MAAO,EACP,SAAW,GAAM,EAAY,EAAE,OAAO,KAAK,EAC3C,YAAY,gCACZ,SAAU,CAAC,CAAC,CACb,CAAA,CACE,IAEL,EAAC,MAAD,CAAK,UAAU,kCAAf,CACE,EAAC,MAAD,CAAK,UAAU,qBAAf,CACE,EAAC,EAAD,CAAO,QAAQ,uBAAf,CACG,EAAO,MAAM,KAAG,EAAO,GAAG,GACtB,IACP,EAAC,EAAD,CACE,GAAG,cACH,KAAK,SACL,MAAO,EACP,SAAW,GAAM,EAAa,EAAE,OAAO,KAAK,EAC5C,YAAY,WACZ,IAAK,CACN,CAAA,CACE,IACL,EAAC,MAAD,CAAK,UAAU,qBAAf,CACE,EAAC,EAAD,CAAO,QAAQ,wBAAf,CACG,EAAO,OAAO,KAAG,EAAO,GAAG,GACvB,IACP,EAAC,EAAD,CACE,GAAG,eACH,KAAK,SACL,MAAO,EACP,SAAW,GAAM,EAAc,EAAE,OAAO,KAAK,EAC7C,YAAY,WACZ,IAAK,CACN,CAAA,CACE,GACF,IAEL,EAAC,MAAD,CAAK,UAAU,kCAAf,CACE,EAAC,MAAD,CAAK,UAAU,qBAAf,CACE,EAAC,EAAD,CAAO,QAAQ,qBAAa,EAAO,GAAW,CAAA,EAC9C,EAAC,EAAD,CAAQ,GAAG,YAAY,MAAO,EAAS,SAAW,GAAM,EAAW,EAAE,OAAO,KAAK,WAC9E,GAAY,IAAK,GAChB,EAAC,SAAD,CAAgB,MAAO,WACpB,CACK,EAFK,CAEL,CACT,CACK,CAAA,CACL,IACL,EAAC,MAAD,CAAK,UAAU,qBAAf,CACE,EAAC,EAAD,CAAO,QAAQ,wBAAgB,EAAO,MAAc,CAAA,EACpD,EAAC,EAAD,CACE,GAAG,eACH,MAAO,EACP,SAAW,GAAM,EAAc,EAAE,OAAO,KAAK,WAE5C,GAAe,IAAK,GACnB,EAAC,SAAD,CAAgB,MAAO,WACpB,CACK,EAFK,CAEL,CACT,CACK,CAAA,CACL,GACF,IAEL,EAAC,MAAD,CAAK,UAAU,qBAAf,CACE,EAAC,EAAD,CAAO,QAAQ,yBAAf,CAAgC,EAAO,QAAQ,UAAe,IAC9D,EAAC,EAAD,CACE,GAAG,gBACH,KAAK,SACL,MAAO,EACP,SAAW,GAAM,EAAe,EAAE,OAAO,KAAK,EAC9C,IAAK,EACL,IAAK,GACN,CAAA,CACE,GACF,IAEL,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAQ,QAAQ,UAAU,KAAK,SAAS,YAAe,EAAgB,EAAK,WACzE,EAAO,MACF,CAAA,EACR,EAAC,EAAD,CAAQ,KAAK,SAAS,QAAS,YAC5B,EAAO,IACF,CAAA,CACI,CAAA,CAAA,CACD,GACT,CAAA,EAER,EAAC,EAAD,CAAQ,KAAM,EAAc,aAAc,WACxC,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAA,SAAc,EAAO,kBAAgC,CAAA,EACrD,EAAC,EAAD,CAAA,SAAA,CACG,EAAO,yBACP,GACC,EAAC,OAAD,CAAM,UAAU,gDAAwC,CAAgB,CAAA,CAEzD,CAAA,CAAA,CACP,CAAA,CAAA,EACd,EAAC,EAAD,CAAA,SAAA,CACE,EAAC,EAAD,CAAQ,QAAQ,UAAU,KAAK,SAAS,YAAe,EAAgB,EAAK,WACzE,EAAO,MACF,CAAA,EACR,EAAC,EAAD,CAAQ,QAAQ,cAAc,KAAK,SAAS,QAAS,YAClD,EAAO,MACF,CAAA,CACI,CAAA,CAAA,CACD,CAAA,CAAA,CACT,CAAA,CACL,GAET,CC1YA,MAAM,EAA2D,CAC/D,WAAY,0BACZ,aAAc,iBAChB,EAgBA,SAAgB,EAAyB,CACvC,cACA,OAAQ,GACwB,CAChC,IAAM,EAAS,CAAE,GAAG,EAAgB,GAAG,CAAW,EAC5C,CAAC,EAAW,GAAgB,EAAS,EAAK,EAC1C,CAAC,EAAQ,GAAa,EAAkC,IAAI,EAC5D,CAAC,EAAO,GAAY,EAAwB,IAAI,EAEhD,EAAc,EAAY,SAAY,CAC1C,EAAa,EAAI,EACjB,EAAU,IAAI,EACd,EAAS,IAAI,EACb,GAAI,CACF,IAAM,EAAM,MAAM,MAAM,GAAG,EAAY,4BAA6B,CAClE,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAC,CAAC,CACzB,CAAC,EACD,GAAI,CAAC,EAAI,GAAI,CACX,IAAM,EAAQ,MAAM,EAAI,KAAK,CAAC,CAAC,WAAa,CAAC,EAAE,EAC/C,MAAU,MAAM,EAAK,OAAS,wBAAwB,EAAI,OAAO,EAAE,CACrE,CAEA,EAAU,MADU,EAAI,KAAK,CACf,CAChB,OAAS,EAAK,CACZ,EAAS,aAAe,MAAQ,EAAI,QAAU,qBAAqB,CACrE,QAAU,CACR,EAAa,EAAK,CACpB,CACF,EAAG,CAAC,CAAW,CAAC,EAEhB,OACE,EAAC,MAAD,CAAK,UAAU,mCAAf,CACG,GACC,EAAC,OAAD,CAAM,UAAU,yCAAhB,CAAgD,aACnC,EAAO,UAAU,OAAK,EAAO,MACvC,EAAO,QAAU,GAAK,KAAK,EAAO,QAAQ,UAC1C,EAAO,OAAS,GACf,EAAC,OAAD,CAAM,UAAU,4BAAhB,CAAmC,KAAG,EAAO,OAAO,SAAa,GAE/D,IAEP,GAAS,EAAC,OAAD,CAAM,UAAU,oCAA4B,CAAY,CAAA,EAClE,EAAC,EAAD,CAAQ,QAAQ,UAAU,KAAK,KAAK,KAAK,SAAS,QAAS,EAAa,SAAU,WAC/E,EACC,EAAA,EAAA,CAAA,SAAA,CACE,EAAC,EAAD,CAAS,UAAU,2BAA6B,CAAA,EAC/C,EAAO,YACR,CAAA,CAAA,EAEF,EAAA,EAAA,CAAA,SAAA,CACE,EAAC,EAAD,CAAW,UAAU,cAAgB,CAAA,EACpC,EAAO,UACR,CAAA,CAAA,CAEE,CAAA,CACL,GAET"}
|
|
1
|
+
{"version":3,"file":"image-styles.mjs","names":["COPY","interpolate"],"sources":["../src/image-styles/image-styles-manager.tsx","../src/image-styles/image-vocabulary-editor.tsx","../src/image-styles/regenerate-variants-action.tsx"],"sourcesContent":["'use client'\n\nimport type { ImageStyle } from '../types.js'\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 { 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. Every image gets a file for each style, generated in the background by the queue worker. 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 type { ImageShape, ImageVocabulary } from '../types.js'\nimport { Button, cn, Input, Label } from '@murumets-ee/ui'\nimport { Plus, Trash2 } from 'lucide-react'\nimport { useTranslations } from 'next-intl'\nimport { type KeyboardEvent, useId, useRef, useState } from 'react'\nimport {\n DEFAULT_IMAGE_VOCABULARY,\n MAX_LADDER_RUNGS,\n MAX_SHAPES,\n MAX_VARIANT_WIDTH,\n MIN_VARIANT_WIDTH,\n NATURAL_SHAPE,\n parseShapeRatio,\n plannedFileCount,\n SHAPE_NAME_PATTERN,\n} from '../shapes.js'\nimport { MAX_VARIANTS_PER_IMAGE } from '../variant-plan.js'\nimport type { ImageVocabularyEditorProps } from './types.js'\n\n/**\n * Every visible string, as a next-intl KEY plus the English fallback — the\n * interface-copy rule. The keys live in this package's catalogue\n * (`src/messages/*.json`, `Media.imageVocabulary.*`).\n */\nconst COPY = {\n description: [\n 'Media.imageVocabulary.description',\n 'Every photograph is generated in each shape below, at each width. A slot that names a shape draws these files; a slot with its own ratio is cropped in the browser instead.',\n ],\n usingDefaults: [\n 'Media.imageVocabulary.usingDefaults',\n 'Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.',\n ],\n widthsLabel: ['Media.imageVocabulary.widthsLabel', 'Widths (px)'],\n widthsHelp: [\n 'Media.imageVocabulary.widthsHelp',\n 'Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.',\n ],\n widthsInvalid: [\n 'Media.imageVocabulary.widthsInvalid',\n 'Use up to {count} ascending whole numbers between {min} and {max}.',\n ],\n shapesLabel: ['Media.imageVocabulary.shapesLabel', 'Shapes'],\n naturalNote: [\n 'Media.imageVocabulary.naturalNote',\n 'Plus “natural” — each photograph at its own shape, over the same widths.',\n ],\n columnName: ['Media.imageVocabulary.columnName', 'Name'],\n columnRatio: ['Media.imageVocabulary.columnRatio', 'Ratio (W:H)'],\n columnWidths: ['Media.imageVocabulary.columnWidths', 'Own widths'],\n ownWidthsPlaceholder: ['Media.imageVocabulary.ownWidthsPlaceholder', 'All widths'],\n ratioInvalid: ['Media.imageVocabulary.ratioInvalid', 'Write the ratio as W:H, for example 3:2.'],\n nameInvalid: [\n 'Media.imageVocabulary.nameInvalid',\n 'Lowercase letters, digits, - and _, starting with a letter — and not “natural”.',\n ],\n nameTaken: ['Media.imageVocabulary.nameTaken', 'A shape named “{name}” already exists.'],\n tooMany: ['Media.imageVocabulary.tooMany', 'At most {count} shapes.'],\n tooManyFiles: [\n 'Media.imageVocabulary.tooManyFiles',\n 'That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.',\n ],\n add: ['Media.imageVocabulary.add', 'Add shape'],\n remove: ['Media.imageVocabulary.remove', 'Remove {name}'],\n} as const satisfies Record<string, readonly [string, string]>\n\n/** Every catalogue key this component reads — exported for the catalogue test. */\nexport const IMAGE_VOCABULARY_MESSAGE_KEYS: readonly string[] = Object.values(COPY).map(([k]) => k)\n\ntype CopyId = keyof typeof COPY\ntype CopyValues = Record<string, string | number>\n\nfunction interpolate(template: string, values: CopyValues | undefined): string {\n if (!values) return template\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) =>\n name in values ? String(values[name]) : whole,\n )\n}\n\n/**\n * Resolve copy with an UNSCOPED translator, falling back to the English LABEL\n * — never to the key — when the app has not merged this package's catalogue.\n */\nfunction useCopy(): (id: CopyId, values?: CopyValues) => string {\n const t = useTranslations()\n return (id, values) => {\n const [key, fallback] = COPY[id]\n return t.has(key) ? t(key, values) : interpolate(fallback, values)\n }\n}\n\n/** `'320, 640,960'` → `[320, 640, 960]`, or `null` unless ascending, distinct, in bounds. */\nexport function parseWidthList(raw: string): number[] | null {\n const parts = raw\n .split(',')\n .map((p) => p.trim())\n .filter((p) => p !== '')\n if (parts.length === 0 || parts.length > MAX_LADDER_RUNGS) return null\n const widths: number[] = []\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) return null\n const w = Number(part)\n if (w < MIN_VARIANT_WIDTH || w > MAX_VARIANT_WIDTH) return null\n const previous = widths[widths.length - 1]\n if (previous !== undefined && w <= previous) return null\n widths.push(w)\n }\n return widths\n}\n\nfunction sameWidths(a: readonly number[], b: readonly number[]): boolean {\n return a.length === b.length && a.every((w, i) => w === b[i])\n}\n\nfunction formatWidths(widths: readonly number[] | undefined): string {\n return widths ? widths.join(', ') : ''\n}\n\n/**\n * Enter commits the field it was pressed in, and never submits the settings\n * form: fields commit on blur, so a form submitted from inside one would save\n * the value from BEFORE the edit and report success.\n */\nfunction onEnter(commit: () => void) {\n return (event: KeyboardEvent<HTMLInputElement>) => {\n // Enter that CONFIRMS an IME candidate is not a commit — Chrome flags it\n // `isComposing`, Safari sends keyCode 229.\n if (event.key !== 'Enter' || event.nativeEvent.isComposing || event.keyCode === 229) return\n event.preventDefault()\n commit()\n }\n}\n\n/** The seeded vocabulary as an editable value — the shared constant is frozen. */\nfunction copyOfDefaults(): ImageVocabulary {\n return {\n shapes: Object.fromEntries(\n Object.entries(DEFAULT_IMAGE_VOCABULARY.shapes).map(([name, shape]) => [name, { ...shape }]),\n ),\n ladder: [...DEFAULT_IMAGE_VOCABULARY.ladder],\n }\n}\n\n/**\n * The editor for `media.imageStyles` → `vocabulary`: the shape vocabulary and\n * the width ladder (images D003). Controlled — every valid edit commits through\n * `onChange`, and the settings form's Save persists it; an invalid one stays in\n * its own field with the reason beside it and commits nothing.\n *\n * `value` is `null` until something is saved, because the key deliberately\n * has no settings default (so the plugin-config tier stays reachable). The\n * editor then shows the seeded vocabulary and says so.\n */\nexport function ImageVocabularyEditor(props: ImageVocabularyEditorProps) {\n // The fields keep DRAFTS (a half-typed ratio must not be thrown away), so an\n // EXTERNAL change to `value` — a form reset, a reload — has to re-seed them.\n // Our own commits must not: remounting on those would drop focus from the\n // field the operator just tabbed into. So the fields remount on a value this\n // editor did not produce, tracked during render rather than in an effect.\n const serialized = JSON.stringify(props.value ?? null)\n const [committed, setCommitted] = useState<string | null>(null)\n const [seen, setSeen] = useState(serialized)\n const [epoch, setEpoch] = useState(0)\n if (serialized !== seen) {\n setSeen(serialized)\n if (serialized !== committed) setEpoch(epoch + 1)\n }\n const onChange = (next: ImageVocabulary) => {\n setCommitted(JSON.stringify(next))\n props.onChange(next)\n }\n return <VocabularyFields key={epoch} {...props} onChange={onChange} />\n}\n\nfunction VocabularyFields({ value, onChange, error, className }: ImageVocabularyEditorProps) {\n const copy = useCopy()\n const ids = useId()\n const vocabulary: ImageVocabulary = value ?? copyOfDefaults()\n const names = Object.keys(vocabulary.shapes).sort()\n\n const [ladderDraft, setLadderDraft] = useState(formatWidths(vocabulary.ladder))\n const [ladderError, setLadderError] = useState(false)\n const [newName, setNewName] = useState('')\n const [newRatio, setNewRatio] = useState('')\n const [addError, setAddError] = useState<string | null>(null)\n const [capError, setCapError] = useState<string | null>(null)\n const ratioInput = useRef<HTMLInputElement>(null)\n\n /**\n * Commit, unless the result plans more files per photograph than the cap —\n * the settings route would refuse it on Save, so refuse it here, where the\n * operator is looking, instead.\n */\n const commit = (next: ImageVocabulary): boolean => {\n const count = plannedFileCount(next)\n if (count > MAX_VARIANTS_PER_IMAGE) {\n setCapError(copy('tooManyFiles', { count, max: MAX_VARIANTS_PER_IMAGE }))\n return false\n }\n setCapError(null)\n onChange(next)\n return true\n }\n\n const commitLadder = () => {\n const parsed = parseWidthList(ladderDraft)\n setLadderError(parsed === null)\n // Blurring an untouched field must commit NOTHING: with no stored value the\n // editor is showing the defaults, and committing them would make the next\n // Save store them — silently shadowing a vocabulary configured in code.\n if (parsed && !sameWidths(parsed, vocabulary.ladder)) commit({ ...vocabulary, ladder: parsed })\n }\n\n const addShape = () => {\n const name = newName.trim()\n const ratio = newRatio.trim()\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n setAddError(copy('nameInvalid'))\n return\n }\n // `hasOwn`: `constructor` passes the name rule, and a plain lookup finds\n // `Object.prototype.constructor` — \"taken\" for a name nobody used.\n if (Object.hasOwn(vocabulary.shapes, name)) {\n setAddError(copy('nameTaken', { name }))\n return\n }\n if (names.length >= MAX_SHAPES) {\n setAddError(copy('tooMany', { count: MAX_SHAPES }))\n return\n }\n if (parseShapeRatio(ratio) === null) {\n setAddError(copy('ratioInvalid'))\n return\n }\n setAddError(null)\n if (commit({ ...vocabulary, shapes: { ...vocabulary.shapes, [name]: { ratio } } })) {\n setNewName('')\n setNewRatio('')\n }\n }\n\n const replaceShape = (name: string, shape: ImageShape) =>\n commit({ ...vocabulary, shapes: { ...vocabulary.shapes, [name]: shape } })\n\n const removeShape = (name: string) => {\n const { [name]: _removed, ...rest } = vocabulary.shapes\n commit({ ...vocabulary, shapes: rest })\n }\n\n return (\n <div className={cn('space-y-6', className)}>\n <div className=\"space-y-1 text-sm text-muted-foreground\">\n <p>{copy('description')}</p>\n {value === null || value === undefined ? <p>{copy('usingDefaults')}</p> : null}\n </div>\n\n {capError ? (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {capError}\n </p>\n ) : null}\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 ) : null}\n\n <div className=\"space-y-2\">\n <Label htmlFor={`${ids}-ladder`}>{copy('widthsLabel')}</Label>\n <Input\n id={`${ids}-ladder`}\n value={ladderDraft}\n onChange={(e) => setLadderDraft(e.target.value)}\n onBlur={commitLadder}\n onKeyDown={onEnter(commitLadder)}\n aria-invalid={ladderError}\n aria-describedby={`${ids}-ladder-help`}\n inputMode=\"numeric\"\n />\n <p\n id={`${ids}-ladder-help`}\n className={cn('text-xs', ladderError ? 'text-destructive' : 'text-muted-foreground')}\n >\n {ladderError\n ? copy('widthsInvalid', {\n count: MAX_LADDER_RUNGS,\n min: MIN_VARIANT_WIDTH,\n max: MAX_VARIANT_WIDTH,\n })\n : copy('widthsHelp')}\n </p>\n </div>\n\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium\">{copy('shapesLabel')}</legend>\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] gap-2 text-xs text-muted-foreground\">\n <span>{copy('columnName')}</span>\n <span>{copy('columnRatio')}</span>\n <span>{copy('columnWidths')}</span>\n <span />\n </div>\n {names.map((name) => {\n const shape = vocabulary.shapes[name]\n return shape ? (\n <ShapeRow\n key={name}\n name={name}\n shape={shape}\n copy={copy}\n onReplace={(next) => replaceShape(name, next)}\n onRemove={() => removeShape(name)}\n />\n ) : null\n })}\n <p className=\"text-xs text-muted-foreground\">{copy('naturalNote')}</p>\n\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_auto] items-start gap-2 pt-2\">\n <Input\n aria-label={copy('columnName')}\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n onKeyDown={onEnter(() => ratioInput.current?.focus())}\n />\n <Input\n ref={ratioInput}\n aria-label={copy('columnRatio')}\n value={newRatio}\n onChange={(e) => setNewRatio(e.target.value)}\n onKeyDown={onEnter(addShape)}\n placeholder=\"3:2\"\n />\n <Button type=\"button\" size=\"sm\" onClick={addShape}>\n <Plus className=\"mr-1 h-4 w-4\" />\n {copy('add')}\n </Button>\n </div>\n {addError ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {addError}\n </p>\n ) : null}\n </fieldset>\n </div>\n )\n}\n\nfunction ShapeRow({\n name,\n shape,\n copy,\n onReplace,\n onRemove,\n}: {\n name: string\n shape: ImageShape\n copy: ReturnType<typeof useCopy>\n onReplace: (next: ImageShape) => boolean\n onRemove: () => void\n}) {\n const [ratio, setRatio] = useState(shape.ratio)\n const [widths, setWidths] = useState(formatWidths(shape.widths))\n const [problem, setProblem] = useState<CopyId | null>(null)\n\n const commitRatio = () => {\n const ok = parseShapeRatio(ratio.trim()) !== null\n setProblem(ok ? null : 'ratioInvalid')\n if (ok && ratio.trim() !== shape.ratio) onReplace({ ...shape, ratio: ratio.trim() })\n }\n\n const commitWidths = () => {\n if (widths.trim() === '') {\n setProblem(null)\n const { widths: _dropped, ...rest } = shape\n if (shape.widths) onReplace(rest)\n return\n }\n const parsed = parseWidthList(widths)\n setProblem(parsed ? null : 'widthsInvalid')\n if (parsed && !sameWidths(parsed, shape.widths ?? [])) onReplace({ ...shape, widths: parsed })\n }\n\n return (\n <div className=\"space-y-1\">\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] items-center gap-2\">\n <span className=\"font-mono text-sm\">{name}</span>\n <Input\n aria-label={`${name} ${copy('columnRatio')}`}\n value={ratio}\n onChange={(e) => setRatio(e.target.value)}\n onBlur={commitRatio}\n onKeyDown={onEnter(commitRatio)}\n aria-invalid={problem === 'ratioInvalid'}\n />\n <Input\n aria-label={`${name} ${copy('columnWidths')}`}\n value={widths}\n onChange={(e) => setWidths(e.target.value)}\n onBlur={commitWidths}\n onKeyDown={onEnter(commitWidths)}\n placeholder={copy('ownWidthsPlaceholder')}\n aria-invalid={problem === 'widthsInvalid'}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onRemove}\n title={copy('remove', { name })}\n aria-label={copy('remove', { name })}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n {problem ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {copy(problem, {\n count: MAX_LADDER_RUNGS,\n min: MIN_VARIANT_WIDTH,\n max: MAX_VARIANT_WIDTH,\n })}\n </p>\n ) : null}\n </div>\n )\n}\n","'use client'\n\nimport { Button } from '@murumets-ee/ui'\nimport { Loader2, RefreshCw } from 'lucide-react'\nimport { useTranslations } from 'next-intl'\nimport { useCallback, useState } from 'react'\nimport type { RegenerateVariantsActionProps } from './types.js'\n\n/**\n * Every visible string, as a next-intl KEY plus the English fallback — the\n * interface-copy rule. Keys live in `src/messages/*.json` (`Media.variantGeneration.*`).\n */\nconst COPY = {\n workerRequired: [\n 'Media.variantGeneration.workerRequired',\n 'Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.',\n ],\n regenerate: ['Media.variantGeneration.regenerate', 'Catch up all images'],\n regenerating: ['Media.variantGeneration.regenerating', 'Starting…'],\n queued: [\n 'Media.variantGeneration.queued',\n 'Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.',\n ],\n alreadyRunning: [\n 'Media.variantGeneration.alreadyRunning',\n 'A catch-up is already running — follow it on the Queue page.',\n ],\n inlineDone: [\n 'Media.variantGeneration.inlineDone',\n 'Processed {processed} of {total}; skipped {skipped}; errors {errors}.',\n ],\n failed: ['Media.variantGeneration.failed', 'Could not start the catch-up: {reason}'],\n} as const satisfies Record<string, readonly [string, string]>\n\n/** Every catalogue key this component reads — exported for the catalogue test. */\nexport const REGENERATE_VARIANTS_MESSAGE_KEYS: readonly string[] = Object.values(COPY).map(\n ([k]) => k,\n)\n\ntype CopyId = keyof typeof COPY\ntype CopyValues = Record<string, string | number>\n\nfunction interpolate(template: string, values: CopyValues | undefined): string {\n if (!values) return template\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) =>\n name in values ? String(values[name]) : whole,\n )\n}\n\n/** What `POST /media/regenerate-variants` answers. */\ntype RegenerateResponse =\n | { mode: 'queued'; jobId: string; alreadyRunning: boolean }\n | { mode: 'inline'; total: number; processed: number; skipped: number; errors: number }\n\ntype Outcome =\n | { kind: 'queued'; alreadyRunning: boolean }\n | { kind: 'inline'; total: number; processed: number; skipped: number; errors: number }\n | { kind: 'error'; reason: string }\n\n/**\n * The header action on `/admin/settings/media.imageStyles`.\n *\n * Says, always and before anyone clicks, that sizes come from the queue\n * worker — an operator who has never heard of the worker must learn it here,\n * not from a slow page (images C6). The button then asks the server to catch\n * the library up: with a queue that QUEUES a background backfill and returns at\n * once (202); only an app with no queue runs it inside the request, as before.\n */\nexport function RegenerateVariantsAction({\n apiBasePath,\n labels: userLabels,\n}: RegenerateVariantsActionProps) {\n const t = useTranslations()\n const copy = (id: CopyId, values?: CopyValues): string => {\n const [key, fallback] = COPY[id]\n return t.has(key) ? t(key, values) : interpolate(fallback, values)\n }\n const [isRunning, setIsRunning] = useState(false)\n const [outcome, setOutcome] = useState<Outcome | null>(null)\n\n const handleClick = useCallback(async () => {\n setIsRunning(true)\n setOutcome(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 setOutcome({ kind: 'error', reason: data.error ?? String(res.status) })\n return\n }\n const data = (await res.json()) as RegenerateResponse\n setOutcome(\n data.mode === 'queued'\n ? { kind: 'queued', alreadyRunning: data.alreadyRunning }\n : { kind: 'inline', ...data },\n )\n } catch (err) {\n setOutcome({ kind: 'error', reason: err instanceof Error ? err.message : String(err) })\n } finally {\n setIsRunning(false)\n }\n }, [apiBasePath])\n\n return (\n <div className=\"flex max-w-xl flex-col items-end gap-2\">\n <p className=\"text-right text-xs text-muted-foreground\" data-variant-worker-required=\"\">\n {copy('workerRequired')}\n </p>\n <div className=\"flex items-center gap-3\">\n {outcome?.kind === 'queued' && (\n <span className=\"text-xs text-muted-foreground\" role=\"status\">\n {copy(outcome.alreadyRunning ? 'alreadyRunning' : 'queued')}\n </span>\n )}\n {outcome?.kind === 'inline' && (\n <span className=\"text-xs text-muted-foreground\" role=\"status\">\n {copy('inlineDone', {\n processed: outcome.processed,\n total: outcome.total,\n skipped: outcome.skipped,\n errors: outcome.errors,\n })}\n </span>\n )}\n {outcome?.kind === 'error' && (\n <span className=\"text-xs text-destructive\" role=\"alert\">\n {copy('failed', { reason: outcome.reason })}\n </span>\n )}\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={handleClick}\n disabled={isRunning}\n >\n {isRunning ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {userLabels?.regenerating ?? copy('regenerating')}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {userLabels?.regenerate ?? copy('regenerate')}\n </>\n )}\n </Button>\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as ImageVocabulary, c as MediaPluginConfig, d as MediaUploadOptions, f as MediaUploadResult, i as ImageStyle, l as MediaRecord, m as Media, n as ImageFormat, o as MediaListOptions, r as ImageShape, s as MediaListResult, t as CropShape, u as MediaType } from "./types-CgkJF5dc.mjs";
|
|
2
2
|
import { defaultImageStyles, imageStylesSettings } from "./image-styles-settings.mjs";
|
|
3
|
+
import { i as MediaPickerListResult, r as MediaPickerItem, t as MediaPickerCallbacks } from "./types-DwzblZfW.mjs";
|
|
4
|
+
import { n as DEFAULT_IMAGE_SHAPES, r as DEFAULT_IMAGE_VOCABULARY, t as DEFAULT_IMAGE_LADDER } from "./shapes-40dHtTb-.mjs";
|
|
5
|
+
//#region src/crop/shapes.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Which of `styles` declare a fixed ratio, sorted by name for a stable tab
|
|
8
|
+
* order.
|
|
9
|
+
*
|
|
10
|
+
* A style with only ONE dimension set (`full: { width: 2400, fit: 'inside' }`)
|
|
11
|
+
* constrains one axis and lets the other float — there is no fixed hole to
|
|
12
|
+
* drag a crop window over, so it is excluded rather than offered a tab that
|
|
13
|
+
* cannot mean anything.
|
|
14
|
+
*/
|
|
15
|
+
declare function deriveCropShapes(styles: Record<string, ImageStyle>): CropShape[];
|
|
16
|
+
//#endregion
|
|
3
17
|
//#region src/enrich.d.ts
|
|
4
18
|
/**
|
|
5
19
|
* Enrich entity list items by resolving media field UUIDs to variant URLs.
|
|
@@ -16,45 +30,5 @@ declare function enrichWithMediaUrls(entity: {
|
|
|
16
30
|
}>;
|
|
17
31
|
}, items: Record<string, unknown>[], styleName?: string): Promise<void>;
|
|
18
32
|
//#endregion
|
|
19
|
-
|
|
20
|
-
/** Lightweight media item for the picker (no server-only deps) */
|
|
21
|
-
interface MediaPickerItem {
|
|
22
|
-
id: string;
|
|
23
|
-
title: string | null;
|
|
24
|
-
alt: string | null;
|
|
25
|
-
filename: string;
|
|
26
|
-
mimeType: string;
|
|
27
|
-
size: number;
|
|
28
|
-
width: number | null;
|
|
29
|
-
height: number | null;
|
|
30
|
-
mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
31
|
-
url: string;
|
|
32
|
-
}
|
|
33
|
-
/** Paginated result from fetchMedia callback */
|
|
34
|
-
interface MediaPickerListResult {
|
|
35
|
-
items: MediaPickerItem[];
|
|
36
|
-
total: number;
|
|
37
|
-
/**
|
|
38
|
-
* False when the server reports storage is not configured (missing
|
|
39
|
-
* env vars etc.). UI renders a banner with `reason` instead of the
|
|
40
|
-
* upload zone + grid. Omitted/true when everything is wired.
|
|
41
|
-
*/
|
|
42
|
-
configured?: boolean;
|
|
43
|
-
/** Human-readable explanation when `configured === false`. */
|
|
44
|
-
reason?: string;
|
|
45
|
-
}
|
|
46
|
-
/** Callbacks the consumer provides — framework-agnostic */
|
|
47
|
-
interface MediaPickerCallbacks {
|
|
48
|
-
/** Fetch media items with filtering and pagination */
|
|
49
|
-
fetchMedia: (options: {
|
|
50
|
-
search?: string | undefined;
|
|
51
|
-
mediaType?: string | undefined;
|
|
52
|
-
limit: number;
|
|
53
|
-
offset: number;
|
|
54
|
-
}) => Promise<MediaPickerListResult>;
|
|
55
|
-
/** Upload a file and return the created media item */
|
|
56
|
-
uploadMedia: (file: File) => Promise<MediaPickerItem>;
|
|
57
|
-
}
|
|
58
|
-
//#endregion
|
|
59
|
-
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 };
|
|
33
|
+
export { type CropShape, DEFAULT_IMAGE_LADDER, DEFAULT_IMAGE_SHAPES, DEFAULT_IMAGE_VOCABULARY, type ImageFormat, type ImageShape, type ImageStyle, type ImageVocabulary, Media, type MediaListOptions, type MediaListResult, type MediaPickerCallbacks, type MediaPickerItem, type MediaPickerListResult, type MediaPluginConfig, type MediaRecord, type MediaType, type MediaUploadOptions, type MediaUploadResult, defaultImageStyles, deriveCropShapes, enrichWithMediaUrls, imageStylesSettings };
|
|
60
34
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/crop/shapes.ts","../src/enrich.ts"],"mappings":";;;;;;;;;;;;;;iBAmBgB,gBAAA,CAAiB,MAAA,EAAQ,MAAA,SAAe,UAAA,IAAc,SAAA;;;;AAAS;;;;ACW/E;;;;iBAAsB,mBAAA,CACpB,MAAA;EAAU,SAAA,EAAW,MAAA;IAAiB,IAAA;EAAA;AAAA,GACtC,KAAA,EAAO,MAAA,qBACP,SAAA,YACC,OAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./entity-
|
|
1
|
+
import{t as e}from"./entity-CsDdjKz6.mjs";import{n as t,r as n,t as r}from"./shapes-BlKW-0C1.mjs";import{defaultImageStyles as i,imageStylesSettings as a}from"./image-styles-settings.mjs";import"server-only";function o(e){let t=[];for(let[n,r]of Object.entries(e))r.width&&r.width>0&&r.height&&r.height>0&&t.push({name:n,ratio:r.width/r.height});return t.sort((e,t)=>e.name.localeCompare(t.name))}async function s(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{r as DEFAULT_IMAGE_LADDER,t as DEFAULT_IMAGE_SHAPES,n as DEFAULT_IMAGE_VOCABULARY,e as Media,i as defaultImageStyles,o as deriveCropShapes,s as enrichWithMediaUrls,a as imageStylesSettings};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/crop/shapes.ts","../src/enrich.ts"],"sourcesContent":["/**\n * The crop editor's shape vocabulary — which configured image styles have a\n * fixed aspect ratio, and so get a crop tab (images PR03).\n *\n * Admin-only: unlike `focal-point.ts` / `crop-rect.ts`, this never reaches\n * the render spine. It runs once, server-side, on the media edit page.\n */\n\nimport type { CropShape, ImageStyle } from '../types.js'\n\n/**\n * Which of `styles` declare a fixed ratio, sorted by name for a stable tab\n * order.\n *\n * A style with only ONE dimension set (`full: { width: 2400, fit: 'inside' }`)\n * constrains one axis and lets the other float — there is no fixed hole to\n * drag a crop window over, so it is excluded rather than offered a tab that\n * cannot mean anything.\n */\nexport function deriveCropShapes(styles: Record<string, ImageStyle>): CropShape[] {\n const shapes: CropShape[] = []\n for (const [name, style] of Object.entries(styles)) {\n if (!(style.width && style.width > 0 && style.height && style.height > 0)) continue\n shapes.push({ name, ratio: style.width / style.height })\n }\n return shapes.sort((a, b) => a.name.localeCompare(b.name))\n}\n","/**\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":"gNAmBA,SAAgB,EAAiB,EAAiD,CAChF,IAAM,EAAsB,CAAC,EAC7B,IAAK,GAAM,CAAC,EAAM,KAAU,OAAO,QAAQ,CAAM,EACzC,EAAM,OAAS,EAAM,MAAQ,GAAK,EAAM,QAAU,EAAM,OAAS,GACvE,EAAO,KAAK,CAAE,OAAM,MAAO,EAAM,MAAQ,EAAM,MAAO,CAAC,EAEzD,OAAO,EAAO,MAAM,EAAG,IAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,CAC3D,CCIA,eAAsB,EACpB,EACA,EACA,EAAY,YACG,CAEf,IAAM,EAAc,OAAO,QAAQ,EAAO,SAAS,CAAC,CACjD,QAAQ,EAAG,KAAY,EAAO,OAAS,OAAO,CAAC,CAC/C,KAAK,CAAC,KAAU,CAAI,EAEvB,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,CAAG,CAEpB,CAGF,GAAI,EAAS,OAAS,EAAG,OAGzB,GAAM,CAAE,kBAAmB,MAAM,OAAO,gBAElC,EAAS,MAAM,MADA,EAAe,EAAA,CACR,eAAe,CAAC,GAAG,CAAQ,EAAG,CAAS,EAGnE,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAK,EAAa,CAC3B,IAAM,EAAM,EAAK,GACb,OAAO,GAAQ,UAAY,EAAO,IAAI,CAAG,IAC3C,EAAK,GAAG,EAAE,MAAQ,EAAO,IAAI,CAAG,EAEpC,CAEJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as e}from"./shapes-BlKW-0C1.mjs";import{getPluginConfig as t,hasPluginConfig as n}from"@murumets-ee/core";const r=`@murumets-ee/media`,i=[`image/*`,`video/*`,`audio/*`,`application/pdf`,`application/vnd.openxmlformats-officedocument.*`,`application/vnd.oasis.opendocument.*`,`text/plain`,`text/csv`];function a(t){return{acceptedTypes:t?.acceptedTypes??[...i],maxUploadSize:t?.maxUploadSize!==void 0&&t.maxUploadSize>0?t.maxUploadSize:50*1024*1024,defaultVisibility:t?.defaultVisibility??`public`,imageStyles:t?.imageStyles??{thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80}},vocabulary:t?.vocabulary??{shapes:Object.fromEntries(Object.entries(e.shapes).map(([e,t])=>[e,{...t}])),ladder:[...e.ladder]}}}function o(){return n(`@murumets-ee/media`)?t(r):a()}function s(){if(!n(`@murumets-ee/media`))throw Error(`@murumets-ee/media plugin not initialized. Add media() to your plugins array.`);return t(r)}export{s as n,a as r,o as t};
|
|
2
|
+
//# sourceMappingURL=media-config-DmcTxuDM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-config-DmcTxuDM.mjs","names":[],"sources":["../src/media-config.ts"],"sourcesContent":["/**\n * The media plugin's resolved configuration — defaults in one place, read\n * from the framework's `PluginConfigRegistry`.\n *\n * The plugin used to stash its config in a module variable from `init`, and\n * the upload route hardcoded 50 MB with a comment explaining it could not\n * trust that variable to be set yet. So `acceptedTypes` and `maxUploadSize`\n * were declared, documented, logged at boot — and enforced nowhere (F007).\n * `server.config` is placed in the registry at the merge step, BEFORE any\n * `init`, which removes the reason the route could not read it.\n */\n\nimport { getPluginConfig, hasPluginConfig } from '@murumets-ee/core'\nimport { DEFAULT_IMAGE_VOCABULARY } from './shapes.js'\nimport type { MediaPluginConfig } from './types.js'\n\ndeclare module '@murumets-ee/core' {\n interface PluginConfigRegistry {\n '@murumets-ee/media': Required<MediaPluginConfig>\n }\n}\n\nexport const MEDIA_PLUGIN_NAME = '@murumets-ee/media'\n\n/**\n * The default `acceptedTypes`, matched against what `detectMimeType` (file-type\n * 21) actually REPORTS — not what a browser claims.\n *\n * Wider than the image/video/audio/PDF the option's docs used to list, on\n * purpose: nothing enforced that list, so modern Office and OpenDocument files\n * were accepted and are in libraries today. Enforcing the narrow list would\n * have turned an F007 memory fix into a silent feature removal.\n *\n * Deliberately NOT in the default, each refused with 415 unless an app lists it:\n * - **Legacy binary Office** (`.doc`, `.xls`, `.ppt`). file-type reports them as\n * `application/x-cfb` — the same container as `.msi` installers and Outlook\n * `.msg` files, so it cannot be allowed for one without the others.\n * - **Macro-enabled Office** (`.docm`, `.xlsm`, `.pptm`), reported with their own\n * `…macroenabled…` types.\n * - **Archives and executables**, and the text types (`application/json`,\n * `text/markdown`, TSV) that `detectMimeType` trusts but the library does not\n * classify as documents.\n *\n * Named office families rather than `application/vnd.*`, which also matches\n * executables and archives.\n */\nexport const DEFAULT_MEDIA_ACCEPTED_TYPES: readonly string[] = [\n 'image/*',\n 'video/*',\n 'audio/*',\n 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.*',\n 'application/vnd.oasis.opendocument.*',\n 'text/plain',\n 'text/csv',\n]\n\n/** Fill every unset option with its default. */\nexport function resolveMediaConfig(config?: MediaPluginConfig): Required<MediaPluginConfig> {\n return {\n acceptedTypes: config?.acceptedTypes ?? [...DEFAULT_MEDIA_ACCEPTED_TYPES],\n // Non-positive means unset, as it does for storage's `maxFileSize`.\n maxUploadSize:\n config?.maxUploadSize !== undefined && config.maxUploadSize > 0\n ? config.maxUploadSize\n : 50 * 1024 * 1024,\n // Resolved for the picker's own use, and deliberately NOT applied by the\n // upload route: an upload that sends no visibility stays private (rent's\n // inspection photos rely on it). See the route's deny-by-default comment.\n defaultVisibility: config?.defaultVisibility ?? 'public',\n imageStyles: config?.imageStyles ?? {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n },\n vocabulary: config?.vocabulary ?? {\n shapes: Object.fromEntries(\n Object.entries(DEFAULT_IMAGE_VOCABULARY.shapes).map(([name, shape]) => [\n name,\n { ...shape },\n ]),\n ),\n ladder: [...DEFAULT_IMAGE_VOCABULARY.ladder],\n },\n }\n}\n\n/**\n * The registered config, or the defaults when the plugin is not registered\n * (a unit test, a CLI path that never ran `createApp`). The defaults are what\n * `media()` with no arguments resolves to, so the fallback is never looser\n * than a registered plugin that configured nothing.\n */\nexport function readMediaConfig(): Required<MediaPluginConfig> {\n return hasPluginConfig(MEDIA_PLUGIN_NAME)\n ? getPluginConfig(MEDIA_PLUGIN_NAME)\n : resolveMediaConfig()\n}\n\n/**\n * The resolved media configuration, read from the framework's registry.\n * THROWS when the `media()` plugin is not registered — unlike\n * {@link readMediaConfig}, which falls back to the defaults. The image-styles\n * waterfall needs the throw: \"not registered\" must reach its hardcoded tier.\n *\n * Lives here, not in `plugin.ts`, so a module that needs the config does not\n * have to import the plugin entry: `resolve-image-styles.ts` used to reach it\n * through `await import('./plugin.js')`, which closed a file cycle through the\n * admin routes and the client (the `KNOWN_CYCLES` media entry, now gone).\n */\nexport function requireMediaConfig(): Required<MediaPluginConfig> {\n if (!hasPluginConfig(MEDIA_PLUGIN_NAME)) {\n throw new Error('@murumets-ee/media plugin not initialized. Add media() to your plugins array.')\n }\n return getPluginConfig(MEDIA_PLUGIN_NAME)\n}\n"],"mappings":"mHAsBA,MAAa,EAAoB,qBAwBpB,EAAkD,CAC7D,UACA,UACA,UACA,kBACA,kDACA,uCACA,aACA,UACF,EAGA,SAAgB,EAAmB,EAAyD,CAC1F,MAAO,CACL,cAAe,GAAQ,eAAiB,CAAC,GAAG,CAA4B,EAExE,cACE,GAAQ,gBAAkB,IAAA,IAAa,EAAO,cAAgB,EAC1D,EAAO,cACP,GAAK,KAAO,KAIlB,kBAAmB,GAAQ,mBAAqB,SAChD,YAAa,GAAQ,aAAe,CAClC,UAAW,CAAE,MAAO,IAAK,OAAQ,IAAK,IAAK,QAAS,OAAQ,OAAQ,QAAS,EAAG,CAClF,EACA,WAAY,GAAQ,YAAc,CAChC,OAAQ,OAAO,YACb,OAAO,QAAQ,EAAyB,MAAM,CAAC,CAAC,KAAK,CAAC,EAAM,KAAW,CACrE,EACA,CAAE,GAAG,CAAM,CACb,CAAC,CACH,EACA,OAAQ,CAAC,GAAG,EAAyB,MAAM,CAC7C,CACF,CACF,CAQA,SAAgB,GAA+C,CAC7D,OAAO,EAAA,oBAAiC,EACpC,EAAgB,CAAiB,EACjC,EAAmB,CACzB,CAaA,SAAgB,GAAkD,CAChE,GAAI,CAAC,EAAA,oBAAiC,EACpC,MAAU,MAAM,+EAA+E,EAEjG,OAAO,EAAgB,CAAiB,CAC1C"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
async function e(e,t,n,r){let i=0;for(let a of n)if(a!==t)try{let n=await e.getMetadata(a);if(!n)continue;if(n.metadata?.variantOf!==t){r?.warn({originalKey:t,key:a},`media variants: refused to delete a file that is not a variant of this original`);continue}await e.delete(a),i++}catch(e){r?.warn({originalKey:t,key:a,err:e},`media variants: variant file could not be deleted`)}return i}export{e as t};
|
|
2
|
+
//# sourceMappingURL=owned-delete-Cn_KFFjS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-delete-Cn_KFFjS.mjs","names":[],"sources":["../src/jobs/owned-delete.ts"],"sourcesContent":["/**\n * Deleting a photograph's variant files — only files that say they ARE its\n * variants.\n *\n * The keys to delete come from the original's `toolkit_files.metadata`, and that\n * column is caller-writable: `PATCH /api/admin/storage/<key>` accepts arbitrary\n * `metadata`, gated by `storage:update` and ownership, and an editor owns the\n * originals they uploaded. Trusting the list would let them write another\n * user's private key into `unconfirmedVariants` (or `shaped`, or the legacy\n * `variants` map) and have the variant job — running elevated — or\n * `MediaClient.delete` remove it.\n *\n * So a key is deleted only when its OWN file record says `variantOf` this\n * original. Every variant file this package writes carries that field (legacy\n * and shaped alike), and a victim's file cannot be made to claim it without\n * write access to the victim's own record. The same check ticketing's\n * `resolveThumbnailKey` makes.\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { StorageClient } from '@murumets-ee/storage'\n\n/** Delete each of `keys` that is a variant OF `originalKey`; return how many were deleted. */\nexport async function deleteOwnedVariants(\n storage: Pick<StorageClient, 'getMetadata' | 'delete'>,\n originalKey: string,\n keys: Iterable<string>,\n logger?: Pick<Logger, 'warn'>,\n): Promise<number> {\n let deleted = 0\n for (const key of keys) {\n if (key === originalKey) continue\n try {\n const file = await storage.getMetadata(key)\n // Already gone — nothing to do. Not ours — never ours to delete.\n if (!file) continue\n if (file.metadata?.variantOf !== originalKey) {\n logger?.warn(\n { originalKey, key },\n 'media variants: refused to delete a file that is not a variant of this original',\n )\n continue\n }\n await storage.delete(key)\n deleted++\n } catch (err) {\n logger?.warn({ originalKey, key, err }, 'media variants: variant file could not be deleted')\n }\n }\n return deleted\n}\n"],"mappings":"AAuBA,eAAsB,EACpB,EACA,EACA,EACA,EACiB,CACjB,IAAI,EAAU,EACd,IAAK,IAAM,KAAO,EACZ,OAAQ,EACZ,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,YAAY,CAAG,EAE1C,GAAI,CAAC,EAAM,SACX,GAAI,EAAK,UAAU,YAAc,EAAa,CAC5C,GAAQ,KACN,CAAE,cAAa,KAAI,EACnB,iFACF,EACA,QACF,CACA,MAAM,EAAQ,OAAO,CAAG,EACxB,GACF,OAAS,EAAK,CACZ,GAAQ,KAAK,CAAE,cAAa,MAAK,KAAI,EAAG,mDAAmD,CAC7F,CAEF,OAAO,CACT"}
|
package/dist/picker.d.mts
CHANGED
|
@@ -1,100 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { a as MediaPickerMode, i as MediaPickerListResult, n as MediaPickerClassNames, o as MediaPickerProps, r as MediaPickerItem, t as MediaPickerCallbacks } from "./types-DwzblZfW.mjs";
|
|
2
2
|
import { LucideIcon } from "lucide-react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
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
|
-
* False when the server reports storage is not configured (missing
|
|
25
|
-
* env vars etc.). UI renders a banner with `reason` instead of the
|
|
26
|
-
* upload zone + grid. Omitted/true when everything is wired.
|
|
27
|
-
*/
|
|
28
|
-
configured?: boolean;
|
|
29
|
-
/** Human-readable explanation when `configured === false`. */
|
|
30
|
-
reason?: string;
|
|
31
|
-
}
|
|
32
|
-
/** Callbacks the consumer provides — framework-agnostic */
|
|
33
|
-
interface MediaPickerCallbacks {
|
|
34
|
-
/** Fetch media items with filtering and pagination */
|
|
35
|
-
fetchMedia: (options: {
|
|
36
|
-
search?: string | undefined;
|
|
37
|
-
mediaType?: string | undefined;
|
|
38
|
-
limit: number;
|
|
39
|
-
offset: number;
|
|
40
|
-
}) => Promise<MediaPickerListResult>;
|
|
41
|
-
/** Upload a file and return the created media item */
|
|
42
|
-
uploadMedia: (file: File) => Promise<MediaPickerItem>;
|
|
43
|
-
}
|
|
44
|
-
/** Selection mode */
|
|
45
|
-
type MediaPickerMode = 'single' | 'multiple';
|
|
46
|
-
/** Per-element class overrides for MediaPicker */
|
|
47
|
-
interface MediaPickerClassNames {
|
|
48
|
-
overlay?: string;
|
|
49
|
-
content?: string;
|
|
50
|
-
header?: string;
|
|
51
|
-
title?: string;
|
|
52
|
-
toolbar?: string;
|
|
53
|
-
searchInput?: string;
|
|
54
|
-
filterTabs?: string;
|
|
55
|
-
grid?: string;
|
|
56
|
-
card?: string;
|
|
57
|
-
cardSelected?: string;
|
|
58
|
-
cardImage?: string;
|
|
59
|
-
cardLabel?: string;
|
|
60
|
-
uploadZone?: string;
|
|
61
|
-
uploadZoneActive?: string;
|
|
62
|
-
footer?: string;
|
|
63
|
-
confirmButton?: string;
|
|
64
|
-
cancelButton?: string;
|
|
65
|
-
loading?: string;
|
|
66
|
-
empty?: string;
|
|
67
|
-
}
|
|
68
|
-
/** Props for the MediaPicker dialog */
|
|
69
|
-
interface MediaPickerProps {
|
|
70
|
-
/** Whether the dialog is open */
|
|
71
|
-
open: boolean;
|
|
72
|
-
/** Called when open state changes */
|
|
73
|
-
onOpenChange: (open: boolean) => void;
|
|
74
|
-
/** Called when user confirms selection */
|
|
75
|
-
onSelect: (items: MediaPickerItem[]) => void;
|
|
76
|
-
/** Selection mode (default: 'single') */
|
|
77
|
-
mode?: MediaPickerMode | undefined;
|
|
78
|
-
/** MIME patterns for upload restriction (e.g., ['image/*', 'video/*']) — passed to file input */
|
|
79
|
-
accept?: string[] | undefined;
|
|
80
|
-
/** Filter browse results to a specific media classification (e.g., 'image', 'video') */
|
|
81
|
-
mediaType?: string | undefined;
|
|
82
|
-
/** Maximum items selectable in multi mode */
|
|
83
|
-
maxSelect?: number | undefined;
|
|
84
|
-
/** Currently selected item IDs (for pre-selection) */
|
|
85
|
-
selectedIds?: string[] | undefined;
|
|
86
|
-
/** Dialog title (default: 'Select Media') */
|
|
87
|
-
title?: string | undefined;
|
|
88
|
-
/** Dialog description for screen readers (optional) */
|
|
89
|
-
description?: string | undefined;
|
|
90
|
-
/** Per-element class overrides */
|
|
91
|
-
classNames?: MediaPickerClassNames | undefined;
|
|
92
|
-
/** Additional className for the dialog content */
|
|
93
|
-
className?: string | undefined;
|
|
94
|
-
/** Children rendered in dialog footer (extra actions) */
|
|
95
|
-
children?: ReactNode | undefined;
|
|
96
|
-
}
|
|
97
|
-
//#endregion
|
|
98
5
|
//#region src/picker/admin-callbacks.d.ts
|
|
99
6
|
/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */
|
|
100
7
|
type AdminMediaCallbacks = MediaPickerCallbacks & {
|
package/dist/picker.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/
|
|
1
|
+
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/grid/media-card.tsx","../src/grid/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/media-type-icon.ts","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":";;;;;;KAaY,mBAAA,GAAsB,oBAAA;EAChC,WAAA,GAAc,EAAA,aAAe,OAAO;AAAA;AAAA;AAoBtC;;;;AAA0F;;;;AC/B9B;;;;AAYrC;AAmBvB;;;ADpBsC,iBAoBtB,yBAAA,CAA0B,WAAA,YAA6B,mBAAmB;;;;;;;AArB1F;;;;;;KCEK,kBAAA;;;ADDiC;AAoBtC;;;;AAA0F;;;;AC/B9B;;;;AAYrC;AAmBvB;UAAiB,cAAA;;EAEf,GAAA;EAAA;EAEA,KAAA;EAYA;;;;;;;;;;AAqCkC;EArClC,GAAA;EAwCuB;;;;;EAlCvB,SAAA,EAAW,kBAAA;EACX,UAAA;EACA,QAAA;EAuCA;;;;;;;;;;;;EA1BA,IAAA;EAuBA;;;;;;;;;;;;;;EARA,kBAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAGpB,SAAA,CAAA;EACd,GAAA;EACA,KAAA;EACA,GAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,IAAA;EACA,kBAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UChGP,cAAA;EACR,KAAA,EAAO,eAAA;EACP,QAAA,EAAU,GAAA;EACV,QAAA,GAAW,IAAA,EAAM,eAAA;EACjB,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;EACf,UAAA,GAAa,qBAAA;AAAA;;;AFCuB;AAoBtC;;;iBEZgB,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;iBCgCD,WAAA,CAAA;EACd,IAAA;EACA,YAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,WAAA;EACA,UAAA;EACA,SAAA;EACA;AAAA,GACC,gBAAA,+BAAgB,GAAA,CAAA,OAAA;;;;;;;AHjEnB;;;;;;;;;AACsC;AAoBtC;;KIfK,iBAAA;;AJeqF;;;;AC/B9B;;;;cG2B/C,0BAAA,EAA4B,MAAA,CAAO,iBAAA,EAAmB,UAAA;AHInE;AAAA,iBGIgB,aAAA,CAAc,SAAA,YAAqB,iBAAA,GAAoB,UAAU;;;;;;;;;;;;;;iBAiBjE,YAAA,CAAa,QAAA,WAAmB,UAAU;;;UClDzC,wBAAA,SAAiC,oBAAoB;EACpE,QAAA,EAAU,SAAA;AAAA;ALOZ;;;;;;;;;AACsC;AAoBtC;;;;AArBA,iBKUgB,mBAAA,CAAA;EACd,QAAA;EACA,UAAA;EACA;AAAA,GACC,wBAAA,+BAAwB,GAAA,CAAA,OAAA;AAAA,iBASX,cAAA,CAAA,GAAkB,oBAAoB;;;UChC5C,cAAA;EACR,KAAA;EACA,QAAA,GAAW,KAAA;EACX,SAAA;EACA,iBAAA,GAAoB,IAAA;ENKS;EMH7B,MAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAWpB,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,SAAA;EACA,iBAAA;EACA,MAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCxBP,eAAA;EACR,QAAA,GAAW,IAAA,EAAM,IAAA,KAAS,OAAA;EAC1B,WAAA;EACA,MAAA;EACA,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,UAAA,CAAA;EAAa,QAAA;EAAU,WAAA;EAAa,MAAA;EAAQ;AAAA,GAAc,eAAA,+BAAe,GAAA,CAAA,OAAA"}
|
package/dist/picker.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import{Check as e,File as t,FileText as n,Film as r,Image as i,ImageIcon as a,Music as o,Search as s,Upload as c,X as l}from"lucide-react";import{clsx as u}from"clsx";import{twMerge as d}from"tailwind-merge";import{Fragment as f,jsx as p,jsxs as m}from"react/jsx-runtime";import*as h from"@radix-ui/react-dialog";import*as g from"@radix-ui/react-visually-hidden";import{createContext as _,useCallback as v,useContext as y,useEffect as b,useMemo as x,useRef as S,useState as C}from"react";function w(e=`/api/admin`){let t=`${e}/media`,n=async e=>{try{let t=await e.clone().json();if(typeof t.error==`string`&&t.error.length>0)return t.error}catch{}try{let t=await e.text();if(t)return t}catch{}return`HTTP ${String(e.status)}`};return{fetchMedia:async e=>{let r=new URLSearchParams;e.search&&r.set(`search`,e.search),e.mediaType&&r.set(`mediaType`,e.mediaType),r.set(`limit`,String(e.limit)),r.set(`offset`,String(e.offset));let i=`${t}?${r.toString()}`,a=await fetch(i);if(!a.ok)throw Error(await n(a));return a.json()},uploadMedia:async e=>{let r=new FormData;r.append(`file`,e),r.append(`visibility`,`public`);let i=await fetch(t,{method:`POST`,body:r});if(!i.ok)throw Error(await n(i));return i.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function T(...e){return d(u(e))}function E(e){return e.startsWith(`image/`)?`image`:e.startsWith(`video/`)?`video`:e.startsWith(`audio/`)?`audio`:e===`application/pdf`||e.startsWith(`application/msword`)||e.startsWith(`application/vnd.`)?`document`:`other`}const D={video:r,audio:o,document:n,other:t};function O(e){return e===`image`?null:D[e]}function k(e){let t=E(e);return t===`image`?i:D[t]}function A({url:t,title:n,alt:r,mediaType:i,isSelected:o,onToggle:s,href:c,hideSelectionBadge:l,classNames:u}){let d=i===`image`&&!!t,h=r??n,g=i===void 0?null:i===`image`?a:O(i),_=T(`group relative block 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`,o?`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`,u?.card,o&&u?.cardSelected),v=m(f,{children:[d?p(`img`,{src:t,alt:h,className:T(`h-full w-full object-cover`,u?.cardImage),loading:`lazy`}):p(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:g&&p(g,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),o&&!l&&p(`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:p(e,{className:`h-3 w-3`})}),p(`div`,{className:T(`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`,u?.cardLabel),children:p(`p`,{className:`truncate text-xs text-white`,children:n})})]});return c?p(`a`,{href:c,"aria-label":n,className:_,children:v}):p(`button`,{type:`button`,onClick:s,"aria-pressed":o,"aria-label":n,className:_,children:v})}function j({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return p(`div`,{className:T(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>p(`div`,{className:T(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return p(`div`,{className:T(`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),u=Math.floor(a/o)+1;return m(f,{children:[p(`div`,{className:T(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>p(A,{url:e.mediaType===`image`?e.url:null,title:e.title||e.filename,alt:e.alt,mediaType:e.mediaType,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&m(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[p(`button`,{type:`button`,disabled:u<=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`}),m(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[u,` / `,l]}),p(`button`,{type:`button`,disabled:u>=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 M=_(null);function N({children:e,fetchMedia:t,uploadMedia:n}){return p(M,{value:x(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function P(){let e=y(M);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const F=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function I({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:a}){let o=!i;return m(`div`,{className:`flex items-center gap-3`,children:[m(`div`,{className:`relative flex-1`,children:[p(s,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),p(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:T(`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`,a?.searchInput)})]}),o&&p(`div`,{className:T(`flex gap-1`,a?.filterTabs),children:F.map(e=>p(`button`,{type:`button`,onClick:()=>r(e.value),className:T(`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 L({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=S(null),[a,o]=C(!1),s=v(async t=>{t.preventDefault(),o(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),l=v(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return m(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),o(!0)},onDragLeave:()=>o(!1),onDrop:s,onClick:()=>i.current?.click(),className:T(`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:[p(c,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),p(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),p(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:l,className:`hidden`})]})}const R=new Set([`image`,`video`,`audio`]);function ee(e){return e!==void 0&&R.has(e)?[`${e}/*`]:void 0}const z=`file`;function te(e,t){return e===void 0||t.type===``?!0:e===z?!t.type.startsWith(`image/`)&&!t.type.startsWith(`video/`):R.has(e)?t.type.startsWith(`${e}/`):!0}function B(e,t){if(e===void 0||t.mediaType===e)return null;let n=t.title??t.filename;return e===z?t.mediaType!==`image`&&t.mediaType!==`video`?null:`This field takes a document or other file — “${n}” is ${V(t.mediaType)}.`:`This field takes ${e} files — “${n}” is ${t.mediaType===`other`?`not one`:V(t.mediaType)}.`}function V(e){return/^[aeiou]/.test(e)?`an ${e}`:`a ${e}`}function H({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:s=[],title:c=`Select Media`,description:u,classNames:d,className:_,children:y}){let{fetchMedia:x,uploadMedia:w}=P(),[E,D]=C([]),[O,k]=C(0),[A,M]=C(()=>new Set(s)),[N,F]=C(``),[R,V]=C(void 0),H=a??R,U=a===z?void 0:H,ne=i??ee(a),[re,W]=C(!1),[ie,G]=C(!1),[K,q]=C(0),[J,Y]=C(null),[X,Z]=C(null),Q=v(async()=>{W(!0),Y(null);try{let e=await x({search:N||void 0,mediaType:U,limit:24,offset:K});e.configured===!1?(Z(e.reason??`Media storage is not configured on this server.`),D([]),k(0)):(Z(null),D(e.items),k(e.total))}catch(e){Y(e instanceof Error&&e.message?`Failed to load media: ${e.message}`:`Failed to load media.`)}finally{W(!1)}},[x,N,U,K]);b(()=>{e&&Q()},[e,Q]);let $=S(e),ae=S(s);ae.current=s,b(()=>{$.current&&!e&&(F(``),q(0),M(new Set(ae.current))),$.current=e},[e]);let oe=v(e=>{let i=B(a,e);if(i!==null){Y(i);return}if(r===`single`){n([e]),t(!1);return}M(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,a,n,t]),se=v(()=>{n(E.filter(e=>A.has(e.id))),t(!1)},[E,A,n,t]),ce=v(async e=>{if(Y(null),!te(a,e)){Y(a===z?`This field takes a document or other file — “${e.name}” is an image or a video.`:`This field takes ${a} files — “${e.name}” is not one.`);return}G(!0);try{let i=await w(e),o=B(a,i);if(o!==null){Y(o);return}if(r===`single`){n([i]),t(!1);return}D(e=>[i,...e]),k(e=>e+1),M(e=>new Set([...e,i.id]))}catch(e){Y(e instanceof Error&&e.message?`Upload failed: ${e.message}`:`Upload failed. Please try again.`)}finally{G(!1)}},[w,r,a,n,t]);return p(h.Root,{open:e,onOpenChange:t,children:m(h.Portal,{children:[p(h.Overlay,{className:T(`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`,d?.overlay)}),m(h.Content,{...!u&&{"aria-describedby":void 0},className:T(`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`,d?.content,_),children:[m(`div`,{className:T(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,d?.header),children:[p(h.Title,{className:T(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,d?.title),children:c}),u&&p(g.Root,{asChild:!0,children:p(h.Description,{children:u})}),m(h.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:[p(l,{className:`h-4 w-4`}),p(g.Root,{children:`Close`})]})]}),p(`div`,{className:T(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,d?.toolbar),children:p(I,{value:N,onChange:F,mediaType:H,onMediaTypeChange:V,locked:!!a,classNames:d})}),m(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[J&&p(`div`,{role:`alert`,className:`mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400`,children:J}),X?m(`div`,{role:`alert`,className:`rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30`,children:[p(`p`,{className:`text-sm font-medium text-amber-900 dark:text-amber-200`,children:`Media storage is not configured`}),p(`p`,{className:`mt-1 text-xs text-amber-800 dark:text-amber-300`,children:X}),p(`p`,{className:`mt-3 text-xs text-amber-700 dark:text-amber-400`,children:`Set the required environment variables and restart the app to enable uploads.`})]}):m(f,{children:[p(L,{onUpload:ce,isUploading:ie,accept:ne,classNames:d}),p(j,{items:E,selected:A,onToggle:oe,isLoading:re,total:O,offset:K,limit:24,onPageChange:q,classNames:d})]})]}),m(`div`,{className:T(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,d?.footer),children:[p(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${O.toString()} item${O===1?``:`s`} — click to select`:A.size>0?`${A.size.toString()} selected`:`${O.toString()} item${O===1?``:`s`}`}),m(`div`,{className:`flex gap-2`,children:[y,p(h.Close,{asChild:!0,children:p(`button`,{type:`button`,className:T(`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`,d?.cancelButton),children:`Cancel`})}),r!==`single`&&p(`button`,{type:`button`,onClick:se,disabled:A.size===0,className:T(`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`,d?.confirmButton),children:`Select (${A.size.toString()})`})]})]})]})]})})}export{A as MediaCard,j as MediaGrid,H as MediaPicker,N as MediaPickerProvider,D as NON_IMAGE_MEDIA_TYPE_ICONS,I as SearchBar,L as UploadZone,w as createAdminMediaCallbacks,O as mediaTypeIcon,k as mimeTypeIcon,P as useMediaPicker};
|
|
1
|
+
"use client";import{t as e}from"./derive-media-type-C38ZA_Cl.mjs";import{Check as t,File as n,FileText as r,Film as i,Image as a,ImageIcon as o,Music as s,Search as c,Upload as l,X as u}from"lucide-react";import{clsx as d}from"clsx";import{twMerge as f}from"tailwind-merge";import{Fragment as p,jsx as m,jsxs as h}from"react/jsx-runtime";import*as g from"@radix-ui/react-dialog";import*as _ from"@radix-ui/react-visually-hidden";import{createContext as v,useCallback as y,useContext as b,useEffect as x,useMemo as S,useRef as C,useState as w}from"react";function T(e=`/api/admin`){let t=`${e}/media`,n=async e=>{try{let t=await e.clone().json();if(typeof t.error==`string`&&t.error.length>0)return t.error}catch{}try{let t=await e.text();if(t)return t}catch{}return`HTTP ${String(e.status)}`};return{fetchMedia:async e=>{let r=new URLSearchParams;e.search&&r.set(`search`,e.search),e.mediaType&&r.set(`mediaType`,e.mediaType),r.set(`limit`,String(e.limit)),r.set(`offset`,String(e.offset));let i=`${t}?${r.toString()}`,a=await fetch(i);if(!a.ok)throw Error(await n(a));return a.json()},uploadMedia:async e=>{let r=new FormData;r.append(`file`,e),r.append(`visibility`,`public`);let i=await fetch(t,{method:`POST`,body:r});if(!i.ok)throw Error(await n(i));return i.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function E(...e){return f(d(e))}const D={video:i,audio:s,document:r,other:n};function O(e){return e===`image`?null:D[e]}function k(t){let n=e(t);return n===`image`?a:D[n]}function A({url:e,title:n,alt:r,mediaType:i,isSelected:a,onToggle:s,href:c,hideSelectionBadge:l,classNames:u}){let d=i===`image`&&!!e,f=r??n,g=i===void 0?null:i===`image`?o:O(i),_=E(`group relative block 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`,a?`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`,u?.card,a&&u?.cardSelected),v=h(p,{children:[d?m(`img`,{src:e,alt:f,className:E(`h-full w-full object-cover`,u?.cardImage),loading:`lazy`}):m(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:g&&m(g,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),a&&!l&&m(`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:m(t,{className:`h-3 w-3`})}),m(`div`,{className:E(`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`,u?.cardLabel),children:m(`p`,{className:`truncate text-xs text-white`,children:n})})]});return c?m(`a`,{href:c,"aria-label":n,className:_,children:v}):m(`button`,{type:`button`,onClick:s,"aria-pressed":a,"aria-label":n,className:_,children:v})}function j({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return m(`div`,{className:E(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>m(`div`,{className:E(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return m(`div`,{className:E(`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),u=Math.floor(a/o)+1;return h(p,{children:[m(`div`,{className:E(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>m(A,{url:e.mediaType===`image`?e.url:null,title:e.title||e.filename,alt:e.alt,mediaType:e.mediaType,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&h(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[m(`button`,{type:`button`,disabled:u<=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`}),h(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[u,` / `,l]}),m(`button`,{type:`button`,disabled:u>=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 M=v(null);function N({children:e,fetchMedia:t,uploadMedia:n}){return m(M,{value:S(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function P(){let e=b(M);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const F=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function I({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:a}){let o=!i;return h(`div`,{className:`flex items-center gap-3`,children:[h(`div`,{className:`relative flex-1`,children:[m(c,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),m(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:E(`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`,a?.searchInput)})]}),o&&m(`div`,{className:E(`flex gap-1`,a?.filterTabs),children:F.map(e=>m(`button`,{type:`button`,onClick:()=>r(e.value),className:E(`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 L({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=C(null),[a,o]=w(!1),s=y(async t=>{t.preventDefault(),o(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),c=y(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return h(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),o(!0)},onDragLeave:()=>o(!1),onDrop:s,onClick:()=>i.current?.click(),className:E(`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:[m(l,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),m(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),m(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:c,className:`hidden`})]})}const R=new Set([`image`,`video`,`audio`]);function ee(e){return e!==void 0&&R.has(e)?[`${e}/*`]:void 0}const z=`file`;function te(e,t){return e===void 0||t.type===``?!0:e===z?!t.type.startsWith(`image/`)&&!t.type.startsWith(`video/`):R.has(e)?t.type.startsWith(`${e}/`):!0}function B(e,t){if(e===void 0||t.mediaType===e)return null;let n=t.title??t.filename;return e===z?t.mediaType!==`image`&&t.mediaType!==`video`?null:`This field takes a document or other file — “${n}” is ${V(t.mediaType)}.`:`This field takes ${e} files — “${n}” is ${t.mediaType===`other`?`not one`:V(t.mediaType)}.`}function V(e){return/^[aeiou]/.test(e)?`an ${e}`:`a ${e}`}function H({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:s=[],title:c=`Select Media`,description:l,classNames:d,className:f,children:v}){let{fetchMedia:b,uploadMedia:S}=P(),[T,D]=w([]),[O,k]=w(0),[A,M]=w(()=>new Set(s)),[N,F]=w(``),[R,V]=w(void 0),H=a??R,U=a===z?void 0:H,ne=i??ee(a),[re,W]=w(!1),[ie,G]=w(!1),[K,q]=w(0),[J,Y]=w(null),[X,Z]=w(null),Q=y(async()=>{W(!0),Y(null);try{let e=await b({search:N||void 0,mediaType:U,limit:24,offset:K});e.configured===!1?(Z(e.reason??`Media storage is not configured on this server.`),D([]),k(0)):(Z(null),D(e.items),k(e.total))}catch(e){Y(e instanceof Error&&e.message?`Failed to load media: ${e.message}`:`Failed to load media.`)}finally{W(!1)}},[b,N,U,K]);x(()=>{e&&Q()},[e,Q]);let $=C(e),ae=C(s);ae.current=s,x(()=>{$.current&&!e&&(F(``),q(0),M(new Set(ae.current))),$.current=e},[e]);let oe=y(e=>{let i=B(a,e);if(i!==null){Y(i);return}if(r===`single`){n([e]),t(!1);return}M(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,a,n,t]),se=y(()=>{n(T.filter(e=>A.has(e.id))),t(!1)},[T,A,n,t]),ce=y(async e=>{if(Y(null),!te(a,e)){Y(a===z?`This field takes a document or other file — “${e.name}” is an image or a video.`:`This field takes ${a} files — “${e.name}” is not one.`);return}G(!0);try{let i=await S(e),o=B(a,i);if(o!==null){Y(o);return}if(r===`single`){n([i]),t(!1);return}D(e=>[i,...e]),k(e=>e+1),M(e=>new Set([...e,i.id]))}catch(e){Y(e instanceof Error&&e.message?`Upload failed: ${e.message}`:`Upload failed. Please try again.`)}finally{G(!1)}},[S,r,a,n,t]);return m(g.Root,{open:e,onOpenChange:t,children:h(g.Portal,{children:[m(g.Overlay,{className:E(`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`,d?.overlay)}),h(g.Content,{...!l&&{"aria-describedby":void 0},className:E(`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`,d?.content,f),children:[h(`div`,{className:E(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,d?.header),children:[m(g.Title,{className:E(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,d?.title),children:c}),l&&m(_.Root,{asChild:!0,children:m(g.Description,{children:l})}),h(g.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:[m(u,{className:`h-4 w-4`}),m(_.Root,{children:`Close`})]})]}),m(`div`,{className:E(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,d?.toolbar),children:m(I,{value:N,onChange:F,mediaType:H,onMediaTypeChange:V,locked:!!a,classNames:d})}),h(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[J&&m(`div`,{role:`alert`,className:`mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400`,children:J}),X?h(`div`,{role:`alert`,className:`rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30`,children:[m(`p`,{className:`text-sm font-medium text-amber-900 dark:text-amber-200`,children:`Media storage is not configured`}),m(`p`,{className:`mt-1 text-xs text-amber-800 dark:text-amber-300`,children:X}),m(`p`,{className:`mt-3 text-xs text-amber-700 dark:text-amber-400`,children:`Set the required environment variables and restart the app to enable uploads.`})]}):h(p,{children:[m(L,{onUpload:ce,isUploading:ie,accept:ne,classNames:d}),m(j,{items:T,selected:A,onToggle:oe,isLoading:re,total:O,offset:K,limit:24,onPageChange:q,classNames:d})]})]}),h(`div`,{className:E(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,d?.footer),children:[m(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${O.toString()} item${O===1?``:`s`} — click to select`:A.size>0?`${A.size.toString()} selected`:`${O.toString()} item${O===1?``:`s`}`}),h(`div`,{className:`flex gap-2`,children:[v,m(g.Close,{asChild:!0,children:m(`button`,{type:`button`,className:E(`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`,d?.cancelButton),children:`Cancel`})}),r!==`single`&&m(`button`,{type:`button`,onClick:se,disabled:A.size===0,className:E(`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`,d?.confirmButton),children:`Select (${A.size.toString()})`})]})]})]})]})})}export{A as MediaCard,j as MediaGrid,H as MediaPicker,N as MediaPickerProvider,D as NON_IMAGE_MEDIA_TYPE_ICONS,I as SearchBar,L as UploadZone,T as createAdminMediaCallbacks,O as mediaTypeIcon,k as mimeTypeIcon,P as useMediaPicker};
|
|
3
2
|
//# sourceMappingURL=picker.mjs.map
|