19-Apr-2026 — focal-point picker, save flow, screenshot fidelity #243
Photo screenshot fidelity (originally #243):
The banner editor's saved/exported screenshot stretched the photo bitmap
because html2canvas (via ngx-capture) does not honour object-fit: cover
on <img> elements, while the live preview does (a global
img { object-fit: cover } rule applies). The captured PNG embedded in
booking confirmation emails therefore showed the photo distorted, even
though the editable preview looked correct.
Render the photo as a <div> with background-image and
background-size: cover instead. html2canvas handles CSS background
images correctly, so the captured screenshot now matches the live
preview pixel-for-pixel. The cropper transform (scale + translate) and
right/bottom anchoring are preserved.
Also moves the (load)="setPaperCutImageHeight()" trigger onto the
background image (the actual source of the height measurement) and
drops a stray empty <img/> left over from the original markup.
Banner editor:
- Photo focal-point picker driven by FocalPoint (x/y/zoom) on Image, with the
picker's resizable handle controlling editableBanner.bannerHeight (200-900px).
Replaces the fixed 42.1% padding-bottom with explicit height.px when set.
- Photo URL uses imageSource(..., true) to skip /admin/ prefixing.
- New Edit Saved Image button (with disabled "Not Saved Yet" tooltip when no
fileNameData) that opens app-image-cropper-and-resizer preloaded with the
saved banner PNG. On save, fileNameData.awsFileName is updated and the
banner is re-saved.
- Banner select uses [compareWith] so it stays bound to the canonical record
after a save swaps the response banner back into the list by id.
- URL sync via StoredValue.BANNER ("banner") query param using kebab-case slug;
reads on init, writes on bannerSelected via router.navigate({...}, merge).
- Page title shows "Banners
- {ordinal} of {total}".
- Saved image summary ("Saved image: W x H px, NN kb.") on its own row
beneath the action buttons, populated via a hidden img + fetch(blob.size)
whether or not the saved preview is toggled. Resets on banner switch
and on save.
- Save flow now sets notify busy=true so the alert shows a spinning faSpinner
during the upload, replacing the static success icon until notify.hide().
- Cache-busting via savedImageVersion timestamp, and the saved view auto-
toggles after a save.
- normaliseBanner strips any rootFolder/ prefix doubled into awsFileName so
legacy persisted state still resolves; savedImagedSaved strips the prefix
on the way back from the cropper to prevent the doubling at source. The
prefix stripper is implemented as a small recursive helper to keep
no-restricted-syntax (no imperative loops) happy.
- HostListener onResize signature trimmed to drop the unused $event param.
Image cropper:
- Default cropper position no longer forced from natural-pixel coordinates;
when no saved cropperPosition is supplied the cropper component handles its
own full-bounds default in display pixels (avoids the giant rectangle when
natural >> display).
- 12px padding on the cropper host via ::ng-deep so the corner/edge resize
handles render fully inside the host's overflow:hidden region instead of
being clipped at the right and bottom edges.