Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@
"packages/studio/src/hooks/gsapTargetCache.ts",
// Preview helper consumed dynamically from the studio iframe bridge.
"packages/studio/src/hooks/gsapRuntimePreview.ts",
// TEMP(studio-dnd): shipped unwired ahead of the NLE integration;
// the app-shell swap PR (studio-dnd/pr22) wires the consumers and removes this block.
"packages/studio/src/components/nle/NLEContext.tsx",
"packages/studio/src/components/nle/PreviewPane.tsx",
"packages/studio/src/components/nle/AssetPreviewOverlay.tsx",
"packages/studio/src/player/components/TimelineOverlays.tsx",
"packages/studio/src/player/components/timelineClipChildren.tsx",
"packages/studio/src/player/components/timelineClipDragTypes.ts",
"packages/studio/src/player/hooks/useTimelinePlayerLoop.ts",
"packages/studio/src/utils/assetPreviewStore.ts",
// TEMP(studio-dnd): shipped unwired ahead of the NLE integration;
// the app-shell swap PR (studio-dnd/pr22) wires the consumers and removes this block.
"packages/studio/src/components/editor/CanvasContextMenu.tsx",
],
"ignorePatterns": [
"docs/**",
Expand Down Expand Up @@ -90,6 +103,114 @@
"packages/cli/src/cloud/_gen/**",
],
"ignoreExports": [
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/player/lib/timelineElementHelpers.ts",
"exports": ["furthestClipEndFromDocument"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/hooks/timelineEditingHelpers.ts",
"exports": ["patchIframeRootDuration"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/sidebar/AssetContextMenu.tsx",
"exports": ["DeleteConfirm"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/editor/domEditOverlayGeometry.ts",
"exports": ["overlayCornersCentroid", "elementCornerOverlayPoints", "orientedOverlayRect"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/contexts/DomEditContext.tsx",
"exports": ["useDomEditActionsContextOptional"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/contexts/StudioContext.tsx",
"exports": ["useStudioShellContextOptional"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/editor/domEditOverlayGestures.ts",
"exports": ["ResizeHandle"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/sidebar/assetHelpers.ts",
"exports": ["truncateMiddle", "formatDuration"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/utils/studioHelpers.ts",
"exports": ["resolveDroppedAssetDimensions"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/player/lib/timelineElementHelpers.ts",
"exports": ["furthestClipEndFromSource", "readTimelineElementZIndex"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/editor/manualOffsetDrag.ts",
"exports": ["applyManualOffsetNudgeDraft", "applyManualOffsetNudgeCommit"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/player/components/timelineEditing.ts",
"exports": [
"applyTimelineAutoScrollStep",
"resolveTimelineAutoScrollLoopAction",
"resolveTimelineDragEscape",
"TimelineDragEscapeInput",
],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/hooks/timelineEditingHelpers.ts",
"exports": [
"shiftGsapPositionsBatch",
"syncTimingEditPreview",
"GsapMutationOutcome",
"readGsapMutationScriptText",
],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/utils/timelineAssetDrop.ts",
"exports": [
"setCompositionDurationToContent",
"extendCompositionDurationIfNeeded",
"fitTimelineAssetGeometry",
"resolveTimelineAssetCompositionSize",
],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/player/components/timelineLayout.ts",
"exports": [
"getTimelineRowTop",
"getTimelineRowFromY",
"DRAG_EXTEND_MARGIN_PX",
"getTimelineFitPps",
"getTimelineDisplayContentWidth",
"TRACKS_TOP_PAD",
"MIN_TIMELINE_EXTENT_S",
],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/editor/domEditNudge.ts",
"exports": ["CANVAS_NUDGE_COMMIT_DEBOUNCE_MS"],
},
// TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22.
{
"file": "packages/studio/src/components/editor/canvasContextMenuZOrder.ts",
"exports": ["readEffectiveZIndex"],
},
// drawElementService is the bottom of the fast-capture Graphite stack
// (#1917): its consumers (frameCapture in #1919) land two PRs upstack, so
// a per-PR audit diffing against the merge base sees these exports as
Expand Down Expand Up @@ -308,6 +429,51 @@
// require intrusive middleware changes beyond this PR's scope.
"minLines": 6,
"ignore": [
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/components/editor/domEditOverlayGeometry.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/components/nle/NLEContext.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/components/nle/NLELayout.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/components/nle/PreviewPane.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/components/sidebar/AssetContextMenu.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/hooks/timelineEditingHelpers.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/components/Timeline.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/components/TimelineCanvas.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/components/TimelineOverlays.tsx",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/components/timelineClipDragTypes.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/components/useTimelineClipDrag.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/components/useTimelineMarqueeSelection.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/hooks/useTimelinePlayer.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/hooks/useTimelinePlayerLoop.ts",
// TEMP(studio-dnd): coexistence-window clone (extraction vs still-live original);
// studio-dnd pr22 removes this with the final config.
"packages/studio/src/player/lib/timelineElementHelpers.ts",
// sourcePatcher.ts: pre-existing internal clones between the inline-style
// and attribute tag-patchers; only the PatchOperation type gained two
// optional fields here, but the line shift makes fallow re-flag them.
Expand Down Expand Up @@ -433,6 +599,22 @@
// complexity pre-dates the computed-timeline work. Exempted at file level
// rather than refactored as scope creep.
"ignore": [
// TEMP(studio-dnd): coexistence-window complexity flare (inherited/CRAP-no-coverage);
// removed by studio-dnd/pr22 when the final config lands.
"packages/studio/src/components/editor/domEditOverlayGeometry.ts",
"packages/studio/src/components/editor/manualOffsetDrag.ts",
"packages/studio/src/player/components/timelineEditing.ts",
"packages/studio/src/player/lib/timelineElementHelpers.ts",
// TEMP(studio-dnd): coexistence-window complexity flare (inherited/CRAP-no-coverage);
// removed by studio-dnd/pr22 when the final config lands.
"packages/studio/src/player/components/TimelineOverlays.tsx",
// TEMP(studio-dnd): coexistence-window complexity flare (inherited/CRAP-no-coverage);
// removed by studio-dnd/pr22 when the final config lands.
"packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts",
"packages/studio/src/components/editor/CanvasContextMenu.tsx",
"packages/studio/src/components/editor/canvasContextMenuZOrder.test.ts",
"packages/studio/src/player/components/timelineCollision.test.ts",
"packages/studio/src/player/components/timelineStackingSync.test.ts",
// sourcePatcher.ts: resolveSourceFile / splitInlineStyleDeclarations /
// patch*InTag pre-date this PR; only the PatchOperation type gained two
// optional fields, but the line-shift fingerprint re-flags the inherited
Expand Down
115 changes: 115 additions & 0 deletions packages/studio/src/components/editor/CanvasContextMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// @vitest-environment happy-dom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { installReactActEnvironment, makeSelection } from "../../hooks/domSelectionTestHarness";
import { CanvasContextMenu } from "./CanvasContextMenu";
import type { DomEditSelection } from "./domEditing";

installReactActEnvironment();

let host: HTMLDivElement;
let root: Root | null = null;

beforeEach(() => {
host = document.createElement("div");
document.body.append(host);
});

afterEach(() => {
act(() => root?.unmount());
root = null;
document.body.innerHTML = "";
});

function renderMenu(props: {
selection: DomEditSelection;
onApplyZIndex?: () => void;
onDelete?: (selection: DomEditSelection) => void;
}) {
root = createRoot(host);
act(() => {
root!.render(
React.createElement(CanvasContextMenu, {
x: 10,
y: 10,
selection: props.selection,
onClose: () => {},
onApplyZIndex: props.onApplyZIndex,
onDelete: props.onDelete,
}),
);
});
}

/** All menu buttons live in the portal under document.body. */
function menuButtons(): HTMLButtonElement[] {
return [...document.body.querySelectorAll("button")];
}

function hasDeleteItem(): boolean {
return menuButtons().some((b) => b.textContent?.includes("Delete"));
}

function zOrderButtons(): HTMLButtonElement[] {
return menuButtons().filter((b) => !b.textContent?.includes("Delete"));
}

describe("CanvasContextMenu — handler gating", () => {
it("renders all four z-order items, a divider, and Delete when both handlers are present", () => {
const el = document.createElement("div");
el.id = "target";
document.body.append(el);

renderMenu({
selection: makeSelection("Target", el),
onApplyZIndex: vi.fn(),
onDelete: vi.fn(),
});

expect(zOrderButtons()).toHaveLength(4);
expect(hasDeleteItem()).toBe(true);
// The divider only appears between the two groups.
expect(document.body.querySelector(".border-t")).not.toBeNull();
});

it("hides every item and does NOT render the menu when no handlers are present", () => {
const el = document.createElement("div");
el.id = "target";
// A z-index that a stray optimistic write would clobber — assert it is
// untouched, since the menu must not mutate the DOM without a persist path.
el.style.zIndex = "3";
document.body.append(el);

renderMenu({ selection: makeSelection("Target", el) });

// No menu opened at all — no buttons, no dead-end items, no DOM mutation.
expect(menuButtons()).toHaveLength(0);
expect(document.body.querySelector(".fixed.z-50")).toBeNull();
expect(el.style.zIndex).toBe("3");
});

it("shows only the z-order items (no Delete, no divider) when onDelete is absent", () => {
const el = document.createElement("div");
el.id = "target";
document.body.append(el);

renderMenu({ selection: makeSelection("Target", el), onApplyZIndex: vi.fn() });

expect(zOrderButtons()).toHaveLength(4);
expect(hasDeleteItem()).toBe(false);
expect(document.body.querySelector(".border-t")).toBeNull();
});

it("shows only Delete (no z-order items, no divider) when onApplyZIndex is absent", () => {
const el = document.createElement("div");
el.id = "target";
document.body.append(el);

renderMenu({ selection: makeSelection("Target", el), onDelete: vi.fn() });

expect(zOrderButtons()).toHaveLength(0);
expect(hasDeleteItem()).toBe(true);
expect(document.body.querySelector(".border-t")).toBeNull();
});
});
Loading
Loading