diff --git a/packages/ui/registry.json b/packages/ui/registry.json index 10a6e147..c812ff0f 100644 --- a/packages/ui/registry.json +++ b/packages/ui/registry.json @@ -3110,10 +3110,22 @@ "path": "registry/constructive/blocks/org-chart/org-chart-states.tsx", "type": "registry:component" }, + { + "path": "registry/constructive/blocks/org-chart/org-chart-wires.tsx", + "type": "registry:component" + }, { "path": "registry/constructive/blocks/org-chart/layout.ts", "type": "registry:lib" }, + { + "path": "registry/constructive/blocks/org-chart/org-chart-scene.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/org-chart/org-chart-spring.ts", + "type": "registry:lib" + }, { "path": "registry/constructive/blocks/org-chart/org-chart-utils.ts", "type": "registry:lib" diff --git a/packages/ui/src/components/org-chart/org-chart-node.tsx b/packages/ui/src/components/org-chart/org-chart-node.tsx index 3acc1094..e6ed4b35 100644 --- a/packages/ui/src/components/org-chart/org-chart-node.tsx +++ b/packages/ui/src/components/org-chart/org-chart-node.tsx @@ -9,13 +9,12 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSepara import { CANVAS_NODE_ATTRIBUTE, NodeShell } from '../workspace-kit/canvas'; import { focusRingClass, pressClass } from '../workspace-kit/primitives'; import type { PlacedPerson } from './layout'; +import { springTransition } from './org-chart-spring'; import { getInitials, personName, reportsLabel } from './org-chart-utils'; export type NodeHandlers = { + /** Starts a press; the chart follows the pointer from there until it is released anywhere. */ pointerDown: (id: string, event: React.PointerEvent) => void; - pointerMove: (id: string, event: React.PointerEvent) => void; - pointerUp: (id: string, event: React.PointerEvent) => void; - pointerCancel: () => void; keyDown: (id: string, event: React.KeyboardEvent) => void; focus: (id: string, event: React.FocusEvent) => void; toggle: (id: string) => void; @@ -35,80 +34,80 @@ export function PersonAvatar({ person, className }: { person: { displayName: str ); } -type OrgChartNodeProps = { +/** + * The card's actions menu. Until its first click it is a plain button that + * looks the same, because a menu per card is a third of the cost of + * unfolding a large team; the first click mounts the real menu, already open. + */ +function CardMenu({ id, name, selected, handlers }: { id: string; name: string; selected: boolean; handlers: NodeHandlers }) { + const [mounted, setMounted] = React.useState(false); + const trigger = { + tabIndex: -1, + 'aria-label': `Actions for ${name}`, + className: cn( + 'grid size-6 shrink-0 cursor-pointer place-items-center rounded-md text-muted-foreground opacity-0 hover:bg-overlay-hover hover:text-foreground', + 'group-hover/node:opacity-100 group-focus-within/node:opacity-100 data-[popup-open]:opacity-100 pointer-coarse:opacity-100', + selected && 'opacity-100', + 'transition-opacity duration-(--duration-fast) motion-reduce:transition-none', + pressClass, + focusRingClass, + ), + children: