9fae7ab500
Co-authored-by: Kandev Agent <agent@kandev.dev>
79 lines
2.5 KiB
TypeScript
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>
|
|
);
|
|
}
|