feat(layout): supporte la création de layouts plugins (customPluginLayout)

Étend le flux de création de layout backend (DTO, usecases, store) et le
frontend (sélecteur, adaptateurs, LayoutTabs/LayoutGrid) pour permettre
d'ouvrir un layout déclaré par un plugin installé (ex. Android Health),
sans passer par le message bloquant "extension backend pas encore livrée".

Ticket #141 — QA vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 18:34:21 +02:00
parent 168f93df78
commit 45617992ef
29 changed files with 907 additions and 99 deletions

View File

@ -45,9 +45,6 @@ export function LayoutTabs({ projectId, onActiveLayoutChange }: LayoutTabsProps)
// Show/hide the create-kind dropdown.
const [showCreateMenu, setShowCreateMenu] = useState(false);
const { registry: pluginRegistry } = usePluginRuntime();
// Plugin layouts need a `create_layout` backend extension that doesn't exist
// yet (#43, F4 open point) — surfaced rather than silently swallowed.
const [pluginLayoutNotice, setPluginLayoutNotice] = useState<string | null>(null);
async function handleSelect(id: string) {
// The effect above propagates the new active layout (id + kind) to the parent.
@ -88,6 +85,18 @@ export function LayoutTabs({ projectId, onActiveLayoutChange }: LayoutTabsProps)
}
}
async function handleCreatePluginLayout(
name: string,
pluginId: string,
layoutType: string,
) {
setShowCreateMenu(false);
const newId = await vm.create(name, "plugin", { pluginId, layoutType });
if (newId) {
await vm.setActive(newId);
}
}
if (vm.layouts.length === 0) return null;
return (
@ -203,20 +212,21 @@ export function LayoutTabs({ projectId, onActiveLayoutChange }: LayoutTabsProps)
</button>
<PluginLayoutSelectorSection
registry={pluginRegistry}
onSelect={(choice) => {
setShowCreateMenu(false);
setPluginLayoutNotice(
`« ${choice.layout.label} » (${choice.pluginDisplayName}) : la création de layouts plugins nécessite une extension backend pas encore livrée.`,
);
}}
onSelect={(choice) =>
void handleCreatePluginLayout(
choice.layout.label,
choice.pluginId,
choice.layout.type,
)
}
/>
</div>
)}
</div>
{(vm.error || pluginLayoutNotice) && (
{vm.error && (
<span className="ml-2 text-xs text-danger" role="alert">
{vm.error ?? pluginLayoutNotice}
{vm.error}
</span>
)}
</div>