/** * Builds the `PluginHostApi` (docs/plans/plugins/PLUGIN-API.md) passed * into a plugin's `initialize(registry, host)`. */ import * as React from "react"; import type { StoreApi } from "zustand"; import { Alert, AlertDescription, AlertTitle } from "@kandev/ui/alert"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@kandev/ui/card"; import { Checkbox } from "@kandev/ui/checkbox"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@kandev/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@kandev/ui/dropdown-menu"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@kandev/ui/pagination"; import { ScrollArea } from "@kandev/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { Separator } from "@kandev/ui/separator"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "@kandev/ui/sheet"; import { Skeleton } from "@kandev/ui/skeleton"; import { Spinner } from "@kandev/ui/spinner"; import { Switch } from "@kandev/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kandev/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@kandev/ui/tabs"; import { Textarea } from "@kandev/ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { Combobox } from "@/components/combobox"; import { PageTopbar } from "@/components/page-topbar"; import { TaskCreateDialog } from "@/components/task-create-dialog"; import { getBackendConfig } from "@/lib/config"; import { softNavigate } from "@/lib/routing/client-router"; import type { AppState } from "@/lib/state/store"; import { pluginModalManager } from "./modal-manager"; import type { PluginHostApi } from "./types"; /** * Curated `@kandev/ui` subset exposed on `host.ui`, plus a handful of * first-party app components (bottom of the map). Plugins must use these * host instances rather than bundling their own copies — bundling is not an * option for anything that touches React context or portals (Radix), since * the plugin shares the host React instance and a second copy would split * context and break refs/asChild. Pure-React libs (e.g. icon sets) bundle * fine. */ const PLUGIN_UI: Record = { Alert, AlertDescription, AlertTitle, Badge, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, ScrollArea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Spinner, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Tooltip, TooltipContent, TooltipTrigger, // App UI (not shadcn primitives), exposed so plugins compose kandev-native // surfaces instead of re-implementing them: // - Combobox: the app's Command+Popover picker (used by native toolbars). Combobox, // - PageTopbar: the first-party title bar. Plugin routes get one by default // (registerRoute options.topbar); this export is for routes that opt out // (`topbar: false`) and render their own chrome. PageTopbar, // - TaskCreateDialog: kandev's real create-task modal, prefilled via // initialValues, so plugins hand off task creation to the native flow // instead of POSTing directly. TaskCreateDialog, }; export function buildHostApi( pluginId: string, storeApi: StoreApi, theme: "light" | "dark", ): PluginHostApi { return { pluginId, React, jsx: React.createElement, store: { getState: storeApi.getState, setState: storeApi.setState, subscribe: storeApi.subscribe, }, api: { fetch: (path, init) => fetchPluginApi(pluginId, path, init), // Getter so split-origin dev/desktop always sees the current backend // origin, matching what fetchPluginApi resolves per call. get baseUrl() { return getBackendConfig().apiBaseUrl; }, }, ui: PLUGIN_UI, theme, navigate: (href, options) => softNavigate(href, options?.replace ? "replace" : "push"), openModal: (options) => pluginModalManager.openModal(pluginId, options), }; } /** fetch scoped to `/api/plugins/{pluginId}/...` via the kandev reverse proxy. */ function fetchPluginApi(pluginId: string, path: string, init?: RequestInit): Promise { const { apiBaseUrl } = getBackendConfig(); const suffix = path.startsWith("/") ? path : `/${path}`; const url = `${apiBaseUrl}/api/plugins/${encodeURIComponent(pluginId)}${suffix}`; return fetch(url, init); }