/** * `PluginLayoutSelectorSection` — the "Layouts plugins" section building block * (ticket #43, F4, carnet §10: "Section `Layouts plugins` dans le sélecteur"). * * Lists every plugin layout contribution currently loaded (disabled/missing * plugins never appear — carnet §1.3: only `enabled` plugins are loaded, so * there is nothing to filter here beyond what the registry already omits). * * Presentational only: the caller owns whether selecting a contribution creates * a named plugin layout, replaces an unavailable cell, or just previews it. */ import type { PluginLayoutContribution } from "@/domain"; import type { PluginRuntimeRegistry } from "@/plugins/runtime"; export interface PluginLayoutChoice { pluginId: string; pluginDisplayName: string; layout: PluginLayoutContribution; } function nonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value : undefined; } function objectOrNull(value: unknown): Record | null { return value !== null && typeof value === "object" ? (value as Record) : null; } function finiteOrder(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function compareText(a: unknown, b: unknown): number { return String(a ?? "").localeCompare(String(b ?? "")); } export function listPluginLayoutChoices(registry: PluginRuntimeRegistry): PluginLayoutChoice[] { return registry .layoutContributions() .flatMap(({ pluginId, pluginDisplayName, layout }) => { const layoutObject = objectOrNull(layout); if (!layoutObject) return []; const type = nonEmptyString(layoutObject.type); const label = nonEmptyString(layoutObject.label); if (!type || !label) return []; return [ { pluginId, pluginDisplayName: nonEmptyString(pluginDisplayName) ?? pluginId, layout: { ...layout, type, label, order: finiteOrder(layoutObject.order), icon: nonEmptyString(layoutObject.icon), }, }, ]; }) .sort( (a, b) => (a.layout.order ?? 0) - (b.layout.order ?? 0) || compareText(a.pluginDisplayName, b.pluginDisplayName) || compareText(a.layout.label, b.layout.label), ); } interface PluginLayoutSelectorSectionProps { registry: PluginRuntimeRegistry; onSelect: (choice: PluginLayoutChoice) => void; } export function PluginLayoutSelectorSection({ registry, onSelect, }: PluginLayoutSelectorSectionProps) { const choices = listPluginLayoutChoices(registry); if (choices.length === 0) return null; return (

Layouts plugins

{choices.map((choice) => ( ))}
); }