feat(media): reusable media management

Sprint 22.

- MediaAsset gains folder (flat) and MediaListParams gains folder/tag
  filters; MediaRepository.listFolders() derives the folder list from
  existing records
- upload validation: 10MB size cap, mime allow-list (jpeg/png/webp/gif/
  svg+xml/pdf), real error messages surfaced through MediaLibraryFacade
  instead of a generic swallowed string
- SVG uploads are sanitized (script tags and on*= attributes stripped)
  before storage
- raster images (excl. gif) are downscaled to a 2000px max dimension and
  re-encoded via canvas before storage - compression, not a crop UI
- tag editing (window.prompt, comma-separated) via
  MediaLibraryFacade.updateTags()
- MediaPickerComponent wired into Project Editor branding (logo, compact
  logo, favicon) alongside its existing category/product usage - confirmed
  no image fields exist on Static Pages or as a dedicated hero field to
  wire

docs/ADMIN.md updated with the new Sprint 22 section including the storage
abstraction note (MediaRepository was already the abstraction).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-15 10:42:23 +04:00
parent 60c63a0d2a
commit 30afb5d778
14 changed files with 324 additions and 33 deletions

View File

@@ -14,7 +14,7 @@
<input
#fileInput
type="file"
accept="image/*,application/pdf"
accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml,application/pdf"
hidden
(change)="onFileSelected($event)"
/>
@@ -23,6 +23,10 @@
</app-button>
</div>
@if (facade.error()) {
<p class="media-picker__error">{{ facade.error() }}</p>
}
@if (facade.loading()) {
<div class="media-picker__grid">
@for (i of [1, 2, 3, 4]; track i) {