adad46fab1
* feat(plugins): richer frontend SDK — page chrome, nav icons, more host UI
Give native UI plugins the primitives to render kandev-native pages while
keeping everything configurable:
- registerRoute gains an options arg; plugin routes get a first-party
PageTopbar title bar by default (title/icon derived from the nav item for
the same path, or set via { topbar: { title, subtitle, icon, backHref,
backLabel, actions } }). Pass { topbar: false } to render full-bleed and
own the chrome.
- NavItem.icon now resolves against a curated name map (lib/plugins/icons.ts);
unknown/missing names fall back to the puzzle glyph.
- host.ui expands to the primitives a native-feeling page needs (Dialog,
DropdownMenu, Pagination, Sheet, Select, Tabs, Table, Tooltip, ...) plus
first-party PageTopbar, Combobox, and TaskCreateDialog. Plugins must use
these host instances — a bundled second copy of any Radix/portal component
splits React context.
- host.navigate(href, { replace? }) for soft SPA navigation into native
routes, and host.api.baseUrl for reaching first-party REST without
re-deriving the split-origin backend URL.
The registry records each plugin's display name for derived titles and
getRoutes() entries now carry pluginId + options. Contract doc
(PLUGIN-API.md) updated to match.
* fix(plugins): expose full Sheet subcomponent family on host.ui
host.ui advertised Sheet but only exposed Sheet/Content/Header/Title, so
plugins (which must use host instances for portal/context components)
couldn't build an uncontrolled sheet — no Trigger/Close/Footer/Description.
Add the missing four for parity with the Dialog family.
* fix(plugins): clear plugin display name on unregisterPlugin
unregisterPlugin bulk-revoked every other registration but left the entry
in pluginNames, so getPluginName returned a stale name after disable and
names leaked across enable/disable cycles. Delete the name too, with a
regression test.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
"use client";
|
|
|
|
import { AppSidebarNavItem } from "@/components/app-sidebar/app-sidebar-nav-item";
|
|
import { useFeature } from "@/hooks/domains/features/use-feature";
|
|
import { resolvePluginIcon } from "@/lib/plugins/icons";
|
|
import { usePluginRegistry } from "@/lib/plugins/registry";
|
|
|
|
type PluginNavItemsProps = {
|
|
collapsed: boolean;
|
|
};
|
|
|
|
/**
|
|
* Renders every plugin-registered "main" section nav item
|
|
* (`registry.registerNavItem(item)`) in the app sidebar, styled and behaving
|
|
* like a first-party `AppSidebarNavItem`. The contract's opaque `icon` name
|
|
* string resolves against the curated map in `lib/plugins/icons.ts`;
|
|
* unknown/missing names fall back to a generic puzzle-piece glyph. Gated on
|
|
* the "plugins" feature flag for consistency with the other plugin surfaces
|
|
* (settings nav entry, /settings/plugins page) even though the registry is
|
|
* empty and this already renders nothing when the flag is off.
|
|
*/
|
|
export function PluginNavItems({ collapsed }: PluginNavItemsProps) {
|
|
const pluginsEnabled = useFeature("plugins");
|
|
const registry = usePluginRegistry();
|
|
const items = registry.getNavItems().filter((item) => (item.section ?? "main") === "main");
|
|
|
|
if (!pluginsEnabled) return null;
|
|
|
|
return (
|
|
<>
|
|
{items.map((item) => (
|
|
<AppSidebarNavItem
|
|
key={item.id}
|
|
icon={resolvePluginIcon(item.icon)}
|
|
label={item.label}
|
|
href={item.path}
|
|
collapsed={collapsed}
|
|
testId={`plugin-nav-item-${item.id}`}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|