Improve CMS media uploads and admin styling

This commit is contained in:
2026-09-18 00:40:20 +02:00
parent 1970be29e7
commit caa24905ec
7 changed files with 214 additions and 23 deletions
+2
View File
@@ -24,6 +24,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
import { CalendarNavLink as CalendarNavLink_4d06fac03127b339306d5f5ecb073251 } from '../../../components/admin/CalendarNavLink' import { CalendarNavLink as CalendarNavLink_4d06fac03127b339306d5f5ecb073251 } from '../../../components/admin/CalendarNavLink'
import { AnalyticsNavLink as AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962 } from '../../../components/admin/AnalyticsNavLink' import { AnalyticsNavLink as AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962 } from '../../../components/admin/AnalyticsNavLink'
import { ConversationsNavLink as ConversationsNavLink_67a2594cf28806bc76971e67212a6181 } from '../../../components/admin/ConversationsNavLink' import { ConversationsNavLink as ConversationsNavLink_67a2594cf28806bc76971e67212a6181 } from '../../../components/admin/ConversationsNavLink'
import { UploadLimitGuard as UploadLimitGuard_1a82b0e63592dbfb4e9d3c5c946debf3 } from '../../../components/admin/UploadLimitGuard'
import { AdminWelcome as AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6 } from '../../../components/admin/AdminWelcome' import { AdminWelcome as AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6 } from '../../../components/admin/AdminWelcome'
import { AdminCalendarView as AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a } from '../../../components/admin/AdminCalendarView' import { AdminCalendarView as AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a } from '../../../components/admin/AdminCalendarView'
import { AdminAnalyticsView as AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0 } from '../../../components/admin/AdminAnalyticsView' import { AdminAnalyticsView as AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0 } from '../../../components/admin/AdminAnalyticsView'
@@ -58,6 +59,7 @@ export const importMap = {
"/components/admin/CalendarNavLink#CalendarNavLink": CalendarNavLink_4d06fac03127b339306d5f5ecb073251, "/components/admin/CalendarNavLink#CalendarNavLink": CalendarNavLink_4d06fac03127b339306d5f5ecb073251,
"/components/admin/AnalyticsNavLink#AnalyticsNavLink": AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962, "/components/admin/AnalyticsNavLink#AnalyticsNavLink": AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962,
"/components/admin/ConversationsNavLink#ConversationsNavLink": ConversationsNavLink_67a2594cf28806bc76971e67212a6181, "/components/admin/ConversationsNavLink#ConversationsNavLink": ConversationsNavLink_67a2594cf28806bc76971e67212a6181,
"/components/admin/UploadLimitGuard#UploadLimitGuard": UploadLimitGuard_1a82b0e63592dbfb4e9d3c5c946debf3,
"/components/admin/AdminWelcome#AdminWelcome": AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6, "/components/admin/AdminWelcome#AdminWelcome": AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6,
"/components/admin/AdminCalendarView#AdminCalendarView": AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a, "/components/admin/AdminCalendarView#AdminCalendarView": AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a,
"/components/admin/AdminAnalyticsView#AdminAnalyticsView": AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0, "/components/admin/AdminAnalyticsView#AdminAnalyticsView": AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0,
+128 -9
View File
@@ -18,6 +18,36 @@
--font-mono: "SFMono-Regular", Consolas, monospace; --font-mono: "SFMono-Regular", Consolas, monospace;
} }
/* Keep Payload's own color tokens aligned with the light admin surface. The
previous theme could leave the content area pale while retaining white text
from dark mode, which made headings and descriptions effectively invisible. */
html,
html[data-theme="light"],
html[data-theme="dark"] {
--theme-bg: var(--anouma-cream);
--theme-input-bg: var(--anouma-paper);
--theme-text: var(--anouma-ink);
--theme-elevation-0: var(--anouma-paper);
--theme-elevation-50: #fbf7f4;
--theme-elevation-100: #f1e9e4;
--theme-elevation-150: var(--anouma-line);
--theme-elevation-200: #d7c9c1;
--theme-elevation-300: #b9aaa8;
--theme-elevation-400: #8b7b81;
--theme-elevation-500: #776870;
--theme-elevation-600: #66505f;
--theme-elevation-700: #51404b;
--theme-elevation-800: var(--anouma-ink);
--theme-elevation-900: #211a1e;
--theme-elevation-950: #171216;
--theme-elevation-1000: #100c0f;
--font-body: "Anouma Admin", Inter, ui-sans-serif, system-ui, sans-serif;
--font-mono: "SFMono-Regular", Consolas, monospace;
background: var(--anouma-cream);
color: var(--anouma-ink);
color-scheme: light;
}
html, html,
body { body {
background: var(--anouma-cream); background: var(--anouma-cream);
@@ -33,6 +63,18 @@ select {
font-family: var(--font-body); font-family: var(--font-body);
} }
body,
#app,
.template-default,
.template-default__wrap,
.collection-list,
.collection-list__wrap,
.collection-edit,
.dashboard,
.dashboard__wrap {
color: var(--anouma-ink);
}
/* The left navigation is intentionally dark and quiet. Content stays light. */ /* The left navigation is intentionally dark and quiet. Content stays light. */
.nav { .nav {
background: var(--anouma-ink); background: var(--anouma-ink);
@@ -138,13 +180,56 @@ select {
.dashboard__header h1, .dashboard__header h1,
.collection-list__header h1, .collection-list__header h1,
.edit-view__header h1 { .edit-view__header h1,
.list-header__title,
.list-header__title h1,
.document-fields__label {
color: var(--anouma-ink); color: var(--anouma-ink);
font-size: clamp(26px, 3vw, 38px); font-size: clamp(26px, 3vw, 38px);
font-weight: 600; font-weight: 600;
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
.list-header,
.list-header__content,
.collection-list__sub-header,
.list-controls,
.no-results,
.no-results h3,
.no-results p,
.collection-list__table-wrap,
.document-fields,
.document-fields__main,
.field-type,
.field-type__wrap,
.label-wrapper,
.field-label,
.field-label label,
.field-description,
.description {
color: var(--anouma-ink);
}
.list-header__title,
.list-header__title a,
.list-header__title button,
.list-header__actions,
.list-controls button,
.list-controls a {
color: var(--anouma-ink);
}
.list-header__title-actions .btn--style-secondary,
.list-header__actions .btn--style-secondary,
.list-controls .btn--style-subtle {
--color: var(--anouma-ink);
--hover-color: var(--anouma-ink);
--bg-color: var(--anouma-paper);
--hover-bg: #f1e9e4;
--btn-border: 1px solid var(--anouma-line);
--hover-btn-border: 1px solid var(--anouma-mauve);
}
.card, .card,
.dashboard__card, .dashboard__card,
.table-wrap, .table-wrap,
@@ -167,6 +252,7 @@ select {
} }
.btn--style-primary { .btn--style-primary {
--color: #fff;
--bg-color: var(--anouma-mauve); --bg-color: var(--anouma-mauve);
--hover-bg: var(--anouma-plum); --hover-bg: var(--anouma-plum);
} }
@@ -175,8 +261,10 @@ input,
textarea, textarea,
select, select,
.rs__control { .rs__control {
background: var(--anouma-paper) !important;
border-color: #d7c9c1; border-color: #d7c9c1;
border-radius: 4px; border-radius: 4px;
color: var(--anouma-ink) !important;
} }
input:focus, input:focus,
@@ -198,6 +286,45 @@ select:focus,
color: #7b6b72; color: #7b6b72;
} }
.upload,
.file-field,
.upload-field-card,
.upload__dropzoneContent,
.dropzone,
.bulk-upload--add-files,
.bulk-upload--file-manager {
color: var(--anouma-ink);
}
.upload,
.file-field,
.bulk-upload--add-files,
.bulk-upload--file-manager,
.file-field__filename,
.file-field__remote-file,
.upload-field-card {
background: var(--anouma-paper);
border-color: var(--anouma-line);
}
.upload__dragAndDropText,
.upload__orText,
.bulk-upload--add-files__dragAndDropText,
.file-field__filename,
.file-field__remote-file,
.upload-field-card__filename,
.upload-field-card__meta {
color: var(--anouma-plum);
}
.upload a,
.upload button,
.file-field button,
.bulk-upload--add-files button,
.bulk-upload--file-manager button {
color: var(--anouma-ink);
}
.table thead th, .table thead th,
.table th { .table th {
background: #f1e9e4; background: #f1e9e4;
@@ -253,14 +380,6 @@ select:focus,
color: var(--anouma-mauve); color: var(--anouma-mauve);
} }
textarea,
input,
select,
.rs__control {
background: var(--anouma-paper) !important;
color: var(--anouma-ink) !important;
}
textarea::placeholder, textarea::placeholder,
input::placeholder { input::placeholder {
color: #8b7b81; color: #8b7b81;
+4 -3
View File
@@ -14,7 +14,7 @@ export const Media: CollectionConfig = {
admin: { admin: {
group: "Inhalte", group: "Inhalte",
useAsTitle: "alt", useAsTitle: "alt",
description: "Bilder für die Website - Alt-Text ist für Barrierefreiheit und SEO Pflicht.", description: "Bilder für die Website. Alt-Text und Bildbeschreibung sind optional. Bis zu 100 Dateien pro Upload.",
}, },
access: { access: {
// Images are public so the website can display them without a login. // Images are public so the website can display them without a login.
@@ -25,6 +25,7 @@ export const Media: CollectionConfig = {
}, },
upload: { upload: {
staticDir: path.resolve(dirname, "..", "media"), staticDir: path.resolve(dirname, "..", "media"),
bulkUpload: true,
// SVG deliberately excluded: it can carry inline <script>/event-handler // SVG deliberately excluded: it can carry inline <script>/event-handler
// payloads that execute if the raw file is ever opened directly (not via // payloads that execute if the raw file is ever opened directly (not via
// <img>) - not currently needed by any part of the site. // <img>) - not currently needed by any part of the site.
@@ -42,9 +43,9 @@ export const Media: CollectionConfig = {
name: "alt", name: "alt",
type: "text", type: "text",
label: "Alt-Text", label: "Alt-Text",
required: true, required: false,
admin: { admin: {
description: "Kurze Beschreibung des Bildes für Screenreader und Suchmaschinen.", description: "Optional. Kann später für Barrierefreiheit und SEO ergänzt werden.",
}, },
}, },
{ {
+68
View File
@@ -0,0 +1,68 @@
"use client";
import { useEffect } from "react";
const MAX_FILES_PER_UPLOAD = 100;
/** Keeps Payload's native multi-file picker from accepting an unbounded batch. */
export function UploadLimitGuard() {
useEffect(() => {
const attached = new WeakSet<HTMLInputElement>();
const limitedTransfer = (files: FileList) => {
const transfer = new DataTransfer();
Array.from(files)
.slice(0, MAX_FILES_PER_UPLOAD)
.forEach((file) => transfer.items.add(file));
return transfer;
};
const attachToFileInputs = () => {
document.querySelectorAll<HTMLInputElement>('input[type="file"][multiple]').forEach((input) => {
if (attached.has(input)) return;
attached.add(input);
input.addEventListener("change", () => {
if (!input.files || input.files.length <= MAX_FILES_PER_UPLOAD) return;
try {
input.files = limitedTransfer(input.files).files;
} catch {
// Older browsers can expose a read-only FileList. Payload still
// receives the original selection there and can process it.
}
});
});
};
const limitDroppedFiles = (event: DragEvent) => {
const files = event.dataTransfer?.files;
if (!files || files.length <= MAX_FILES_PER_UPLOAD || !(event.target instanceof Element)) return;
event.preventDefault();
event.stopPropagation();
try {
event.target.dispatchEvent(
new DragEvent("drop", {
bubbles: true,
cancelable: true,
dataTransfer: limitedTransfer(files),
}),
);
} catch {
// If a browser cannot construct a replacement drag event, the native
// picker still enforces the same limit for normal file selection.
}
};
attachToFileInputs();
document.addEventListener("drop", limitDroppedFiles, true);
const observer = new MutationObserver(attachToFileInputs);
observer.observe(document.body, { childList: true, subtree: true });
return () => {
document.removeEventListener("drop", limitDroppedFiles, true);
observer.disconnect();
};
}, []);
return null;
}
+1 -1
View File
@@ -17,5 +17,5 @@ export function mediaUrl(
export function mediaAlt(value: Media | number | null | undefined): string | undefined { export function mediaAlt(value: Media | number | null | undefined): string | undefined {
if (!value || typeof value === "number") return undefined; if (!value || typeof value === "number") return undefined;
return value.alt; return value.alt ?? undefined;
} }
+10 -10
View File
@@ -199,7 +199,7 @@ export interface User {
name: string; name: string;
role: 'admin'; role: 'admin';
/** /**
* Steuert, ob dieser Zugang das Analytics-Dashboard im Admin-Bereich sehen kann. * Steuert, ob dieser Zugang die Auswertung im Admin-Bereich sehen kann.
*/ */
canViewAnalytics?: boolean | null; canViewAnalytics?: boolean | null;
calendarFeedTokenHash?: string | null; calendarFeedTokenHash?: string | null;
@@ -261,7 +261,7 @@ export interface Customer {
collection: 'customers'; collection: 'customers';
} }
/** /**
* Bilder für die Website - Alt-Text ist für Barrierefreiheit und SEO Pflicht. * Bilder für die Website. Alt-Text und Bildbeschreibung können später ergänzt werden.
* *
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media". * via the `definition` "media".
@@ -269,9 +269,9 @@ export interface Customer {
export interface Media { export interface Media {
id: number; id: number;
/** /**
* Kurze Beschreibung des Bildes für Screenreader und Suchmaschinen. * Optional. Kann später für Barrierefreiheit und SEO ergänzt werden.
*/ */
alt: string; alt?: string | null;
/** /**
* Optionale, längere Beschreibung (z. B. für eine Bildunterschrift). * Optionale, längere Beschreibung (z. B. für eine Bildunterschrift).
*/ */
@@ -652,7 +652,7 @@ export interface Conversation {
createdAt: string; createdAt: string;
} }
/** /**
* Einzelne Nachrichten innerhalb einer Anfrage. Antworten aus dem CMS werden automatisch per E-Mail verschickt. * Einzelne Nachrichten innerhalb einer Anfrage. Antworten aus der Verwaltung werden automatisch per E-Mail verschickt.
* *
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "conversation-messages". * via the `definition` "conversation-messages".
@@ -661,7 +661,7 @@ export interface ConversationMessage {
id: number; id: number;
conversation: number | Conversation; conversation: number | Conversation;
/** /**
* Für Antworten im CMS „Anouma“ auswählen. Eingehende E-Mails werden automatisch als Anfragende Person gespeichert. * Für Antworten aus der Verwaltung „Anouma“ auswählen. Eingehende E-Mails werden automatisch als anfragende Person gespeichert.
*/ */
senderType: 'visitor' | 'admin'; senderType: 'visitor' | 'admin';
senderName?: string | null; senderName?: string | null;
@@ -1624,7 +1624,7 @@ export interface SeoSetting {
*/ */
defaultDescription?: string | null; defaultDescription?: string | null;
/** /**
* Wird verwendet, wenn eine Seite kein eigenes Open-Graph-Bild hat. * Wird verwendet, wenn eine Seite kein eigenes Bild für soziale Netzwerke hat.
*/ */
defaultOgImage?: (number | null) | Media; defaultOgImage?: (number | null) | Media;
/** /**
@@ -1690,7 +1690,7 @@ export interface CookieSetting {
label: string; label: string;
description: string; description: string;
/** /**
* Globaler Schalter für das gesamte Analytics-System. Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten und Analytics-Daten werden serverseitig abgelehnt - auch bei einer noch gespeicherten alten Zustimmung. * Globaler Schalter für die gesamte Reichweitenmessung. Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten und Messdaten werden serverseitig abgelehnt - auch bei einer noch gespeicherten alten Zustimmung.
*/ */
enabled?: boolean | null; enabled?: boolean | null;
}; };
@@ -1698,7 +1698,7 @@ export interface CookieSetting {
createdAt?: string | null; createdAt?: string | null;
} }
/** /**
* Aufbewahrungsdauer für Analytics-Rohdaten und aggregierte Tagesstatistiken. * Aufbewahrungsdauer für Rohdaten und zusammengefasste Tagesstatistiken.
* *
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "analytics-settings". * via the `definition` "analytics-settings".
@@ -1706,7 +1706,7 @@ export interface CookieSetting {
export interface AnalyticsSetting { export interface AnalyticsSetting {
id: number; id: number;
/** /**
* Wie lange einzelne Analytics-Events gespeichert bleiben. Begrenzt auch den wählbaren Zeitraum im Analytics-Dashboard. * Wie lange einzelne Messwerte gespeichert bleiben. Begrenzt auch den wählbaren Zeitraum in der Auswertung.
*/ */
rawRetentionDays: number; rawRetentionDays: number;
/** /**
+1
View File
@@ -56,6 +56,7 @@ export default buildConfig({
"/components/admin/CalendarNavLink#CalendarNavLink", "/components/admin/CalendarNavLink#CalendarNavLink",
"/components/admin/AnalyticsNavLink#AnalyticsNavLink", "/components/admin/AnalyticsNavLink#AnalyticsNavLink",
"/components/admin/ConversationsNavLink#ConversationsNavLink", "/components/admin/ConversationsNavLink#ConversationsNavLink",
"/components/admin/UploadLimitGuard#UploadLimitGuard",
], ],
views: { views: {
kalender: { kalender: {