feat(builder): implement reusable media library

Media dashboard (real total/images/SVG/logos/unused/storage/alt-coverage, computed from actual assets + a bootstrap usage scan, no fabricated stats); gallery gets grid/list toggle, type filter, sort, drag-and-drop + multi-file upload with cancel/retry and friendly error mapping, lazy thumbnails, multi-select with bulk delete/download/export-metadata; asset details drawer shows real dimensions/size/format/date/usage locations (walks bootstrap config for exact URL matches, reports not-used rather than guessing) plus editable alt text/caption/description/decorative flag with missing-alt warning; shared MediaPickerComponent (already the one reusable picker used by content management) gains type filter, a recent shortcut, and keyboard grid navigation.
This commit is contained in:
sdarbinyan
2026-07-18 16:09:47 +04:00
parent b31c75214d
commit 5d52d81c7d
21 changed files with 1255 additions and 136 deletions

View File

@@ -1152,6 +1152,71 @@ export const en: Translations = {
confirm: 'Delete',
emptyTitle: 'No files yet',
emptyDescription: 'Upload an image or document to get started.',
allFolders: 'All folders',
newFolder: 'New folder',
editTags: 'Edit tags',
totalAssets: 'Total assets',
logos: 'Logos',
unused: 'Unused',
storageUsed: 'Storage used',
altCoverage: 'Alt text coverage',
allTypes: 'All types',
typeImage: 'Images',
typeSvg: 'SVG',
typePdf: 'PDF',
sortRecent: 'Recent',
sortName: 'Name',
sortSize: 'Size',
viewMode: 'View',
viewGrid: 'Grid',
viewList: 'List',
dropHint: 'Drag and drop images here, or use Upload above.',
cancelUpload: 'Cancel',
retry: 'Retry',
errorTooLarge: 'That file is too large. Try a smaller file.',
errorUnsupportedType: 'That file type is not supported. Use JPG, PNG, WebP, GIF, SVG, or PDF.',
errorGeneric: 'Upload failed. Please try again.',
selectedCount: 'selected',
download: 'Download',
exportMetadata: 'Export metadata',
bulkDelete: 'Delete selected',
bulkDeleteConfirm: 'Delete the selected files? This cannot be undone.',
clearSelection: 'Clear selection',
guideCopy: 'Reusing an existing asset avoids duplicate uploads and keeps your storage tidy. For best results upload images around 1200×1200px for hero/banner use and compress large photos before uploading.',
missingAlt: 'Missing alt text',
usageCount: 'uses',
detailsTitle: 'Asset details',
filename: 'Filename',
format: 'Format',
fileSize: 'File size',
dimensions: 'Dimensions',
uploadDate: 'Uploaded',
lastUsed: 'Last used',
usageCountLabel: 'Usage count',
usedIn: 'Used in',
notUsed: 'Not currently used anywhere.',
unknownValue: 'Unknown',
altText: 'Alt text',
altTextHelp: 'Describes the image for screen readers and search engines.',
decorative: 'Decorative (no alt text needed)',
missingAltWarning: 'This image has no alt text and is not marked decorative — screen reader users wont know what it shows.',
caption: 'Caption',
description: 'Description',
usage: {
brand: 'Logo',
theme: 'Theme',
header: 'Header',
footer: 'Footer',
staticPage: 'Static page',
homepage: 'Homepage',
catalog: 'Catalog',
productPage: 'Product page',
widget: 'Widget',
company: 'Company info',
seo: 'SEO',
navigation: 'Navigation',
other: 'Other',
},
},
adminCategories: {
chooseImage: 'Choose image',

View File

@@ -1147,6 +1147,71 @@ export const hy: Translations = {
confirm: 'Ջնջել',
emptyTitle: 'Ֆայլեր դեռ չկան',
emptyDescription: 'Վերբեռնեք պատկեր կամ փաստաթուղթ սկսելու համար։',
allFolders: 'Բոլոր պանակները',
newFolder: 'Նոր պանակ',
editTags: 'Խմբագրել պիտակները',
totalAssets: 'Ընդհանուր ֆայլեր',
logos: 'Լոգոներ',
unused: 'Չօգտագործվող',
storageUsed: 'Օգտագործված տարածք',
altCoverage: 'Alt տեքստի ծածկույթ',
allTypes: 'Բոլոր տեսակները',
typeImage: 'Պատկերներ',
typeSvg: 'SVG',
typePdf: 'PDF',
sortRecent: 'Վերջին',
sortName: 'Անուն',
sortSize: 'Չափ',
viewMode: 'Տեսք',
viewGrid: 'Ցանց',
viewList: 'Ցուցակ',
dropHint: 'Քաշեք և գցեք պատկերները այստեղ, կամ օգտագործեք վերևի «Վերբեռնել» կոճակը։',
cancelUpload: 'Չեղարկել',
retry: 'Կրկնել',
errorTooLarge: 'Ֆայլը չափազանց մեծ է։ Փորձեք ավելի փոքր ֆայլ։',
errorUnsupportedType: 'Այս ֆայլի տեսակը չի աջակցվում։ Օգտագործեք JPG, PNG, WebP, GIF, SVG կամ PDF։',
errorGeneric: 'Վերբեռնումը ձախողվեց։ Փորձեք կրկին։',
selectedCount: 'ընտրված է',
download: 'Ներբեռնել',
exportMetadata: 'Արտահանել մետատվյալները',
bulkDelete: 'Ջնջել ընտրվածները',
bulkDeleteConfirm: 'Ջնջե՞լ ընտրված ֆայլերը։ Հնարավոր չէ հետարկել։',
clearSelection: 'Չեղարկել ընտրությունը',
guideCopy: 'Առկա ֆայլի կրկնակի օգտագործումը խուսափում է կրկնօրինակումներից և խնայում է տարածք։ Հերոս/բաններ նկարների համար խորհուրդ ենք տալիս մոտ 1200×1200px չափս և սեղմել մեծ լուսանկարները վերբեռնելուց առաջ։',
missingAlt: 'Alt տեքստը բացակայում է',
usageCount: 'օգտ.',
detailsTitle: 'Ֆայլի մանրամասներ',
filename: 'Ֆայլի անուն',
format: 'Ձևաչափ',
fileSize: 'Ֆայլի չափ',
dimensions: 'Չափսեր',
uploadDate: 'Վերբեռնված է',
lastUsed: 'Վերջին օգտագործում',
usageCountLabel: 'Օգտագործումների քանակ',
usedIn: 'Օգտագործվում է',
notUsed: 'Այս պահին ոչ մի տեղ չի օգտագործվում։',
unknownValue: 'Անհայտ',
altText: 'Alt տեքստ',
altTextHelp: 'Նկարագրում է պատկերը էկրանընթերցողների և որոնողական համակարգերի համար։',
decorative: 'Դեկորատիվ (alt տեքստ պետք չէ)',
missingAltWarning: 'Այս պատկերը չունի alt տեքստ և նշված չէ որպես դեկորատիվ․ էկրանընթերցող օգտագործողները չեն իմանա, թե ինչ է պատկերված։',
caption: 'Ենթագիր',
description: 'Նկարագրություն',
usage: {
brand: 'Լոգո',
theme: 'Թեմա',
header: 'Վերնագիր',
footer: 'Ստորագիր',
staticPage: 'Ստատիկ էջ',
homepage: 'Գլխավոր էջ',
catalog: 'Կատալոգ',
productPage: 'Ապրանքի էջ',
widget: 'Վիջեթ',
company: 'Ընկերության տվյալներ',
seo: 'SEO',
navigation: 'Նավիգացիա',
other: 'Այլ',
},
},
adminCategories: {
chooseImage: 'Ընտրել պատկեր',

View File

@@ -1147,6 +1147,71 @@ export const ru: Translations = {
confirm: 'Удалить',
emptyTitle: 'Пока нет файлов',
emptyDescription: 'Загрузите изображение или документ, чтобы начать.',
allFolders: 'Все папки',
newFolder: 'Новая папка',
editTags: 'Изменить теги',
totalAssets: 'Всего файлов',
logos: 'Логотипы',
unused: 'Неиспользуемые',
storageUsed: 'Использовано места',
altCoverage: 'Покрытие alt-текстом',
allTypes: 'Все типы',
typeImage: 'Изображения',
typeSvg: 'SVG',
typePdf: 'PDF',
sortRecent: 'Недавние',
sortName: 'Имя',
sortSize: 'Размер',
viewMode: 'Вид',
viewGrid: 'Сетка',
viewList: 'Список',
dropHint: 'Перетащите изображения сюда или используйте кнопку «Загрузить» выше.',
cancelUpload: 'Отменить',
retry: 'Повторить',
errorTooLarge: 'Файл слишком большой. Попробуйте файл меньшего размера.',
errorUnsupportedType: 'Этот тип файла не поддерживается. Используйте JPG, PNG, WebP, GIF, SVG или PDF.',
errorGeneric: 'Не удалось загрузить файл. Попробуйте ещё раз.',
selectedCount: 'выбрано',
download: 'Скачать',
exportMetadata: 'Экспортировать метаданные',
bulkDelete: 'Удалить выбранные',
bulkDeleteConfirm: 'Удалить выбранные файлы? Это действие нельзя отменить.',
clearSelection: 'Снять выделение',
guideCopy: 'Повторное использование существующего файла избавляет от дублей и экономит место. Для баннеров и главных изображений рекомендуем размер около 1200×1200px и сжатие крупных фото перед загрузкой.',
missingAlt: 'Нет alt-текста',
usageCount: 'исп.',
detailsTitle: 'Информация о файле',
filename: 'Имя файла',
format: 'Формат',
fileSize: 'Размер файла',
dimensions: 'Размеры',
uploadDate: 'Загружен',
lastUsed: 'Последнее использование',
usageCountLabel: 'Количество использований',
usedIn: 'Используется в',
notUsed: 'Пока нигде не используется.',
unknownValue: 'Неизвестно',
altText: 'Alt-текст',
altTextHelp: 'Описывает изображение для программ чтения с экрана и поисковых систем.',
decorative: 'Декоративное (alt-текст не нужен)',
missingAltWarning: 'У этого изображения нет alt-текста, и оно не отмечено как декоративное — пользователи с программами чтения с экрана не узнают, что на нём изображено.',
caption: 'Подпись',
description: 'Описание',
usage: {
brand: 'Логотип',
theme: 'Тема',
header: 'Шапка',
footer: 'Подвал',
staticPage: 'Статическая страница',
homepage: 'Главная страница',
catalog: 'Каталог',
productPage: 'Страница товара',
widget: 'Виджет',
company: 'Информация о компании',
seo: 'SEO',
navigation: 'Навигация',
other: 'Другое',
},
},
adminCategories: {
chooseImage: 'Выбрать изображение',

View File

@@ -1150,6 +1150,71 @@ export interface Translations {
confirm: string;
emptyTitle: string;
emptyDescription: string;
allFolders: string;
newFolder: string;
editTags: string;
totalAssets: string;
logos: string;
unused: string;
storageUsed: string;
altCoverage: string;
allTypes: string;
typeImage: string;
typeSvg: string;
typePdf: string;
sortRecent: string;
sortName: string;
sortSize: string;
viewMode: string;
viewGrid: string;
viewList: string;
dropHint: string;
cancelUpload: string;
retry: string;
errorTooLarge: string;
errorUnsupportedType: string;
errorGeneric: string;
selectedCount: string;
download: string;
exportMetadata: string;
bulkDelete: string;
bulkDeleteConfirm: string;
clearSelection: string;
guideCopy: string;
missingAlt: string;
usageCount: string;
detailsTitle: string;
filename: string;
format: string;
fileSize: string;
dimensions: string;
uploadDate: string;
lastUsed: string;
usageCountLabel: string;
usedIn: string;
notUsed: string;
unknownValue: string;
altText: string;
altTextHelp: string;
decorative: string;
missingAltWarning: string;
caption: string;
description: string;
usage: {
brand: string;
theme: string;
header: string;
footer: string;
staticPage: string;
homepage: string;
catalog: string;
productPage: string;
widget: string;
company: string;
seo: string;
navigation: string;
other: string;
};
};
// Sprint 28: only the new empty-state copy this sprint's skeleton/empty-
// state consistency fix introduces - NOT the full adminProducts/