Files
João Salavisa 9fae7ab500 fix: deduplicate mobile plugin toolbars (#3908)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-09-24 16:18:07 +01:00

79 lines
2.5 KiB
TypeScript

"use client";
import { useContext } from "react";
import { usePluginRegistry } from "@/lib/plugins/registry";
import type { PluginSlotRegistration } from "@/lib/plugins/registry";
import { PluginErrorBoundary } from "./plugin-error-boundary";
import { ObservedPluginSlotContext } from "./plugin-slot-presence";
export type PluginSlotProps = {
/** Named slot to render — see PLUGIN-API.md for the initial set of slot names. */
name: string;
/** Forwarded to each registered component as `slotProps`. */
slotProps?: unknown;
/**
* When set, render only the components registered by this plugin. Used by
* owner-scoped slots (e.g. "plugin-settings" on a plugin's own settings
* page) so the host isolates by owner and plugin authors don't have to gate
* on the current plugin id themselves.
*/
ownerPluginId?: string;
/** Owners whose contextual toolbar is already rendered in this surface. */
excludePluginIds?: readonly string[];
};
/**
* Renders every plugin component registered for the named slot
* (`registry.registerComponent(name, Component)`), each isolated behind its
* own error boundary so one broken plugin can't break the host surface. Pass
* `ownerPluginId` to restrict rendering to that plugin's own components.
*/
export function PluginSlot({ name, slotProps, ownerPluginId, excludePluginIds }: PluginSlotProps) {
const registry = usePluginRegistry();
const registrations = registry
.getSlotRegistrations(name)
.filter(
({ pluginId }) =>
(!ownerPluginId || pluginId === ownerPluginId) && !excludePluginIds?.includes(pluginId),
);
if (registrations.length === 0) return null;
return (
<>
{registrations.map((registration) => (
<PluginSlotRegistrationView
key={registration.registrationId}
registration={registration}
name={name}
slotProps={slotProps}
/>
))}
</>
);
}
export function PluginSlotRegistrationView({
registration,
name,
slotProps,
}: {
registration: PluginSlotRegistration;
name: string;
slotProps?: unknown;
}) {
const { pluginId, Component } = registration;
const observedSlot = useContext(ObservedPluginSlotContext);
const content = (
<PluginErrorBoundary context={`plugin "${pluginId}" slot "${name}" component`}>
<Component slotProps={slotProps} />
</PluginErrorBoundary>
);
if (observedSlot !== name) return content;
return (
<div className="contents [&>*]:min-w-0 [&>*]:max-w-full" data-plugin-slot-owner={pluginId}>
{content}
</div>
);
}