Add support for hosting forms and links in Panel sections
diff --git a/packages/webui/.gitignore b/packages/webui/.gitignore
index 7bd7f37..6ba266c 100644
--- a/packages/webui/.gitignore
+++ b/packages/webui/.gitignore
@@ -11,4 +11,5 @@
 .wrangler
 .dev.vars*
 
-src/IRIS_SITE.ts
\ No newline at end of file
+src/IRIS_SITE.ts
+src/IRIS_SITE.*.ts
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Panel/FormsAndLinks.tsx b/packages/webui/src/app/components/Panel/FormsAndLinks.tsx
new file mode 100644
index 0000000..a302a48
--- /dev/null
+++ b/packages/webui/src/app/components/Panel/FormsAndLinks.tsx
@@ -0,0 +1,52 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { Button, Link } from "@ffxp/react-dom";
+import type { IrisPluginConduit, IrisPluginConduitFormRef, IrisPluginConduitPanelLayoutLink } from "@jottocraft/iris-sdk";
+import { useEffect, useState } from "react";
+
+import { usePanelNavigation } from "~/app/data/panels";
+import { usePlugins } from "~/app/data/root";
+
+export default function FormsAndLinks({ host, layout, small }: {
+    host: IrisWebuiPanelHost;
+    layout: {
+        forms?: IrisPluginConduitFormRef<IrisPluginConduit>[];
+        links?: IrisPluginConduitPanelLayoutLink[];
+    };
+    small?: boolean;
+}) {
+    const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
+    const plugins = usePlugins()!;
+
+    const plugin = plugins[panelNavigation.pluginId];
+
+    return (
+        <div className="space-y-2">
+            {layout.forms?.length ? (
+                <div>
+                    {layout.forms.map((ref) => {
+                        const form = plugin.plugin.forms[ref.id];
+
+                        return (
+                            <div className="inline-block m-1">
+                                <Button size={small ? "small" : "default"} onClick={() => host.setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
+                            </div>
+                        );
+                    })}
+                </div>
+            ) : null}
+            {layout.links?.length ? (
+                <div>
+                    {layout.links.map((link) => {
+                        return (
+                            <div className="inline-block m-1">
+                                <Link leadingIcon={link.icon} trailingIcon="open_in_new" href={link.href} inNew>{link.label}</Link>
+                            </div>
+                        );
+                    })}
+                </div>
+            ) : null}
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Panel/index.tsx b/packages/webui/src/app/components/Panel/index.tsx
index 4a847d0..09bef0d 100644
--- a/packages/webui/src/app/components/Panel/index.tsx
+++ b/packages/webui/src/app/components/Panel/index.tsx
@@ -7,17 +7,16 @@
 import { useSearchParams } from "react-router";
 import { YggdrasilStatus } from "@jottocraft/yggdrasil";
 
-import { usePlugins } from "~/app/data/root";
 import PanelSection from "~/app/components/PanelSection";
 import { usePanelNavigation } from "~/app/data/panels";
 import Form from "~/app/components/Form";
 import Loading from "~/app/components/Loading";
 import { useError } from "~/app/data/error";
 import ErrorScreen from "~/app/components/ErrorScreen";
+import FormsAndLinks from "~/app/components/Panel/FormsAndLinks";
 
 export default function Panel() {
     const panelNavigation = usePanelNavigation()!;
-    const plugins = usePlugins()!;
     const [activeForm, setActiveForm] = useState<IrisWebuiFormRef | null>(null);
     const [selectedSection, setSelectedSection] = useState<string | null>(null);
     const error = useError();
@@ -45,10 +44,11 @@
         );
     }
 
-    const plugin = plugins[panelNavigation.pluginId];
     const layout = panelNavigation.layout;
     const activeSection = layout.sections?.length === 1 ? layout.sections[0] : (layout.sections?.find(section => (section.id === selectedSection)) ?? null);
 
+    const host = { setActiveForm };
+
     return (
         <>
             {activeForm ? (
@@ -64,17 +64,9 @@
                         <h1>{layout.displayName}</h1>
                     </div>
                 ) : null}
-                {layout.forms ? (
+                {layout.forms?.length || layout.links?.length ? (
                     <div>
-                        {layout.forms?.map((ref) => {
-                            const form = plugin.plugin.forms[ref.id];
-
-                            return (
-                                <div className="inline-block m-1">
-                                    <Button size={panelNavigation.root ? "default" : "small"} onClick={() => setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
-                                </div>
-                            );
-                        })}
+                        <FormsAndLinks host={host} layout={layout} small={!panelNavigation.root} />
                     </div>
                 ) : null}
                 {layout.main ? (
@@ -107,7 +99,7 @@
                             }))} />
                         ) : null}
                         {activeSection ? (
-                            <PanelSection section={activeSection} />
+                            <PanelSection host={host} section={activeSection} />
                         ) : null}
                     </div>
                 ) : null}
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx
index c851c5b..efa87b4 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx
@@ -6,7 +6,7 @@
 import Form from "~/app/components/Form";
 import { usePanelNavigation } from "~/app/data/panels";
 
