feat(plugin): let users select a sidebar view

An exclusive view replaces the sidebar content while the host retains navigation, callouts, and footer. The device remembers the selection, unavailable views fall back to the default, and throwing views provide recovery. Device-scoped plugin settings persist locally with schema validation and revision conflicts. Web E2E and Android tablet QA cover selection, preserved chrome, disabled-view fallback, and error recovery.
This commit is contained in:
Mohamed Boudra
2026-11-06 09:22:19 +01:00
parent e4106b4e2d
commit dc309586f0
45 changed files with 1059 additions and 79 deletions
@@ -0,0 +1,54 @@
import { useCallback, useMemo } from "react";
import {
useWorkspace,
useWorkspaces,
type WorkspaceRef,
type PluginSidebarViewProps,
} from "@getpaseo/plugin/client";
import { Pressable, ScrollView, Text } from "react-native";
function WorkspaceRow({
reference,
view,
}: {
reference: WorkspaceRef;
view: PluginSidebarViewProps;
}) {
const workspace = useWorkspace(reference);
const open = useCallback(
() => view.navigation.openWorkspace(reference),
[view.navigation, reference],
);
const textStyle = useMemo(
() => ({ color: view.theme.colors.foreground, fontSize: 12, lineHeight: 16 }),
[view.theme],
);
const labelStyle = useMemo(
() => ({ color: view.theme.colors.foregroundMuted, fontSize: 11 }),
[view.theme],
);
if (!workspace) return null;
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Open ${workspace.name}`}
onPress={open}
style={rowStyle}
>
<Text style={textStyle}>{workspace.title ?? workspace.name}</Text>
<Text style={labelStyle}>{workspace.indicatorLabel}</Text>
</Pressable>
);
}
export function Workspaces(view: PluginSidebarViewProps) {
const workspaces = useWorkspaces({ orderBy: "name" });
return (
<ScrollView>
{workspaces.map((reference) => (
<WorkspaceRow key={reference.key} reference={reference} view={view} />
))}
</ScrollView>
);
}
const rowStyle = { minHeight: 28, paddingHorizontal: 12, justifyContent: "center" } as const;
@@ -0,0 +1,10 @@
import type { PluginClientContext } from "@getpaseo/plugin/client";
import { Workspaces } from "./client/workspaces";
export default function contribute(client: PluginClientContext) {
return client.addSidebarView({
id: "workspaces",
title: "Example workspaces",
Component: Workspaces,
});
}
@@ -0,0 +1,4 @@
{
"id": "sidebar-views",
"requirements": { "paseo": ">=0.16.0" }
}