"use client"; import * as React from "react"; import { Panel, type ControlChangeMeta } from "@repo/ui"; import type { ToolcraftControlSchema, ResolvedToolcraftControlSectionSchema, ResolvedToolcraftControlsPanelSchema, } from "../../schema/types"; import { getToolcraftHistorySourceGeneration, type ToolcraftStoreDependency, } from "../../state/toolcraft-external-store-dependencies"; import { getToolcraftControlsResetHistorySource } from "../../state/history-patch-metadata"; import type { ToolcraftCommand, ToolcraftPanelState, ToolcraftState, } from "../../state/types"; import { PanelContainer } from "../panel-host/panel-host"; import { useToolcraftPanelBinding } from "../panel-host/use-toolcraft-panel-binding"; import type { PanelPlacement, PanelStateChange, } from "../panel-host/panel-host-types"; import { useToolcraftDependencySelector } from "../app-shell/toolcraft-selectors"; import { useToolcraftStore } from "../app-shell/toolcraft-store-context"; import { useToolcraftDispatch } from "../app-shell/use-toolcraft"; import { type ToolcraftControlsSceneExport, type ToolcraftPanelActionHandler, useControlsPanelActions, } from "./actions/controls-panel-actions"; import { PanelActionFeedback } from "./actions/panel-action-feedback"; import type { ToolcraftControlRendererMap } from "./control-renderers"; import { getToolcraftControlVisibilityTargets, getToolcraftSectionVisibilityTargets, isToolcraftControlVisible, isToolcraftSectionVisible, } from "./conditions/control-conditions"; import { getControlsPanelTargetDependencies } from "./conditions/control-condition-dependencies"; import type { ControlsPanelSetControlValue } from "./layout/controls-panel-control-group"; import { ControlsPanelSectionNavigation } from "./layout/controls-panel-section-navigation"; import { type ControlEntry, countControlsByType, getControlsRecord, isControlsPanelRenderableControl, } from "./layout/controls-panel-layout"; import { ControlsPanelSection } from "./layout/controls-panel-section"; export type { ToolcraftPanelActionContext, ToolcraftPanelActionHandler, } from "./actions/controls-panel-actions"; export type ControlsPanelProps = { className?: string; controlRenderers?: ToolcraftControlRendererMap; framed?: boolean; onPanelAction?: ToolcraftPanelActionHandler; onPanelStateChange?: PanelStateChange; panelPlacement?: PanelPlacement; panelState?: ToolcraftPanelState; sceneExport?: ToolcraftControlsSceneExport; }; type VisibleSection = { entries: readonly ControlEntry[]; section: ResolvedToolcraftControlSectionSchema; }; type ControlsPanelSelection = { controlsResetKey: number; keyframeControlsEnabled: boolean; vectorPadShape: "compact" | "square"; visibleSections: readonly VisibleSection[]; }; function cn(...classNames: Array): string { return classNames.filter(Boolean).join(" "); } function entriesEqual( previous: readonly ControlEntry[], next: readonly ControlEntry[], ): boolean { return ( previous.length === next.length && previous.every( ([id, control], index) => id === next[index]?.[0] && Object.is(control, next[index]?.[1]), ) ); } function panelSelectionsEqual( previous: ControlsPanelSelection, next: ControlsPanelSelection, ): boolean { return ( previous.controlsResetKey === next.controlsResetKey && previous.keyframeControlsEnabled === next.keyframeControlsEnabled && previous.vectorPadShape === next.vectorPadShape && previous.visibleSections.length === next.visibleSections.length && previous.visibleSections.every( (visibleSection, index) => visibleSection.section === next.visibleSections[index]?.section && entriesEqual( visibleSection.entries, next.visibleSections[index]?.entries ?? [], ), ) ); } function getPanelDependencies({ controlsPanel, keyframesSupported, }: { controlsPanel: ResolvedToolcraftControlsPanelSchema | undefined; keyframesSupported: boolean; }): ToolcraftStoreDependency[] { const visibilityTargets = controlsPanel?.sections.flatMap((section) => [ ...getToolcraftSectionVisibilityTargets(section), ...Object.values(section.controls).flatMap((control) => getToolcraftControlVisibilityTargets(control), ), ]) ?? []; return [ { kind: "history.source", source: getToolcraftControlsResetHistorySource(), }, ...(keyframesSupported ? [{ kind: "timeline.expanded" } as const] : []), ...getControlsPanelTargetDependencies(visibilityTargets), ]; } function selectControlsPanel( state: ToolcraftState, controlsPanel: ResolvedToolcraftControlsPanelSchema | undefined, keyframesSupported: boolean, ): ControlsPanelSelection { const visibleSections = controlsPanel?.sections .map( (section): VisibleSection => ({ entries: Object.entries(section.controls).filter(([, control]) => isToolcraftControlVisible(state, control), ), section, }), ) .filter( ({ entries, section }) => entries.some(([, control]) => isControlsPanelRenderableControl(control), ) && isToolcraftSectionVisible(state, section), ) ?? []; const visibleControlsPanelSections = visibleSections.map( ({ entries, section }) => ({ ...section, controls: getControlsRecord(entries), }), ); const vectorControlCount = countControlsByType( visibleControlsPanelSections, "vector", ); return { controlsResetKey: getToolcraftHistorySourceGeneration( state.history, getToolcraftControlsResetHistorySource(), ), keyframeControlsEnabled: keyframesSupported && state.timeline.expanded, vectorPadShape: vectorControlCount === 1 ? "square" : "compact", visibleSections, }; } export function ControlsPanel({ className, controlRenderers, framed = true, onPanelAction, onPanelStateChange, panelPlacement, panelState, sceneExport, }: ControlsPanelProps): React.JSX.Element | null { const dispatch = useToolcraftDispatch(); const store = useToolcraftStore(); const schema = store.getCommittedState().schema; const controlsPanel = schema.panels.controls; const keyframesSupported = schema.assembly.capabilities.includes("timeline.keyframes"); const dependencies = React.useMemo( () => getPanelDependencies({ controlsPanel, keyframesSupported }), [controlsPanel, keyframesSupported], ); const selection = useToolcraftDependencySelector( React.useCallback( (state: ToolcraftState) => selectControlsPanel(state, controlsPanel, keyframesSupported), [controlsPanel, keyframesSupported], ), panelSelectionsEqual, dependencies, ); const { panelActionFeedback, runAction, stickyFooterActive, stickyFooterProgress, } = useControlsPanelActions({ dispatch, getState: store.getState, onPanelAction, sceneExport, }); const runActionRef = React.useRef(runAction); runActionRef.current = runAction; const stableRunAction = React.useCallback( (action, options) => { runActionRef.current(action, options); }, [], ); const panelBinding = useToolcraftPanelBinding({ onPanelStateChange, panelId: "controls", panelState, }); const resolvedPanelState = panelBinding.panelState; const collapsedSectionByKey = resolvedPanelState.collapsedSections ?? {}; const [navigationRootElement, setNavigationRootElement] = React.useState(null); const dispatchCommand = React.useCallback( (command: ToolcraftCommand): void => { dispatch(command); }, [dispatch], ); const setControlValue = React.useCallback( (target, value, label, meta?: ControlChangeMeta): void => { dispatchCommand({ history: meta?.history, historyGroup: meta?.historyGroup, label, target, type: "controls.setValue", value, }); }, [dispatchCommand], ); const handleSectionCollapsedChange = React.useCallback( (sectionId: string, collapsed: boolean): void => { dispatch({ collapsed, sectionId, type: "panels.setSectionCollapsed" }); }, [dispatch], ); if (!controlsPanel || resolvedPanelState.hidden) { return null; } const placement = panelPlacement ?? (framed ? "frame" : "surface"); const renderedSections = selection.visibleSections.map( ({ entries, section }) => ({ entries, navigationId: `toolcraft-controls-section-${section.id}`, section, sectionCollapseKey: section.id, }), ); const navigationItems = renderedSections.flatMap( ({ navigationId, section }) => section.actionGroup === undefined && section.title ? [{ id: navigationId, title: section.title }] : [], ); const panel = (
panelBinding.onPanelStateChange({ collapsed }) } onResetControls={() => dispatchCommand({ type: "controls.reset" })} stickyFooterActive={stickyFooterActive} stickyFooterProgress={stickyFooterProgress} title={controlsPanel.title} > {renderedSections.map( ({ entries, navigationId, section, sectionCollapseKey, }) => ( ), )}
); if (placement === "surface") { return panel; } return ( {panel} ); }