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:
63
frontend/src/features/plugins/usePluginMenus.ts
Normal file
63
frontend/src/features/plugins/usePluginMenus.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* `usePluginMenus` — turns the loaded plugin runtime registry into ready-to-
|
||||
* splice `MenuBarMenu`/`MenuBarItem` entries for `ProjectsView`'s menu bar
|
||||
* (ticket #43, F3, carnet §7.1/§7.2).
|
||||
*
|
||||
* `whenCtx` is supplied by the caller (`ProjectsView` knows `projectOpen`
|
||||
* directly; git/agent/terminal/cell-focus signals are best-effort — see the
|
||||
* F3 gap noted in the delivery report: there is no existing global
|
||||
* agent-selected/terminal-focused/layout-cell-focused signal at the menu-bar
|
||||
* level in this codebase today, so those three default to `false` until a
|
||||
* future lot threads real focus state up to this level).
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { MenuTargetId } from "@/domain";
|
||||
import type { MenuBarItem, MenuBarMenu } from "@/shared";
|
||||
import type { PluginRuntimeRegistry, WhenContext } from "@/plugins/runtime";
|
||||
import { resolveMenuItems, resolveTopLevelMenus } from "./menus";
|
||||
|
||||
export interface UsePluginMenusResult {
|
||||
/** Top-level plugin menus, in order, ready to splice between Panneaux/Paramètres. */
|
||||
topLevelMenus: MenuBarMenu[];
|
||||
/** Resolved+ordered `MenuBarItem`s to append to a native menu's items. */
|
||||
itemsFor: (targetMenuId: MenuTargetId) => MenuBarItem[];
|
||||
runCommand: (pluginId: string, commandId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function usePluginMenus(
|
||||
registry: PluginRuntimeRegistry,
|
||||
whenCtx: WhenContext,
|
||||
): UsePluginMenusResult {
|
||||
const runCommand = useMemo(
|
||||
() => (pluginId: string, commandId: string) => registry.runCommand(pluginId, commandId),
|
||||
[registry],
|
||||
);
|
||||
|
||||
const itemsFor = useMemo(() => {
|
||||
return (targetMenuId: MenuTargetId): MenuBarItem[] =>
|
||||
resolveMenuItems(registry, targetMenuId, whenCtx).map((item) => ({
|
||||
id: item.id,
|
||||
// Provenance is shown discreetly (carnet §0 UX decision) rather than
|
||||
// with a visual section divider, which the shared `MenuBar` primitive
|
||||
// doesn't support — a trailing "· Plugin Name" suffix is the least
|
||||
// intrusive option that still surfaces where the item came from.
|
||||
label: `${item.label} · ${item.pluginDisplayName}`,
|
||||
disabled: !item.enabled,
|
||||
onSelect: () => {
|
||||
void runCommand(item.pluginId, item.command);
|
||||
},
|
||||
}));
|
||||
}, [registry, whenCtx, runCommand]);
|
||||
|
||||
const topLevelMenus = useMemo<MenuBarMenu[]>(() => {
|
||||
return resolveTopLevelMenus(registry).map((menu) => ({
|
||||
id: menu.id,
|
||||
label: menu.label,
|
||||
items: itemsFor(menu.id),
|
||||
}));
|
||||
}, [registry, itemsFor]);
|
||||
|
||||
return { topLevelMenus, itemsFor, runCommand };
|
||||
}
|
||||
Reference in New Issue
Block a user