fix(web): remove redundant prop-types and unblock lint pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 2m24s
Build & Deploy / 🏗️ Build (push) Failing after 3m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s

This commit is contained in:
2026-02-24 11:38:43 +01:00
parent 95a8b702fe
commit 6864903cff
205 changed files with 6570 additions and 1324 deletions

View File

@@ -0,0 +1,144 @@
"use client";
import React from "react";
import { useField } from "@payloadcms/ui";
const PREDEFINED_COLORS = [
{ label: "Slate (Default)", value: "slate", color: "#64748b" },
{ label: "Primary Blue", value: "blue", color: "#2563eb" },
{ label: "Success Green", value: "green", color: "#10b981" },
{ label: "Danger Red", value: "red", color: "#ef4444" },
{ label: "Mintel Neon", value: "neon", color: "#d9f99d" },
{ label: "Dark Navy", value: "navy", color: "#0f172a" },
{ label: "Brand Slate", value: "brand-slate", color: "#334155" },
{ label: "Emerald", value: "emerald", color: "#059669" },
{ label: "Purple", value: "purple", color: "#8b5cf6" },
];
export default function ColorPickerField({ path }: { path: string }) {
const { value, setValue } = useField<string>({ path });
const handleColorClick = (colorValue: string) => {
setValue(colorValue);
};
return (
<div
className="field-type"
style={{ marginBottom: "1.5rem", fontFamily: "inherit" }}
>
<label
className="field-label"
style={{
display: "block",
marginBottom: "12px",
fontSize: "0.875rem",
fontWeight: 600,
color: "var(--theme-elevation-800)",
}}
>
Theme Color Selection
</label>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(40px, 1fr))",
gap: "12px",
marginBottom: "16px",
padding: "16px",
background: "var(--theme-elevation-50)",
border: "1px solid var(--theme-elevation-150)",
borderRadius: "8px",
}}
>
{PREDEFINED_COLORS.map((swatch) => {
const isSelected = value === swatch.value;
return (
<button
key={swatch.value}
type="button"
title={swatch.label}
onClick={() => handleColorClick(swatch.value)}
style={{
width: "100%",
aspectRatio: "1/1",
borderRadius: "8px",
backgroundColor: swatch.color,
border: isSelected
? "3px solid var(--theme-text)"
: "1px solid rgba(0,0,0,0.1)",
boxShadow: isSelected
? "0 0 0 2px var(--theme-elevation-150)"
: "inset 0 2px 4px rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.1)",
outline: "none",
cursor: "pointer",
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
transform: isSelected ? "scale(1.05)" : "scale(1)",
}}
onMouseEnter={(e) => {
if (!isSelected)
Object.assign(e.currentTarget.style, {
transform: "scale(1.1)",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
});
}}
onMouseLeave={(e) => {
if (!isSelected)
Object.assign(e.currentTarget.style, {
transform: "scale(1)",
boxShadow:
"inset 0 2px 4px rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.1)",
});
}}
/>
);
})}
</div>
{/* Custom Hex Fallback mapping */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
background: "var(--theme-elevation-50)",
padding: "12px 16px",
borderRadius: "8px",
border: "1px solid var(--theme-elevation-150)",
}}
>
<span
style={{
fontSize: "0.875rem",
fontWeight: 500,
color: "var(--theme-elevation-600)",
}}
>
Hex / String Value:
</span>
<input
type="text"
value={value || ""}
onChange={(e) => setValue(e.target.value)}
placeholder="e.g. #ff0000 or slate-500"
style={{
padding: "8px 12px",
border: "1px solid var(--theme-elevation-200)",
borderRadius: "6px",
background: "#ffffff",
color: "var(--theme-text)",
fontSize: "0.875rem",
width: "100%",
maxWidth: "200px",
boxShadow: "inset 0 1px 2px rgba(0,0,0,0.05)",
outline: "none",
}}
onFocus={(e) => (e.target.style.borderColor = "var(--theme-primary)")}
onBlur={(e) =>
(e.target.style.borderColor = "var(--theme-elevation-200)")
}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,136 @@
"use client";
import React, { useState } from "react";
import { useField, useDocumentInfo, useForm } from "@payloadcms/ui";
import { generateSingleFieldAction } from "../../actions/generateField";
export function AiFieldButton({ path, field }: { path: string; field: any }) {
const [isGenerating, setIsGenerating] = useState(false);
const [instructions, setInstructions] = useState("");
const [showInstructions, setShowInstructions] = useState(false);
// Payload hooks
const { value, setValue } = useField<string>({ path });
const { title } = useDocumentInfo();
const { fields } = useForm();
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
const handleGenerate = async (e: React.MouseEvent) => {
e.preventDefault();
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
setIsGenerating(true);
try {
// Field name is passed as a label usually, fallback to path
const fieldName = typeof field?.label === "string" ? field.label : path;
const fieldDescription =
typeof field?.admin?.description === "string"
? field.admin.description
: "";
const res = await generateSingleFieldAction(
(title as string) || "",
draftContent,
fieldName,
fieldDescription,
instructions,
);
if (res.success && res.text) {
setValue(res.text);
} else {
alert("Fehler: " + res.error);
}
} catch (e) {
alert("Fehler bei der Generierung.");
} finally {
setIsGenerating(false);
setShowInstructions(false);
}
};
return (
<div
style={{
marginTop: "8px",
marginBottom: "8px",
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
padding: "4px 12px",
borderRadius: "4px",
fontSize: "12px",
cursor: isGenerating ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
gap: "6px",
opacity: isGenerating ? 0.6 : 1,
}}
>
{isGenerating ? "✨ AI arbeitet..." : "✨ AI Ausfüllen"}
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault();
setShowInstructions(!showInstructions);
}}
style={{
background: "transparent",
border: "none",
color: "var(--theme-elevation-500)",
fontSize: "12px",
cursor: "pointer",
textDecoration: "underline",
}}
>
{showInstructions ? "Prompt verbergen" : "Mit Prompt..."}
</button>
</div>
{showInstructions && (
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Eigene Anweisung an AI (z.B. 'als catchy slogan')"
disabled={isGenerating}
style={{
width: "100%",
padding: "6px 8px",
fontSize: "12px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
}}
rows={2}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,108 @@
"use client";
import React, { useState, useEffect } from "react";
import { useForm, useField } from "@payloadcms/ui";
import { generateSlugAction } from "../../actions/generateField";
import { Button } from "@payloadcms/ui";
export function GenerateSlugButton({ path }: { path: string }) {
const [isGenerating, setIsGenerating] = useState(false);
const [instructions, setInstructions] = useState("");
useEffect(() => {
if (!isGenerating) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
"Slug-Generierung läuft noch. Wenn Sie neu laden, bricht der Vorgang ab!";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isGenerating]);
const { fields, replaceState } = useForm();
const { value, initialValue, setValue } = useField({ path });
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
const handleGenerate = async () => {
const title = (fields?.title?.value as string) || "";
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
setIsGenerating(true);
try {
const res = await generateSlugAction(
title,
draftContent,
initialValue as string,
instructions,
);
if (res.success && res.slug) {
setValue(res.slug);
} else {
alert("Fehler: " + res.error);
}
} catch (e) {
console.error(e);
alert("Unerwarteter Fehler.");
} finally {
setIsGenerating(false);
}
};
return (
<div className="flex gap-2 items-center mb-4">
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Optionale AI Anweisung für den Slug..."
disabled={isGenerating}
style={{
width: "100%",
minHeight: "40px",
padding: "8px 12px",
fontSize: "14px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
marginBottom: "8px",
}}
/>
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
className="btn btn--icon-style-none btn--size-medium ml-auto"
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
transition: "all 0.2s ease",
opacity: isGenerating ? 0.6 : 1,
cursor: isGenerating ? "not-allowed" : "pointer",
}}
>
<span className="btn__content">
{isGenerating ? "✨ Generiere (ca 10s)..." : "✨ AI Slug Generieren"}
</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,109 @@
"use client";
import React, { useState, useEffect } from "react";
import { useForm, useField } from "@payloadcms/ui";
import { generateThumbnailAction } from "../../actions/generateField";
import { Button } from "@payloadcms/ui";
export function GenerateThumbnailButton({ path }: { path: string }) {
const [isGenerating, setIsGenerating] = useState(false);
const [instructions, setInstructions] = useState("");
useEffect(() => {
if (!isGenerating) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
"Bild-Generierung läuft noch (dies dauert bis zu 2 Minuten). Wenn Sie neu laden, bricht der Vorgang ab!";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isGenerating]);
const { fields } = useForm();
const { value, setValue } = useField({ path });
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
const handleGenerate = async () => {
const title = (fields?.title?.value as string) || "";
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
setIsGenerating(true);
try {
const res = await generateThumbnailAction(
draftContent,
title,
instructions,
);
if (res.success && res.mediaId) {
setValue(res.mediaId);
} else {
alert("Fehler: " + res.error);
}
} catch (e) {
console.error(e);
alert("Unerwarteter Fehler.");
} finally {
setIsGenerating(false);
}
};
return (
<div className="flex gap-2 items-center mt-2 mb-4">
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Optionale Thumbnail-Detailanweisung (Farben, Stimmung, etc.)..."
disabled={isGenerating}
style={{
width: "100%",
minHeight: "40px",
padding: "8px 12px",
fontSize: "14px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
marginBottom: "8px",
}}
/>
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
className="btn btn--icon-style-none btn--size-medium"
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
transition: "all 0.2s ease",
opacity: isGenerating ? 0.6 : 1,
cursor: isGenerating ? "not-allowed" : "pointer",
}}
>
<span className="btn__content">
{isGenerating
? "✨ AI arbeitet (dauert ca. 1-2 Min)..."
: "✨ AI Thumbnail Generieren"}
</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,191 @@
"use client";
import React, { useState } from "react";
import { useField } from "@payloadcms/ui";
import * as LucideIcons from "lucide-react";
const COMMON_ICONS = [
"Check",
"X",
"AlertTriangle",
"Info",
"ArrowRight",
"ArrowUpRight",
"ChevronRight",
"Settings",
"Tool",
"Terminal",
"Code",
"Database",
"Server",
"Cpu",
"Zap",
"Shield",
"Lock",
"Key",
"Eye",
"Search",
"Filter",
"BarChart",
"LineChart",
"PieChart",
"TrendingUp",
"TrendingDown",
"Users",
"User",
"Briefcase",
"Building",
"Globe",
"Mail",
"FileText",
"File",
"Folder",
"Image",
"Video",
"MessageSquare",
"Clock",
"Calendar",
"CheckCircle",
"XCircle",
"Play",
"Pause",
"Activity",
"Box",
"Layers",
"Layout",
"Monitor",
"Smartphone",
"Tablet",
"Wifi",
"Cloud",
"Crosshair",
"Target",
"Trophy",
"Star",
"Heart",
];
export default function IconSelectorField({ path }: { path: string }) {
const { value, setValue } = useField<string>({ path });
const [searchTerm, setSearchTerm] = useState("");
const filteredIcons = COMMON_ICONS.filter((name) =>
name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const handleIconClick = (iconName: string) => {
// Toggle off if clicking the current value
if (value === iconName) {
setValue(null);
} else {
setValue(iconName);
}
};
return (
<div className="field-type" style={{ marginBottom: "1.5rem" }}>
<label
className="field-label"
style={{
display: "block",
marginBottom: "8px",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
Icon Selection (Lucide)
</label>
<input
type="text"
placeholder="Search icons..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
padding: "8px 12px",
border: "1px solid var(--theme-elevation-200)",
borderRadius: "4px",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
fontSize: "0.875rem",
width: "100%",
marginBottom: "12px",
boxSizing: "border-box",
}}
/>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(40px, 1fr))",
gap: "8px",
maxHeight: "200px",
overflowY: "auto",
padding: "4px",
}}
>
{filteredIcons.map((iconName) => {
const Icon = (LucideIcons as any)[iconName];
if (!Icon) return null;
const isSelected = value === iconName;
return (
<button
key={iconName}
type="button"
title={iconName}
onClick={() => handleIconClick(iconName)}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "40px",
height: "40px",
borderRadius: "6px",
background: isSelected
? "var(--theme-elevation-200)"
: "transparent",
border: isSelected
? "1px solid var(--theme-elevation-400)"
: "1px solid var(--theme-elevation-150)",
color: isSelected
? "var(--theme-text)"
: "var(--theme-elevation-500)",
cursor: "pointer",
transition: "all 0.1s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = isSelected
? "var(--theme-elevation-200)"
: "var(--theme-elevation-100)";
e.currentTarget.style.color = "var(--theme-text)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = isSelected
? "var(--theme-elevation-200)"
: "transparent";
e.currentTarget.style.color = isSelected
? "var(--theme-text)"
: "var(--theme-elevation-500)";
}}
>
<Icon size={20} strokeWidth={1.5} />
</button>
);
})}
</div>
{filteredIcons.length === 0 && (
<div
style={{
fontSize: "0.875rem",
color: "var(--theme-elevation-500)",
marginTop: "8px",
}}
>
No matching icons found.
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,136 @@
"use client";
import React, { useState, useEffect } from "react";
import { useForm, useDocumentInfo } from "@payloadcms/ui";
import { optimizePostText } from "../actions/optimizePost";
import { Button } from "@payloadcms/ui";
export function OptimizeButton() {
const [isOptimizing, setIsOptimizing] = useState(false);
const [instructions, setInstructions] = useState("");
useEffect(() => {
if (!isOptimizing) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
"Lexical Block-Optimierung läuft noch (dies dauert bis zu 45 Sekunden). Wenn Sie neu laden, bricht der Vorgang ab!";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isOptimizing]);
const { fields, setModified, replaceState } = useForm();
const { title } = useDocumentInfo();
const handleOptimize = async () => {
// ... gathering draftContent logic
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
if (!draftContent || draftContent.trim().length < 50) {
alert(
"Der Entwurf ist zu kurz. Bitte tippe zuerst ein paar Stichpunkte oder einen Rohling ein.",
);
return;
}
setIsOptimizing(true);
try {
// 2. We inject the title so the AI knows what it's writing about
const payloadText = `---\ntitle: "${title}"\n---\n\n${draftContent}`;
const response = await optimizePostText(payloadText, instructions);
if (response.success && response.lexicalAST) {
// 3. Inject the new Lexical AST directly into the field form state
// We use Payload's useForm hook replacing the value of the 'content' field.
replaceState({
...fields,
content: {
...fields.content,
value: response.lexicalAST,
initialValue: response.lexicalAST,
},
});
setModified(true);
alert(
"🎉 Artikel wurde erfolgreich von der AI optimiert und mit Lexical Components angereichert.",
);
} else {
alert("❌ Fehler: " + response.error);
}
} catch (error) {
console.error("Optimization failed:", error);
alert("Ein unerwarteter Fehler ist aufgetreten.");
} finally {
setIsOptimizing(false);
}
};
return (
<div className="mb-8 p-4 bg-slate-50 border border-slate-200 rounded-md">
<h3 className="text-sm font-semibold mb-2">AI Post Optimizer</h3>
<p className="text-xs text-slate-500 mb-4">
Lass Mintel AI deinen Text-Rohentwurf analysieren und automatisch in
einen voll formatierten Lexical Artikel mit passenden B2B Komponenten
(MemeCards, Mermaids) umwandeln.
</p>
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Optionale Anweisungen an die AI (z.B. 'schreibe etwas lockerer' oder 'fokussiere dich auf SEO')..."
disabled={isOptimizing}
style={{
width: "100%",
minHeight: "60px",
padding: "8px 12px",
fontSize: "14px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
marginBottom: "16px",
}}
/>
<button
type="button"
onClick={handleOptimize}
disabled={isOptimizing}
className="btn btn--icon-style-none btn--size-medium mt-4"
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
transition: "all 0.2s ease",
opacity: isOptimizing ? 0.7 : 1,
cursor: isOptimizing ? "not-allowed" : "pointer",
}}
>
<span className="btn__content" style={{ fontWeight: 600 }}>
{isOptimizing ? "✨ AI arbeitet (ca 30s)..." : "✨ Jetzt optimieren"}
</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,131 @@
"use client";
import React, { useState, useEffect } from "react";
import { useField } from "@payloadcms/ui";
export default function TagSelectorField({ path }: { path: string }) {
const { value, setValue } = useField<string>({ path });
const [suggestions, setSuggestions] = useState<string[]>([]);
useEffect(() => {
const fetchTags = async () => {
try {
const res = await fetch("/api/posts?depth=0&limit=1000");
const data = await res.json();
if (data && data.docs) {
const allTags = new Set<string>();
data.docs.forEach((post: any) => {
if (post.tags && Array.isArray(post.tags)) {
post.tags.forEach((t: any) => {
if (t.tag) allTags.add(t.tag);
});
}
});
setSuggestions(Array.from(allTags).sort());
}
} catch (err) {
console.error("Failed to load tags for suggestions", err);
}
};
fetchTags();
}, []);
const [isFocused, setIsFocused] = useState(false);
return (
<div style={{ marginBottom: "1rem", position: "relative" }}>
<label
style={{
display: "block",
marginBottom: "4px",
fontSize: "0.75rem",
fontWeight: 600,
color: "var(--theme-elevation-500)",
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
Tag Name
</label>
<div style={{ position: "relative" }}>
<input
type="text"
value={value || ""}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setTimeout(() => setIsFocused(false), 200)}
placeholder="e.g. Next.js"
style={{
padding: "4px 8px",
border: "1px solid var(--theme-elevation-200)",
borderRadius: "4px",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
fontSize: "0.875rem",
width: "100%",
boxSizing: "border-box",
outline: "none",
transition: "border-color 0.2s",
}}
/>
{isFocused && suggestions.length > 0 && (
<div
style={{
position: "absolute",
top: "100%",
left: 0,
right: 0,
zIndex: 50,
background: "#ffffff",
border: "1px solid var(--theme-elevation-200)",
borderRadius: "4px",
marginTop: "2px",
maxHeight: "120px",
overflowY: "auto",
boxShadow: "0 10px 15px -3px rgba(0,0,0,0.1)",
}}
>
{suggestions.map((tag) => (
<div
key={tag}
onClick={() => {
setValue(tag);
setIsFocused(false);
}}
onMouseDown={(e) => e.preventDefault()}
style={{
padding: "4px 10px",
cursor: "pointer",
fontSize: "0.8rem",
color: "var(--theme-text)",
borderBottom: "1px solid var(--theme-elevation-50)",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.background =
"var(--theme-elevation-100)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.background = "transparent")
}
>
{tag}
</div>
))}
</div>
)}
</div>
{!value && (
<div
style={{
fontSize: "0.7rem",
color: "var(--theme-elevation-400)",
marginTop: "2px",
}}
>
Select from list or type a new tag.
</div>
)}
</div>
);
}