qr code layouts im mobile admin perfektioniert.
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -60,6 +60,7 @@
|
|||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-i18next": "^16.4.1",
|
"react-i18next": "^16.4.1",
|
||||||
@@ -16276,6 +16277,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-colorful": {
|
||||||
|
"version": "5.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||||
|
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-devtools-core": {
|
"node_modules/react-devtools-core": {
|
||||||
"version": "6.1.5",
|
"version": "6.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz",
|
||||||
|
|||||||
@@ -95,7 +95,8 @@
|
|||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"laravel-vite-plugin": "^2.0.1",
|
"laravel-vite-plugin": "^2.0.1",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
|||||||
@@ -1847,6 +1847,7 @@
|
|||||||
"title": "QR-Code & Druck-Layouts",
|
"title": "QR-Code & Druck-Layouts",
|
||||||
"heroTitle": "Einlass-QR-Code",
|
"heroTitle": "Einlass-QR-Code",
|
||||||
"description": "Scannen, um zur Gäste-App zu gelangen.",
|
"description": "Scannen, um zur Gäste-App zu gelangen.",
|
||||||
|
"bottomNote": "Unterer Hinweistext",
|
||||||
"missing": "Kein QR-Link vorhanden",
|
"missing": "Kein QR-Link vorhanden",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadStarted": "Download gestartet",
|
"downloadStarted": "Download gestartet",
|
||||||
@@ -1859,6 +1860,13 @@
|
|||||||
"createLink": "Neuen QR-Link erstellen",
|
"createLink": "Neuen QR-Link erstellen",
|
||||||
"created": "Neuer QR-Link erstellt",
|
"created": "Neuer QR-Link erstellt",
|
||||||
"createFailed": "Link konnte nicht erstellt werden.",
|
"createFailed": "Link konnte nicht erstellt werden.",
|
||||||
|
"headline": "Headline",
|
||||||
|
"subtitle": "Untertitel",
|
||||||
|
"align": "Ausrichtung",
|
||||||
|
"lineHeight": "Zeilenhöhe",
|
||||||
|
"fontFamily": "Schriftfamilie",
|
||||||
|
"exportPdf": "PDF exportieren",
|
||||||
|
"exportPng": "PNG exportieren",
|
||||||
"format": {
|
"format": {
|
||||||
"poster": "A4 Poster",
|
"poster": "A4 Poster",
|
||||||
"posterSubtitle": "Hochformat für Aushänge",
|
"posterSubtitle": "Hochformat für Aushänge",
|
||||||
|
|||||||
@@ -1868,6 +1868,7 @@
|
|||||||
"title": "QR Code & Print Layouts",
|
"title": "QR Code & Print Layouts",
|
||||||
"heroTitle": "Entrance QR Code",
|
"heroTitle": "Entrance QR Code",
|
||||||
"description": "Scan to access the event guest app.",
|
"description": "Scan to access the event guest app.",
|
||||||
|
"bottomNote": "Bottom note text",
|
||||||
"missing": "No QR link available",
|
"missing": "No QR link available",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadStarted": "Download started",
|
"downloadStarted": "Download started",
|
||||||
@@ -1880,6 +1881,13 @@
|
|||||||
"createLink": "Create new QR link",
|
"createLink": "Create new QR link",
|
||||||
"created": "New QR link created",
|
"created": "New QR link created",
|
||||||
"createFailed": "Could not create link.",
|
"createFailed": "Could not create link.",
|
||||||
|
"headline": "Headline",
|
||||||
|
"subtitle": "Subtitle",
|
||||||
|
"align": "Align",
|
||||||
|
"lineHeight": "Line height",
|
||||||
|
"fontFamily": "Font family",
|
||||||
|
"exportPdf": "Export PDF",
|
||||||
|
"exportPng": "Export PNG",
|
||||||
"format": {
|
"format": {
|
||||||
"poster": "A4 Poster",
|
"poster": "A4 Poster",
|
||||||
"posterSubtitle": "Portrait for posters",
|
"posterSubtitle": "Portrait for posters",
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ArrowLeft, RefreshCcw, Plus } from 'lucide-react';
|
import { ArrowLeft, RefreshCcw, Plus, ChevronDown } from 'lucide-react';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
import { Input, TextArea } from 'tamagui';
|
import { Input, TextArea } from 'tamagui';
|
||||||
|
import { Accordion } from '@tamagui/accordion';
|
||||||
|
import { HexColorPicker } from 'react-colorful';
|
||||||
|
import { Portal } from '@tamagui/portal';
|
||||||
import { MobileShell } from './components/MobileShell';
|
import { MobileShell } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
||||||
import {
|
import {
|
||||||
@@ -394,11 +397,11 @@ function renderEventName(name: TenantEvent['name'] | null | undefined): string |
|
|||||||
|
|
||||||
function getDefaultSlots(): Record<string, SlotDefinition> {
|
function getDefaultSlots(): Record<string, SlotDefinition> {
|
||||||
return {
|
return {
|
||||||
headline: { x: 0.08, y: 0.15, w: 0.84, fontSize: 50, fontWeight: 800, fontFamily: 'Playfair Display', align: 'center' },
|
headline: { x: 0.08, y: 0.12, w: 0.84, fontSize: 90, fontWeight: 800, fontFamily: 'Playfair Display', align: 'center' },
|
||||||
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: 'Montserrat', align: 'center' },
|
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: 'Montserrat', align: 'center' },
|
||||||
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 13, fontFamily: 'Lora', lineHeight: 1.4, align: 'center' },
|
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 26, fontFamily: 'Lora', lineHeight: 1.4, align: 'center' },
|
||||||
instructions: { x: 0.1, y: 0.34, w: 0.8, fontSize: 14, fontFamily: 'Lora', lineHeight: 1.35 },
|
instructions: { x: 0.1, y: 0.7, w: 0.8, fontSize: 24, fontFamily: 'Lora', lineHeight: 1.5 },
|
||||||
qr: { x: 0.3, y: 0.3, w: 0.28 },
|
qr: { x: 0.39, y: 0.37, w: 0.27 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,26 +524,28 @@ function buildFabricOptions({
|
|||||||
const widthPx = localWidth * scaleX;
|
const widthPx = localWidth * scaleX;
|
||||||
const heightPx = localHeight * scaleY;
|
const heightPx = localHeight * scaleY;
|
||||||
|
|
||||||
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localWidth / 2;
|
// Default: top-left positioning for non-rotated panels
|
||||||
const baseCenterY = slot.y * panelHeight + localHeight / 2;
|
let targetX = opts.panelOffset + slot.x * panelWidth;
|
||||||
let targetCenterX = baseCenterX;
|
let targetY = slot.y * panelHeight;
|
||||||
let targetCenterY = baseCenterY;
|
|
||||||
let rotation = 0;
|
let rotation = 0;
|
||||||
|
|
||||||
|
// Foldable layouts rotate each panel around its own center; Fabric expects center-based coords when rotated.
|
||||||
if (isFoldable) {
|
if (isFoldable) {
|
||||||
|
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localWidth / 2;
|
||||||
|
const baseCenterY = slot.y * panelHeight + localHeight / 2;
|
||||||
const panelCenterX = opts.panelOffset + panelWidth / 2;
|
const panelCenterX = opts.panelOffset + panelWidth / 2;
|
||||||
const panelCenterY = panelHeight / 2;
|
const panelCenterY = panelHeight / 2;
|
||||||
rotation = opts.panelOffset === 0 ? 90 : -90;
|
rotation = opts.panelOffset === 0 ? 90 : -90;
|
||||||
const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
|
const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
|
||||||
targetCenterX = rotated.x;
|
targetX = rotated.x;
|
||||||
targetCenterY = rotated.y;
|
targetY = rotated.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push({
|
elements.push({
|
||||||
id: `${type}-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
|
id: `${type}-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
|
||||||
type,
|
type,
|
||||||
x: targetCenterX * scaleX,
|
x: targetX * scaleX,
|
||||||
y: targetCenterY * scaleY,
|
y: targetY * scaleY,
|
||||||
width: widthPx,
|
width: widthPx,
|
||||||
height: heightPx,
|
height: heightPx,
|
||||||
fontSize: slot.fontSize ?? (type === 'headline' ? 30 : type === 'subtitle' ? 18 : 16),
|
fontSize: slot.fontSize ?? (type === 'headline' ? 30 : type === 'subtitle' ? 18 : 16),
|
||||||
@@ -557,26 +562,26 @@ function buildFabricOptions({
|
|||||||
if (!qrUrl) return;
|
if (!qrUrl) return;
|
||||||
const localSize = slot.w * panelWidth;
|
const localSize = slot.w * panelWidth;
|
||||||
const sizePx = localSize * scaleX;
|
const sizePx = localSize * scaleX;
|
||||||
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localSize / 2;
|
let targetX = opts.panelOffset + slot.x * panelWidth;
|
||||||
const baseCenterY = slot.y * panelHeight + localSize / 2;
|
let targetY = slot.y * panelHeight;
|
||||||
let targetCenterX = baseCenterX;
|
|
||||||
let targetCenterY = baseCenterY;
|
|
||||||
let rotation = 0;
|
let rotation = 0;
|
||||||
|
|
||||||
if (isFoldable) {
|
if (isFoldable) {
|
||||||
|
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localSize / 2;
|
||||||
|
const baseCenterY = slot.y * panelHeight + localSize / 2;
|
||||||
const panelCenterX = opts.panelOffset + panelWidth / 2;
|
const panelCenterX = opts.panelOffset + panelWidth / 2;
|
||||||
const panelCenterY = panelHeight / 2;
|
const panelCenterY = panelHeight / 2;
|
||||||
rotation = opts.panelOffset === 0 ? 90 : -90;
|
rotation = opts.panelOffset === 0 ? 90 : -90;
|
||||||
const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
|
const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
|
||||||
targetCenterX = rotated.x;
|
targetX = rotated.x;
|
||||||
targetCenterY = rotated.y;
|
targetY = rotated.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push({
|
elements.push({
|
||||||
id: `qr-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
|
id: `qr-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
|
||||||
type: 'qr',
|
type: 'qr',
|
||||||
x: targetCenterX * scaleX,
|
x: targetX * scaleX,
|
||||||
y: targetCenterY * scaleY,
|
y: targetY * scaleY,
|
||||||
width: sizePx,
|
width: sizePx,
|
||||||
height: sizePx,
|
height: sizePx,
|
||||||
rotation,
|
rotation,
|
||||||
@@ -858,7 +863,7 @@ function TextStep({
|
|||||||
size="$4"
|
size="$4"
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder={t('events.qr.description', 'Beschreibung')}
|
placeholder={t('events.qr.bottomNote', 'Unterer Hinweistext')}
|
||||||
value={textFields.description}
|
value={textFields.description}
|
||||||
onChangeText={(val) => updateField('description', val)}
|
onChangeText={(val) => updateField('description', val)}
|
||||||
size="$4"
|
size="$4"
|
||||||
@@ -1082,7 +1087,7 @@ function PreviewStep({
|
|||||||
|
|
||||||
<LayoutControls slots={resolvedSlots} slotOverrides={slotOverrides} onUpdateSlot={onUpdateSlot} tenantFonts={tenantFonts} qrUrl={qrImageSrc} />
|
<LayoutControls slots={resolvedSlots} slotOverrides={slotOverrides} onUpdateSlot={onUpdateSlot} tenantFonts={tenantFonts} qrUrl={qrImageSrc} />
|
||||||
|
|
||||||
<XStack space="$2">
|
<XStack space="$2" width="100%" flexWrap="wrap">
|
||||||
<CTAButton
|
<CTAButton
|
||||||
label={t('events.qr.exportPdf', 'Export PDF')}
|
label={t('events.qr.exportPdf', 'Export PDF')}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
@@ -1096,6 +1101,7 @@ function PreviewStep({
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
/>
|
/>
|
||||||
<CTAButton
|
<CTAButton
|
||||||
label={t('events.qr.exportPng', 'Export PNG')}
|
label={t('events.qr.exportPng', 'Export PNG')}
|
||||||
@@ -1109,6 +1115,7 @@ function PreviewStep({
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
/>
|
/>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
@@ -1129,6 +1136,7 @@ function LayoutControls({
|
|||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
|
const [openColorSlot, setOpenColorSlot] = React.useState<string | null>(null);
|
||||||
const fontOptions = React.useMemo(() => {
|
const fontOptions = React.useMemo(() => {
|
||||||
const preset = ['Playfair Display', 'Lora', 'Montserrat', 'Inter', 'Roboto'];
|
const preset = ['Playfair Display', 'Lora', 'Montserrat', 'Inter', 'Roboto'];
|
||||||
const tenant = tenantFonts.map((font) => font.family);
|
const tenant = tenantFonts.map((font) => font.family);
|
||||||
@@ -1169,6 +1177,8 @@ function LayoutControls({
|
|||||||
}) => {
|
}) => {
|
||||||
const dec = () => onChange(clampValue(value - step, min, max));
|
const dec = () => onChange(clampValue(value - step, min, max));
|
||||||
const inc = () => onChange(clampValue(value + step, min, max));
|
const inc = () => onChange(clampValue(value + step, min, max));
|
||||||
|
const decimals = step % 1 === 0 ? 0 : Math.min(4, `${step}`.split('.')[1]?.length ?? 1);
|
||||||
|
const formatValue = (val: number) => val.toFixed(decimals);
|
||||||
return (
|
return (
|
||||||
<XStack space="$1" alignItems="center">
|
<XStack space="$1" alignItems="center">
|
||||||
<Pressable onPress={dec}>
|
<Pressable onPress={dec}>
|
||||||
@@ -1183,8 +1193,13 @@ function LayoutControls({
|
|||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
value={value.toFixed(step < 1 ? 1 : 0)}
|
value={formatValue(value)}
|
||||||
onChange={(event) => onChange(Number(event.target.value))}
|
onChange={(event) => {
|
||||||
|
const next = Number(event.target.value);
|
||||||
|
if (Number.isFinite(next)) {
|
||||||
|
onChange(next);
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={numberInputStyle}
|
style={numberInputStyle}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={inc}>
|
<Pressable onPress={inc}>
|
||||||
@@ -1222,140 +1237,221 @@ function LayoutControls({
|
|||||||
onUpdateSlot(slotKey, { fontFamily: value || undefined });
|
onUpdateSlot(slotKey, { fontFamily: value || undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const currentX = override.x ?? slot.x;
|
||||||
<YStack key={slotKey} space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
|
const currentY = override.y ?? slot.y;
|
||||||
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
const currentW = override.w ?? slot.w;
|
||||||
{label}
|
const centerX = () => {
|
||||||
</Text>
|
const centered = clampValue((1 - currentW) / 2, 0, 1);
|
||||||
<XStack space="$3">
|
onUpdateSlot(slotKey, { x: centered });
|
||||||
<YStack flex={1} space="$1">
|
};
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
|
||||||
X (%)
|
|
||||||
</Text>
|
|
||||||
<XStack space="$1" alignItems="center">
|
|
||||||
<Pressable onPress={() => onPercentChange('x')(((override.x ?? slot.x) * 100) - 0.5)}>
|
|
||||||
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor="#e5e7eb" backgroundColor="#fff">
|
|
||||||
<Text fontSize="$md" fontWeight="800" color="#111827">
|
|
||||||
–
|
|
||||||
</Text>
|
|
||||||
</XStack>
|
|
||||||
</Pressable>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
step="0.5"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={((override.x ?? slot.x) * 100).toFixed(1)}
|
|
||||||
onChange={(event) => onPercentChange('x')(Number(event.target.value))}
|
|
||||||
style={numberInputStyle}
|
|
||||||
/>
|
|
||||||
<Pressable onPress={() => onPercentChange('x')(((override.x ?? slot.x) * 100) + 0.5)}>
|
|
||||||
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor="#e5e7eb" backgroundColor="#fff">
|
|
||||||
<Text fontSize="$md" fontWeight="800" color="#111827">
|
|
||||||
+
|
|
||||||
</Text>
|
|
||||||
</XStack>
|
|
||||||
</Pressable>
|
|
||||||
</XStack>
|
|
||||||
</YStack>
|
|
||||||
<YStack flex={1} space="$1">
|
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
|
||||||
Y (%)
|
|
||||||
</Text>
|
|
||||||
<StepperInput
|
|
||||||
value={(override.y ?? slot.y) * 100}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={0.5}
|
|
||||||
onChange={(val) => onPercentChange('y')(val)}
|
|
||||||
/>
|
|
||||||
</YStack>
|
|
||||||
<YStack flex={1} space="$1">
|
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
|
||||||
Breite (%)
|
|
||||||
</Text>
|
|
||||||
<StepperInput
|
|
||||||
value={(override.w ?? slot.w) * 100}
|
|
||||||
min={10}
|
|
||||||
max={100}
|
|
||||||
step={0.5}
|
|
||||||
onChange={(val) => onPercentChange('w')(val)}
|
|
||||||
/>
|
|
||||||
</YStack>
|
|
||||||
</XStack>
|
|
||||||
|
|
||||||
<XStack space="$3">
|
return (
|
||||||
<YStack flex={1} space="$1">
|
<Accordion.Item value={slotKey} key={slotKey}>
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
<Accordion.Trigger padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12} backgroundColor="#f8fafc">
|
||||||
Font Size (px)
|
<XStack justifyContent="space-between" alignItems="center" flex={1}>
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
||||||
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
|
<ChevronDown size={16} color="#6b7280" />
|
||||||
</YStack>
|
</XStack>
|
||||||
<YStack flex={1} space="$1">
|
</Accordion.Trigger>
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
<Accordion.Content paddingTop="$2">
|
||||||
Font Family
|
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
|
||||||
</Text>
|
<XStack space="$3">
|
||||||
<select
|
<YStack flex={1} space="$1">
|
||||||
value={override.fontFamily ?? slot.fontFamily ?? ''}
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
onChange={(event) => onFontFamilyChange(event.target.value)}
|
X (%)
|
||||||
style={selectStyle}
|
</Text>
|
||||||
>
|
<XStack space="$2" alignItems="center">
|
||||||
<option value="">{t('events.qr.defaultFont', 'Standard')}</option>
|
<StepperInput
|
||||||
{fontOptions.map((family) => (
|
value={currentX * 100}
|
||||||
<option key={family} value={family}>
|
min={0}
|
||||||
{family}
|
max={100}
|
||||||
</option>
|
step={0.5}
|
||||||
))}
|
onChange={(val) => onPercentChange('x')(val)}
|
||||||
</select>
|
/>
|
||||||
</YStack>
|
<Pressable onPress={centerX}>
|
||||||
<YStack flex={1} space="$1">
|
<XStack
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
paddingHorizontal="$3"
|
||||||
{t('events.qr.fontColor', 'Schriftfarbe')}
|
paddingVertical="$2"
|
||||||
</Text>
|
borderRadius={10}
|
||||||
<XStack space="$2" alignItems="center">
|
borderWidth={1}
|
||||||
<input
|
borderColor="#e5e7eb"
|
||||||
type="color"
|
backgroundColor="#fff"
|
||||||
value={override.color ?? slot.color ?? '#0f172a'}
|
>
|
||||||
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
|
<Text fontSize="$xs" color="#111827">
|
||||||
style={{ width: 48, height: 36, border: '1px solid #e5e7eb', borderRadius: 8, padding: 0, background: '#fff' }}
|
{t('common.center', 'Zentrieren')}
|
||||||
/>
|
</Text>
|
||||||
<input
|
</XStack>
|
||||||
type="text"
|
</Pressable>
|
||||||
value={override.color ?? slot.color ?? ''}
|
</XStack>
|
||||||
placeholder="#0f172a"
|
</YStack>
|
||||||
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
|
<YStack flex={1} space="$1">
|
||||||
style={numberInputStyle}
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
/>
|
Y (%)
|
||||||
|
</Text>
|
||||||
|
<StepperInput
|
||||||
|
value={currentY * 100}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={0.5}
|
||||||
|
onChange={(val) => onPercentChange('y')(val)}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
<YStack flex={1} space="$1">
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
Breite (%)
|
||||||
|
</Text>
|
||||||
|
<StepperInput
|
||||||
|
value={currentW * 100}
|
||||||
|
min={10}
|
||||||
|
max={100}
|
||||||
|
step={0.5}
|
||||||
|
onChange={(val) => onPercentChange('w')(val)}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<XStack space="$3">
|
||||||
|
<YStack flex={1} space="$1">
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
Font Size (px)
|
||||||
|
</Text>
|
||||||
|
<StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
|
||||||
|
</YStack>
|
||||||
|
<YStack flex={1} space="$1">
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
Font Family
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
value={override.fontFamily ?? slot.fontFamily ?? ''}
|
||||||
|
onChange={(event) => onFontFamilyChange(event.target.value)}
|
||||||
|
style={selectStyle}
|
||||||
|
>
|
||||||
|
<option value="">{t('events.qr.defaultFont', 'Standard')}</option>
|
||||||
|
{fontOptions.map((family) => (
|
||||||
|
<option key={family} value={family}>
|
||||||
|
{family}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</YStack>
|
||||||
|
<YStack flex={1} space="$1">
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
{t('events.qr.fontColor', 'Schriftfarbe')}
|
||||||
|
</Text>
|
||||||
|
<YStack space="$2">
|
||||||
|
<XStack space="$2" alignItems="center">
|
||||||
|
<Pressable onPress={() => setOpenColorSlot(openColorSlot === slotKey ? null : slotKey)}>
|
||||||
|
<XStack
|
||||||
|
width={48}
|
||||||
|
height={36}
|
||||||
|
borderRadius={10}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor="#e5e7eb"
|
||||||
|
backgroundColor={override.color ?? slot.color ?? '#0f172a'}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={override.color ?? slot.color ?? ''}
|
||||||
|
placeholder="#0f172a"
|
||||||
|
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
|
||||||
|
style={{ ...numberInputStyle, width: 110 }}
|
||||||
|
/>
|
||||||
|
</XStack>
|
||||||
|
{openColorSlot === slotKey ? (
|
||||||
|
<Portal>
|
||||||
|
<YStack
|
||||||
|
position="fixed"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor="rgba(0,0,0,0.35)"
|
||||||
|
zIndex={9999}
|
||||||
|
onPress={() => setOpenColorSlot(null)}
|
||||||
|
>
|
||||||
|
<YStack
|
||||||
|
padding="$3"
|
||||||
|
borderRadius={16}
|
||||||
|
backgroundColor="#fff"
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor="#e5e7eb"
|
||||||
|
elevation="$4"
|
||||||
|
shadowColor="rgba(0,0,0,0.08)"
|
||||||
|
shadowOffset={{ width: 0, height: 4 }}
|
||||||
|
shadowOpacity={0.2}
|
||||||
|
shadowRadius={12}
|
||||||
|
gap="$2"
|
||||||
|
>
|
||||||
|
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
||||||
|
{t('events.qr.fontColor', 'Schriftfarbe')}
|
||||||
|
</Text>
|
||||||
|
<HexColorPicker
|
||||||
|
color={override.color ?? slot.color ?? '#0f172a'}
|
||||||
|
onChange={(val) => onUpdateSlot(slotKey, { color: val })}
|
||||||
|
style={{ width: 240, height: 200 }}
|
||||||
|
/>
|
||||||
|
<XStack space="$2" justifyContent="flex-end">
|
||||||
|
<Pressable onPress={() => setOpenColorSlot(null)}>
|
||||||
|
<XStack
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
paddingVertical="$2"
|
||||||
|
borderRadius={10}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor="#e5e7eb"
|
||||||
|
backgroundColor="#fff"
|
||||||
|
>
|
||||||
|
<Text fontSize="$xs" color="#111827">
|
||||||
|
{t('common.close', 'Schließen')}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
</Pressable>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</Portal>
|
||||||
|
) : null}
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<XStack space="$2">
|
||||||
|
<YStack flex={1} space="$1">
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
Align
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
value={override.align ?? slot.align ?? 'left'}
|
||||||
|
onChange={(event) => onUpdateSlot(slotKey, { align: event.target.value as SlotDefinition['align'] })}
|
||||||
|
style={selectStyle}
|
||||||
|
>
|
||||||
|
<option value="left">{t('common.left', 'Links')}</option>
|
||||||
|
<option value="center">{t('common.center', 'Zentriert')}</option>
|
||||||
|
<option value="right">{t('common.right', 'Rechts')}</option>
|
||||||
|
</select>
|
||||||
|
</YStack>
|
||||||
|
<YStack flex={1} space="$1">
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
Line Height
|
||||||
|
</Text>
|
||||||
|
<StepperInput
|
||||||
|
value={override.lineHeight ?? slot.lineHeight ?? 1.35}
|
||||||
|
min={0.8}
|
||||||
|
max={3}
|
||||||
|
step={0.05}
|
||||||
|
onChange={(val) => onLineHeightChange(val)}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
<YStack flex={1} />
|
||||||
|
<YStack flex={1} />
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack flex={1} space="$1">
|
</Accordion.Content>
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
</Accordion.Item>
|
||||||
Line Height
|
|
||||||
</Text>
|
|
||||||
<StepperInput value={override.lineHeight ?? slot.lineHeight ?? 1.35} min={0.8} max={3} step={0.05} onChange={(val) => onLineHeightChange(val)} />
|
|
||||||
</YStack>
|
|
||||||
</XStack>
|
|
||||||
<XStack space="$2">
|
|
||||||
<YStack flex={1} space="$1">
|
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
|
||||||
Align
|
|
||||||
</Text>
|
|
||||||
<select
|
|
||||||
value={override.align ?? slot.align ?? 'left'}
|
|
||||||
onChange={(event) => onUpdateSlot(slotKey, { align: event.target.value as SlotDefinition['align'] })}
|
|
||||||
style={selectStyle}
|
|
||||||
>
|
|
||||||
<option value="left">{t('common.left', 'Links')}</option>
|
|
||||||
<option value="center">{t('common.center', 'Zentriert')}</option>
|
|
||||||
<option value="right">{t('common.right', 'Rechts')}</option>
|
|
||||||
</select>
|
|
||||||
</YStack>
|
|
||||||
<YStack flex={1} />
|
|
||||||
<YStack flex={1} />
|
|
||||||
<YStack flex={1} />
|
|
||||||
</XStack>
|
|
||||||
</YStack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1367,74 +1463,79 @@ function LayoutControls({
|
|||||||
onUpdateSlot('qr', { [field]: pct / 100 });
|
onUpdateSlot('qr', { [field]: pct / 100 });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const accordionDefaults = ['headline'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack space="$3" marginTop="$2">
|
<YStack space="$3" marginTop="$2">
|
||||||
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
||||||
{t('events.qr.layoutControls', 'Layout & Schrift')}
|
{t('events.qr.layoutControls', 'Layout & Schrift')}
|
||||||
</Text>
|
</Text>
|
||||||
<YStack space="$2">
|
<Accordion type="multiple" defaultValue={accordionDefaults}>
|
||||||
{renderTextSlot('headline', t('events.qr.headline', 'Headline'))}
|
{renderTextSlot('headline', t('events.qr.headline', 'Headline'))}
|
||||||
{renderTextSlot('subtitle', t('events.qr.subtitle', 'Subtitle'))}
|
{renderTextSlot('subtitle', t('events.qr.subtitle', 'Subtitle'))}
|
||||||
{renderTextSlot('description', t('events.qr.description', 'Beschreibung'))}
|
{renderTextSlot('description', t('events.qr.bottomNote', 'Unterer Hinweistext'))}
|
||||||
{renderTextSlot('instructions', t('events.qr.instructions', 'Anleitung'))}
|
{renderTextSlot('instructions', t('events.qr.instructions', 'Anleitung'))}
|
||||||
</YStack>
|
|
||||||
|
|
||||||
{qrSlot ? (
|
{qrSlot ? (
|
||||||
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
|
<Accordion.Item value="qr">
|
||||||
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
<Accordion.Trigger padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12} backgroundColor="#f8fafc">
|
||||||
{t('events.qr.qr_code_label', 'QR‑Code')}
|
<XStack justifyContent="space-between" alignItems="center" flex={1}>
|
||||||
</Text>
|
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
||||||
<XStack space="$2">
|
{t('events.qr.qr_code_label', 'QR‑Code')}
|
||||||
<YStack flex={1} space="$1">
|
</Text>
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
<ChevronDown size={16} color="#6b7280" />
|
||||||
X (%)
|
</XStack>
|
||||||
</Text>
|
</Accordion.Trigger>
|
||||||
<input
|
<Accordion.Content paddingTop="$2">
|
||||||
type="number"
|
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
|
||||||
step="0.5"
|
<XStack space="$2">
|
||||||
min="0"
|
<YStack flex={1} space="$1">
|
||||||
max="100"
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
value={((qrOverride.x ?? qrSlot.x) * 100).toFixed(1)}
|
X (%)
|
||||||
onChange={(event) => onQrPercentChange('x')(Number(event.target.value))}
|
</Text>
|
||||||
style={numberInputStyle}
|
<StepperInput
|
||||||
/>
|
value={(qrOverride.x ?? qrSlot.x) * 100}
|
||||||
</YStack>
|
min={0}
|
||||||
<YStack flex={1} space="$1">
|
max={100}
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
step={0.5}
|
||||||
Y (%)
|
onChange={(val) => onQrPercentChange('x')(val)}
|
||||||
</Text>
|
/>
|
||||||
<input
|
</YStack>
|
||||||
type="number"
|
<YStack flex={1} space="$1">
|
||||||
step="0.5"
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
min="0"
|
Y (%)
|
||||||
max="100"
|
</Text>
|
||||||
value={((qrOverride.y ?? qrSlot.y) * 100).toFixed(1)}
|
<StepperInput
|
||||||
onChange={(event) => onQrPercentChange('y')(Number(event.target.value))}
|
value={(qrOverride.y ?? qrSlot.y) * 100}
|
||||||
style={numberInputStyle}
|
min={0}
|
||||||
/>
|
max={100}
|
||||||
</YStack>
|
step={0.5}
|
||||||
<YStack flex={1} space="$1">
|
onChange={(val) => onQrPercentChange('y')(val)}
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
/>
|
||||||
Größe (%)
|
</YStack>
|
||||||
</Text>
|
<YStack flex={1} space="$1">
|
||||||
<input
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
type="number"
|
Größe (%)
|
||||||
step="0.5"
|
</Text>
|
||||||
min="10"
|
<StepperInput
|
||||||
max="100"
|
value={(qrOverride.w ?? qrSlot.w) * 100}
|
||||||
value={((qrOverride.w ?? qrSlot.w) * 100).toFixed(1)}
|
min={10}
|
||||||
onChange={(event) => onQrPercentChange('w')(Number(event.target.value))}
|
max={100}
|
||||||
style={numberInputStyle}
|
step={0.5}
|
||||||
/>
|
onChange={(val) => onQrPercentChange('w')(val)}
|
||||||
</YStack>
|
/>
|
||||||
</XStack>
|
</YStack>
|
||||||
{!qrUrl ? (
|
</XStack>
|
||||||
<Text fontSize="$xs" color="#b91c1c">
|
{!qrUrl ? (
|
||||||
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
|
<Text fontSize="$xs" color="#b91c1c">
|
||||||
</Text>
|
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
|
||||||
) : null}
|
</Text>
|
||||||
</YStack>
|
) : null}
|
||||||
) : null}
|
</YStack>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
) : null}
|
||||||
|
</Accordion>
|
||||||
</YStack>
|
</YStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,18 +103,23 @@ export default function MobileQrPrintPage() {
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{qrUrl ? (
|
{qrImage ? (
|
||||||
<img
|
<img
|
||||||
src={qrImage || `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(qrUrl)}`}
|
src={qrImage}
|
||||||
alt="QR"
|
alt="QR"
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text color="#9ca3af" fontSize="$sm">
|
<Text color="#9ca3af" fontSize="$sm">
|
||||||
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
|
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
|
{qrUrl ? (
|
||||||
|
<Text fontSize="$xs" color="#334155" textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
|
||||||
|
{qrUrl}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
{t('events.qr.description', 'Scan to access the event guest app.')}
|
{t('events.qr.description', 'Scan to access the event guest app.')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -123,12 +128,12 @@ export default function MobileQrPrintPage() {
|
|||||||
label={t('events.qr.download', 'Download')}
|
label={t('events.qr.download', 'Download')}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!qrUrl) {
|
if (!qrImage) {
|
||||||
toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden'));
|
toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = qrImage || `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(qrUrl)}`;
|
link.href = qrImage;
|
||||||
link.download = 'event-qr.png';
|
link.download = 'event-qr.png';
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
@@ -577,12 +582,17 @@ function PreviewStep({
|
|||||||
))}
|
))}
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack alignItems="center" justifyContent="center">
|
<YStack alignItems="center" justifyContent="center">
|
||||||
{qrUrl ? (
|
{qrImage ? (
|
||||||
<img
|
<>
|
||||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(qrUrl)}`}
|
<img
|
||||||
alt="QR"
|
src={qrImage}
|
||||||
style={{ width: 140, height: 140, objectFit: 'contain' }}
|
alt="QR"
|
||||||
/>
|
style={{ width: 140, height: 140, objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
<Text fontSize="$xs" color="#334155" textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
|
||||||
|
{qrUrl}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text fontSize="$xs" color="#6b7280">
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
|
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
|
||||||
|
|||||||
@@ -705,7 +705,7 @@ export async function createFabricObject({
|
|||||||
width: element.width,
|
width: element.width,
|
||||||
height: element.height,
|
height: element.height,
|
||||||
fontSize: element.fontSize ?? 36,
|
fontSize: element.fontSize ?? 36,
|
||||||
fill: textColor,
|
fill: element.fill ?? textColor,
|
||||||
fontFamily: element.fontFamily ?? 'Lora',
|
fontFamily: element.fontFamily ?? 'Lora',
|
||||||
textAlign: mapTextAlign(element.align),
|
textAlign: mapTextAlign(element.align),
|
||||||
lineHeight: element.lineHeight ?? 1.5,
|
lineHeight: element.lineHeight ?? 1.5,
|
||||||
@@ -718,7 +718,7 @@ export async function createFabricObject({
|
|||||||
width: element.width,
|
width: element.width,
|
||||||
height: element.height,
|
height: element.height,
|
||||||
fontSize: element.fontSize ?? 24,
|
fontSize: element.fontSize ?? 24,
|
||||||
fill: accentColor,
|
fill: element.fill ?? accentColor,
|
||||||
fontFamily: element.fontFamily ?? 'Montserrat',
|
fontFamily: element.fontFamily ?? 'Montserrat',
|
||||||
underline: true,
|
underline: true,
|
||||||
textAlign: mapTextAlign(element.align),
|
textAlign: mapTextAlign(element.align),
|
||||||
@@ -732,7 +732,7 @@ export async function createFabricObject({
|
|||||||
text: element.content ?? '',
|
text: element.content ?? '',
|
||||||
width: element.width,
|
width: element.width,
|
||||||
height: element.height,
|
height: element.height,
|
||||||
backgroundColor: badgeColor,
|
backgroundColor: element.fill ?? badgeColor,
|
||||||
textColor: '#ffffff',
|
textColor: '#ffffff',
|
||||||
fontSize: element.fontSize ?? 22,
|
fontSize: element.fontSize ?? 22,
|
||||||
lineHeight: element.lineHeight ?? 1.5,
|
lineHeight: element.lineHeight ?? 1.5,
|
||||||
@@ -744,7 +744,7 @@ export async function createFabricObject({
|
|||||||
text: element.content ?? '',
|
text: element.content ?? '',
|
||||||
width: element.width,
|
width: element.width,
|
||||||
height: element.height,
|
height: element.height,
|
||||||
backgroundColor: accentColor,
|
backgroundColor: element.fill ?? accentColor,
|
||||||
textColor: '#ffffff',
|
textColor: '#ffffff',
|
||||||
fontSize: element.fontSize ?? 24,
|
fontSize: element.fontSize ?? 24,
|
||||||
cornerRadius: 18,
|
cornerRadius: 18,
|
||||||
|
|||||||
Reference in New Issue
Block a user