/** * Floating field styling panel — appears beside the badge preview. */ import React, { useRef, useState } from "react"; import { Trash2, Upload } from "lucide-react"; import { Label, SelectWrap, RadioGroup, HelperText, IS, SS, focusIn, focusOut } from "./PanelShell"; import type { BadgeLayoutElement } from "./badgeLayout"; import { BADGE_FONT_FAMILIES, RESIZABLE_SIZE_MAX, RESIZABLE_SIZE_MIN, applyElementSize, heightPctForImageWidth, imageUrlForElement, isResizableElement, readFileImageAspectRatio, isTextElement, usesFullWidthCenter, type BadgeTextAlign, } from "./badgeLayout"; import { uploadMediaFile } from "../../../services/api"; const FONT_SIZE_OPTIONS = [3, 3.5, 4, 4.5, 5, 6, 7, 8, 9, 10]; const panelStyle: React.CSSProperties = { position: "absolute", left: "calc(100% + 12px)", top: 36, width: 240, zIndex: 10, padding: 14, borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", boxShadow: "0 8px 28px rgba(0,0,0,0.15)", display: "flex", flexDirection: "column", gap: 10, maxHeight: "calc(100vh - 120px)", overflowY: "auto", }; export function BadgeFieldInspector({ element, customTextValue, customImages, badgeAspectRatio, onChange, onRemoveCustom, onCustomImageUploaded, }: { element: BadgeLayoutElement | null; customTextValue?: string; customImages: { id: string; imageUrl: string }[]; badgeAspectRatio: number; onChange: (patch: Partial) => void; onRemoveCustom?: () => void; onCustomImageUploaded?: (id: string, url: string, imageAspectRatio: number) => void; }) { if (!element) return null; if (isTextElement(element)) { return ( ); } if (isResizableElement(element)) { return ( ); } return null; } function TextInspector({ element, customTextValue, onChange, onRemoveCustom, }: { element: BadgeLayoutElement; customTextValue?: string; onChange: (patch: Partial) => void; onRemoveCustom?: () => void; }) { const fullWidth = usesFullWidthCenter(element); const setFullWidth = (on: boolean) => { if (on) { onChange({ fullWidthCenter: true, x: 0, w: 100, textAlign: element.textAlign ?? "center", }); } else { onChange({ fullWidthCenter: false, x: element.x > 0 ? element.x : 6, w: 72, textAlign: element.textAlign === "center" ? "left" : element.textAlign ?? "left", }); } }; return (
e.stopPropagation()} style={panelStyle}>

Selected field styling

{element.kind === "custom_text" && ( <>
Text onChange({ text: e.target.value })} onFocus={focusIn} onBlur={focusOut} />
{onRemoveCustom && ( )} )}
Font colour
onChange({ color: e.target.value })} style={{ width: 40, height: 32, border: "none", cursor: "pointer" }} /> onChange({ color: e.target.value })} onFocus={focusIn} onBlur={focusOut} />
Font family
Font size
Weight onChange({ fontWeight: v as "normal" | "bold" })} options={[ { label: "Normal", value: "normal" }, { label: "Bold", value: "bold" }, ]} />
); } function ResizableInspector({ element, customImages, badgeAspectRatio, onChange, onRemoveCustom, onCustomImageUploaded, }: { element: BadgeLayoutElement; customImages: { id: string; imageUrl: string }[]; badgeAspectRatio: number; onChange: (patch: Partial) => void; onRemoveCustom?: () => void; onCustomImageUploaded?: (id: string, url: string, imageAspectRatio: number) => void; }) { const fileRef = useRef(null); const [uploading, setUploading] = useState(false); const size = element.w ?? 28; const label = element.field === "qr" ? "QR code" : element.field === "photo" ? "Photo" : "Fixed image"; const onSizeChange = (w: number) => { onChange(applyElementSize(element, w, badgeAspectRatio)); }; const onFile = async (file: File | undefined) => { if (!file?.type.startsWith("image/") || element.kind !== "custom_image") return; setUploading(true); try { const imageAspectRatio = await readFileImageAspectRatio(file); const r = await uploadMediaFile(file, { eventId: "portfolio", module: "badges/fixed-images" }); if (r.url) { const w = element.w ?? 25; const h = heightPctForImageWidth(w, imageAspectRatio, badgeAspectRatio); onChange({ imageUrl: r.url, imageAspectRatio, h }); onCustomImageUploaded?.(element.id, r.url, imageAspectRatio); } } finally { setUploading(false); } }; const imgUrl = element.kind === "custom_image" ? imageUrlForElement(element, customImages) : ""; return (
e.stopPropagation()} style={panelStyle}>

{label}

{element.kind === "custom_image" && ( <> {imgUrl ? ( ) : null} { void onFile(e.target.files?.[0]); e.target.value = ""; }} /> {onRemoveCustom && ( )} )}
); } const checkboxLabelStyle: React.CSSProperties = { display: "flex", alignItems: "flex-start", gap: 8, fontSize: "var(--text-sm)", cursor: "pointer", }; const removeBtnStyle: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: 6, padding: "6px 10px", border: "1px solid var(--destructive)", color: "var(--destructive)", background: "transparent", borderRadius: "var(--radius-button)", cursor: "pointer", fontSize: "var(--text-xs)", alignSelf: "flex-start", }; const uploadBtnStyle: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: 8, padding: "8px 14px", borderRadius: "var(--radius-button)", background: "var(--primary)", color: "var(--primary-foreground)", border: "none", cursor: "pointer", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", fontWeight: "var(--font-weight-medium)", };