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.
Content dashboard with real published/draft/SEO-health/completion metrics and a recommended-next-action; static pages editor rebuilt as visual page cards (icon/status/SEO badge/last edited) with legal pages surfaced first; full-page editor grouped into Content/SEO/Sharing/Advanced tabs, raw HTML moved behind an Advanced disclosure; hero image now supports alt text and caption; content-health-widget is a reusable checklist+completion component.
MediaLibraryFacade is a root-provided singleton shared by every
app-media-picker instance on a page (branding alone renders 4; static-pages
with N pages renders 2N). ngOnInit called facade.load() unconditionally on
mount regardless of whether the dialog was ever opened, and search/folder/
page filters set in one dialog leaked into whichever picker instance was
opened next, since they all read/write the same signals.
Replaced ngOnInit with an effect() that resets search/folder/page and loads
only when this instance's own input becomes true. Verified live:
searched in one field's picker, closed it, opened a different field's
picker on the same page - search is now reset to empty (previously it
would've carried over 'leftover-search-term').
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Milestone 1 of the Static Pages Module sprint.
- StaticPageConfig / ContentPage / ContentPageBootstrapInput gain: explicit
editable route (defaults from slug, independently overridable), enabled
(master on/off), status: 'draft'|'published' (per-page publish lifecycle,
independent of the whole-bootstrap draft/publish cycle), customTemplate,
heroImage/thumbnail/gallery, and seo.robots.
- ContentPageService: normalizePage/normalizePages default missing
enabled/status to enabled+published so existing bootstrap data never gets
silently un-published; only the editor's createPage() opts a brand-new page
into 'draft'. Legacy array-format pages get the same treatment.
- resolvePage now returns null (storefront 404) for a disabled or draft page,
regardless of whether the surrounding bootstrap itself is published -
affects the storefront static-page route AND the auto-generated footer nav
group (both go through this same resolver), which is the correct behavior.
- validatePages extended: duplicateRoutes (route can now diverge from slug),
invalidHtml, invalidSeo (canonical/ogImage URL shape, known robots tokens).
- New schema/validators/primitives.validateHtml: stack-based tag-balance
check (void/self-closing elements skipped, comments stripped). Caught and
fixed a real bug during its own spec run: the initial implementation popped
the stack back to the nearest matching ancestor on a mismatched closing
tag, which silently swallowed a genuinely unclosed inner tag instead of
flagging it - now a closing tag must match the top of the stack exactly.
- toBootstrapRecord serializes the new fields; visible mirrors enabled so any
reader of the older field name stays truthful.
- Specs: content-page.service.spec.ts (new), primitives.spec.ts (validateHtml).
Gate: tsc --noEmit, npm test (57/57), arch:check, build all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
FormFieldComponent's label had [for]=fieldId but nothing ever gave the
projected control a matching id, and aria-describedby was set on a wrapper
div instead of the actual input - so screen readers announced neither the
label association nor the hint/error text.
Added FormFieldContext, an injectable abstract class FormFieldComponent
provides via its own providers array (visible to content-projected children,
same mechanism Angular Material's mat-form-field/matInput relationship
relies on). InputComponent optionally injects it and self-applies id and
aria-describedby when nested inside app-form-field.
This retroactively fixes every form built across Sprint 3-5 (Static Pages
editor, Media Manager, Products List/Form, Project Editor sections) with
no template changes needed anywhere else.
Verified in browser: input id matches label's for attribute, and
aria-describedby correctly points to the rendered hint/error text.
Completes Sprint 5 (UI/UX Polish): Admin Products List, Admin Product Form,
Project Editor sections (9 of 11), and this accessibility fix.
Standalone dialog (app-dialog, size lg) reusing MediaLibraryFacade so it
shares the exact same asset list/search/pagination/upload state as the
Media Manager page. Emits (selected) with the chosen MediaAsset and
(closed) to dismiss - selecting a tile emits both.
Completes Sprint 4 (Media Manager): ADR-0002 contract, MediaRepository +
mock IndexedDB adapter, Media Manager UI, reusable Media Picker.
Not yet wired into Product Editor or Static Pages editor - that's the
next natural step when those features gain image fields, not part of
this sprint's scope.
Standalone, OnPush. Computes truncated page range with ellipsis, composes
existing app-button (ghost/sm) for prev/next controls rather than duplicating
button styling. aria-current on active page, nav aria-label. Colors/radius/
spacing via existing tenant CSS vars with fallbacks.
Completes Sprint 6 Design System primitives: Button, Input, Card, Badge,
Dialog, FormField, EmptyState, Skeleton, Table, Pagination.
Standalone, OnPush, ViewEncapsulation.None scoped under .app-table/.app-table-wrapper
so projected thead/tbody markup receives consistent styling. Scroll container for
responsive overflow. Colors/radius/spacing via existing tenant CSS vars with fallbacks.
Standalone, OnPush. Label/hint/error slots, required marker, aria-describedby
wiring, role=alert on error. Colors via existing tenant CSS vars with fallbacks.
Standalone, OnPush. Backdrop click and Escape close, focus trap with
return-focus on close, role=dialog/aria-modal. Sizes sm/md/lg. Colors/
radius/shadow/spacing via existing tenant CSS vars with fallbacks.
Standalone, OnPush, content-projection container. Padding none/sm/md/lg,
bordered and interactive variants. Colors/radius/shadow/spacing consumed
via existing tenant CSS vars with fallbacks.
ControlValueAccessor-based, standalone, OnPush. Sizes sm/md/lg, states default/error/success.
Colors/radius/spacing/transitions consumed via existing tenant CSS vars (--border-color,
--primary-color, --error-color, --success-color, etc) with fallbacks - compatible with
bootstrap JSON theming and Project Editor live preview.
- Project editor: persist draft to localStorage, restore on reload,
last-saved/draft-restored status indicators, section/whole-draft reset
with confirmation.
- Extract shared QR/polling/expiry engine from TelegramLoginComponent
(shared/qr-login) and reuse it for a new admin login flow.
- Admin authentication kept fully separate from customer session:
own cookie/localStorage keys, signals, guard, and header interceptor
(core/admin-auth).
- ?login=true / ?adminLogin=true open the respective login dialog for
manual testing.
- Ed25519 challenge/verify interfaces (fail-closed no-op binding) ready
for backend delivery.
- Document autosave/reset/admin-auth/QR-reuse/Ed25519 model and the
remaining full-field-coverage gap in docs/Project-Editor.md.
Unify product details modules behind config-driven contracts so teams can
extend UX without changing runtime architecture or bootstrap flow.
Keep backward compatibility with existing product payloads by treating new
media/specification/variant/related structures as optional extensions.
Improve conversion and content discoverability with reusable actions,
typed media rendering, grouped specifications, dynamic variants, and
multi-collection related products.