@webx-ui/module-media 0.0.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/DirectoryTree.vue","../src/FileGrid.vue","../src/MediaToolbar.vue","../src/MoveDialog.vue","../src/NameDialog.vue","../src/api.ts","../src/messages.ts","../src/i18n.ts","../src/MediaManager.vue","../src/MediaPage.vue","../src/MediaPicker.vue","../src/openMediaPicker.ts","../src/MediaField.vue","../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { WxTree, type TreeDropEvent } from '@webx-ui/core'\nimport type { MediaDirectory } from './types'\n\n/**\n * The folders, on the left.\n *\n * The root carries a translated label rather than the title it was given in the database: it is\n * the library itself, and an editor should not have to know it is a row.\n */\nconst props = defineProps<{\n directories: MediaDirectory[]\n}>()\n\nconst selected = defineModel<number | null>('selected', { default: null })\n\nconst emit = defineEmits<{\n move: [id: number, parentId: number]\n}>()\n\nconst t = useTranslate('webx-media')\n\ntype Node = MediaDirectory & { label: string }\n\n// The tree rearranges this list itself when something is dropped, and then says so; the answer\n// from the server replaces it a moment later. A computed would have nothing to rearrange.\nconst nodes = ref<Node[]>([])\nconst expanded = ref<number[]>([])\n\nwatch(\n () => props.directories,\n (directories) => {\n nodes.value = directories.map(label)\n expanded.value = keysOf(directories)\n },\n { immediate: true, deep: true },\n)\n\nfunction label(directory: MediaDirectory): Node {\n return {\n ...directory,\n label: directory.is_root ? t('manager.root') : directory.title,\n children: (directory.children ?? []).map(label),\n }\n}\n\nfunction keysOf(directories: MediaDirectory[]): number[] {\n return directories.flatMap((directory) => [directory.id, ...keysOf(directory.children ?? [])])\n}\n\nfunction onDrop(event: TreeDropEvent<Node>): void {\n const parent = event.zone === 'inside' ? event.target : event.parent\n\n if (parent) {\n emit('move', event.node.id, parent.id)\n }\n}\n</script>\n\n<template>\n <wx-tree\n v-model:selected=\"selected\"\n v-model:expanded=\"expanded\"\n v-model=\"nodes\"\n class=\"wx-media-tree\"\n node-key=\"id\"\n draggable\n show-lines\n @drop=\"onDrop\"\n />\n</template>\n\n<style>\n.wx-media-tree {\n min-width: 0;\n overflow: auto;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { toast, WxEmpty, WxFileCard, WxSelectionArea } from '@webx-ui/core'\nimport type { MediaApi } from './api'\nimport type { MediaFile } from './types'\n\n/**\n * The files, as cards, with a rubber band over them.\n *\n * Previews come from the server's thumbnail endpoint rather than the files themselves: a grid of\n * forty photographs is forty full-size images otherwise, and on a phone that is the difference\n * between a screen and a wait.\n */\nconst props = defineProps<{\n files: MediaFile[]\n api: MediaApi\n query?: string\n /** A picker takes one file; the manager selects to act on a batch. */\n single?: boolean\n}>()\n\nconst selected = defineModel<number[]>('selected', { default: () => [] })\n\nconst emit = defineEmits<{\n open: [file: MediaFile]\n rename: [file: MediaFile, name: string]\n edit: [file: MediaFile]\n remove: [file: MediaFile]\n}>()\n\nconst t = useTranslate('webx-media')\n\nconst empty = computed(() =>\n props.query ? t('manager.empty-search', { query: props.query }) : t('manager.empty'),\n)\n\nfunction copied(): void {\n toast.success(t('manager.link-copied'))\n}\n\n/**\n * The clipboard is not always there: it is missing outside a secure context, and an iframe has\n * to be granted it. Rather than let the card fail silently, try the old way — and if that is\n * refused too, put the address on screen, where it can at least be copied by hand.\n */\nfunction copyFailed(file: MediaFile): void {\n const area = document.createElement('textarea')\n area.value = file.url\n area.setAttribute('readonly', '')\n area.style.position = 'fixed'\n area.style.opacity = '0'\n document.body.appendChild(area)\n area.select()\n\n let done = false\n\n try {\n done = document.execCommand('copy')\n } catch {\n done = false\n }\n\n area.remove()\n\n if (done) {\n copied()\n\n return\n }\n\n toast.danger(`${t('errors.copy')} ${file.url}`, { duration: 0 })\n}\n</script>\n\n<template>\n <wx-selection-area\n v-if=\"files.length > 0\"\n v-slot=\"{ isSelected }\"\n v-model=\"selected\"\n class=\"wx-media-grid\"\n :multiple=\"!single\"\n >\n <wx-file-card\n v-for=\"file in files\"\n :key=\"file.id\"\n v-wx-select=\"file.id\"\n class=\"wx-media-grid__card\"\n :name=\"file.name\"\n :url=\"file.url\"\n :thumbnail=\"api.thumb(file, 320, 320) ?? undefined\"\n :type=\"file.mime\"\n :selected=\"isSelected(file.id)\"\n renamable\n :editable=\"file.editable\"\n removable\n copyable\n :rename-label=\"t('manager.rename')\"\n :edit-label=\"t('manager.edit')\"\n :remove-label=\"t('manager.delete')\"\n :copy-label=\"t('manager.copy-link')\"\n :copied-label=\"t('manager.link-copied')\"\n @rename=\"(name) => emit('rename', file, name)\"\n @edit=\"emit('edit', file)\"\n @remove=\"emit('remove', file)\"\n @copy=\"copied\"\n @copy-error=\"copyFailed(file)\"\n @dblclick=\"emit('open', file)\"\n />\n </wx-selection-area>\n\n <wx-empty v-else :description=\"empty\" />\n</template>\n\n<style>\n.wx-media-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n gap: var(--wx-space-12);\n align-content: start;\n min-height: 0;\n overflow: auto;\n padding: var(--wx-space-2);\n /* A rubber band over names would otherwise select the names. */\n user-select: none;\n}\n\n.wx-media--compact .wx-media-grid {\n grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n gap: var(--wx-space-8);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { WxAction, WxActions, WxDropdown, WxDropdownItem, WxInput } from '@webx-ui/core'\nimport type { MediaKind } from './types'\n\n/**\n * One row of icons over the files.\n *\n * Actions rather than labelled buttons: the row has to survive a phone and a picker dialog half\n * the width of a window, and a row of words does not. What each icon means is a tooltip away,\n * and what is chosen is said in the icon's own label.\n */\nconst props = defineProps<{\n canUpload: boolean\n canManage: boolean\n selected: number\n /** Narrow screens get the folders behind a button instead of beside the files. */\n compact: boolean\n}>()\n\nconst search = defineModel<string>('search', { default: '' })\nconst type = defineModel<MediaKind | 'all'>('type', { default: 'all' })\nconst sort = defineModel<string>('sort', { default: '-created_at' })\n\nconst emit = defineEmits<{\n upload: []\n move: []\n remove: []\n folders: []\n}>()\n\nconst t = useTranslate('webx-media')\n\nconst types = computed<{ value: MediaKind | 'all'; label: string }[]>(() => [\n { value: 'all', label: t('manager.all-types') },\n ...(['image', 'video', 'audio', 'document', 'other'] as MediaKind[]).map((kind) => ({\n value: kind,\n label: t(`manager.${kind}`),\n })),\n])\n\nconst sorts = computed(() => [\n { value: '-created_at', label: t('manager.sort-newest') },\n { value: 'created_at', label: t('manager.sort-oldest') },\n { value: 'name', label: t('manager.sort-name') },\n { value: '-size', label: t('manager.sort-size') },\n])\n\nconst typeLabel = computed(() => types.value.find((one) => one.value === type.value)?.label ?? '')\nconst sortLabel = computed(() => sorts.value.find((one) => one.value === sort.value)?.label ?? '')\n</script>\n\n<template>\n <header class=\"wx-media-toolbar\">\n <wx-action\n v-if=\"compact\"\n icon=\"folder\"\n :title=\"t('manager.folders')\"\n size=\"sm\"\n @click=\"emit('folders')\"\n />\n\n <wx-input\n v-model=\"search\"\n class=\"wx-media-toolbar__search\"\n :placeholder=\"t('manager.search')\"\n clearable\n size=\"sm\"\n />\n\n <wx-dropdown>\n <template #trigger>\n <wx-action icon=\"filter\" :title=\"`${t('manager.all-types')}: ${typeLabel}`\" size=\"sm\" />\n </template>\n\n <wx-dropdown-item\n v-for=\"option in types\"\n :key=\"String(option.value)\"\n :active=\"option.value === type\"\n @click=\"type = option.value\"\n >\n {{ option.label }}\n </wx-dropdown-item>\n </wx-dropdown>\n\n <wx-dropdown>\n <template #trigger>\n <!-- Not `type=\"sort\"`: in this set that means reordering by hand, and its icon is a drag handle. -->\n <wx-action icon=\"arrow-down\" :title=\"sortLabel\" size=\"sm\" />\n </template>\n\n <wx-dropdown-item\n v-for=\"option in sorts\"\n :key=\"option.value\"\n :active=\"option.value === sort\"\n @click=\"sort = option.value\"\n >\n {{ option.label }}\n </wx-dropdown-item>\n </wx-dropdown>\n\n <wx-action\n v-if=\"canUpload\"\n type=\"upload\"\n :title=\"t('manager.upload')\"\n size=\"sm\"\n @click=\"emit('upload')\"\n />\n\n <!-- Kept in the row rather than added to it: the icons are always here and simply cannot\n be used while nothing is selected, so the grid never moves down a line. -->\n <wx-actions v-if=\"canManage\" size=\"sm\">\n <wx-action\n icon=\"folder\"\n :title=\"t('manager.move')\"\n :disabled=\"props.selected === 0\"\n @click=\"emit('move')\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"props.selected === 0\"\n @click=\"emit('remove')\"\n />\n </wx-actions>\n </header>\n</template>\n\n<style>\n.wx-media-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-6);\n}\n\n.wx-media-toolbar__search {\n flex: 1 1 180px;\n min-width: 120px;\n}\n\n.wx-media-toolbar__count {\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n margin-inline-start: auto;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { useModal, WxButton, WxDialog, WxSelect, WxSpace } from '@webx-ui/core'\nimport type { MediaDirectory } from './types'\n\n/**\n * Where to put the selected files.\n *\n * A list of folders rather than a box to type in: an editor knows a folder by its name and its\n * place in the tree, and has no reason to ever learn that it also has a number.\n */\nconst props = defineProps<{\n directories: MediaDirectory[]\n /** The folder they are in now, which is not worth offering. */\n from?: number | null\n count: number\n}>()\n\nconst { open, resolve, dismiss } = useModal<number>()\n\nconst t = useTranslate('webx-media')\n\nconst options = computed(() =>\n flatten(props.directories).filter((option) => option.value !== props.from),\n)\nconst target = ref<number | null>(options.value[0]?.value ?? null)\n\nfunction flatten(directories: MediaDirectory[]): { value: number; label: string }[] {\n return directories.flatMap((directory) => [\n {\n value: directory.id,\n // Indented, so the tree is still readable once it is a flat list.\n label:\n ' '.repeat(directory.depth * 3) + (directory.is_root ? t('manager.root') : directory.title),\n },\n ...flatten(directory.children ?? []),\n ])\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('manager.move-to')\" :width=\"420\">\n <wx-select v-model=\"target\" :options=\"options\" />\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('manager.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"target === null\" @click=\"resolve(target!)\">\n {{ t('manager.move') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useTemplateRef } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { useModal, WxButton, WxDialog, WxInput, WxSpace } from '@webx-ui/core'\n\n/**\n * A name, asked for properly.\n *\n * `window.prompt` is not an option: a browser is free to refuse it — an embedded frame usually\n * does — and then the button simply does nothing, which is exactly how \"new folder\" looked.\n */\nconst props = withDefaults(\n defineProps<{\n title: string\n label?: string\n value?: string\n confirmText?: string\n }>(),\n { label: undefined, value: '', confirmText: undefined },\n)\n\nconst { open, resolve, dismiss } = useModal<string>()\n\nconst t = useTranslate('webx-media')\n\nconst name = ref(props.value)\nconst input = useTemplateRef<HTMLElement>('input')\n\nonMounted(async () => {\n await nextTick()\n input.value?.querySelector('input')?.focus()\n})\n\nfunction submit(): void {\n const value = name.value.trim()\n\n if (value !== '') {\n resolve(value)\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"title\" :width=\"420\">\n <div ref=\"input\">\n <!-- The dialog's title says what is being named, so the field carries the same words as a\n placeholder and as its accessible name rather than repeating them above itself. -->\n <wx-input\n v-model=\"name\"\n :placeholder=\"label ?? t('manager.folder-name')\"\n :aria-label=\"label ?? t('manager.folder-name')\"\n @keyup.enter=\"submit\"\n />\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('manager.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"name.trim() === ''\" @click=\"submit\">\n {{ confirmText ?? t('manager.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","import type { AdminContext } from '@webx-ui/admin'\nimport type { EditOperations, FileQuery, MediaDirectory, MediaFile, MediaPage } from './types'\n\nexport interface MediaApi {\n directories(): Promise<MediaDirectory[]>\n createDirectory(parentId: number, title: string): Promise<MediaDirectory>\n renameDirectory(id: number, title: string): Promise<MediaDirectory>\n moveDirectory(id: number, parentId: number): Promise<MediaDirectory>\n /** Without `force` the server refuses a folder that holds anything, and says what it holds. */\n deleteDirectory(id: number, force?: boolean): Promise<void>\n\n files(query?: FileQuery): Promise<MediaPage>\n file(id: number): Promise<MediaFile>\n upload(\n directoryId: number,\n files: File[],\n onProgress?: (percent: number) => void,\n ): Promise<MediaFile[]>\n rename(id: number, name: string): Promise<MediaFile>\n move(ids: number[], directoryId: number): Promise<number>\n remove(ids: number[]): Promise<number>\n removeOne(id: number): Promise<void>\n\n edit(id: number, operations: EditOperations): Promise<MediaFile>\n copy(id: number): Promise<MediaFile>\n restoreOriginal(id: number): Promise<MediaFile>\n\n /** The address of a preview at a size the server allows. */\n thumb(file: MediaFile, width: number, height?: number, fit?: 'cover' | 'contain'): string | null\n}\n\nexport function createMediaApi(admin: AdminContext): MediaApi {\n const base = `${admin.apiPath}/media`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n directories: () => admin.http.get<{ data: MediaDirectory[] }>(`${base}/directories`).then(data),\n\n createDirectory: (parentId, title) =>\n admin.http\n .post<{ data: MediaDirectory }>(`${base}/directories`, { parent_id: parentId, title })\n .then(data),\n\n renameDirectory: (id, title) =>\n admin.http.patch<{ data: MediaDirectory }>(`${base}/directories/${id}`, { title }).then(data),\n\n moveDirectory: (id, parentId) =>\n admin.http\n .patch<{ data: MediaDirectory }>(`${base}/directories/${id}/move`, { parent_id: parentId })\n .then(data),\n\n deleteDirectory: (id, force = false) =>\n admin.http.delete<void>(`${base}/directories/${id}`, {\n query: { force: force ? 1 : undefined },\n }),\n\n files: (query = {}) =>\n admin.http.get<MediaPage>(`${base}/files`, {\n query: {\n directory_id: query.directory_id ?? undefined,\n q: query.q || undefined,\n type: query.type ?? undefined,\n sort: query.sort,\n page: query.page,\n per_page: query.per_page,\n },\n }),\n\n file: (id) => admin.http.get<{ data: MediaFile }>(`${base}/files/${id}`).then(data),\n\n async upload(directoryId, files, onProgress) {\n const body = new FormData()\n body.append('directory_id', String(directoryId))\n files.forEach((file) => body.append('files[]', file))\n\n // XHR rather than fetch, for the one thing fetch still cannot do: say how far an upload\n // has got. A person watching a forty-megabyte video wants a bar, not a spinner.\n return new Promise<MediaFile[]>((resolve, reject) => {\n const request = new XMLHttpRequest()\n\n request.open('POST', `${base}/files`)\n request.responseType = 'json'\n request.withCredentials = true\n request.setRequestHeader('Accept', 'application/json')\n request.setRequestHeader('X-Requested-With', 'XMLHttpRequest')\n // Without this the refusal comes back in English while the panel is in Russian: the\n // panel's own client sends it on every request, and this one is not that client.\n request.setRequestHeader('X-Webx-Locale', admin.i18n.state.locale)\n\n const token = csrfToken()\n if (token) {\n request.setRequestHeader('X-XSRF-TOKEN', token)\n }\n\n request.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * 100))\n }\n })\n\n request.addEventListener('load', () => {\n const answer = request.response as { data?: MediaFile[]; message?: string } | null\n\n if (request.status >= 200 && request.status < 300 && answer?.data) {\n resolve(answer.data)\n\n return\n }\n\n reject(\n Object.assign(new Error(answer?.message ?? 'Upload failed'), {\n status: request.status,\n body: answer,\n }),\n )\n })\n\n request.addEventListener('error', () => reject(new Error('Upload failed')))\n request.send(body)\n })\n },\n\n rename: (id, name) =>\n admin.http.patch<{ data: MediaFile }>(`${base}/files/${id}`, { name }).then(data),\n\n move: (ids, directoryId) =>\n admin.http\n .post<{ data: { moved: number } }>(`${base}/files/move`, { ids, directory_id: directoryId })\n .then((body) => body.data.moved),\n\n // A POST for the batch, not a DELETE: the panel's own client sends no body on DELETE, and\n // a list of ids in a query string is a worse answer than an honest verb.\n remove: (ids) =>\n admin.http\n .post<{ data: { deleted: number } }>(`${base}/files/delete`, { ids })\n .then((body) => body.data.deleted),\n\n removeOne: (id) => admin.http.delete<void>(`${base}/files/${id}`),\n\n edit: (id, operations) =>\n admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/edit`, operations).then(data),\n\n copy: (id) => admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/copy`).then(data),\n\n restoreOriginal: (id) =>\n admin.http.post<{ data: MediaFile }>(`${base}/files/${id}/restore-original`).then(data),\n\n thumb(file, width, height, fit = 'cover') {\n if (!file.thumb) {\n return null\n }\n\n const url = new URL(file.thumb, window.location.origin)\n url.searchParams.set('w', String(width))\n if (height) {\n url.searchParams.set('h', String(height))\n }\n url.searchParams.set('fit', fit)\n\n return url.pathname + url.search\n },\n }\n}\n\nfunction csrfToken(): string | null {\n const cookie = document.cookie.split('; ').find((part) => part.startsWith('XSRF-TOKEN='))\n\n return cookie ? decodeURIComponent(cookie.slice('XSRF-TOKEN='.length)) : null\n}\n","import type { Messages } from '@webx-ui/admin'\n\n/**\n * What this package says, in English.\n *\n * The same keys `webx-ui/module-media` ships as `lang/en/*.php`, and the same ones every other\n * language there has. Kept here so a manager placed by hand, with no WebX UI server behind it,\n * still has words — and so a key never reaches the screen when a translation is missing.\n */\nexport const mediaMessages: Record<string, Messages> = {\n module: {\n title: 'Files',\n },\n manager: {\n root: 'Library',\n search: 'Search files',\n 'new-folder': 'New folder',\n 'folder-name': 'Folder name',\n rename: 'Rename',\n move: 'Move',\n 'move-to': 'Move to…',\n delete: 'Delete',\n upload: 'Upload',\n 'upload-hint': 'Drop files here or choose them',\n select: 'Select',\n selected: ':count selected',\n empty: 'This folder is empty',\n 'empty-search': 'Nothing matches :query',\n 'all-types': 'All types',\n image: 'Images',\n video: 'Video',\n audio: 'Audio',\n document: 'Documents',\n other: 'Other',\n 'sort-newest': 'Newest first',\n 'sort-oldest': 'Oldest first',\n 'sort-name': 'By name',\n 'sort-size': 'Largest first',\n 'duplicate-added': 'That file was already in this folder',\n uploaded: ':count file(s) added',\n cancel: 'Cancel',\n save: 'Save',\n folders: 'Folders',\n edit: 'Edit',\n 'copy-link': 'Copy the link',\n 'link-copied': 'Link copied',\n 'status-files': 'Files: :count',\n 'status-selected': 'selected: :count',\n 'status-size': 'size: :size',\n },\n dialogs: {\n 'delete-files-title': 'Delete :count file(s)?',\n 'delete-files-text': 'Anywhere they are already used, they will stop opening.',\n 'delete-folder-title': 'Delete the folder :title?',\n 'delete-folder-contents': 'It holds :files file(s) and :directories folder(s).',\n 'delete-folder-warning':\n 'Everything inside goes too, and pictures already placed in content will stop opening.',\n confirm: 'Delete',\n },\n errors: {\n 'directory-not-empty': 'This folder is not empty.',\n 'root-immutable': 'The library root cannot be moved or deleted.',\n 'directory-into-itself': 'A folder cannot be moved into itself.',\n 'not-an-image': 'This file is not an image.',\n 'image-too-large': 'This image is larger than :megapixels megapixels.',\n upload: 'That file was not accepted.',\n copy: 'The clipboard is not available here. The address:',\n 'unsupported-type': 'Only these can be uploaded: :types.',\n 'file-too-large': 'The file is larger than :size MB.',\n 'too-many-files': 'No more than :count files at a time.',\n },\n files: {\n copy: ':name (copy)',\n },\n validation: {\n title: 'name',\n parent_id: 'parent folder',\n directory_id: 'folder',\n files: 'files',\n },\n}\n","import { useAdmin } from '@webx-ui/admin'\nimport { mediaMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/**\n * Put this package's English under the panel's dictionary, once per panel.\n *\n * The server's translations land on top of it when they arrive. What this prevents is the half\n * second — or the whole session, in a panel assembled without a server — where a screen shows\n * `manager.upload` instead of a word.\n */\nexport function useMediaMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-media', mediaMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/admin'\nimport {\n confirm,\n createModal,\n openImageEditor,\n toast,\n useElementWidth,\n WxAction,\n WxActions,\n WxDrawer,\n WxPagination,\n} from '@webx-ui/core'\nimport DirectoryTree from './DirectoryTree.vue'\nimport FileGrid from './FileGrid.vue'\nimport MediaToolbar from './MediaToolbar.vue'\nimport MoveDialog from './MoveDialog.vue'\nimport NameDialog from './NameDialog.vue'\nimport { createMediaApi } from './api'\nimport { useMediaMessages } from './i18n'\nimport type { MediaDirectory, MediaFile, MediaKind, MediaPage } from './types'\n\n/**\n * The library: folders on the left, files on the right, a row of icons over both.\n *\n * Everything the screen does is one request and a reload of the part that changed — there is no\n * cache here on purpose. A library is edited by several people, and a panel that believes its\n * own copy shows a folder that was emptied an hour ago.\n *\n * The width that decides the layout is this component's own, not the window's: the same manager\n * is a page, half of a picker dialog, and a phone.\n */\nconst props = withDefaults(\n defineProps<{\n /** A picker hands one file back instead of managing the library. */\n picking?: boolean\n /** Only these kinds can be chosen, when picking. */\n accept?: MediaKind | null\n }>(),\n { picking: false, accept: null },\n)\n\nconst emit = defineEmits<{ pick: [file: MediaFile] }>()\n\nconst admin = useAdmin()\nconst api = createMediaApi(admin)\nuseMediaMessages()\n\nconst t = useTranslate('webx-media')\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst width = useElementWidth(root)\nconst compact = computed(() => width.value > 0 && width.value < 640)\n\nconst directories = ref<MediaDirectory[]>([])\nconst current = ref<number | null>(null)\nconst page = ref<MediaPage | null>(null)\nconst selected = ref<number[]>([])\nconst search = ref('')\nconst type = ref<MediaKind | 'all'>(props.accept ?? 'all')\nconst sort = ref('-created_at')\nconst busy = ref(false)\nconst foldersOpen = ref(false)\nconst picker = useTemplateRef<HTMLInputElement>('picker')\n\nconst rows = computed(() => page.value?.data ?? [])\nconst canManage = computed(() => admin.can('media.manage'))\nconst canUpload = computed(() => admin.can('media.upload') || canManage.value)\nconst folder = computed(() => find(current.value))\n\nonMounted(load)\n\nwatch([current, type, sort], () => loadFiles(1))\nwatch(\n search,\n debounce(() => loadFiles(1), 300),\n)\nwatch(compact, (narrow) => {\n if (!narrow) {\n foldersOpen.value = false\n }\n})\n\nasync function load(): Promise<void> {\n directories.value = await api.directories()\n current.value ??= directories.value[0]?.id ?? null\n await loadFiles(1)\n}\n\nasync function loadFiles(to = page.value?.meta.current_page ?? 1): Promise<void> {\n busy.value = true\n\n try {\n page.value = await api.files({\n directory_id: search.value ? null : current.value,\n q: search.value,\n type: type.value === 'all' ? null : type.value,\n sort: sort.value,\n page: to,\n })\n selected.value = []\n } finally {\n busy.value = false\n }\n}\n\nfunction choose(): void {\n picker.value?.click()\n}\n\n/**\n * Uploading says what happened in a toast and nowhere else.\n *\n * A list of what was just added under the toolbar is a second place to look and a thing to\n * dismiss; the files themselves appear in the grid a moment later, which is the answer.\n */\nasync function upload(event: Event): Promise<void> {\n const input = event.target as HTMLInputElement\n const chosen = Array.from(input.files ?? [])\n\n // Cleared straight away, so choosing the same file twice in a row still counts as a change.\n input.value = ''\n\n if (chosen.length === 0 || current.value === null) {\n return\n }\n\n try {\n const stored = await api.upload(current.value, chosen)\n const duplicates = stored.filter((file) => file.duplicate).length\n\n if (stored.length > duplicates) {\n toast.success(t('manager.uploaded', { count: stored.length - duplicates }))\n }\n\n if (duplicates > 0) {\n toast.info(t('manager.duplicate-added'))\n }\n\n await Promise.all([load(), loadFiles(1)])\n } catch (error) {\n toast.danger(messageOf(error) ?? t('errors.upload'))\n }\n}\n\nconst askName = createModal<\n string,\n { title: string; label?: string; value?: string; confirmText?: string }\n>(NameDialog)\n\nasync function createFolder(): Promise<void> {\n const title = await askName({\n title: t('manager.new-folder'),\n confirmText: t('manager.new-folder'),\n })\n\n if (title === undefined || current.value === null) {\n return\n }\n\n await api.createDirectory(current.value, title)\n await load()\n}\n\nasync function renameFolder(): Promise<void> {\n const target = folder.value\n\n if (!target || target.is_root) {\n return\n }\n\n const title = await askName({\n title: t('manager.rename'),\n value: target.title,\n confirmText: t('manager.save'),\n })\n\n if (title !== undefined) {\n await api.renameDirectory(target.id, title)\n await load()\n }\n}\n\nasync function deleteFolder(): Promise<void> {\n const target = folder.value\n\n if (!target || target.is_root) {\n return\n }\n\n // Asked twice on purpose, and the second question carries the counts the server sent back:\n // what goes with the folder is somebody's article illustrations.\n try {\n await api.deleteDirectory(target.id)\n } catch (error) {\n const counts = countsOf(error)\n\n if (!counts) {\n throw error\n }\n\n const agreed = await confirm({\n title: t('dialogs.delete-folder-title', { title: target.title }),\n message: `${t('dialogs.delete-folder-contents', counts)} ${t('dialogs.delete-folder-warning')}`,\n confirmText: t('dialogs.confirm'),\n cancelText: t('manager.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) {\n return\n }\n\n await api.deleteDirectory(target.id, true)\n }\n\n current.value = null\n await load()\n}\n\nasync function moveFolder(id: number, parentId: number): Promise<void> {\n try {\n await api.moveDirectory(id, parentId)\n } catch (error) {\n toast.danger(messageOf(error) ?? t('errors.directory-into-itself'))\n }\n\n await load()\n}\n\nconst askWhereTo = createModal<\n number,\n { directories: MediaDirectory[]; from: number | null; count: number }\n>(MoveDialog)\n\nasync function moveSelected(): Promise<void> {\n const to = await askWhereTo({\n directories: directories.value,\n from: current.value,\n count: selected.value.length,\n })\n\n if (to === undefined) {\n return\n }\n\n await api.move([...selected.value], to)\n await Promise.all([load(), loadFiles()])\n}\n\nasync function removeSelected(): Promise<void> {\n const ids = [...selected.value]\n\n const agreed = await confirm({\n title: t('dialogs.delete-files-title', { count: ids.length }),\n message: t('dialogs.delete-files-text'),\n confirmText: t('dialogs.confirm'),\n cancelText: t('manager.cancel'),\n tone: 'danger',\n })\n\n if (agreed) {\n await api.remove(ids)\n await Promise.all([load(), loadFiles()])\n }\n}\n\nasync function rename(file: MediaFile, name: string): Promise<void> {\n await api.rename(file.id, name)\n await loadFiles()\n}\n\nasync function remove(file: MediaFile): Promise<void> {\n await api.removeOne(file.id)\n await Promise.all([load(), loadFiles()])\n}\n\n/**\n * The editor works on a preview and hands back a blob; the server is told the operations\n * instead and applies them to the original, at full size.\n *\n * Filters and adjustments are switched off for the same reason: they cannot be said as an\n * operation, and a picture that is quietly smaller than it was is worse than a button that is\n * not there.\n */\nasync function edit(file: MediaFile): Promise<void> {\n const result = await openImageEditor({ src: file.url, fileName: file.file_name, filters: false })\n\n if (!result) {\n return\n }\n\n await api.edit(file.id, {\n // The editor's frame is read against the picture as it is on screen — already turned — and\n // the server applies the operations in that order for the same reason.\n crop: result.crop,\n rotate: (((result.rotation % 360) + 360) % 360) as 0 | 90 | 180 | 270,\n flip: result.flipX ? 'horizontal' : result.flipY ? 'vertical' : undefined,\n resize: { width: result.width, height: result.height },\n })\n\n // Both addresses — the picture and its preview — carry the new version, so what is on screen\n // a moment later is what was just saved rather than what the browser kept.\n await loadFiles()\n}\n\nfunction find(id: number | null): MediaDirectory | null {\n const walk = (nodes: MediaDirectory[]): MediaDirectory | null => {\n for (const node of nodes) {\n if (node.id === id) {\n return node\n }\n\n const deeper = walk(node.children ?? [])\n\n if (deeper) {\n return deeper\n }\n }\n\n return null\n }\n\n return id === null ? null : walk(directories.value)\n}\n\nfunction countsOf(error: unknown): { files: number; directories: number } | null {\n const body = (\n error as { body?: { code?: string; counts?: { files: number; directories: number } } }\n )?.body\n\n return body?.code === 'directory_not_empty' ? (body.counts ?? null) : null\n}\n\nfunction messageOf(error: unknown): string | null {\n return (error as { message?: string })?.message ?? null\n}\n\n/** Bytes as somebody would say them: 32 MB, not 33 554 432. */\nfunction readable(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n let size = bytes\n let unit = 0\n\n while (size >= 1024 && unit < units.length - 1) {\n size /= 1024\n unit++\n }\n\n return `${size >= 10 || unit === 0 ? Math.round(size) : size.toFixed(1)} ${units[unit]}`\n}\n\nfunction debounce(run: () => void, wait: number): () => void {\n let timer: ReturnType<typeof setTimeout> | undefined\n\n return () => {\n clearTimeout(timer)\n timer = setTimeout(run, wait)\n }\n}\n</script>\n\n<template>\n <div ref=\"root\" class=\"wx-media\" :class=\"{ 'wx-media--compact': compact }\">\n <aside v-if=\"!compact\" class=\"wx-media__folders\">\n <directory-tree v-model:selected=\"current\" :directories=\"directories\" @move=\"moveFolder\" />\n\n <wx-actions v-if=\"canManage\" size=\"sm\" align=\"start\">\n <wx-action type=\"add\" :title=\"t('manager.new-folder')\" @click=\"createFolder\" />\n <wx-action\n type=\"edit\"\n :title=\"t('manager.rename')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"renameFolder\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"deleteFolder\"\n />\n </wx-actions>\n </aside>\n\n <section class=\"wx-media__files\">\n <media-toolbar\n v-model:search=\"search\"\n v-model:type=\"type\"\n v-model:sort=\"sort\"\n :can-upload=\"canUpload && !picking\"\n :can-manage=\"canManage\"\n :selected=\"selected.length\"\n :compact=\"compact\"\n @upload=\"choose\"\n @move=\"moveSelected\"\n @remove=\"removeSelected\"\n @folders=\"foldersOpen = true\"\n />\n\n <input ref=\"picker\" type=\"file\" multiple hidden @change=\"upload\" />\n\n <file-grid\n v-model:selected=\"selected\"\n :files=\"rows\"\n :api=\"api\"\n :query=\"search\"\n :single=\"picking\"\n @rename=\"rename\"\n @edit=\"edit\"\n @remove=\"remove\"\n @open=\"(file) => emit('pick', file)\"\n />\n\n <footer class=\"wx-media__status\">\n <span>{{ t('manager.status-files', { count: page?.stats.files ?? 0 }) }}</span>\n <span v-if=\"selected.length > 0\">\n {{ t('manager.status-selected', { count: selected.length }) }}\n </span>\n <span>{{ t('manager.status-size', { size: readable(page?.stats.size ?? 0) }) }}</span>\n </footer>\n\n <wx-pagination\n v-if=\"page && page.meta.last_page > 1\"\n :page=\"page.meta.current_page\"\n :per-page=\"page.meta.per_page\"\n :total=\"page.meta.total\"\n :last-page=\"page.meta.last_page\"\n :disabled=\"busy\"\n size=\"sm\"\n @change=\"({ page: to }) => loadFiles(to)\"\n />\n </section>\n\n <!-- On a narrow screen the folders are a drawer: a tree beside a grid leaves room for\n neither, and an editor on a phone is looking for one thing at a time. -->\n <wx-drawer v-model:open=\"foldersOpen\" side=\"left\" :title=\"t('manager.folders')\" :size=\"280\">\n <div class=\"wx-media__drawer\">\n <directory-tree\n v-model:selected=\"current\"\n :directories=\"directories\"\n @move=\"moveFolder\"\n @update:selected=\"foldersOpen = false\"\n />\n\n <wx-actions v-if=\"canManage\" size=\"sm\" align=\"start\">\n <wx-action type=\"add\" :title=\"t('manager.new-folder')\" @click=\"createFolder\" />\n <wx-action\n type=\"edit\"\n :title=\"t('manager.rename')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"renameFolder\"\n />\n <wx-action\n type=\"remove\"\n :title=\"t('manager.delete')\"\n :disabled=\"!folder || folder.is_root\"\n @click=\"deleteFolder\"\n />\n </wx-actions>\n </div>\n </wx-drawer>\n </div>\n</template>\n\n<style>\n.wx-media {\n display: grid;\n grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);\n gap: var(--wx-space-16);\n min-height: 0;\n height: 100%;\n}\n\n.wx-media--compact {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--wx-space-8);\n}\n\n.wx-media__folders {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-8);\n min-height: 0;\n border-right: 1px solid var(--wx-color-border);\n padding-right: var(--wx-space-12);\n}\n\n/* The drawer hands its content the edge of the screen; the tree and its actions want air. */\n.wx-media__drawer {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-10);\n padding-block: var(--wx-space-4);\n}\n\n.wx-media__status {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n border-top: 1px solid var(--wx-color-border);\n padding-top: var(--wx-space-6);\n}\n\n.wx-media__files {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-10);\n min-height: 0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { WxCard } from '@webx-ui/core'\nimport MediaManager from './MediaManager.vue'\n\n/**\n * The library as a page of the panel.\n *\n * On its own, the manager sits straight on the panel's background, which reads as something\n * that has not finished loading. In a dialog it already has walls; here it needs its own.\n */\n</script>\n\n<template>\n <wx-card class=\"wx-media-page\" padding=\"md\">\n <media-manager />\n </wx-card>\n</template>\n\n<style>\n.wx-media-page {\n height: 100%;\n min-height: 0;\n}\n\n/* The card is the page: its body is what scrolls, not the page behind it. */\n.wx-media-page > .wx-card__body {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useTranslate } from '@webx-ui/admin'\nimport { useModal, WxDialog } from '@webx-ui/core'\nimport MediaManager from './MediaManager.vue'\nimport type { MediaFile, MediaKind } from './types'\n\n/**\n * The library as a dialog, for a form that needs a picture.\n *\n * The same manager, not a second one: a picker that browses differently from the manager is a\n * second thing to learn and a second thing to keep working.\n */\nwithDefaults(\n defineProps<{\n accept?: MediaKind | null\n title?: string\n /** Opened to manage the library rather than to choose from it. */\n manage?: boolean\n }>(),\n { accept: 'image', title: undefined, manage: false },\n)\n\nconst { open, resolve } = useModal<MediaFile>()\n\nconst t = useTranslate('webx-media')\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"title ?? t('module.title')\" :width=\"960\">\n <div class=\"wx-media-picker\">\n <media-manager\n :picking=\"!manage\"\n :accept=\"manage ? null : accept\"\n @pick=\"(file) => resolve(file)\"\n />\n </div>\n </wx-dialog>\n</template>\n\n<style>\n.wx-media-picker {\n height: min(70vh, 560px);\n min-height: 0;\n}\n</style>\n","import { createModal } from '@webx-ui/core'\nimport MediaPicker from './MediaPicker.vue'\nimport type { MediaFile, MediaKind } from './types'\n\nexport interface MediaPickerOptions {\n /** Narrow what can be chosen; `null` offers everything. */\n accept?: MediaKind | null\n title?: string\n}\n\n/**\n * The library, opened from code, resolving with the file that was chosen.\n *\n * A form does not need to own a dialog for this — `const file = await openMediaPicker()` is the\n * whole integration, and `undefined` means the person closed it.\n */\nexport const openMediaPicker = createModal<MediaFile, MediaPickerOptions>(MediaPicker)\n\n/**\n * The whole library as a dialog, from anywhere.\n *\n * The same manager, the same folders, the same uploads — for a screen that needs to put files in\n * order without navigating away from what it was doing. It resolves when a file is opened, and\n * with `undefined` when the dialog is simply closed.\n */\nexport const openMediaLibrary = createModal<MediaFile, Omit<MediaPickerOptions, 'accept'>>(\n MediaPicker,\n { props: { manage: true } },\n)\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { WxButton, WxImage, WxInput, WxSpace } from '@webx-ui/core'\nimport { openMediaPicker } from './openMediaPicker'\nimport { useMediaMessages } from './i18n'\nimport type { MediaKind, MediaValue } from './types'\n\n/**\n * A picture on a form, with the words this entity uses for it.\n *\n * `alt` and `title` live here rather than on the file because one picture is used by two\n * articles and needs two captions. What the field stores is the key and those words; the\n * address is worked out when the page is drawn, so moving the library to another disk changes\n * nothing here.\n */\nwithDefaults(\n defineProps<{\n label?: string\n accept?: MediaKind | null\n /** Off for a decorative picture, where a caption is noise. */\n captions?: boolean\n }>(),\n { label: undefined, accept: 'image', captions: true },\n)\n\nconst value = defineModel<MediaValue | null>({ default: null })\n\nuseMediaMessages()\n\nconst t = useTranslate('webx-media')\n\nconst preview = computed(() => value.value?.url ?? null)\n\nasync function choose(): Promise<void> {\n const file = await openMediaPicker({ accept: 'image' })\n\n if (file) {\n value.value = { ...value.value, path: file.path, url: file.url }\n }\n}\n\nfunction clear(): void {\n value.value = null\n}\n</script>\n\n<template>\n <div class=\"wx-media-field\">\n <span v-if=\"label\" class=\"wx-media-field__label\">{{ label }}</span>\n\n <div class=\"wx-media-field__body\">\n <wx-image v-if=\"preview\" class=\"wx-media-field__preview\" :src=\"preview\" fit=\"cover\" />\n\n <wx-space direction=\"vertical\" size=\"sm\" class=\"wx-media-field__controls\">\n <wx-space size=\"xs\">\n <wx-button size=\"sm\" icon=\"image\" @click=\"choose\">{{ t('manager.select') }}</wx-button>\n <wx-button v-if=\"value\" size=\"sm\" variant=\"text\" type=\"danger\" @click=\"clear\">\n {{ t('manager.delete') }}\n </wx-button>\n </wx-space>\n\n <template v-if=\"captions && value\">\n <wx-input\n :model-value=\"value.alt ?? ''\"\n size=\"sm\"\n placeholder=\"alt\"\n @update:model-value=\"(alt) => (value = { ...value!, alt: String(alt) })\"\n />\n <wx-input\n :model-value=\"value.title ?? ''\"\n size=\"sm\"\n placeholder=\"title\"\n @update:model-value=\"(title) => (value = { ...value!, title: String(title) })\"\n />\n </template>\n </wx-space>\n </div>\n </div>\n</template>\n\n<style>\n.wx-media-field {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-4);\n}\n\n.wx-media-field__label {\n font-size: var(--wx-font-size-sm);\n color: var(--wx-color-text-muted);\n}\n\n.wx-media-field__body {\n display: flex;\n gap: var(--wx-space-12);\n align-items: flex-start;\n}\n\n.wx-media-field__preview {\n width: 96px;\n height: 96px;\n border-radius: var(--wx-radius-md);\n overflow: hidden;\n flex: none;\n}\n\n.wx-media-field__controls {\n min-width: 0;\n flex: 1;\n}\n</style>\n","import type { AdminModule } from '@webx-ui/admin'\nimport MediaPage from './MediaPage.vue'\nimport { mediaMessages } from './messages'\n\nexport interface MediaOptions {\n /** Where the section lives inside the panel. */\n path?: string\n}\n\n/**\n * The file manager as a section of the panel.\n *\n * The id matches the module the server reports, which is what makes the entry appear in the\n * navigation: a section shows up when both halves are installed, and stays out of the way when\n * only one is.\n */\nexport function media(options: MediaOptions = {}): AdminModule {\n const path = options.path ?? '/media'\n\n return {\n id: 'media',\n path,\n routes: [\n {\n path,\n name: 'webx.media',\n component: MediaPage,\n },\n ],\n }\n}\n\nexport { mediaMessages }\nexport { createMediaApi, type MediaApi } from './api'\nexport { openMediaLibrary, openMediaPicker, type MediaPickerOptions } from './openMediaPicker'\nexport { default as WxMediaManager } from './MediaManager.vue'\nexport { default as WxMediaPage } from './MediaPage.vue'\nexport { default as WxMediaPicker } from './MediaPicker.vue'\nexport { default as WxMediaField } from './MediaField.vue'\nexport type {\n DirectoryNotEmpty,\n EditOperations,\n FileQuery,\n MediaDirectory,\n MediaFile,\n MediaKind,\n MediaPage,\n MediaValue,\n} from './types'\n"],"names":["props","__props","selected","_useModel","emit","__emit","t","useTranslate","nodes","ref","expanded","watch","directories","label","keysOf","directory","onDrop","event","parent","_createBlock","_unref","WxTree","$event","empty","computed","copied","toast","copyFailed","file","area","done","WxSelectionArea","_withCtx","isSelected","_createElementBlock","_Fragment","_renderList","WxFileCard","name","_directive_wx_select","WxEmpty","search","type","sort","types","kind","sorts","typeLabel","one","sortLabel","_openBlock","_hoisted_1","WxAction","_createVNode","WxInput","WxDropdown","option","WxDropdownItem","_createTextVNode","_toDisplayString","WxActions","open","resolve","dismiss","useModal","options","flatten","target","WxDialog","WxSpace","WxButton","_cache","WxSelect","input","useTemplateRef","onMounted","nextTick","submit","value","_createElementVNode","createMediaApi","admin","base","data","body","parentId","title","id","force","query","directoryId","files","onProgress","reject","request","token","csrfToken","answer","ids","operations","width","height","fit","url","cookie","part","mediaMessages","seeded","useMediaMessages","i18n","useAdmin","api","root","useElementWidth","compact","current","page","busy","foldersOpen","picker","rows","canManage","canUpload","folder","find","load","loadFiles","debounce","narrow","to","choose","upload","chosen","stored","duplicates","error","messageOf","askName","createModal","NameDialog","createFolder","renameFolder","deleteFolder","counts","countsOf","confirm","moveFolder","askWhereTo","MoveDialog","moveSelected","removeSelected","rename","remove","edit","result","openImageEditor","walk","node","deeper","readable","bytes","units","size","unit","run","wait","timer","_normalizeClass","DirectoryTree","_hoisted_2","MediaToolbar","FileGrid","_hoisted_3","_hoisted_4","WxPagination","WxDrawer","_hoisted_5","WxCard","MediaManager","openMediaPicker","MediaPicker","openMediaLibrary","preview","clear","WxImage","alt","media","path","MediaPage"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { Messages } from '@webx-ui/admin';
2
+ /**
3
+ * What this package says, in English.
4
+ *
5
+ * The same keys `webx-ui/module-media` ships as `lang/en/*.php`, and the same ones every other
6
+ * language there has. Kept here so a manager placed by hand, with no WebX UI server behind it,
7
+ * still has words — and so a key never reaches the screen when a translation is missing.
8
+ */
9
+ export declare const mediaMessages: Record<string, Messages>;
@@ -0,0 +1,21 @@
1
+ import { MediaFile, MediaKind } from './types';
2
+ export interface MediaPickerOptions {
3
+ /** Narrow what can be chosen; `null` offers everything. */
4
+ accept?: MediaKind | null;
5
+ title?: string;
6
+ }
7
+ /**
8
+ * The library, opened from code, resolving with the file that was chosen.
9
+ *
10
+ * A form does not need to own a dialog for this — `const file = await openMediaPicker()` is the
11
+ * whole integration, and `undefined` means the person closed it.
12
+ */
13
+ export declare const openMediaPicker: (props?: MediaPickerOptions | undefined, options?: import('@webx-ui/core').ModalOptions) => import('@webx-ui/core').ModalPromise<MediaFile>;
14
+ /**
15
+ * The whole library as a dialog, from anywhere.
16
+ *
17
+ * The same manager, the same folders, the same uploads — for a screen that needs to put files in
18
+ * order without navigating away from what it was doing. It resolves when a file is opened, and
19
+ * with `undefined` when the dialog is simply closed.
20
+ */
21
+ export declare const openMediaLibrary: (props?: Omit<MediaPickerOptions, "accept"> | undefined, options?: import('@webx-ui/core').ModalOptions) => import('@webx-ui/core').ModalPromise<MediaFile>;
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .wx-media-tree{min-width:0;overflow:auto}.wx-media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:var(--wx-space-12);align-content:start;min-height:0;overflow:auto;padding:var(--wx-space-2);user-select:none}.wx-media--compact .wx-media-grid{grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--wx-space-8)}.wx-media-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-6)}.wx-media-toolbar__search{flex:1 1 180px;min-width:120px}.wx-media-toolbar__count{font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted);margin-inline-start:auto}.wx-media{display:grid;grid-template-columns:minmax(180px,240px) minmax(0,1fr);gap:var(--wx-space-16);min-height:0;height:100%}.wx-media--compact{grid-template-columns:minmax(0,1fr);gap:var(--wx-space-8)}.wx-media__folders{display:flex;flex-direction:column;gap:var(--wx-space-8);min-height:0;border-right:1px solid var(--wx-color-border);padding-right:var(--wx-space-12)}.wx-media__drawer{display:flex;flex-direction:column;gap:var(--wx-space-10);padding-block:var(--wx-space-4)}.wx-media__status{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted);border-top:1px solid var(--wx-color-border);padding-top:var(--wx-space-6)}.wx-media__files{display:flex;flex-direction:column;gap:var(--wx-space-10);min-height:0}.wx-media-page{height:100%;min-height:0}.wx-media-page>.wx-card__body{height:100%;min-height:0;display:flex;flex-direction:column}.wx-media-picker{height:min(70vh,560px);min-height:0}.wx-media-field{display:flex;flex-direction:column;gap:var(--wx-space-4)}.wx-media-field__label{font-size:var(--wx-font-size-sm);color:var(--wx-color-text-muted)}.wx-media-field__body{display:flex;gap:var(--wx-space-12);align-items:flex-start}.wx-media-field__preview{width:96px;height:96px;border-radius:var(--wx-radius-md);overflow:hidden;flex:none}.wx-media-field__controls{min-width:0;flex:1}
@@ -0,0 +1,83 @@
1
+ /** The shapes `webx-ui/module-media` answers with. */
2
+ export type MediaKind = 'image' | 'video' | 'audio' | 'document' | 'other';
3
+ export interface MediaDirectory {
4
+ id: number;
5
+ parent_id: number | null;
6
+ title: string;
7
+ depth: number;
8
+ is_root: boolean;
9
+ files_count?: number;
10
+ children: MediaDirectory[];
11
+ }
12
+ export interface MediaFile {
13
+ id: number;
14
+ directory_id: number;
15
+ name: string;
16
+ file_name: string;
17
+ extension: string;
18
+ mime: string;
19
+ type: MediaKind;
20
+ size: number;
21
+ width: number | null;
22
+ height: number | null;
23
+ /** What an entity stores. The address beside it is worked out on every read. */
24
+ path: string;
25
+ url: string;
26
+ thumb: string | null;
27
+ editable: boolean;
28
+ has_original: boolean;
29
+ duplicate: boolean;
30
+ created_at: string | null;
31
+ }
32
+ export interface MediaPage {
33
+ data: MediaFile[];
34
+ meta: {
35
+ current_page: number;
36
+ last_page: number;
37
+ per_page: number;
38
+ total: number;
39
+ };
40
+ /** Everything the current filters match, not just this page. */
41
+ stats: {
42
+ files: number;
43
+ size: number;
44
+ };
45
+ }
46
+ export interface FileQuery {
47
+ directory_id?: number | null;
48
+ q?: string;
49
+ type?: MediaKind | null;
50
+ sort?: string;
51
+ page?: number;
52
+ per_page?: number;
53
+ }
54
+ export interface EditOperations {
55
+ crop?: {
56
+ x: number;
57
+ y: number;
58
+ width: number;
59
+ height: number;
60
+ };
61
+ rotate?: 0 | 90 | 180 | 270;
62
+ flip?: 'horizontal' | 'vertical';
63
+ resize?: {
64
+ width?: number;
65
+ height?: number;
66
+ };
67
+ }
68
+ /** What a form keeps about a picture it uses: the file, and this entity's words for it. */
69
+ export interface MediaValue {
70
+ path: string;
71
+ alt?: string;
72
+ title?: string;
73
+ /** Filled in when the value was picked in this session, so a field can draw a preview. */
74
+ url?: string;
75
+ }
76
+ export interface DirectoryNotEmpty {
77
+ code: 'directory_not_empty';
78
+ message: string;
79
+ counts: {
80
+ files: number;
81
+ directories: number;
82
+ };
83
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@webx-ui/module-media",
3
+ "version": "0.0.1",
4
+ "description": "The file manager of a WebX UI admin panel: folders, files, uploads and an image editor. Pairs with webx-ui/module-media on the server.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "keywords": [
11
+ "webx-ui",
12
+ "vue",
13
+ "vue3",
14
+ "admin",
15
+ "media",
16
+ "file-manager",
17
+ "laravel"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/webx-ui/webx-ui.git",
22
+ "directory": "packages/module-media"
23
+ },
24
+ "homepage": "https://webx-ui.github.io/webx-ui/guide/roadmap.html",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./style.css": "./dist/style.css",
40
+ "./dist/style.css": "./dist/style.css",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "peerDependencies": {
44
+ "vue": "^3.5.0",
45
+ "vue-router": "^4.5.0"
46
+ },
47
+ "dependencies": {
48
+ "@webx-ui/admin": "^0.2.0",
49
+ "@webx-ui/core": "^0.14.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^24.10.1",
53
+ "@vitejs/plugin-vue": "^6.0.1",
54
+ "typescript": "^5.9.3",
55
+ "vite": "^7.1.14",
56
+ "vite-plugin-dts": "^4.5.4",
57
+ "vue": "^3.5.24",
58
+ "vue-router": "^4.5.1",
59
+ "vue-tsc": "^3.1.3"
60
+ },
61
+ "scripts": {
62
+ "build": "vite build",
63
+ "dev": "vite build --watch",
64
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit"
65
+ }
66
+ }