-export default function PanelSectionForm({ section }: { section: IrisPluginConduitPanelLayoutSectionForm<IrisPluginConduit>; }) {
+export default function PanelSectionForm({ section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionForm<IrisPluginConduit>; }) {
     const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
 
     return (
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx
index a998bdd..4e4e497 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx
@@ -9,7 +9,7 @@
 import Panel from "~/app/components/Panel";
 import PanelBreadcrumbs from "~/app/components/PanelBreadcrumbs";
 
-export default function PanelSectionPanel({ section }: { section: IrisPluginConduitPanelLayoutSectionPanel<IrisPluginConduit>; }) {
+export default function PanelSectionPanel({ section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionPanel<IrisPluginConduit>; }) {
     const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
 
     useEffect(() => {
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
index 4f05e9c..7fbb691 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
@@ -4,26 +4,35 @@
 import { Icon } from "@ffxp/react-dom";
 import type { IrisPluginConduit, IrisPluginConduitPanelLayoutSectionProperties } from "@jottocraft/iris-sdk";
 
-export default function PanelSectionProperties({ section }: { section: IrisPluginConduitPanelLayoutSectionProperties<IrisPluginConduit>; }) {
+import FormsAndLinks from "~/app/components/Panel/FormsAndLinks";
+
+export default function PanelSectionProperties({ host, section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionProperties<IrisPluginConduit>; }) {
     return (
-        <div className="w-full overflow-auto">
-            <table>
-                <tbody>
-                    {section.properties.map(property => (
-                        <tr>
-                            <td className="p-2">
-                                <div className="flex flex-row flex-shrink-0 items-center space-x-1 text-type-10 font-medium">
-                                    {property.icon ? <Icon>{property.icon}</Icon> : null}
-                                    <div className="whitespace-nowrap">{property.displayName}</div>
-                                </div>
-                            </td>
-                            <td className="p-2">
-                                <div className="text-sm text-type-20">{property.value}</div>
-                            </td>
-                        </tr>
-                    ))}
-                </tbody>
-            </table>
+        <div className="space-y-4">
+            {section.forms?.length || section.links?.length ? (
+                <div>
+                    <FormsAndLinks host={host} layout={section} small />
+                </div>
+            ) : null}
+            <div className="w-full overflow-auto">
+                <table>
+                    <tbody>
+                        {section.properties.map(property => (
+                            <tr>
+                                <td className="p-2">
+                                    <div className="flex flex-row flex-shrink-0 items-center space-x-1 text-type-10 font-medium">
+                                        {property.icon ? <Icon>{property.icon}</Icon> : null}
+                                        <div className="whitespace-nowrap">{property.displayName}</div>
+                                    </div>
+                                </td>
+                                <td className="p-2">
+                                    <div className="text-sm text-type-20">{property.value}</div>
+                                </td>
+                            </tr>
+                        ))}
+                    </tbody>
+                </table>
+            </div>
         </div>
     );
 }
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
index 50c40d1..0ba30c9 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
@@ -8,16 +8,11 @@
 
 import { panelRefHref, usePanelNavigation } from "~/app/data/panels";
 import { usePlugins } from "~/app/data/root";
-import Form from "~/app/components/Form";
+import FormsAndLinks from "~/app/components/Panel/FormsAndLinks";
 
-export default function PanelSectionTable({ section }: { section: IrisPluginConduitPanelLayoutSectionTable<IrisPluginConduit>; }) {
+export default function PanelSectionTable({ host, section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionTable<IrisPluginConduit>; }) {
     const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
     const plugins = usePlugins()!;
-    const [activeForm, setActiveForm] = useState<IrisWebuiFormRef | null>(null);
-
-    useEffect(() => {
-        setActiveForm(null);
-    }, [section]);
 
     const plugin = plugins[panelNavigation.pluginId];
 
@@ -44,122 +39,119 @@
     }
 
     return (
-        <>
-            {activeForm ? (
-                <Form ref={activeForm} onFinished={() => {
-                    setActiveForm(null);
-                    panelNavigation.refreshLayout();
-                }} />
+        <div className="space-y-4">
+            {section.forms?.length || section.links?.length ? (
+                <div>
+                    <FormsAndLinks host={host} layout={section} small />
+                </div>
             ) : null}
-            <div className="space-y-4">
-                {section.canSearch ? (
-                    <div className="flex flex-row items-center justify-end">
-                        <TextInput defaultValue={panelNavigation.activeState.sections[section.id]?.table?.search ?? ""} onKeyDown={e => {
-                            const search = e.currentTarget.value || undefined;
-                            if (e.key === "Enter") {
+            {section.canSearch ? (
+                <div className="flex flex-row items-center justify-end">
+                    <TextInput defaultValue={panelNavigation.activeState.sections[section.id]?.table?.search ?? ""} onKeyDown={e => {
+                        const search = e.currentTarget.value || undefined;
+                        if (e.key === "Enter") {
+                            panelNavigation.patchActiveSectionState(section.id, {
+                                table: {
+                                    ...panelNavigation.activeState.sections[section.id]?.table,
+                                    search,
+                                    offset: 0
+                                }
+                            });
+                        }
+                    }} placeholder="Search" icon="search" />
+                </div>
+            ) : null}
+            <div className="w-full overflow-auto">
+                <table className="w-full">
+                    <thead>
+                        <tr className="border-b border-ui-30 text-left">
+                            {section.columns.map(column => (
+                                <th className="p-2 whitespace-nowrap font-medium">
+                                    <div className="flex flex-row items-center flex-nowrap space-x-1">
+                                        {column.icon ? <Icon>{column.icon}</Icon> : null}
+                                        <div>{column.displayName}</div>
+                                    </div>
+                                </th>
+                            ))}
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {section.rows.map((row, i) => (
+                            <tr className={i % 2 === 0 ? undefined : "bg-ui-5"}>
+                                {section.columns.map(column => {
+                                    const link = row.links?.[column.key];
+                                    return (
+                                        <td className="p-2 whitespace-nowrap text-sm text-type-20">
+                                            {link ? (
+                                                <RouterLink to={panelRefHref(panelNavigation.pluginId, link)} onClick={(e) => panelNavigation.navigate(link, e)}>
+                                                    <Link>{row.vals[column.key] ?? null}</Link>
+                                                </RouterLink>
+                                            ) : (
+                                                row.vals[column.key] ?? null
+                                            )}
+                                        </td>
+                                    );
+                                })}
+                                {row.forms ? (
+                                    <td>
+                                        {row.forms.map(ref => {
+                                            const form = plugin.plugin.forms[ref.id];
+
+                                            return (
+                                                <div className="inline-block m-1">
+                                                    <Button size="small" onClick={() => host.setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
+                                                </div>
+                                            );
+                                        })}
+                                    </td>
+                                ) : null}
+                            </tr>
+                        ))}
+                    </tbody>
+                </table>
+            </div>
+            {pagination ? (
+                <div className="flex flex-row items-center justify-between">
+                    <div className="text-sm text-type-30">Showing rows {pagination.firstRow + 1}-{pagination.firstRow + section.rows.length} of {pagination.totalRows}</div>
+                    <div className="flex flex-row items-center">
+                        {(pagination.visiblePages[0] !== 1) ? (
+                            <>
+                                <Icon className="text-type-60">more_horiz</Icon>
+                                <div className="px-1" />
+                            </>
+                        ) : null}
+                        {pagination.visiblePages.map(page => (
+                            <Button onClick={() => {
                                 panelNavigation.patchActiveSectionState(section.id, {
                                     table: {
                                         ...panelNavigation.activeState.sections[section.id]?.table,
-                                        search,
-                                        offset: 0
+                                        offset: (page - 1) * pagination.rowsPerPage
                                     }
                                 });
+                            }} variant={page === pagination.activePage ? "primary" : "borderless"} size="small">{String(page)}</Button>
+                        ))}
+                        {(pagination.visiblePages[pagination.visiblePages.length - 1] !== pagination.totalPages) ? (
+                            <>
+                                <div className="px-1" />
+                                <Icon className="text-type-60">more_horiz</Icon>
+                            </>
+                        ) : null}
+                        <div className="px-4" />
+                        <TextInput onKeyDown={e => {
+                            const val = Number(e.currentTarget.value);
+                            if ((e.key === "Enter") && !isNaN(val) && (val >= 1) && (val <= pagination.totalPages)) {
+                                panelNavigation.patchActiveSectionState(section.id, {
+                                    table: {
+                                        ...panelNavigation.activeState.sections[section.id]?.table,
+                                        offset: (val - 1) * pagination.rowsPerPage
+                                    }
+                                });
+                                e.currentTarget.value = "";
                             }
-                        }} placeholder="Search" icon="search" />
+                        }} type="number" placeholder="Jump to" width={100} />
                     </div>
-                ) : null}
-                <div className="w-full overflow-auto">
-                    <table className="w-full">
-                        <thead>
-                            <tr className="border-b border-ui-30 text-left">
-                                {section.columns.map(column => (
-                                    <th className="p-2 whitespace-nowrap font-medium">
-                                        <div className="flex flex-row items-center flex-nowrap space-x-1">
-                                            {column.icon ? <Icon>{column.icon}</Icon> : null}
-                                            <div>{column.displayName}</div>
-                                        </div>
-                                    </th>
-                                ))}
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {section.rows.map((row, i) => (
-                                <tr className={i % 2 === 0 ? undefined : "bg-ui-5"}>
-                                    {section.columns.map(column => {
-                                        const link = row.links?.[column.key];
-                                        return (
-                                            <td className="p-2 whitespace-nowrap text-sm text-type-20">
-                                                {link ? (
-                                                    <RouterLink to={panelRefHref(panelNavigation.pluginId, link)} onClick={(e) => panelNavigation.navigate(link, e)}>
-                                                        <Link>{row.vals[column.key] ?? null}</Link>
-                                                    </RouterLink>
-                                                ) : (
-                                                    row.vals[column.key] ?? null
-                                                )}
-                                            </td>
-                                        );
-                                    })}
-                                    {row.forms ? (
-                                        <td>
-                                            {row.forms.map(ref => {
-                                                const form = plugin.plugin.forms[ref.id];
-
-                                                return (
-                                                    <div className="inline-block m-1">
-                                                        <Button size="small" onClick={() => setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
-                                                    </div>
-                                                );
-                                            })}
-                                        </td>
-                                    ) : null}
-                                </tr>
-                            ))}
-                        </tbody>
-                    </table>
                 </div>
-                {pagination ? (
-                    <div className="flex flex-row items-center justify-between">
-                        <div className="text-sm text-type-30">Showing rows {pagination.firstRow + 1}-{pagination.firstRow + section.rows.length} of {pagination.totalRows}</div>
-                        <div className="flex flex-row items-center">
-                            {(pagination.visiblePages[0] !== 1) ? (
-                                <>
-                                    <Icon className="text-type-60">more_horiz</Icon>
-                                    <div className="px-1" />
-                                </>
-                            ) : null}
-                            {pagination.visiblePages.map(page => (
-                                <Button onClick={() => {
-                                    panelNavigation.patchActiveSectionState(section.id, {
-                                        table: {
-                                            ...panelNavigation.activeState.sections[section.id]?.table,
-                                            offset: (page - 1) * pagination.rowsPerPage
-                                        }
-                                    });
-                                }} variant={page === pagination.activePage ? "primary" : "borderless"} size="small">{String(page)}</Button>
-                            ))}
-                            {(pagination.visiblePages[pagination.visiblePages.length - 1] !== pagination.totalPages) ? (
-                                <>
-                                    <div className="px-1" />
-                                    <Icon className="text-type-60">more_horiz</Icon>
-                                </>
-                            ) : null}
-                            <div className="px-4" />
-                            <TextInput onKeyDown={e => {
-                                const val = Number(e.currentTarget.value);
-                                if ((e.key === "Enter") && !isNaN(val) && (val >= 1) && (val <= pagination.totalPages)) {
-                                    panelNavigation.patchActiveSectionState(section.id, {
-                                        table: {
-                                            ...panelNavigation.activeState.sections[section.id]?.table,
-                                            offset: (val - 1) * pagination.rowsPerPage
-                                        }
-                                    });
-                                    e.currentTarget.value = "";
-                                }
-                            }} type="number" placeholder="Jump to" width={100} />
-                        </div>
-                    </div>
-                ) : null}
-            </div>
-        </>
+            ) : null}
+        </div>
     );
 }
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/index.tsx b/packages/webui/src/app/components/PanelSection/index.tsx
index f79aaae..746e68d 100644
--- a/packages/webui/src/app/components/PanelSection/index.tsx
+++ b/packages/webui/src/app/components/PanelSection/index.tsx
@@ -8,22 +8,22 @@
 import PanelSectionPanel from "~/app/components/PanelSection/PanelSectionPanel";
 import PanelSectionForm from "~/app/components/PanelSection/PanelSectionForm";
 
-export default function PanelSection({ section }: { section: IrisPluginConduitPanelLayoutSection<IrisPluginConduit>; }) {
+export default function PanelSection({ host, section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSection<IrisPluginConduit>; }) {
     if (section.type === "properties") {
         return (
-            <PanelSectionProperties section={section} />
+            <PanelSectionProperties host={host} section={section} />
         );
     } else if (section.type === "table") {
         return (
-            <PanelSectionTable section={section} />
+            <PanelSectionTable host={host} section={section} />
         );
     } else if (section.type === "panel") {
         return (
-            <PanelSectionPanel section={section} />
+            <PanelSectionPanel host={host} section={section} />
         );
-    } else  if (section.type === "form") {
+    } else if (section.type === "form") {
         return (
-            <PanelSectionForm section={section} />
+            <PanelSectionForm host={host} section={section} />
         );
     } else {
         throw new Error("Unrecognized panel layout section type " + (section as any).type);
diff --git a/packages/webui/src/app/components/Shell.tsx b/packages/webui/src/app/components/Shell.tsx
index 99c8118..45e6f0f 100644
--- a/packages/webui/src/app/components/Shell.tsx
+++ b/packages/webui/src/app/components/Shell.tsx
@@ -39,7 +39,7 @@
                             window.location.reload();
                         }
                     }}>
-                        Signed in as {auth?.claims.email as string}
+                        {"Signed in as " + auth?.claims.email as string}
                     </Button>
                 </>
             )}>
diff --git a/packages/webui/src/app/root.tsx b/packages/webui/src/app/root.tsx
index 55ea1d6..a131528 100644
--- a/packages/webui/src/app/root.tsx
+++ b/packages/webui/src/app/root.tsx
@@ -60,7 +60,7 @@
                 authUrl.searchParams.set("client_id", IRIS_SITE.auth.clientId);
                 authUrl.searchParams.set("redirect_uri", redirectUrl.href);
                 authUrl.searchParams.set("response_type", "id_token");
-                authUrl.searchParams.set("scope", IRIS_SITE.auth.scope ?? "openid email profile");
+                authUrl.searchParams.set("scope", "openid email profile");
                 authUrl.searchParams.set("nonce", nonce);
                 window.location.href = authUrl.href;
             }
diff --git a/packages/webui/src/webui.d.ts b/packages/webui/src/webui.d.ts
index d17ec39..1188de6 100644
--- a/packages/webui/src/webui.d.ts
+++ b/packages/webui/src/webui.d.ts
@@ -43,4 +43,8 @@
     displayName: string;
     icon?: string;
     ref: IrisWebuiPanelRef;
+};
+
+declare type IrisWebuiPanelHost = {
+    setActiveForm: (ref: IrisWebuiFormRef) => void;
 };
\ No newline at end of file