@hyperframes/studio 0.8.50 → 0.8.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-Cn_PpHqP.js} +3 -3
- package/dist/assets/index-C05SNrvY.js +360 -0
- package/dist/assets/{index-DUAIWxkI.js → index-CPS3Cvj8.js} +1 -1
- package/dist/assets/{index-Dt3qBI-9.js → index-Clrb11lj.js} +1 -1
- package/dist/assets/index-DUR9qzQM.css +1 -0
- package/dist/index.d.ts +18 -13
- package/dist/index.html +2 -2
- package/dist/index.js +1867 -3335
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +11 -12
- package/src/App.tsx +128 -141
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.tsx +4 -7
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.tsx +2 -62
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/StudioRightPanel.tsx +4 -4
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +7 -7
- package/src/components/dock/Dock.test.tsx +192 -0
- package/src/components/dock/Dock.tsx +264 -0
- package/src/components/dock/dock.css +23 -0
- package/src/components/dock/dockLayout.ts +58 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +2 -2
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +4 -3
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEPreview.tsx +3 -3
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.tsx +2 -2
- package/src/components/nle/TimelinePane.tsx +3 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/panels/SlideshowPanel.tsx +2 -2
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +6 -6
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
- package/src/components/sidebar/LeftSidebar.tsx +19 -5
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/ui/Button.test.tsx +60 -0
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Tooltip.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +1 -1
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +2 -2
- package/src/player/components/ClipContextMenu.tsx +3 -3
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +2 -1
- package/src/player/components/TimelineCanvas.tsx +17 -10
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +32 -0
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +2 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +61 -1
- package/src/player/components/timelineDragDrop.ts +57 -15
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/lib/describeClips.ts +3 -54
- package/src/styles/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +213 -0
- package/src/styles/theme.test.ts +196 -0
- package/src/utils/studioUiPreferences.ts +7 -0
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/dist/assets/index-n2ml4tUn.js +0 -442
- package/dist/assets/index-pO6p0H27.css +0 -1
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
snapMoveToTargets,
|
|
13
13
|
snapTimelineTime,
|
|
14
14
|
type TimelineSnapTarget,
|
|
15
|
+
type TimelineSnapType,
|
|
15
16
|
} from "./timelineSnapping";
|
|
16
17
|
import { resolveInsertRow, resolveZoneDropPlacement } from "./timelineCollision";
|
|
17
18
|
import {
|
|
@@ -21,11 +22,13 @@ import {
|
|
|
21
22
|
import { clampGroupMoveDelta } from "./timelineMultiDragPreview";
|
|
22
23
|
import type { DraggedClipState, ResizingClipState } from "./timelineClipDragTypes";
|
|
23
24
|
import { resolveDragLandingStart } from "./timelineDragLanding";
|
|
25
|
+
import { STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
24
26
|
|
|
25
27
|
/** Snap-target builder closure supplied by the hook (closes over refs + store). */
|
|
26
28
|
type BuildSnapTargets = (
|
|
27
29
|
excludeElementKey: string | null,
|
|
28
30
|
includeBeats: boolean,
|
|
31
|
+
includePlayhead?: boolean,
|
|
29
32
|
) => TimelineSnapTarget[];
|
|
30
33
|
|
|
31
34
|
export interface DragPreviewContext {
|
|
@@ -223,6 +226,14 @@ export function computeDragPreview(
|
|
|
223
226
|
};
|
|
224
227
|
}
|
|
225
228
|
|
|
229
|
+
/** One frame: the last visible frame of a clip sits just before its end time. */
|
|
230
|
+
const TRIM_END_FRAME_LEAD_S = 1 / STUDIO_PREVIEW_FPS;
|
|
231
|
+
|
|
232
|
+
/** The composition time whose frame a trim shows: the edge being dragged. */
|
|
233
|
+
export function trimPreviewTime(edge: "start" | "end", start: number, duration: number): number {
|
|
234
|
+
return edge === "start" ? start : Math.max(start, start + duration - TRIM_END_FRAME_LEAD_S);
|
|
235
|
+
}
|
|
236
|
+
|
|
226
237
|
export interface ResizePreviewContext {
|
|
227
238
|
scroll: HTMLDivElement | null;
|
|
228
239
|
pps: number;
|
|
@@ -234,6 +245,9 @@ export interface ResizePreviewResult {
|
|
|
234
245
|
previewStart: number;
|
|
235
246
|
previewDuration: number;
|
|
236
247
|
previewPlaybackStart?: number;
|
|
248
|
+
/** The target the trimmed edge snapped to; null when the edge is free. */
|
|
249
|
+
snapTime: number | null;
|
|
250
|
+
snapType: TimelineSnapType | null;
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
/** Compute the trim preview for a pointer x (pure — the hook applies the state). */
|
|
@@ -287,28 +301,31 @@ export function computeResizePreview(
|
|
|
287
301
|
effectiveClientX,
|
|
288
302
|
);
|
|
289
303
|
|
|
290
|
-
// Snap
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// still snaps to the playhead and other clip edges.
|
|
304
|
+
// Snap to beats and clip edges, never the playhead (the dragged edge drives
|
|
305
|
+
// its own preview seek, so that would be circular). Stay inside the same
|
|
306
|
+
// limits resolveTimelineResize enforces. The music track defines the
|
|
307
|
+
// beats, so it must not snap to them, but still snaps to clip edges.
|
|
295
308
|
const trimTargets = buildSnapTargets(
|
|
296
309
|
resize.element.key ?? resize.element.id,
|
|
297
310
|
!isMusicTrack(resize.element),
|
|
311
|
+
false,
|
|
298
312
|
);
|
|
313
|
+
let snap: TimelineSnapTarget | null = null;
|
|
299
314
|
if (trimTargets.length > 0) {
|
|
300
315
|
const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
|
|
301
316
|
if (resize.edge === "end") {
|
|
302
317
|
const edgeTime = nextResize.start + nextResize.duration;
|
|
303
|
-
const snapped = snapTimelineTime(edgeTime, trimTargets, snapSecs)
|
|
318
|
+
const { time: snapped, target } = snapTimelineTime(edgeTime, trimTargets, snapSecs);
|
|
304
319
|
// Stay within [start+minDuration, maxEnd] so the snap can't create a
|
|
305
320
|
// degenerate clip or run past the source/composition limit.
|
|
306
321
|
const snappedDuration = Math.round((snapped - nextResize.start) * 1000) / 1000;
|
|
307
|
-
if (
|
|
308
|
-
|
|
322
|
+
if (target && snapped <= maxEnd + 1e-6 && snappedDuration >= 0.05) {
|
|
323
|
+
// An edge already on the target still owns the guide; only move it when off.
|
|
324
|
+
if (snapped !== edgeTime) nextResize = { ...nextResize, duration: snappedDuration };
|
|
325
|
+
snap = target;
|
|
309
326
|
}
|
|
310
327
|
} else {
|
|
311
|
-
const snapped = snapTimelineTime(nextResize.start, trimTargets, snapSecs)
|
|
328
|
+
const { time: snapped, target } = snapTimelineTime(nextResize.start, trimTargets, snapSecs);
|
|
312
329
|
const delta = nextResize.start - snapped; // >0 when snapping left
|
|
313
330
|
// Leftward snap reveals more source; cap so playbackStart can't go < 0.
|
|
314
331
|
const maxLeftDelta =
|
|
@@ -318,22 +335,20 @@ export function computeResizePreview(
|
|
|
318
335
|
// Also require the resulting duration to stay >= minDuration so a rightward
|
|
319
336
|
// snap (delta < 0) can't collapse the clip to zero/negative.
|
|
320
337
|
const snappedDuration = Math.round((nextResize.duration + delta) * 1000) / 1000;
|
|
321
|
-
if (
|
|
322
|
-
snapped !== nextResize.start
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
: undefined,
|
|
336
|
-
};
|
|
338
|
+
if (target && snapped >= 0 && delta <= maxLeftDelta + 1e-6 && snappedDuration >= 0.05) {
|
|
339
|
+
if (snapped !== nextResize.start) {
|
|
340
|
+
nextResize = {
|
|
341
|
+
...nextResize,
|
|
342
|
+
start: snapped,
|
|
343
|
+
duration: snappedDuration,
|
|
344
|
+
playbackStart:
|
|
345
|
+
nextResize.playbackStart != null
|
|
346
|
+
? Math.round(Math.max(0, nextResize.playbackStart - delta * playbackRate) * 1000) /
|
|
347
|
+
1000
|
|
348
|
+
: undefined,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
snap = target;
|
|
337
352
|
}
|
|
338
353
|
}
|
|
339
354
|
}
|
|
@@ -343,6 +358,8 @@ export function computeResizePreview(
|
|
|
343
358
|
previewStart: nextResize.start,
|
|
344
359
|
previewDuration: nextResize.duration,
|
|
345
360
|
previewPlaybackStart: nextResize.playbackStart,
|
|
361
|
+
snapTime: snap?.time ?? null,
|
|
362
|
+
snapType: snap?.type ?? null,
|
|
346
363
|
};
|
|
347
364
|
}
|
|
348
365
|
|
|
@@ -359,11 +376,18 @@ export function previewGroupResize(
|
|
|
359
376
|
) => void,
|
|
360
377
|
): void {
|
|
361
378
|
const grabbedChange = applyTimelineGroupResizePreview(session, next);
|
|
379
|
+
const previewStart = grabbedChange?.start ?? next.previewStart;
|
|
380
|
+
const previewDuration = grabbedChange?.duration ?? next.previewDuration;
|
|
381
|
+
// A member clamp can pull the grabbed edge off the raw snap target; then no guide.
|
|
382
|
+
const edgeTime = session.edge === "end" ? previewStart + previewDuration : previewStart;
|
|
383
|
+
const stillSnapped = next.snapTime != null && Math.abs(edgeTime - next.snapTime) < 1e-3;
|
|
362
384
|
setResizeState({
|
|
363
385
|
originScrollLeft: next.originScrollLeft,
|
|
364
|
-
previewStart
|
|
365
|
-
previewDuration
|
|
386
|
+
previewStart,
|
|
387
|
+
previewDuration,
|
|
366
388
|
previewPlaybackStart: grabbedChange?.playbackStart ?? next.previewPlaybackStart,
|
|
389
|
+
snapTime: stillSnapped ? next.snapTime : null,
|
|
390
|
+
snapType: stillSnapped ? next.snapType : null,
|
|
367
391
|
groupPreview: session.changes,
|
|
368
392
|
});
|
|
369
393
|
}
|
|
@@ -57,6 +57,9 @@ export interface ResizingClipState {
|
|
|
57
57
|
previewStart: number;
|
|
58
58
|
previewDuration: number;
|
|
59
59
|
previewPlaybackStart?: number;
|
|
60
|
+
/** Snap target the trimmed edge landed on, for the guide highlight. */
|
|
61
|
+
snapTime?: number | null;
|
|
62
|
+
snapType?: TimelineSnapType | null;
|
|
60
63
|
/** Coordinator-owned group projection; canonical elements change only on commit. */
|
|
61
64
|
groupPreview?: readonly {
|
|
62
65
|
key: string;
|
|
@@ -6,7 +6,8 @@ import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
|
6
6
|
import { TIMELINE_ASSET_MIME, TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
|
7
7
|
import { usePlayerStore } from "../store/playerStore";
|
|
8
8
|
import { createTimelineRowGeometry } from "./timelineLayout";
|
|
9
|
-
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
9
|
+
import { resolveDropInsertRow, useTimelineAssetDrop } from "./timelineDragDrop";
|
|
10
|
+
import { getTimelineRowTop, TRACK_H } from "./timelineLayout";
|
|
10
11
|
import { configureTimelineTestViewport } from "./timelineTestViewport";
|
|
11
12
|
|
|
12
13
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -252,3 +253,62 @@ describe("useTimelineAssetDrop", () => {
|
|
|
252
253
|
act(() => view.root.unmount());
|
|
253
254
|
});
|
|
254
255
|
});
|
|
256
|
+
|
|
257
|
+
describe("resolveDropInsertRow", () => {
|
|
258
|
+
const rows = [TRACK_H, TRACK_H, TRACK_H];
|
|
259
|
+
it("arms a new track at the boundary between two lanes", () => {
|
|
260
|
+
expect(resolveDropInsertRow(getTimelineRowTop(1, rows), rows, 3)).toBe(1);
|
|
261
|
+
});
|
|
262
|
+
it("arms a new track above the first lane", () => {
|
|
263
|
+
expect(resolveDropInsertRow(getTimelineRowTop(0, rows) - 4, rows, 3)).toBe(0);
|
|
264
|
+
});
|
|
265
|
+
it("stays on the lane when the pointer is over its middle", () => {
|
|
266
|
+
expect(resolveDropInsertRow(getTimelineRowTop(1, rows) + TRACK_H / 2, rows, 3)).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
it("leaves the area below the last lane to the append path", () => {
|
|
269
|
+
expect(resolveDropInsertRow(getTimelineRowTop(2, rows) + TRACK_H + 4, rows, 3)).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
it("never arms on an empty timeline", () => {
|
|
272
|
+
expect(resolveDropInsertRow(80, [], 0)).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("useTimelineAssetDrop new-track drops", () => {
|
|
277
|
+
it("previews and commits an insert row for an asset dropped between lanes", () => {
|
|
278
|
+
const onAssetDrop = vi.fn();
|
|
279
|
+
const view = renderHarness(onAssetDrop);
|
|
280
|
+
const transfer = assetTransfer(JSON.stringify({ path: "assets/a.png" }));
|
|
281
|
+
const y = getTimelineRowTop(1) + 1;
|
|
282
|
+
|
|
283
|
+
act(() => view.api.handleAssetDragOver(dragEvent(transfer, 400, y)));
|
|
284
|
+
expect(view.api.dropPreview).toMatchObject({ start: 10, insertRow: 1 });
|
|
285
|
+
act(() => view.api.handleAssetDrop(dragEvent(transfer, 400, y)));
|
|
286
|
+
expect(onAssetDrop).toHaveBeenCalledWith(
|
|
287
|
+
"assets/a.png",
|
|
288
|
+
expect.objectContaining({
|
|
289
|
+
start: 10,
|
|
290
|
+
insertRow: 1,
|
|
291
|
+
trackOrder: Array.from({ length: 100 }, (_, index) => index),
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
act(() => view.root.unmount());
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("does not ask a block drop to insert a track", () => {
|
|
298
|
+
const onBlockDrop = vi.fn();
|
|
299
|
+
const view = renderHarness(vi.fn(), 1, { onBlockDrop });
|
|
300
|
+
const transfer: DropTransfer = {
|
|
301
|
+
types: [TIMELINE_BLOCK_MIME],
|
|
302
|
+
files: [],
|
|
303
|
+
dropEffect: "none",
|
|
304
|
+
getData: (type) => (type === TIMELINE_BLOCK_MIME ? JSON.stringify({ name: "b" }) : ""),
|
|
305
|
+
};
|
|
306
|
+
const y = getTimelineRowTop(1) + 1;
|
|
307
|
+
act(() => {
|
|
308
|
+
view.api.handleAssetDragOver(dragEvent(transfer, 400, y));
|
|
309
|
+
view.api.handleAssetDrop(dragEvent(transfer, 400, y));
|
|
310
|
+
});
|
|
311
|
+
expect(onBlockDrop).toHaveBeenCalledWith("b", { start: 10, track: 1 });
|
|
312
|
+
act(() => view.root.unmount());
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -4,8 +4,14 @@ import {
|
|
|
4
4
|
parseTimelineCompositionPayload,
|
|
5
5
|
TIMELINE_COMPOSITION_MIME,
|
|
6
6
|
} from "../../utils/timelineCompositionDrop";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
getTimelineInsertBoundaryBand,
|
|
9
|
+
getTimelineRowPositionFromY,
|
|
10
|
+
resolveTimelineAssetDrop,
|
|
11
|
+
type TimelineRowGeometry,
|
|
12
|
+
} from "./timelineLayout";
|
|
13
|
+
import { resolveInsertRow } from "./timelineCollision";
|
|
14
|
+
import type { TimelineDropCallbacks, TimelineDropPlacement } from "./timelineCallbacks";
|
|
9
15
|
import {
|
|
10
16
|
applyTimelineAutoScrollStep,
|
|
11
17
|
resolveTimelineAutoScrollLoopAction,
|
|
@@ -20,8 +26,6 @@ interface UseTimelineAssetDropOptions extends TimelineDropCallbacks {
|
|
|
20
26
|
sessionEpoch: number;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
type TimelinePlacement = { start: number; track: number };
|
|
24
|
-
|
|
25
29
|
/**
|
|
26
30
|
* Parse a JSON drag payload and, if it yields a value, forward it to the drop
|
|
27
31
|
* callback. Malformed payloads are ignored. Shared by the asset + block paths so
|
|
@@ -30,8 +34,8 @@ type TimelinePlacement = { start: number; track: number };
|
|
|
30
34
|
function applyJsonDropPayload(
|
|
31
35
|
raw: string,
|
|
32
36
|
pick: (parsed: Record<string, string | undefined>) => string | undefined,
|
|
33
|
-
apply: (value: string, placement:
|
|
34
|
-
placement:
|
|
37
|
+
apply: (value: string, placement: TimelineDropPlacement) => void,
|
|
38
|
+
placement: TimelineDropPlacement,
|
|
35
39
|
): boolean {
|
|
36
40
|
try {
|
|
37
41
|
const value = pick(JSON.parse(raw) as Record<string, string | undefined>);
|
|
@@ -43,6 +47,40 @@ function applyJsonDropPayload(
|
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/** Only file and asset drops are written by the asset op that can open a track. */
|
|
51
|
+
function canInsertTrackFor(types: readonly string[]): boolean {
|
|
52
|
+
return types.includes("Files") || types.includes(TIMELINE_ASSET_MIME);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The row boundary a pointer at `contentY` arms for a new track, or null over a lane's middle. */
|
|
56
|
+
export function resolveDropInsertRow(
|
|
57
|
+
contentY: number,
|
|
58
|
+
rowHeights: readonly number[] | undefined,
|
|
59
|
+
trackCount: number,
|
|
60
|
+
): number | null {
|
|
61
|
+
if (trackCount === 0) return null;
|
|
62
|
+
const { rowFloat, rowHeight } = getTimelineRowPositionFromY(contentY, rowHeights);
|
|
63
|
+
// Past the last lane the drop already appends a track (getDefaultDroppedTrack).
|
|
64
|
+
if (rowFloat >= trackCount) return null;
|
|
65
|
+
return resolveInsertRow(rowFloat, trackCount, getTimelineInsertBoundaryBand(rowHeight));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function placeDrop(
|
|
69
|
+
geometry: Parameters<typeof resolveTimelineAssetDrop>[0],
|
|
70
|
+
clientX: number,
|
|
71
|
+
clientY: number,
|
|
72
|
+
// Blocks and compositions are written by their own installers, which only take a track.
|
|
73
|
+
canInsertTrack: boolean,
|
|
74
|
+
): TimelineDropPlacement {
|
|
75
|
+
const placement = resolveTimelineAssetDrop(geometry, clientX, clientY);
|
|
76
|
+
if (!canInsertTrack) return placement;
|
|
77
|
+
const contentY = clientY - geometry.rectTop + geometry.scrollTop;
|
|
78
|
+
const insertRow = resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length);
|
|
79
|
+
return insertRow == null
|
|
80
|
+
? placement
|
|
81
|
+
: { ...placement, insertRow, trackOrder: geometry.trackOrder };
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
function invokeDropCallback(callback: () => Promise<void> | void): void {
|
|
47
85
|
try {
|
|
48
86
|
void Promise.resolve(callback()).catch(() => undefined);
|
|
@@ -54,7 +92,7 @@ function invokeDropCallback(callback: () => Promise<void> | void): void {
|
|
|
54
92
|
function applyFileDrop(
|
|
55
93
|
transfer: DataTransfer,
|
|
56
94
|
onFileDrop: TimelineDropCallbacks["onFileDrop"],
|
|
57
|
-
placement:
|
|
95
|
+
placement: TimelineDropPlacement,
|
|
58
96
|
): boolean {
|
|
59
97
|
if (!onFileDrop || transfer.files.length === 0) return false;
|
|
60
98
|
invokeDropCallback(() => onFileDrop(Array.from(transfer.files), placement));
|
|
@@ -65,8 +103,8 @@ function applyTypedJsonDrop(
|
|
|
65
103
|
transfer: DataTransfer,
|
|
66
104
|
mime: string,
|
|
67
105
|
field: "name" | "path",
|
|
68
|
-
apply: ((value: string, placement:
|
|
69
|
-
placement:
|
|
106
|
+
apply: ((value: string, placement: TimelineDropPlacement) => Promise<void> | void) | undefined,
|
|
107
|
+
placement: TimelineDropPlacement,
|
|
70
108
|
): boolean {
|
|
71
109
|
if (!apply || !Array.from(transfer.types).includes(mime)) return false;
|
|
72
110
|
const payload = transfer.getData(mime);
|
|
@@ -98,7 +136,7 @@ export function useTimelineAssetDrop({
|
|
|
98
136
|
sessionEpoch,
|
|
99
137
|
}: UseTimelineAssetDropOptions) {
|
|
100
138
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
101
|
-
const [dropPreview, setDropPreview] = useState<
|
|
139
|
+
const [dropPreview, setDropPreview] = useState<TimelineDropPlacement | null>(null);
|
|
102
140
|
const dragPointerRef = useRef<{ clientX: number; clientY: number; sessionEpoch: number } | null>(
|
|
103
141
|
null,
|
|
104
142
|
);
|
|
@@ -144,10 +182,10 @@ export function useTimelineAssetDrop({
|
|
|
144
182
|
);
|
|
145
183
|
|
|
146
184
|
const resolveDropPlacement = useCallback(
|
|
147
|
-
(clientX: number, clientY: number):
|
|
185
|
+
(clientX: number, clientY: number, canInsertTrack: boolean): TimelineDropPlacement => {
|
|
148
186
|
const scroll = scrollRef.current;
|
|
149
187
|
const rect = scroll?.getBoundingClientRect();
|
|
150
|
-
return
|
|
188
|
+
return placeDrop(
|
|
151
189
|
{
|
|
152
190
|
rectLeft: rect?.left ?? 0,
|
|
153
191
|
rectTop: rect?.top ?? 0,
|
|
@@ -160,6 +198,7 @@ export function useTimelineAssetDrop({
|
|
|
160
198
|
},
|
|
161
199
|
clientX,
|
|
162
200
|
clientY,
|
|
201
|
+
canInsertTrack,
|
|
163
202
|
);
|
|
164
203
|
},
|
|
165
204
|
[scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin],
|
|
@@ -177,9 +216,11 @@ export function useTimelineAssetDrop({
|
|
|
177
216
|
e.dataTransfer.dropEffect = "copy";
|
|
178
217
|
activeDropEpochRef.current = sessionEpoch;
|
|
179
218
|
setIsDragOver(true);
|
|
180
|
-
const next = resolveDropPlacement(e.clientX, e.clientY);
|
|
219
|
+
const next = resolveDropPlacement(e.clientX, e.clientY, canInsertTrackFor(types));
|
|
181
220
|
setDropPreview((prev) =>
|
|
182
|
-
prev?.start === next.start && prev.track === next.track
|
|
221
|
+
prev?.start === next.start && prev.track === next.track && prev.insertRow === next.insertRow
|
|
222
|
+
? prev
|
|
223
|
+
: next,
|
|
183
224
|
);
|
|
184
225
|
syncAutoScroll(e.clientX, e.clientY);
|
|
185
226
|
},
|
|
@@ -208,7 +249,8 @@ export function useTimelineAssetDrop({
|
|
|
208
249
|
const canCommit = activeDropEpochRef.current === sessionEpoch;
|
|
209
250
|
clearDropPreview();
|
|
210
251
|
if (!canCommit) return;
|
|
211
|
-
const
|
|
252
|
+
const types = Array.from(e.dataTransfer.types);
|
|
253
|
+
const placement = resolveDropPlacement(e.clientX, e.clientY, canInsertTrackFor(types));
|
|
212
254
|
|
|
213
255
|
const compositionPayload = parseTimelineCompositionPayload(
|
|
214
256
|
e.dataTransfer.getData(TIMELINE_COMPOSITION_MIME),
|
|
@@ -8,6 +8,7 @@ import type { TrackVisualStyle } from "./timelineIcons";
|
|
|
8
8
|
import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
|
|
9
9
|
import type { TimelineClipIndex, TimelineTimeRange } from "../lib/timelineClipIndex";
|
|
10
10
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
11
|
+
import type { TimelineSnapTarget } from "./timelineSnapping";
|
|
11
12
|
import type { TimelineVirtualRow } from "./useTimelineVirtualRows";
|
|
12
13
|
import type { MultiDragPreviewInput } from "./timelineMultiDragPreview";
|
|
13
14
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
@@ -112,6 +113,8 @@ export interface TimelineLaneBaseProps {
|
|
|
112
113
|
export interface TimelineLanesProps extends TimelineLaneBaseProps {
|
|
113
114
|
/** Live-derived by TimelineCanvas from {@link TimelineLaneBaseProps.draggedClip}. */
|
|
114
115
|
draggedElement: TimelineElement | null;
|
|
116
|
+
/** Live move or trim snap target, resolved once by TimelineCanvas. */
|
|
117
|
+
snapGuide: TimelineSnapTarget | null;
|
|
115
118
|
multiDragPreview: MultiDragPreviewInput | null;
|
|
116
119
|
onToggleTrackHidden: TimelineEditCallbacks["onToggleTrackHidden"];
|
|
117
120
|
onTogglePropertyGroupKeyframe: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
collectTimelineSnapTargets,
|
|
5
5
|
snapMoveToTargets,
|
|
6
6
|
snapTimelineTime,
|
|
7
|
+
resolveSnapGuide,
|
|
7
8
|
} from "./timelineSnapping";
|
|
8
9
|
|
|
9
10
|
describe("collectTimelineSnapTargets", () => {
|
|
@@ -26,6 +27,18 @@ describe("collectTimelineSnapTargets", () => {
|
|
|
26
27
|
expect(targets).toContainEqual({ time: 0.5, type: "beat" });
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
it("omits the playhead when includePlayhead is false, for a trim", () => {
|
|
31
|
+
const targets = collectTimelineSnapTargets({
|
|
32
|
+
elements,
|
|
33
|
+
playheadTime: 7.25,
|
|
34
|
+
beatTimes: [0.5],
|
|
35
|
+
includePlayhead: false,
|
|
36
|
+
});
|
|
37
|
+
expect(targets.some((t) => t.type === "playhead")).toBe(false);
|
|
38
|
+
expect(targets).toContainEqual({ time: 2, type: "clip-edge" });
|
|
39
|
+
expect(targets).toContainEqual({ time: 0.5, type: "beat" });
|
|
40
|
+
});
|
|
41
|
+
|
|
29
42
|
it("excludes the dragged element's own edges", () => {
|
|
30
43
|
const targets = collectTimelineSnapTargets({
|
|
31
44
|
elements,
|
|
@@ -132,3 +145,17 @@ describe("snapMoveToTargets", () => {
|
|
|
132
145
|
expect(r.snapTime).toBeNull();
|
|
133
146
|
});
|
|
134
147
|
});
|
|
148
|
+
|
|
149
|
+
describe("resolveSnapGuide", () => {
|
|
150
|
+
it("prefers a started move, falls back to a trim, and is null when neither snapped", () => {
|
|
151
|
+
const move = { started: true, snapTime: 2, snapType: "playhead" as const };
|
|
152
|
+
const trim = { snapTime: 5, snapType: "clip-edge" as const };
|
|
153
|
+
expect(resolveSnapGuide(move, trim)).toEqual({ time: 2, type: "playhead" });
|
|
154
|
+
expect(resolveSnapGuide({ ...move, started: false }, trim)).toEqual({
|
|
155
|
+
time: 5,
|
|
156
|
+
type: "clip-edge",
|
|
157
|
+
});
|
|
158
|
+
expect(resolveSnapGuide(null, { snapTime: null, snapType: null })).toBeNull();
|
|
159
|
+
expect(resolveSnapGuide(null, null)).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -7,6 +7,16 @@ export interface TimelineSnapTarget {
|
|
|
7
7
|
type: TimelineSnapType;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** The guide the canvas draws: the live move's snap target, else the live trim's. */
|
|
11
|
+
export function resolveSnapGuide(
|
|
12
|
+
moving: { started: boolean; snapTime: number | null; snapType: TimelineSnapType | null } | null,
|
|
13
|
+
trimming: { snapTime?: number | null; snapType?: TimelineSnapType | null } | null,
|
|
14
|
+
): TimelineSnapTarget | null {
|
|
15
|
+
const source = moving?.started ? moving : trimming;
|
|
16
|
+
if (source?.snapTime == null || source.snapType == null) return null;
|
|
17
|
+
return { time: source.snapTime, type: source.snapType };
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/** Pixel radius within which a time snaps to a target (matches historical beat snap). */
|
|
11
21
|
export const TIMELINE_SNAP_PX = 8;
|
|
12
22
|
|
|
@@ -21,6 +31,8 @@ export function collectTimelineSnapTargets(input: {
|
|
|
21
31
|
playheadTime: number | null;
|
|
22
32
|
beatTimes: readonly number[];
|
|
23
33
|
excludeElementKey?: string | null;
|
|
34
|
+
/** A trim excludes the playhead: the dragged edge drives it, so snapping to it is circular. */
|
|
35
|
+
includePlayhead?: boolean;
|
|
24
36
|
}): TimelineSnapTarget[] {
|
|
25
37
|
const byTime = new Map<number, TimelineSnapTarget>();
|
|
26
38
|
const add = (time: number, type: TimelineSnapType) => {
|
|
@@ -38,7 +50,8 @@ export function collectTimelineSnapTargets(input: {
|
|
|
38
50
|
add(el.start, "clip-edge");
|
|
39
51
|
add(el.start + el.duration, "clip-edge");
|
|
40
52
|
}
|
|
41
|
-
if (input.playheadTime != null
|
|
53
|
+
if (input.playheadTime != null && input.includePlayhead !== false)
|
|
54
|
+
add(input.playheadTime, "playhead");
|
|
42
55
|
|
|
43
56
|
return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
|
|
44
57
|
}
|
|
@@ -39,7 +39,7 @@ function PropertyGroupNavigation({
|
|
|
39
39
|
// bigger box; focus-visible matches every other control in this header.
|
|
40
40
|
const CHEVRON_BUTTON_CLASS =
|
|
41
41
|
"relative h-5 w-3 border-0 bg-transparent p-0 text-white/55 hover:text-white disabled:text-white/15 " +
|
|
42
|
-
"focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
|
|
42
|
+
"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
|
|
43
43
|
"before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " +
|
|
44
44
|
"before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']";
|
|
45
45
|
const seekTo = (keyframe: { percentage: number } | null) => {
|
|
@@ -154,7 +154,7 @@ export function PropertyGroupHeaderRow({
|
|
|
154
154
|
aria-label={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
155
155
|
title={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
156
156
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target; the ◆ glyph stays 11px.
|
|
157
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
157
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
158
158
|
onClick={(event) => {
|
|
159
159
|
// Same as the disclosure caret and the eye: a control in the label
|
|
160
160
|
// column owns its click, it does not also hit the track row behind it.
|
|
@@ -262,7 +262,7 @@ export function AutomationLaneHeaderRow({
|
|
|
262
262
|
aria-label={onReveal ? `Show ${label} in the effect rack` : undefined}
|
|
263
263
|
title={onReveal ? `Show ${label} in the effect rack` : label}
|
|
264
264
|
disabled={!onReveal}
|
|
265
|
-
className="flex min-w-0 flex-1 flex-col justify-center rounded border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
265
|
+
className="flex min-w-0 flex-1 flex-col justify-center rounded-sm border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
266
266
|
onPointerDown={(event) => event.stopPropagation()}
|
|
267
267
|
onClick={(event) => {
|
|
268
268
|
// The label column owns its click; it does not also fall through to
|
|
@@ -311,7 +311,7 @@ export function AutomationLaneHeaderRow({
|
|
|
311
311
|
aria-label={`Remove ${label} automation`}
|
|
312
312
|
title={`Remove ${label} automation`}
|
|
313
313
|
// h-6 w-6 is the 24x24 WCAG 2.2 target; the glyph stays small.
|
|
314
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
314
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
315
315
|
onPointerDown={(event) => event.stopPropagation()}
|
|
316
316
|
onClick={(event) => {
|
|
317
317
|
// A control in the label column owns its click; it does not also hit
|