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>
This commit is contained in:
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* `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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user