Files
IdeA/frontend/src/features/plugins/PluginLayoutSelectorSection.tsx
Blomios ac726d075e feat(frontend): système de plugins — runtime, menus, layouts custom (#43)
Lots F1-F4 : runtime de chargement/registre plugin, extension des menus
existants, panneau de gestion des plugins, types de layout custom
(sélecteur, fallback, cellule dédiée) branchés sur le port plugin.
Suite npm typecheck/test verte (947/947).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 07:37:11 +02:00

74 lines
2.8 KiB
TypeScript

/**
* `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; not yet mounted in a layout-creation flow. The
* existing "layout" concept in this codebase (`LayoutTabs`, `LayoutKind`) is
* a whole-tab kind (`"terminal" | "gitGraph"`) picked via a fixed two-item
* dropdown, backed by a `create(name, kind)` Tauri command that only knows
* those two kinds. Wiring an actual "create a plugin layout" action needs a
* backend `LayoutKind`/`create_layout` extension (carnet §10 flags F4 as
* "DevFrontend + DevBackend si ajustement DTO layout") — this component is
* the frontend half, ready to drop into that flow once the DTO lands; see the
* F4 delivery report's open point.
*/
import type { PluginLayoutContribution } from "@/domain";
import type { PluginRuntimeRegistry } from "@/plugins/runtime";
export interface PluginLayoutChoice {
pluginId: string;
pluginDisplayName: string;
layout: PluginLayoutContribution;
}
export function listPluginLayoutChoices(registry: PluginRuntimeRegistry): PluginLayoutChoice[] {
return registry
.layoutContributions()
.map(({ pluginId, pluginDisplayName, layout }) => ({ pluginId, pluginDisplayName, layout }))
.sort(
(a, b) =>
(a.layout.order ?? 0) - (b.layout.order ?? 0) ||
a.pluginDisplayName.localeCompare(b.pluginDisplayName) ||
a.layout.label.localeCompare(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 (
<div role="group" aria-label="Layouts plugins">
<p className="px-3 pt-2 text-[0.65rem] font-semibold uppercase tracking-wide text-faint">
Layouts plugins
</p>
{choices.map((choice) => (
<button
key={`${choice.pluginId}:${choice.layout.type}`}
type="button"
role="menuitem"
aria-label={`create ${choice.layout.label} layout`}
onClick={() => onSelect(choice)}
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm text-content hover:bg-raised"
>
<span>{choice.layout.label}</span>
<span className="text-xs text-faint">{choice.pluginDisplayName}</span>
</button>
))}
</div>
);
}