Refine invoice template editor interactions
This commit is contained in:
@@ -2,6 +2,16 @@ import { AUTH_TOKEN_STORAGE_KEY } from "./storage";
|
||||
|
||||
const API_ROOT = import.meta.env.VITE_API_URL ?? (import.meta.env.DEV ? "http://localhost:8090/api" : "/api");
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
type ApiErrorPayload = {
|
||||
message?: string;
|
||||
error?: string;
|
||||
@@ -44,7 +54,7 @@ function authHeaders(): Record<string, string> {
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response));
|
||||
throw new ApiError(await readErrorMessage(response), response.status);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
} from "react";
|
||||
import { apiGet, apiPut } from "../lib/api";
|
||||
import { ApiError, apiGet, apiPut } from "../lib/api";
|
||||
import { useSession } from "../lib/session";
|
||||
import type { UserOption } from "../lib/types";
|
||||
|
||||
const CANVAS_WIDTH = 794;
|
||||
const CANVAS_HEIGHT = 1123;
|
||||
const CANVAS_ASPECT_RATIO = CANVAS_WIDTH / CANVAS_HEIGHT;
|
||||
const GRID_SIZE = 5;
|
||||
const DRAG_DATA_TYPE = "application/x-muh-invoice-template";
|
||||
const PDF_PAGE_WIDTH = 595.28;
|
||||
@@ -25,7 +26,7 @@ const CANVAS_BOTTOM_GAP = 10;
|
||||
const MIN_ELEMENT_WIDTH = 100;
|
||||
const MIN_MEDIA_WIDTH = 40;
|
||||
const MIN_MEDIA_HEIGHT = 40;
|
||||
const MIN_LINE_THICKNESS = 2;
|
||||
const MIN_LINE_THICKNESS = 1;
|
||||
const MIN_LINE_LENGTH = 40;
|
||||
const MIN_FONT_SIZE = 12;
|
||||
const MAX_FONT_SIZE = 44;
|
||||
@@ -54,9 +55,11 @@ type ElementKind = "text" | "line" | "image";
|
||||
type LineOrientation = "horizontal" | "vertical";
|
||||
type TextAlign = "left" | "center" | "right";
|
||||
type FontWeight = 400 | 500 | 600 | 700;
|
||||
type PaletteCategory = "master-data" | "customer-data" | "free-elements";
|
||||
|
||||
interface PaletteItem {
|
||||
id: string;
|
||||
category: PaletteCategory;
|
||||
label: string;
|
||||
description: string;
|
||||
kind?: ElementKind;
|
||||
@@ -158,6 +161,7 @@ const CP1252_MAP: Record<number, number> = {
|
||||
const PALETTE_ITEMS: PaletteItem[] = [
|
||||
{
|
||||
id: "invoice-title",
|
||||
category: "master-data",
|
||||
label: "Rechnungstitel",
|
||||
description: "Markante Ueberschrift fuer die Rechnung",
|
||||
width: 240,
|
||||
@@ -168,6 +172,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "company-name",
|
||||
category: "master-data",
|
||||
label: "Firmenname",
|
||||
description: "Name Ihres Unternehmens",
|
||||
width: 280,
|
||||
@@ -178,6 +183,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "contact-person",
|
||||
category: "master-data",
|
||||
label: "Ansprechpartner",
|
||||
description: "Name des Ansprechpartners",
|
||||
width: 240,
|
||||
@@ -188,6 +194,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "street",
|
||||
category: "master-data",
|
||||
label: "Strasse",
|
||||
description: "Strassenname fuer die Anschrift",
|
||||
width: 220,
|
||||
@@ -198,6 +205,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "house-number",
|
||||
category: "master-data",
|
||||
label: "Hausnummer",
|
||||
description: "Hausnummer der Anschrift",
|
||||
width: 100,
|
||||
@@ -208,6 +216,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "postal-code",
|
||||
category: "master-data",
|
||||
label: "PLZ",
|
||||
description: "Postleitzahl",
|
||||
width: 100,
|
||||
@@ -218,6 +227,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
category: "master-data",
|
||||
label: "Ort",
|
||||
description: "Stadt oder Ort",
|
||||
width: 180,
|
||||
@@ -228,6 +238,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
category: "master-data",
|
||||
label: "E-Mail",
|
||||
description: "Kontakt-E-Mail",
|
||||
width: 260,
|
||||
@@ -238,6 +249,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
category: "master-data",
|
||||
label: "Telefon",
|
||||
description: "Telefonnummer fuer Rueckfragen",
|
||||
width: 220,
|
||||
@@ -248,6 +260,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "invoice-number",
|
||||
category: "master-data",
|
||||
label: "Rechnungsnummer",
|
||||
description: "Eindeutige Kennung",
|
||||
width: 200,
|
||||
@@ -258,6 +271,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "invoice-date",
|
||||
category: "master-data",
|
||||
label: "Rechnungsdatum",
|
||||
description: "Ausstellungsdatum der Rechnung",
|
||||
width: 200,
|
||||
@@ -271,6 +285,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "due-date",
|
||||
category: "master-data",
|
||||
label: "Zahlungsziel",
|
||||
description: "Faelligkeitsdatum",
|
||||
width: 220,
|
||||
@@ -285,6 +300,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "customer-number",
|
||||
category: "master-data",
|
||||
label: "Kundennummer",
|
||||
description: "Referenz zum Kunden",
|
||||
width: 180,
|
||||
@@ -295,6 +311,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "line-item",
|
||||
category: "master-data",
|
||||
label: "Leistungsposition",
|
||||
description: "Beschreibung einer Position",
|
||||
width: 340,
|
||||
@@ -305,6 +322,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "amount",
|
||||
category: "master-data",
|
||||
label: "Betrag",
|
||||
description: "Gesamt- oder Positionsbetrag",
|
||||
width: 180,
|
||||
@@ -315,6 +333,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "iban",
|
||||
category: "master-data",
|
||||
label: "IBAN",
|
||||
description: "Bankverbindung fuer die Zahlung",
|
||||
width: 320,
|
||||
@@ -325,6 +344,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "bic",
|
||||
category: "master-data",
|
||||
label: "BIC",
|
||||
description: "BIC der Bank",
|
||||
width: 180,
|
||||
@@ -335,6 +355,7 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
{
|
||||
id: "footer",
|
||||
category: "master-data",
|
||||
label: "Fusszeile",
|
||||
description: "Hinweis oder Dankeszeile",
|
||||
width: 360,
|
||||
@@ -344,9 +365,76 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
defaultContent: () => "Vielen Dank fuer Ihren Auftrag.",
|
||||
},
|
||||
{
|
||||
id: "horizontal-line",
|
||||
label: "Horizontale Linie",
|
||||
description: "Trennlinie ueber die Seitenbreite",
|
||||
id: "recipient-company",
|
||||
category: "customer-data",
|
||||
label: "Empfaengerfirma",
|
||||
description: "Firma des Rechnungsempfaengers",
|
||||
width: 280,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
textAlign: "left",
|
||||
defaultContent: () => "Firma Rechnungsempfaenger",
|
||||
},
|
||||
{
|
||||
id: "recipient-name",
|
||||
category: "customer-data",
|
||||
label: "Empfaengername",
|
||||
description: "Name des Rechnungsempfaengers",
|
||||
width: 240,
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
textAlign: "left",
|
||||
defaultContent: () => "Name Rechnungsempfaenger",
|
||||
},
|
||||
{
|
||||
id: "recipient-street",
|
||||
category: "customer-data",
|
||||
label: "Empfaengerstrasse",
|
||||
description: "Strasse des Rechnungsempfaengers",
|
||||
width: 220,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
defaultContent: () => "Strasse Rechnungsempfaenger",
|
||||
},
|
||||
{
|
||||
id: "recipient-house-number",
|
||||
category: "customer-data",
|
||||
label: "Empfaenger Hausnummer",
|
||||
description: "Hausnummer des Rechnungsempfaengers",
|
||||
width: 120,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
defaultContent: () => "Hausnummer",
|
||||
},
|
||||
{
|
||||
id: "recipient-postal-code",
|
||||
category: "customer-data",
|
||||
label: "Empfaenger PLZ",
|
||||
description: "Postleitzahl des Rechnungsempfaengers",
|
||||
width: 100,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
defaultContent: () => "PLZ",
|
||||
},
|
||||
{
|
||||
id: "recipient-city",
|
||||
category: "customer-data",
|
||||
label: "Empfaenger Ort",
|
||||
description: "Ort des Rechnungsempfaengers",
|
||||
width: 180,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
defaultContent: () => "Ort Rechnungsempfaenger",
|
||||
},
|
||||
{
|
||||
id: "line",
|
||||
category: "free-elements",
|
||||
label: "Linie",
|
||||
description: "Linie mit umschaltbarer Ausrichtung",
|
||||
kind: "line",
|
||||
width: 260,
|
||||
height: 3,
|
||||
@@ -356,21 +444,9 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
lineOrientation: "horizontal",
|
||||
defaultContent: () => "",
|
||||
},
|
||||
{
|
||||
id: "vertical-line",
|
||||
label: "Vertikale Linie",
|
||||
description: "Senkrechte Trennlinie fuer Bereiche",
|
||||
kind: "line",
|
||||
width: 3,
|
||||
height: 180,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
lineOrientation: "vertical",
|
||||
defaultContent: () => "",
|
||||
},
|
||||
{
|
||||
id: "image",
|
||||
category: "free-elements",
|
||||
label: "Bild",
|
||||
description: "Logo oder Produktbild mit Upload",
|
||||
kind: "image",
|
||||
@@ -383,6 +459,12 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const PALETTE_GROUPS: Array<{ category: PaletteCategory; title: string }> = [
|
||||
{ category: "master-data", title: "Stammdaten" },
|
||||
{ category: "customer-data", title: "Kundendaten" },
|
||||
{ category: "free-elements", title: "Freie Elemente" },
|
||||
];
|
||||
|
||||
function clamp(value: number, minimum: number, maximum: number) {
|
||||
return Math.min(Math.max(value, minimum), maximum);
|
||||
}
|
||||
@@ -850,7 +932,7 @@ function getPalettePreviewText(paletteItem: PaletteItem, user: UserOption | null
|
||||
return "Bild auswaehlen";
|
||||
}
|
||||
if (paletteItem.kind === "line") {
|
||||
return paletteItem.lineOrientation === "vertical" ? "Vertikale Linie" : "Horizontale Linie";
|
||||
return "Linie";
|
||||
}
|
||||
return paletteItem.defaultContent(user);
|
||||
}
|
||||
@@ -1068,6 +1150,7 @@ export default function InvoiceTemplatePage() {
|
||||
const [templateUpdatedAt, setTemplateUpdatedAt] = useState<string | null>(null);
|
||||
const [isTemplateLoading, setIsTemplateLoading] = useState(false);
|
||||
const [isTemplateSaving, setIsTemplateSaving] = useState(false);
|
||||
const [isTemplateApiAvailable, setIsTemplateApiAvailable] = useState(true);
|
||||
const [pdfPreviewUrl, setPdfPreviewUrl] = useState<string | null>(null);
|
||||
const [pageViewportHeight, setPageViewportHeight] = useState<number | null>(null);
|
||||
const [canvasViewport, setCanvasViewport] = useState<CanvasViewport>({
|
||||
@@ -1100,6 +1183,7 @@ export default function InvoiceTemplatePage() {
|
||||
setElements(starterLayout);
|
||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||
setResizingElementId(null);
|
||||
setIsTemplateApiAvailable(true);
|
||||
setIsTemplateLoading(false);
|
||||
setTemplateUpdatedAt(null);
|
||||
setTemplateError(null);
|
||||
@@ -1116,6 +1200,7 @@ export default function InvoiceTemplatePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTemplateApiAvailable(true);
|
||||
setTemplateUpdatedAt(response.updatedAt);
|
||||
if (!response.stored) {
|
||||
const starterLayout = createStarterLayout(user);
|
||||
@@ -1136,6 +1221,16 @@ export default function InvoiceTemplatePage() {
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
if (error instanceof ApiError && error.status === 404) {
|
||||
const starterLayout = createStarterLayout(user);
|
||||
setElements(starterLayout);
|
||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||
setResizingElementId(null);
|
||||
setTemplateUpdatedAt(null);
|
||||
setTemplateError(null);
|
||||
setIsTemplateApiAvailable(false);
|
||||
return;
|
||||
}
|
||||
setTemplateError((error as Error).message);
|
||||
}
|
||||
})
|
||||
@@ -1225,10 +1320,13 @@ export default function InvoiceTemplatePage() {
|
||||
Math.floor(cardRect.bottom - shellRect.top - cardPaddingBottom - verticalPadding - CANVAS_BOTTOM_GAP),
|
||||
240,
|
||||
);
|
||||
const rawScale = Math.min(1, availableWidth / CANVAS_WIDTH, availableHeight / CANVAS_HEIGHT);
|
||||
const nextScale = Math.floor(rawScale * 10000) / 10000;
|
||||
const nextWidth = Math.floor(CANVAS_WIDTH * nextScale);
|
||||
const nextHeight = Math.ceil(CANVAS_HEIGHT * nextScale);
|
||||
const nextWidth = Math.min(
|
||||
CANVAS_WIDTH,
|
||||
availableWidth,
|
||||
Math.floor(availableHeight * CANVAS_ASPECT_RATIO),
|
||||
);
|
||||
const nextHeight = Math.floor(nextWidth / CANVAS_ASPECT_RATIO);
|
||||
const nextScale = nextWidth / CANVAS_WIDTH;
|
||||
|
||||
setCanvasViewport((current) => {
|
||||
if (
|
||||
@@ -1250,12 +1348,32 @@ export default function InvoiceTemplatePage() {
|
||||
});
|
||||
}
|
||||
|
||||
const resizeObserver =
|
||||
typeof ResizeObserver === "undefined"
|
||||
? null
|
||||
: new ResizeObserver(() => {
|
||||
updateCanvasViewport();
|
||||
});
|
||||
|
||||
if (resizeObserver) {
|
||||
if (pageRef.current) {
|
||||
resizeObserver.observe(pageRef.current);
|
||||
}
|
||||
if (cardRef.current) {
|
||||
resizeObserver.observe(cardRef.current);
|
||||
}
|
||||
if (canvasShellRef.current) {
|
||||
resizeObserver.observe(canvasShellRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
updateCanvasViewport();
|
||||
window.addEventListener("resize", updateCanvasViewport);
|
||||
window.addEventListener("scroll", updateCanvasViewport, { passive: true });
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameId);
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener("resize", updateCanvasViewport);
|
||||
window.removeEventListener("scroll", updateCanvasViewport);
|
||||
};
|
||||
@@ -1370,6 +1488,10 @@ export default function InvoiceTemplatePage() {
|
||||
}
|
||||
|
||||
function handleClearCanvas() {
|
||||
if (!window.confirm("Moechten Sie wirklich alle Elemente vom Canvas entfernen?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
mouseDragCleanupRef.current?.();
|
||||
setElements([]);
|
||||
setSelectedElementId(null);
|
||||
@@ -1392,6 +1514,10 @@ export default function InvoiceTemplatePage() {
|
||||
setTemplateError("Vorlagen koennen nur fuer angemeldete Benutzer gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
if (!isTemplateApiAvailable) {
|
||||
setTemplateError("Template-Speicherung ist auf diesem Server nicht verfuegbar.");
|
||||
return;
|
||||
}
|
||||
|
||||
setTemplateError(null);
|
||||
setIsTemplateSaving(true);
|
||||
@@ -1411,8 +1537,14 @@ export default function InvoiceTemplatePage() {
|
||||
);
|
||||
setResizingElementId(null);
|
||||
setTemplateUpdatedAt(response.updatedAt);
|
||||
setIsTemplateApiAvailable(true);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 404) {
|
||||
setIsTemplateApiAvailable(false);
|
||||
setTemplateError("Template-Speicherung ist auf diesem Server nicht verfuegbar.");
|
||||
} else {
|
||||
setTemplateError((error as Error).message);
|
||||
}
|
||||
} finally {
|
||||
setIsTemplateSaving(false);
|
||||
}
|
||||
@@ -1517,16 +1649,59 @@ export default function InvoiceTemplatePage() {
|
||||
const startWidth = currentElement.width;
|
||||
const startHeight = getElementHeight(currentElement);
|
||||
const elementKind = currentElement.kind;
|
||||
const elementFontSize = currentElement.fontSize;
|
||||
const elementLineOrientation = currentElement.lineOrientation;
|
||||
const imageNaturalWidth = currentElement.imageNaturalWidth;
|
||||
const imageNaturalHeight = currentElement.imageNaturalHeight;
|
||||
const startClientX = event.clientX;
|
||||
const startClientY = event.clientY;
|
||||
|
||||
function handleMouseMove(moveEvent: MouseEvent) {
|
||||
const deltaX = (moveEvent.clientX - startClientX) / canvasViewport.scale;
|
||||
const deltaY = (moveEvent.clientY - startClientY) / canvasViewport.scale;
|
||||
const nextWidth = Math.max(
|
||||
getMinimumWidthForElement(elementKind, elementLineOrientation),
|
||||
snapToGrid(startWidth + deltaX),
|
||||
);
|
||||
const nextHeight = Math.max(
|
||||
getMinimumHeightForElement(
|
||||
elementKind,
|
||||
elementFontSize,
|
||||
elementLineOrientation,
|
||||
),
|
||||
snapToGrid(startHeight + deltaY),
|
||||
);
|
||||
|
||||
if (elementKind === "image") {
|
||||
const assetWidth = imageNaturalWidth ?? startWidth;
|
||||
const assetHeight = imageNaturalHeight ?? startHeight;
|
||||
const widthScale = nextWidth / assetWidth;
|
||||
const heightScale = nextHeight / assetHeight;
|
||||
|
||||
if (widthScale <= heightScale) {
|
||||
updateElement(elementId, {
|
||||
height: Math.max(
|
||||
getMinimumHeightForElement(elementKind, elementFontSize),
|
||||
Math.floor(((nextWidth * assetHeight) / assetWidth) / GRID_SIZE) * GRID_SIZE,
|
||||
),
|
||||
width: nextWidth,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateElement(elementId, {
|
||||
width: startWidth + deltaX,
|
||||
...(elementKind === "text" ? {} : { height: startHeight + deltaY }),
|
||||
height: nextHeight,
|
||||
width: Math.max(
|
||||
getMinimumWidthForElement(elementKind),
|
||||
Math.floor(((nextHeight * assetWidth) / assetHeight) / GRID_SIZE) * GRID_SIZE,
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateElement(elementId, {
|
||||
width: nextWidth,
|
||||
...(elementKind === "text" ? {} : { height: nextHeight }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1608,7 +1783,7 @@ export default function InvoiceTemplatePage() {
|
||||
onClick={() => {
|
||||
void handleSaveTemplate();
|
||||
}}
|
||||
disabled={isTemplateLoading || isTemplateSaving}
|
||||
disabled={isTemplateLoading || isTemplateSaving || !isTemplateApiAvailable}
|
||||
>
|
||||
{isTemplateSaving ? "Speichert..." : "Template speichern"}
|
||||
</button>
|
||||
@@ -1633,7 +1808,14 @@ export default function InvoiceTemplatePage() {
|
||||
</div>
|
||||
|
||||
<div className="invoice-template__palette">
|
||||
{PALETTE_ITEMS.map((item) => (
|
||||
{PALETTE_GROUPS.map((group) => (
|
||||
<section key={group.category} className="invoice-template__palette-group">
|
||||
<div className="invoice-template__palette-group-header">
|
||||
<h5>{group.title}</h5>
|
||||
</div>
|
||||
|
||||
<div className="invoice-template__palette-grid">
|
||||
{PALETTE_ITEMS.filter((item) => item.category === group.category).map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
@@ -1643,7 +1825,9 @@ export default function InvoiceTemplatePage() {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData(
|
||||
DRAG_DATA_TYPE,
|
||||
JSON.stringify({ kind: "palette", paletteId: item.id } satisfies DragPayload),
|
||||
JSON.stringify(
|
||||
{ kind: "palette", paletteId: item.id } satisfies DragPayload,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onDragEnd={resetCanvasDragState}
|
||||
@@ -1652,6 +1836,9 @@ export default function InvoiceTemplatePage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="invoice-template__canvas-column">
|
||||
@@ -1661,6 +1848,8 @@ export default function InvoiceTemplatePage() {
|
||||
<span className="invoice-template__panel-note">
|
||||
{isTemplateLoading
|
||||
? "Gespeicherte Vorlage wird geladen..."
|
||||
: !isTemplateApiAvailable
|
||||
? "Template-API auf diesem Server nicht verfuegbar"
|
||||
: templateTimestampLabel
|
||||
? `In Datenbank gespeichert: ${templateTimestampLabel}`
|
||||
: "Noch keine gespeicherte Vorlage vorhanden"}
|
||||
|
||||
@@ -810,8 +810,7 @@ a {
|
||||
|
||||
.invoice-template__palette {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
@@ -819,6 +818,25 @@ a {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.invoice-template__palette-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.invoice-template__palette-group-header h5 {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.invoice-template__palette-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.invoice-template__tile {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -880,7 +898,7 @@ a {
|
||||
min-width: 794px;
|
||||
height: 1123px;
|
||||
border: 1px dashed rgba(17, 109, 99, 0.22);
|
||||
border-radius: 32px;
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(17, 109, 99, 0.03), transparent 18%),
|
||||
linear-gradient(transparent 31px, rgba(37, 49, 58, 0.05) 32px),
|
||||
@@ -902,7 +920,7 @@ a {
|
||||
position: absolute;
|
||||
inset: 28px;
|
||||
border: 1px solid rgba(37, 49, 58, 0.05);
|
||||
border-radius: 20px;
|
||||
border-radius: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -942,6 +960,10 @@ a {
|
||||
background: rgba(17, 109, 99, 0.08);
|
||||
}
|
||||
|
||||
.invoice-template__canvas-element.is-selected {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.invoice-template__canvas-element.is-dragging {
|
||||
cursor: grabbing;
|
||||
z-index: 2;
|
||||
@@ -1141,7 +1163,7 @@ a {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.invoice-template__palette {
|
||||
.invoice-template__palette-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user