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

@@ -11,6 +11,8 @@
(ngModelChange)="facade.setSearch($event)"
[placeholder]="'mediaLibrary.searchPlaceholder' | translate"
/>
<app-select [options]="kindOptions()" [ngModel]="facade.kind() || 'all'" (ngModelChange)="setKind($event)" [ariaLabel]="'mediaLibrary.allTypes' | translate" [fullWidth]="false" />
<app-button variant="ghost" size="sm" (click)="showRecentOnly()">{{ 'mediaLibrary.sortRecent' | translate }}</app-button>
<input
#fileInput
type="file"
@@ -39,11 +41,18 @@
[description]="'mediaLibrary.emptyDescription' | translate"
/>
} @else {
<div class="media-picker__grid">
@for (asset of facade.items(); track asset.id) {
<app-card padding="sm" [interactive]="true" (click)="pick(asset)">
<div class="media-picker__grid" role="listbox" [attr.aria-label]="'mediaLibrary.title' | translate" tabindex="0" (keydown)="onGridKeydown($event)">
@for (asset of facade.items(); track asset.id; let i = $index) {
<app-card
padding="sm"
[interactive]="true"
role="option"
[attr.aria-selected]="i === focusedIndex()"
[class.media-picker__card--focused]="i === focusedIndex()"
(click)="pick(asset)"
>
@if (asset.mimeType.startsWith('image/')) {
<img class="media-picker__preview" [src]="asset.url" [alt]="asset.filename" />
<img class="media-picker__preview" [src]="asset.url" [alt]="asset.filename" loading="lazy" />
} @else {
<div class="media-picker__preview media-picker__preview--file">{{ asset.mimeType }}</div>
}