mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
* feat(plugins): add React Native UI primitives Give client plugins host-owned adaptive UI without exposing app internals. Preserve plugin runtime contexts when modal content moves through the compact portal host. * docs(plugins): clarify host UI reference * refactor(plugins): harden host UI boundary Replace mocked pass-through coverage with a real browser plugin journey, keep host runtime ownership inside the evaluator, preserve toast context across compact portals, and expose modal content as a compound component. * fix(plugins): defer native UI presentation runtime Loading the plugin evaluator initialized the modal and toast presentation stacks even when no plugin requested host UI. Keep the capability stable while delaying platform-heavy presentation code until Modal.Content renders. * refactor(plugins): move modal metadata to root Keep Modal.Content body-only while the top-level Modal owns the host-rendered title and leading icon. This leaves presentation metadata stable as the compound API gains future regions.
67 lines
2.0 KiB
TypeScript
67 lines
2.0 KiB
TypeScript
import { createContext, useContext, type ReactNode } from "react";
|
|
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
|
|
import type { PluginAgentSnapshot, PluginWorkspaceSnapshot } from "./contracts.js";
|
|
import { shallowEqual } from "./shallow.js";
|
|
|
|
export interface PluginClientStateSource {
|
|
subscribe(listener: () => void): () => void;
|
|
getWorkspace(id: string): PluginWorkspaceSnapshot | null;
|
|
getAgent(id: string): PluginAgentSnapshot | null;
|
|
}
|
|
|
|
const PluginClientStateContext = createContext<PluginClientStateSource | null>(null);
|
|
|
|
export function usePluginClientStateSource(): PluginClientStateSource | null {
|
|
return useContext(PluginClientStateContext);
|
|
}
|
|
|
|
function usePluginEntity<Entity, Selection>(input: {
|
|
id: string;
|
|
read(source: PluginClientStateSource, id: string): Entity | null;
|
|
selector(entity: Entity): Selection;
|
|
}): Selection | null {
|
|
const source = usePluginClientStateSource();
|
|
if (!source) throw new Error("Plugin state hooks must run inside a workspace panel");
|
|
return useSyncExternalStoreWithSelector(
|
|
source.subscribe,
|
|
() => input.read(source, input.id),
|
|
() => input.read(source, input.id),
|
|
(entity) => (entity ? input.selector(entity) : null),
|
|
shallowEqual,
|
|
);
|
|
}
|
|
|
|
export function useWorkspace<Selection>(
|
|
workspaceId: string,
|
|
selector: (workspace: PluginWorkspaceSnapshot) => Selection,
|
|
): Selection | null {
|
|
return usePluginEntity({
|
|
id: workspaceId,
|
|
read: (source, id) => source.getWorkspace(id),
|
|
selector,
|
|
});
|
|
}
|
|
|
|
export function useAgent<Selection>(
|
|
agentId: string,
|
|
selector: (agent: PluginAgentSnapshot) => Selection,
|
|
): Selection | null {
|
|
return usePluginEntity({
|
|
id: agentId,
|
|
read: (source, id) => source.getAgent(id),
|
|
selector,
|
|
});
|
|
}
|
|
|
|
export function PluginClientStateProvider({
|
|
children,
|
|
source,
|
|
}: {
|
|
children: ReactNode;
|
|
source: PluginClientStateSource;
|
|
}) {
|
|
return (
|
|
<PluginClientStateContext.Provider value={source}>{children}</PluginClientStateContext.Provider>
|
|
);
|
|
}
|