From a09aa5f68e5bd92d984a42554d43ddf964d41f54 Mon Sep 17 00:00:00 2001 From: viper151 Date: Fri, 6 Mar 2026 12:36:15 +0100 Subject: [PATCH] feat(plugins): add SVG icon support with authenticated inline rendering --- examples/plugins/hello-world/icon.svg | 6 +++++ src/components/plugins/PluginIcon.tsx | 39 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 examples/plugins/hello-world/icon.svg create mode 100644 src/components/plugins/PluginIcon.tsx diff --git a/examples/plugins/hello-world/icon.svg b/examples/plugins/hello-world/icon.svg new file mode 100644 index 00000000..90a4e78a --- /dev/null +++ b/examples/plugins/hello-world/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/plugins/PluginIcon.tsx b/src/components/plugins/PluginIcon.tsx new file mode 100644 index 00000000..38e7127a --- /dev/null +++ b/src/components/plugins/PluginIcon.tsx @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react'; +import { authenticatedFetch } from '../../utils/api'; + +type Props = { + pluginName: string; + iconFile: string; + className?: string; +}; + +// Module-level cache so repeated renders don't re-fetch +const svgCache = new Map(); + +export default function PluginIcon({ pluginName, iconFile, className }: Props) { + const url = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`; + const [svg, setSvg] = useState(svgCache.get(url) ?? null); + + useEffect(() => { + if (svgCache.has(url)) return; + authenticatedFetch(url) + .then((r) => r.text()) + .then((text) => { + if (text.trimStart().startsWith(' {}); + }, [url]); + + if (!svg) return ; + + return ( + + ); +}