feat(primitives): media-editor components, category reorg, perf + type cleanup

Reorganize components into category folders (forms/canvas/overlays/etc.); add the
media-editor headless family (timeline, curve-editor, waveform, crop, color
picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag
state, gesture-leak teardown, shallowRef color state, rect caching) and replace
source `any` with proper types.
This commit is contained in:
2026-06-15 16:54:29 +07:00
parent 661a55719e
commit eefd7abf83
1029 changed files with 65815 additions and 9449 deletions
+150
View File
@@ -0,0 +1,150 @@
import type { ComputedRef, Ref, ShallowRef } from 'vue';
import { useContextFactory } from '@robonen/vue';
import type {
ConnectionMode,
ConnectionState,
Dimensions,
EdgeChange,
FlowEdge,
FlowNode,
HandleType,
InternalNode,
NodeChange,
Position,
SelectionMode,
Viewport,
XYPosition,
} from './types';
/**
* Namespaced collection keys. Two `useCollectionProvider` instances live under
* one `FlowRoot` — one for node elements, one for edge `<g>` elements — so they
* MUST use distinct keys, otherwise the inner provider shadows the outer for
* every descendant (see `useCollection` namespacing note).
*/
export const FLOW_NODES_COLLECTION = 'flow-nodes';
export const FLOW_EDGES_COLLECTION = 'flow-edges';
/** Canonical selection store: Sets give O(1) membership on the hot path. */
export interface FlowSelection {
nodes: Set<string>;
edges: Set<string>;
}
/** A handle registering itself into its node's sub-context. */
export interface HandleRegistration {
id: string | null;
type: HandleType;
position: Position;
element: HTMLElement;
}
/**
* Root context shared by every flow part. Reactive fields are `Ref`/`ShallowRef`
* (never raw values — descendants would lose reactivity); mutations go through
* the action functions so behaviour stays in one place.
*/
export interface FlowContext {
/** Stable id for scoping DOM ids / marker ids per flow instance. */
flowId: string;
// ── viewport + measurement ──────────────────────────────────────────────
/** The master transform `{x,y,zoom}`. */
viewport: Ref<Viewport>;
/** Live bounding rect of `FlowPane`; the screen origin for coord math. */
paneRect: Readonly<Ref<{ left: number; top: number; width: number; height: number }>>;
minZoom: Ref<number>;
maxZoom: Ref<number>;
/** True while the viewport is actively panning/zooming (gates `will-change`). */
isInteracting: Readonly<Ref<boolean>>;
// ── capability flags (global defaults, overridable per element) ───────────
nodesDraggable: Ref<boolean>;
nodesConnectable: Ref<boolean>;
elementsSelectable: Ref<boolean>;
snapToGrid: Ref<boolean>;
snapGrid: Ref<[number, number]>;
connectionMode: Ref<ConnectionMode>;
connectionRadius: Ref<number>;
selectionMode: Ref<SelectionMode>;
/** Master interactivity switch (lock). */
interactive: Ref<boolean>;
/** Disable the keyboard a11y layer (and `role=application`). */
disableKeyboardA11y: Ref<boolean>;
// ── data access (shallow Maps — mutate in place + triggerRef) ────────────
/** Read path for components, keyed by node id. */
nodeLookup: ShallowRef<Map<string, InternalNode>>;
/** Read path for components, keyed by edge id. */
edgeLookup: ShallowRef<Map<string, FlowEdge>>;
/** Canonical selection state (replace wholesale + triggerRef). */
selection: ShallowRef<FlowSelection>;
/** Live connection-drag state (only the preview + hovered handle subscribe). */
connection: ShallowRef<ConnectionState>;
/** Ordered id list of currently rendered (post-virtualization) nodes. */
visibleNodeIds: ComputedRef<string[]>;
/** Ordered id list of currently rendered edges. */
visibleEdgeIds: ComputedRef<string[]>;
// ── coordinate actions ───────────────────────────────────────────────────
/** Screen (client) point → flow space. */
screenToFlow: (point: XYPosition) => XYPosition;
/** Flow space → screen (client) point. */
flowToScreen: (point: XYPosition) => XYPosition;
/** `FlowPane` reports its live bounding rect here (the screen origin). */
setPaneRect: (rect: { left: number; top: number; width: number; height: number }) => void;
// ── node actions ─────────────────────────────────────────────────────────
/** Apply a batch of per-node flow-space deltas (drag). Transient. */
updateNodePositions: (deltas: Map<string, XYPosition>, dragging: boolean) => void;
/** Commit the in-flight drag overlay into the model (one change). */
commitNodeDrag: () => void;
/** Write a measured size for a node into `nodeLookup`. */
setNodeMeasured: (id: string, size: Dimensions, handleBounds: InternalNode['handleBounds']) => void;
/** Patch a node in the model (e.g. resize/move) and emit the matching change. */
updateNode: (id: string, patch: Partial<FlowNode>) => void;
/** True while a node drag is in progress (blocks external sync clobber). */
isDragging: Readonly<Ref<boolean>>;
// ── selection actions ────────────────────────────────────────────────────
selectNode: (id: string, additive?: boolean) => void;
selectEdge: (id: string, additive?: boolean) => void;
setSelection: (nodes: string[], edges: string[]) => void;
clearSelection: () => void;
removeSelected: () => void;
// ── connection actions ───────────────────────────────────────────────────
startConnection: (from: HandleRegistration, nodeId: string) => void;
updateConnection: (flowPoint: XYPosition) => void;
endConnection: () => void;
// ── change emission ──────────────────────────────────────────────────────
emitNodesChange: (changes: NodeChange[]) => void;
emitEdgesChange: (changes: EdgeChange[]) => void;
}
const flow = useContextFactory<FlowContext>('FlowContext');
export const provideFlowContext = flow.provide;
export const useFlowContext = flow.inject;
/**
* Per-node sub-context. Read by `FlowHandle`, `FlowNodeResizer`,
* `FlowNodeToolbar` so they never DOM-walk to find their node's state.
*/
export interface FlowNodeContext {
nodeId: string;
node: ComputedRef<InternalNode | undefined>;
positionAbsolute: ComputedRef<XYPosition>;
measured: ComputedRef<Dimensions>;
selected: ComputedRef<boolean>;
dragging: ComputedRef<boolean>;
connectable: ComputedRef<boolean>;
/** Element of the node wrapper, for handle measurement. */
nodeRef: Readonly<Ref<HTMLElement | undefined>>;
registerHandle: (reg: HandleRegistration) => void;
unregisterHandle: (id: string | null, type: HandleType) => void;
}
const flowNode = useContextFactory<FlowNodeContext>('FlowNodeContext');
export const provideFlowNodeContext = flowNode.provide;
export const useFlowNodeContext = flowNode.inject;