/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * `ExtensionToolbarSlot` — render extension toolbar contributions. * * Reads contributions from the slot registry. Each contribution * declares a slot (`toolbar.left | toolbar.right | toolbar.center`) * and a command id. Clicking dispatches through the host's command * runner. * * Ordering: by `order` ascending; ties broken by command id alpha. * Visibility: respects the `when` clause if present (evaluated against * a minimal viewer context — model loaded / selection count / etc). */ import { useMemo } from 'react'; import type { ResolvedToolbarContribution } from '@ifc-lite/extensions'; import { evaluateWhen, parseWhen } from '@ifc-lite/extensions'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui/toast'; import { describeRunCommandError } from '@/services/extensions/runtime-errors'; import { useSlotContributions } from '@/hooks/useSlotContributions'; import { useOptionalExtensionHost } from '@/sdk/ExtensionHostProvider'; import { useViewerStore } from '@/store'; import { resolveExtensionIcon } from './icon-registry'; interface ExtensionToolbarSlotProps { slot: 'toolbar.left' | 'toolbar.right' | 'toolbar.center'; } export function ExtensionToolbarSlot({ slot }: ExtensionToolbarSlotProps) { const host = useOptionalExtensionHost(); // Loader enriches the toolbar payload with the linked command's // `icon` + `title` — see manifestToContributions in loader.ts. const contributions = useSlotContributions(slot); const models = useViewerStore((s) => s.models); const selectedCount = useViewerStore((s) => s.selectedEntityIds.size); const whenContext = useMemo(() => ({ 'model.loaded': models.size > 0, 'model.schema': undefined, 'model.count': models.size, 'selection.count': selectedCount, 'selection.type': undefined, 'viewer.open': true, desktop: false, embed: false, }), [models.size, selectedCount]); const visible = useMemo(() => { return contributions .filter((c) => { const when = c.payload.when; if (!when) return true; const parsed = parseWhen(when); if (!parsed.ok) return false; return evaluateWhen(parsed.value, whenContext); }) .sort((a, b) => { const oa = a.payload.order ?? 100; const ob = b.payload.order ?? 100; if (oa !== ob) return oa - ob; return a.payload.command.localeCompare(b.payload.command); }); }, [contributions, whenContext]); if (visible.length === 0 || !host) return null; const handleClick = (commandId: string) => { void host.dispatcher .fire(`onCommand:${commandId}` as `onCommand:${string}`) .then(() => host.runCommand(commandId)) .catch((err) => { toast.error(describeRunCommandError(commandId, err)); }); }; return (
{visible.map((c) => { const cmd = c.payload.command; const title = c.payload.title ?? cmd; const Icon = resolveExtensionIcon(c.payload.icon); return ( {title} ); })}
); }