refactor(file-upload): SSR-first state, click-to-replace, cleaner preview

- Server renders initial file-upload state (src, alt, label text,
  has-current class) so there is no flash of broken-image icon while
  JS boots. JS no longer duplicates that work — it only reacts to
  user input (select, clear, drag-and-drop) and uses data-current-src
  solely to restore the server file after a blob preview.
- Clicking the current preview image now opens the file dialog, mirrors
  the Replace button, with hover affordance (cursor + primary border).
- Drop the transparency checker pattern in the preview — the admin UI
  does not need Figma-style transparency semantics. Preview is now a
  flat theme-aware surface (--app-preview-bg) that keeps white or
  black logos visible against a neutral gray.
- Move tenant favicon out of the aside into the Master-data tab as its
  own details block next to the Tenant-logos section. Uses the same
  barrier-form pattern (HTML5 form attribute + app-file-upload.phtml)
  for a consistent instant-upload UX.
- Tenant-logo slot label is a native <label> element — picks up the
  global form-label typography and spacing, no custom muted-color
  class needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 20:53:58 +02:00
parent 6e3fc63c1d
commit e814b5fd11
10 changed files with 78 additions and 85 deletions

View File

@@ -68,27 +68,16 @@ export function initFileUpload(root) {
const filenameEl = root.querySelector('.app-file-upload-filename');
const filesizeEl = root.querySelector('.app-file-upload-filesize');
const clearButton = root.querySelector('.app-file-upload-clear');
const currentImage = root.querySelector('.app-file-upload-current-image');
const currentLabel = root.querySelector('.app-file-upload-current-label');
const replaceButton = root.querySelector('.app-file-upload-replace-button');
const currentImage = root.querySelector('.app-file-upload-current-image');
const cleanupFns = [];
let dragCounter = 0;
// ── Initialize current file state from data attributes ──
// Server rendered the initial state (img src + label + has-current class).
// `data-current-src` is kept so clearFile() can restore the server file
// after a user previewed a different selection.
const currentSrc = (root.dataset.currentSrc || '').trim();
const currentLabelText = (root.dataset.currentLabel || '').trim();
if (currentSrc) {
root.classList.add('has-current');
if (currentImage) {
currentImage.src = currentSrc;
currentImage.alt = currentLabelText;
}
if (currentLabel) {
currentLabel.textContent = currentLabelText;
}
}
// ── Show pending file (new selection) ──
const showFile = (file) => {
@@ -175,15 +164,19 @@ export function initFileUpload(root) {
cleanupFns.push(() => clearButton.removeEventListener('click', onClear));
}
// ── Replace button → open file dialog ──
// ── Replace button + click on current preview → open file dialog ──
const openPicker = (e) => {
e.preventDefault();
e.stopPropagation();
input.click();
};
if (replaceButton) {
const onReplace = (e) => {
e.preventDefault();
e.stopPropagation();
input.click();
};
replaceButton.addEventListener('click', onReplace);
cleanupFns.push(() => replaceButton.removeEventListener('click', onReplace));
replaceButton.addEventListener('click', openPicker);
cleanupFns.push(() => replaceButton.removeEventListener('click', openPicker));
}
if (currentImage) {
currentImage.addEventListener('click', openPicker);
cleanupFns.push(() => currentImage.removeEventListener('click', openPicker));
}
// ── Drag and drop ──