@hyperframes/studio 0.8.6 → 0.8.7
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-DdNgW-Np.js → hyperframes-player-bDcU464i.js} +1 -1
- package/dist/assets/index-Ba9zbpT9.css +1 -0
- package/dist/assets/index-CUXnyoIa.js +428 -0
- package/dist/assets/{index-bv8b5eiQ.js → index-FkAs3zRu.js} +1 -1
- package/dist/assets/{index-Ci7Uy8fR.js → index-MSOSP1-J.js} +1 -1
- package/dist/index.d.ts +98 -17
- package/dist/index.html +2 -2
- package/dist/index.js +10336 -7195
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +22 -23
- package/src/captions/components/CaptionAnimationPanel.tsx +46 -13
- package/src/captions/components/CaptionOverlay.tsx +172 -29
- package/src/captions/components/CaptionOverlayUtils.ts +54 -0
- package/src/captions/components/CaptionPropertyPanel.tsx +49 -23
- package/src/captions/components/CaptionTimeline.tsx +36 -79
- package/src/captions/components/shared.tsx +76 -1
- package/src/captions/hooks/useCaptionSync.ts +72 -13
- package/src/captions/keyboard.test.ts +12 -0
- package/src/captions/keyboard.ts +12 -1
- package/src/captions/store.ts +120 -14
- package/src/components/EditorShell.selectionSync.test.tsx +1 -1
- package/src/components/EditorShell.tsx +13 -1
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/StudioRightPanel.types.ts +1 -0
- package/src/components/editor/AnimationCard.tsx +2 -1
- package/src/components/editor/AnimationCardParts.tsx +6 -3
- package/src/components/editor/ArcPathControls.tsx +10 -4
- package/src/components/editor/BlockParamsPanel.test.tsx +170 -0
- package/src/components/editor/BlockParamsPanel.tsx +153 -11
- package/src/components/editor/BorderRadiusEditor.tsx +3 -22
- package/src/components/editor/FileTree.tsx +41 -12
- package/src/components/editor/FileTreeNodes.tsx +90 -20
- package/src/components/editor/GsapAnimationList.tsx +5 -0
- package/src/components/editor/KeyframeDiamond.tsx +3 -1
- package/src/components/editor/KeyframeEaseList.tsx +3 -2
- package/src/components/editor/KeyframeNavigation.tsx +6 -2
- package/src/components/editor/LayersPanel.tsx +11 -2
- package/src/components/editor/MotionPathNode.tsx +1 -0
- package/src/components/editor/PropertyPanel.test.tsx +7 -6
- package/src/components/editor/PropertyPanel.tsx +13 -5
- package/src/components/editor/PropertyPanelFlat.tsx +2 -0
- package/src/components/editor/SnapToolbar.tsx +20 -4
- package/src/components/editor/TimelineFxPopover.test.tsx +113 -0
- package/src/components/editor/TimelineFxPopover.tsx +138 -0
- package/src/components/editor/Transform3DCube.tsx +22 -1
- package/src/components/editor/manualOffsetDrag.test.ts +49 -0
- package/src/components/editor/manualOffsetDrag.ts +13 -1
- package/src/components/editor/propertyPanel3dTransform.tsx +11 -3
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +139 -3
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +4 -0
- package/src/components/editor/propertyPanelColor.tsx +9 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +28 -7
- package/src/components/editor/propertyPanelCommitField.tsx +9 -1
- package/src/components/editor/propertyPanelFill.tsx +50 -2
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFont.tsx +45 -9
- package/src/components/editor/propertyPanelFxPresetStyle.ts +18 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +3 -3
- package/src/components/editor/propertyPanelFxSection.tsx +8 -23
- package/src/components/editor/propertyPanelPrimitives.tsx +9 -12
- package/src/components/editor/propertyPanelStyleSections.tsx +21 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +3 -3
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useApplyAudioFxPreset.ts +36 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.ts +132 -0
- package/src/components/nle/PreviewOverlays.tsx +69 -2
- package/src/components/nle/PreviewPane.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +81 -12
- package/src/components/panels/SlideshowSubPanels.tsx +61 -7
- package/src/components/sidebar/AssetCard.test.tsx +2 -2
- package/src/components/sidebar/AssetCard.tsx +94 -46
- package/src/components/sidebar/AssetContextMenu.tsx +196 -42
- package/src/components/sidebar/AssetsTab.tsx +156 -96
- package/src/components/sidebar/AudioRow.tsx +95 -40
- package/src/components/sidebar/BlocksTab.tsx +40 -128
- package/src/components/sidebar/CompositionsTab.tsx +46 -34
- package/src/components/sidebar/LeftSidebar.tsx +87 -75
- package/src/components/sidebar/PromptPreviewModal.tsx +126 -0
- package/src/components/sidebar/assetHelpers.ts +8 -0
- package/src/contexts/TimelineEditContext.tsx +5 -0
- package/src/hooks/timelineAudioGroupVolume.ts +148 -0
- package/src/hooks/timelineEditingHelpers.ts +78 -1
- package/src/hooks/timelineElementFxAttribute.ts +150 -0
- package/src/hooks/timelineTrackVisibility.test.ts +222 -1
- package/src/hooks/timelineTrackVisibility.ts +187 -4
- package/src/hooks/useAppHotkeys.ts +22 -0
- package/src/hooks/useAudioSoloBridge.ts +61 -0
- package/src/hooks/useCaptionDetection.ts +22 -2
- package/src/hooks/useDomEditCommits.test.tsx +24 -0
- package/src/hooks/useEffectiveTimelineDuration.ts +20 -0
- package/src/hooks/useGroupLevel.ts +36 -0
- package/src/hooks/useHydrateActiveCompPathFromUrl.ts +36 -0
- package/src/hooks/useMusicBeatAnalysis.ts +13 -0
- package/src/hooks/useTimelineDeleteOps.ts +170 -0
- package/src/hooks/useTimelineEditing.ts +53 -129
- package/src/player/components/AudioWaveform.tsx +29 -26
- package/src/player/components/BeatStrip.test.tsx +5 -9
- package/src/player/components/BeatStrip.tsx +12 -5
- package/src/player/components/ClipContextMenu.tsx +8 -2
- package/src/player/components/EditModal.tsx +48 -7
- package/src/player/components/KeyframeDiamondContextMenu.tsx +90 -10
- package/src/player/components/LayerDisclosureRow.tsx +12 -13
- package/src/player/components/Player.tsx +43 -8
- package/src/player/components/PlayerControls.tsx +107 -63
- package/src/player/components/ShortcutsPanel.tsx +23 -4
- package/src/player/components/SpeedMenu.tsx +34 -42
- package/src/player/components/Timeline.test.ts +7 -7
- package/src/player/components/Timeline.tsx +7 -10
- package/src/player/components/TimelineFxButton.test.tsx +81 -0
- package/src/player/components/TimelineFxButton.tsx +143 -0
- package/src/player/components/TimelineGroupBusStrip.test.tsx +146 -0
- package/src/player/components/TimelineGroupBusStrip.tsx +103 -0
- package/src/player/components/TimelineGroupHeader.tsx +176 -0
- package/src/player/components/TimelineGroupRow.tsx +146 -0
- package/src/player/components/TimelineLanes.test.tsx +5 -0
- package/src/player/components/TimelineLanes.tsx +39 -33
- package/src/player/components/TimelineOverlays.tsx +34 -0
- package/src/player/components/TimelineSoloButton.tsx +32 -0
- package/src/player/components/TimelineTrackHeader.tsx +74 -98
- package/src/player/components/TimelineTrackPlainHeader.tsx +119 -0
- package/src/player/components/VideoThumbnail.tsx +6 -1
- package/src/player/components/menuKeyboardNav.ts +47 -0
- package/src/player/components/timelineCallbacks.ts +26 -0
- package/src/player/components/timelineKeyboardNavigation.test.ts +8 -0
- package/src/player/components/timelineKeyboardNavigation.ts +147 -30
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineLayout.ts +1 -1
- package/src/player/components/timelineNavigationIdentity.ts +4 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +38 -0
- package/src/player/components/useTimelineLogicalFocus.ts +9 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +8 -0
- package/src/player/components/useTimelineLogicalRows.ts +17 -12
- package/src/player/components/useTimelineMultiDragActorWindows.ts +30 -0
- package/src/player/components/useTimelineTrackDerivations.ts +150 -6
- package/src/player/components/useTimelineTrackLayout.ts +61 -4
- package/src/player/hooks/previewMessageRouter.ts +127 -0
- package/src/player/hooks/useTimelinePlayer.ts +9 -47
- package/src/player/lib/timelineDOM.ts +51 -0
- package/src/player/store/audioSoloSlice.test.ts +113 -0
- package/src/player/store/audioSoloSlice.ts +43 -0
- package/src/player/store/editingModeSlice.ts +45 -0
- package/src/player/store/groupLevels.ts +33 -0
- package/src/player/store/keyframeSlice.ts +26 -0
- package/src/player/store/playerStore.ts +21 -22
- package/src/player/store/timelineElement.ts +10 -0
- package/src/telemetry/events.ts +12 -0
- package/dist/assets/index-Cy6fGN1u.js +0 -428
- package/dist/assets/index-DcF4s1PG.css +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// fallow-ignore-file code-duplication
|
|
2
2
|
import { memo, useState, useCallback, useRef, useEffect } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
+
import { SearchInput } from "../ui/SearchInput";
|
|
5
|
+
import { PromptPreviewModal } from "./PromptPreviewModal";
|
|
4
6
|
import { useBlockCatalog } from "../../hooks/useBlockCatalog";
|
|
5
7
|
import {
|
|
6
8
|
BLOCK_CATEGORIES,
|
|
@@ -18,7 +20,7 @@ export interface BlockPreviewInfo {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
interface BlocksTabProps {
|
|
21
|
-
onAddBlock?: (blockName: string) => void
|
|
23
|
+
onAddBlock?: (blockName: string) => void | Promise<void>;
|
|
22
24
|
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -48,29 +50,12 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
|
|
|
48
50
|
<div className="flex flex-col flex-1 min-h-0">
|
|
49
51
|
{/* Search */}
|
|
50
52
|
<div className="px-3 pt-2 pb-1 flex-shrink-0">
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fill="none"
|
|
58
|
-
stroke="currentColor"
|
|
59
|
-
strokeWidth="2"
|
|
60
|
-
strokeLinecap="round"
|
|
61
|
-
strokeLinejoin="round"
|
|
62
|
-
>
|
|
63
|
-
<circle cx="11" cy="11" r="8" />
|
|
64
|
-
<path d="m21 21-4.35-4.35" />
|
|
65
|
-
</svg>
|
|
66
|
-
<input
|
|
67
|
-
type="text"
|
|
68
|
-
value={search}
|
|
69
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
70
|
-
placeholder="Search by name, category, or tag…"
|
|
71
|
-
className="w-full bg-neutral-900 border border-neutral-800 rounded-md pl-7 pr-2 py-1.5 text-[11px] text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-neutral-700 transition-colors"
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
53
|
+
<SearchInput
|
|
54
|
+
value={search}
|
|
55
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
56
|
+
placeholder="Search by name, category, or tag…"
|
|
57
|
+
aria-label="Search blocks"
|
|
58
|
+
/>
|
|
74
59
|
</div>
|
|
75
60
|
|
|
76
61
|
{/* Category pills */}
|
|
@@ -165,7 +150,8 @@ function CategoryPill({
|
|
|
165
150
|
<button
|
|
166
151
|
type="button"
|
|
167
152
|
onClick={onClick}
|
|
168
|
-
|
|
153
|
+
aria-pressed={active}
|
|
154
|
+
className={`flex-shrink-0 px-2 py-1 rounded-full text-[10px] font-medium transition-colors active:scale-[0.98] ${
|
|
169
155
|
active
|
|
170
156
|
? colors
|
|
171
157
|
? `${colors.bg} ${colors.text}`
|
|
@@ -304,12 +290,12 @@ function BlockCard({
|
|
|
304
290
|
tags?: string[];
|
|
305
291
|
posterUrl?: string;
|
|
306
292
|
videoUrl?: string;
|
|
307
|
-
onAdd?: () => void
|
|
293
|
+
onAdd?: () => void | Promise<void>;
|
|
308
294
|
onShowPrompt?: (info: { title: string; prompt: string }) => void;
|
|
309
295
|
onPreview?: (preview: BlockPreviewInfo | null) => void;
|
|
310
296
|
}) {
|
|
311
297
|
const [hovered, setHovered] = useState(false);
|
|
312
|
-
const [
|
|
298
|
+
const [addState, setAddState] = useState<"idle" | "adding" | "added" | "failed">("idle");
|
|
313
299
|
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
314
300
|
const colors = getCategoryColors(category);
|
|
315
301
|
const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
|
|
@@ -337,14 +323,20 @@ function BlockCard({
|
|
|
337
323
|
}, []);
|
|
338
324
|
|
|
339
325
|
const handleAdd = useCallback(
|
|
340
|
-
(e: React.MouseEvent) => {
|
|
326
|
+
async (e: React.MouseEvent) => {
|
|
341
327
|
e.stopPropagation();
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
328
|
+
if (addState !== "idle" || !onAdd) return;
|
|
329
|
+
setAddState("adding");
|
|
330
|
+
try {
|
|
331
|
+
// Confirm only what actually happened — no optimistic "Added!".
|
|
332
|
+
await onAdd();
|
|
333
|
+
setAddState("added");
|
|
334
|
+
} catch {
|
|
335
|
+
setAddState("failed");
|
|
336
|
+
}
|
|
337
|
+
setTimeout(() => setAddState("idle"), 1500);
|
|
346
338
|
},
|
|
347
|
-
[onAdd,
|
|
339
|
+
[onAdd, addState],
|
|
348
340
|
);
|
|
349
341
|
|
|
350
342
|
const { activeCompPath, compositionDimensions } = useStudioShellContext();
|
|
@@ -422,14 +414,18 @@ function BlockCard({
|
|
|
422
414
|
</div>
|
|
423
415
|
)}
|
|
424
416
|
|
|
425
|
-
{/* Action overlay */}
|
|
426
|
-
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 bg-black/60 opacity-0 group-hover/card:opacity-100 transition-opacity">
|
|
417
|
+
{/* Action overlay — also revealed when a button inside receives focus */}
|
|
418
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 bg-black/60 opacity-0 group-hover/card:opacity-100 group-focus-within/card:opacity-100 transition-opacity">
|
|
427
419
|
{onAdd && (
|
|
428
420
|
<button
|
|
429
421
|
type="button"
|
|
430
422
|
onClick={handleAdd}
|
|
431
423
|
title="Add to composition at current time"
|
|
432
|
-
className=
|
|
424
|
+
className={`flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-semibold transition-colors active:scale-[0.97] ${
|
|
425
|
+
addState === "failed"
|
|
426
|
+
? "bg-red-500 text-white"
|
|
427
|
+
: "bg-white text-black hover:bg-neutral-200"
|
|
428
|
+
}`}
|
|
433
429
|
>
|
|
434
430
|
<svg
|
|
435
431
|
width="10"
|
|
@@ -441,14 +437,20 @@ function BlockCard({
|
|
|
441
437
|
>
|
|
442
438
|
<path d="M12 5v14M5 12h14" />
|
|
443
439
|
</svg>
|
|
444
|
-
{
|
|
440
|
+
{addState === "adding"
|
|
441
|
+
? "Adding…"
|
|
442
|
+
: addState === "added"
|
|
443
|
+
? "Added!"
|
|
444
|
+
: addState === "failed"
|
|
445
|
+
? "Failed"
|
|
446
|
+
: "Add"}
|
|
445
447
|
</button>
|
|
446
448
|
)}
|
|
447
449
|
<button
|
|
448
450
|
type="button"
|
|
449
451
|
onClick={handleShowPrompt}
|
|
450
452
|
title="Generate a prompt to paste into your AI agent"
|
|
451
|
-
className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors ${
|
|
453
|
+
className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors active:scale-[0.97] ${
|
|
452
454
|
onAdd
|
|
453
455
|
? "bg-white/15 text-white/90 hover:bg-white/25 text-[9px]"
|
|
454
456
|
: "bg-white text-black hover:bg-neutral-200 text-[10px] font-semibold"
|
|
@@ -499,93 +501,3 @@ function BlockCard({
|
|
|
499
501
|
</div>
|
|
500
502
|
);
|
|
501
503
|
}
|
|
502
|
-
|
|
503
|
-
function PromptPreviewModal({
|
|
504
|
-
title,
|
|
505
|
-
prompt,
|
|
506
|
-
onClose,
|
|
507
|
-
}: {
|
|
508
|
-
title: string;
|
|
509
|
-
prompt: string;
|
|
510
|
-
onClose: () => void;
|
|
511
|
-
}) {
|
|
512
|
-
const [value, setValue] = useState(prompt);
|
|
513
|
-
const [copied, setCopied] = useState(false);
|
|
514
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
515
|
-
|
|
516
|
-
useEffect(() => {
|
|
517
|
-
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
518
|
-
}, []);
|
|
519
|
-
|
|
520
|
-
const handleCopy = useCallback(() => {
|
|
521
|
-
navigator.clipboard.writeText(value);
|
|
522
|
-
setCopied(true);
|
|
523
|
-
setTimeout(() => setCopied(false), 1500);
|
|
524
|
-
}, [value]);
|
|
525
|
-
|
|
526
|
-
return (
|
|
527
|
-
<div
|
|
528
|
-
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
529
|
-
onClick={onClose}
|
|
530
|
-
>
|
|
531
|
-
<div
|
|
532
|
-
className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl"
|
|
533
|
-
onClick={(e) => e.stopPropagation()}
|
|
534
|
-
>
|
|
535
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
536
|
-
<div>
|
|
537
|
-
<h3 className="text-sm font-medium text-neutral-200">Ask agent</h3>
|
|
538
|
-
<p className="text-xs text-neutral-500 mt-0.5">{title}</p>
|
|
539
|
-
</div>
|
|
540
|
-
<button
|
|
541
|
-
className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50"
|
|
542
|
-
onClick={onClose}
|
|
543
|
-
>
|
|
544
|
-
<svg
|
|
545
|
-
width="14"
|
|
546
|
-
height="14"
|
|
547
|
-
viewBox="0 0 24 24"
|
|
548
|
-
fill="none"
|
|
549
|
-
stroke="currentColor"
|
|
550
|
-
strokeWidth="2"
|
|
551
|
-
strokeLinecap="round"
|
|
552
|
-
>
|
|
553
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
554
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
555
|
-
</svg>
|
|
556
|
-
</button>
|
|
557
|
-
</div>
|
|
558
|
-
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
559
|
-
<p className="text-[11px] text-neutral-500 mb-2">
|
|
560
|
-
Edit the prompt below, then copy and paste into your AI agent
|
|
561
|
-
</p>
|
|
562
|
-
<textarea
|
|
563
|
-
ref={textareaRef}
|
|
564
|
-
value={value}
|
|
565
|
-
onChange={(e) => setValue(e.target.value)}
|
|
566
|
-
onKeyDown={(e) => {
|
|
567
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleCopy();
|
|
568
|
-
if (e.key === "Escape") onClose();
|
|
569
|
-
}}
|
|
570
|
-
className="w-full min-h-[240px] text-[11px] text-neutral-200 leading-relaxed font-mono bg-neutral-900/60 rounded-lg p-3 border border-neutral-800 resize-y focus:outline-none focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
|
|
571
|
-
/>
|
|
572
|
-
</div>
|
|
573
|
-
<div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
|
|
574
|
-
<span className="text-[11px] text-neutral-600">
|
|
575
|
-
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to copy
|
|
576
|
-
</span>
|
|
577
|
-
<button
|
|
578
|
-
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
|
579
|
-
copied
|
|
580
|
-
? "bg-emerald-500 text-white"
|
|
581
|
-
: "bg-studio-accent/90 text-neutral-950 hover:bg-studio-accent"
|
|
582
|
-
}`}
|
|
583
|
-
onClick={handleCopy}
|
|
584
|
-
>
|
|
585
|
-
{copied ? "Copied!" : "Copy prompt"}
|
|
586
|
-
</button>
|
|
587
|
-
</div>
|
|
588
|
-
</div>
|
|
589
|
-
</div>
|
|
590
|
-
);
|
|
591
|
-
}
|
|
@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
|
|
3
3
|
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";
|
|
4
4
|
import { TIMELINE_COMPOSITION_MIME } from "../../utils/timelineCompositionDrop";
|
|
5
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
5
6
|
|
|
6
7
|
interface CompositionsTabProps {
|
|
7
8
|
projectId: string;
|
|
@@ -201,6 +202,8 @@ function CompCard({
|
|
|
201
202
|
role="button"
|
|
202
203
|
tabIndex={0}
|
|
203
204
|
draggable
|
|
205
|
+
aria-label={`Open composition ${name}`}
|
|
206
|
+
aria-pressed={isActive}
|
|
204
207
|
onDragStart={(event) => {
|
|
205
208
|
draggedRef.current = true;
|
|
206
209
|
event.dataTransfer.effectAllowed = "copy";
|
|
@@ -215,6 +218,8 @@ function CompCard({
|
|
|
215
218
|
if (!draggedRef.current) onSelect();
|
|
216
219
|
}}
|
|
217
220
|
onKeyDown={(event) => {
|
|
221
|
+
// Only when the row itself is focused — keydowns bubbling from the
|
|
222
|
+
// inner controls (play button) must keep their native activation.
|
|
218
223
|
if (event.target !== event.currentTarget) return;
|
|
219
224
|
if (event.key === "Enter" || event.key === " ") {
|
|
220
225
|
event.preventDefault();
|
|
@@ -223,7 +228,7 @@ function CompCard({
|
|
|
223
228
|
}}
|
|
224
229
|
onPointerEnter={handleEnter}
|
|
225
230
|
onPointerLeave={handleLeave}
|
|
226
|
-
className={`group/card w-full select-none text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-grab active:cursor-grabbing ${
|
|
231
|
+
className={`group/card w-full select-none text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-grab active:cursor-grabbing outline-none focus-visible:bg-neutral-800/60 ${
|
|
227
232
|
isActive
|
|
228
233
|
? "bg-studio-accent/10 border-l-2 border-studio-accent"
|
|
229
234
|
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
@@ -288,7 +293,12 @@ function CompCard({
|
|
|
288
293
|
<div className="flex items-center gap-1">
|
|
289
294
|
<span className="text-[11px] font-medium text-neutral-300 truncate">{name}</span>
|
|
290
295
|
{lintInfo && lintInfo.count > 0 && (
|
|
291
|
-
<span
|
|
296
|
+
<span
|
|
297
|
+
aria-label={`${lintInfo.count} lint finding${lintInfo.count === 1 ? "" : "s"}`}
|
|
298
|
+
className="flex-shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400"
|
|
299
|
+
>
|
|
300
|
+
{lintInfo.count}
|
|
301
|
+
</span>
|
|
292
302
|
)}
|
|
293
303
|
</div>
|
|
294
304
|
<span className="text-[9px] text-neutral-600 truncate block">{comp}</span>
|
|
@@ -308,39 +318,41 @@ function CompCard({
|
|
|
308
318
|
</button>
|
|
309
319
|
)}
|
|
310
320
|
{onRender && (
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
>
|
|
329
|
-
<svg
|
|
330
|
-
width="14"
|
|
331
|
-
height="14"
|
|
332
|
-
viewBox="0 0 24 24"
|
|
333
|
-
fill="none"
|
|
334
|
-
stroke="currentColor"
|
|
335
|
-
strokeWidth="2"
|
|
336
|
-
strokeLinecap="round"
|
|
337
|
-
strokeLinejoin="round"
|
|
321
|
+
<Tooltip label={isRendering ? "A render is already in progress" : `Render ${name}`}>
|
|
322
|
+
<button
|
|
323
|
+
type="button"
|
|
324
|
+
aria-label={isRendering ? "A render is already in progress" : `Render ${name}`}
|
|
325
|
+
disabled={isRendering}
|
|
326
|
+
onClick={(e) => {
|
|
327
|
+
e.stopPropagation();
|
|
328
|
+
onRender();
|
|
329
|
+
}}
|
|
330
|
+
// h-6 w-6 = the 24x24 WCAG 2.2 (2.5.8) minimum target; the 14px glyph
|
|
331
|
+
// is unchanged, only the box grows. The sibling "+" button is h-8 w-8,
|
|
332
|
+
// so the card row already has the room.
|
|
333
|
+
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded transition-colors ${
|
|
334
|
+
isRendering
|
|
335
|
+
? "text-neutral-600 cursor-not-allowed"
|
|
336
|
+
: "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
|
|
337
|
+
}`}
|
|
338
338
|
>
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
<svg
|
|
340
|
+
width="14"
|
|
341
|
+
height="14"
|
|
342
|
+
viewBox="0 0 24 24"
|
|
343
|
+
fill="none"
|
|
344
|
+
stroke="currentColor"
|
|
345
|
+
strokeWidth="2"
|
|
346
|
+
strokeLinecap="round"
|
|
347
|
+
strokeLinejoin="round"
|
|
348
|
+
aria-hidden="true"
|
|
349
|
+
>
|
|
350
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
351
|
+
<polyline points="7 10 12 15 17 10" />
|
|
352
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
353
|
+
</svg>
|
|
354
|
+
</button>
|
|
355
|
+
</Tooltip>
|
|
344
356
|
)}
|
|
345
357
|
</div>
|
|
346
358
|
);
|
|
@@ -24,6 +24,13 @@ export interface LeftSidebarHandle {
|
|
|
24
24
|
|
|
25
25
|
const STORAGE_KEY = "hf-studio-sidebar-tab";
|
|
26
26
|
|
|
27
|
+
const SIDEBAR_TABS: Array<{ id: SidebarTab; label: string; tooltip: string }> = [
|
|
28
|
+
{ id: "code", label: "Code", tooltip: "Source code editor" },
|
|
29
|
+
{ id: "compositions", label: "Comps", tooltip: "Compositions and sub-compositions" },
|
|
30
|
+
{ id: "assets", label: "Assets", tooltip: "Videos, images, audio, fonts" },
|
|
31
|
+
{ id: "blocks", label: "Catalog", tooltip: "Browse blocks and components" },
|
|
32
|
+
];
|
|
33
|
+
|
|
27
34
|
// Both the `localStorage` reference and `getItem` itself can throw when the
|
|
28
35
|
// browsing context is partitioned or site data is blocked — the same case
|
|
29
36
|
// telemetry/config.ts documents. This runs as a `useState` initializer, so an
|
|
@@ -49,7 +56,7 @@ interface LeftSidebarProps {
|
|
|
49
56
|
assets: string[];
|
|
50
57
|
activeComposition: string | null;
|
|
51
58
|
onSelectComposition: (comp: string) => void;
|
|
52
|
-
onImportFiles?: (files: FileList, dir?: string) => void
|
|
59
|
+
onImportFiles?: (files: FileList, dir?: string) => void | Promise<void>;
|
|
53
60
|
fileTree?: string[];
|
|
54
61
|
editingFile?: { path: string; content: string | null } | null;
|
|
55
62
|
onSelectFile?: (path: string) => void;
|
|
@@ -67,7 +74,7 @@ interface LeftSidebarProps {
|
|
|
67
74
|
lintFindingCount?: number;
|
|
68
75
|
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
69
76
|
onToggleCollapse?: () => void;
|
|
70
|
-
onAddBlock?: (blockName: string) => void
|
|
77
|
+
onAddBlock?: (blockName: string) => void | Promise<void>;
|
|
71
78
|
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
72
79
|
takeoverContent?: ReactNode;
|
|
73
80
|
onAddAssetToTimeline?: (path: string) => void;
|
|
@@ -112,6 +119,7 @@ export const LeftSidebar = memo(
|
|
|
112
119
|
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
|
|
113
120
|
const tabRef = useRef(tab);
|
|
114
121
|
tabRef.current = tab;
|
|
122
|
+
const tablistRef = useRef<HTMLDivElement>(null);
|
|
115
123
|
|
|
116
124
|
const selectTab = useCallback((t: SidebarTab) => {
|
|
117
125
|
setTab(t);
|
|
@@ -127,6 +135,20 @@ export const LeftSidebar = memo(
|
|
|
127
135
|
|
|
128
136
|
useImperativeHandle(ref, () => ({ selectTab, getTab }), [selectTab, getTab]);
|
|
129
137
|
|
|
138
|
+
// APG tabs pattern: Left/Right move focus AND selection between tabs.
|
|
139
|
+
const handleTablistKeyDown = useCallback(
|
|
140
|
+
(e: React.KeyboardEvent) => {
|
|
141
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
const ids = SIDEBAR_TABS.map((t) => t.id);
|
|
144
|
+
const idx = ids.indexOf(tabRef.current);
|
|
145
|
+
const next = ids[(idx + (e.key === "ArrowRight" ? 1 : -1) + ids.length) % ids.length];
|
|
146
|
+
selectTab(next);
|
|
147
|
+
tablistRef.current?.querySelector<HTMLButtonElement>(`[data-tab-id="${next}"]`)?.focus();
|
|
148
|
+
},
|
|
149
|
+
[selectTab],
|
|
150
|
+
);
|
|
151
|
+
|
|
130
152
|
return (
|
|
131
153
|
<div
|
|
132
154
|
className="flex flex-col h-full overflow-hidden rounded-lg border border-neutral-800/50 bg-neutral-950"
|
|
@@ -140,61 +162,33 @@ export const LeftSidebar = memo(
|
|
|
140
162
|
<div className="border-b border-neutral-800/50 px-3 py-3 flex-shrink-0">
|
|
141
163
|
<div className="flex items-center gap-2">
|
|
142
164
|
<div
|
|
165
|
+
ref={tablistRef}
|
|
166
|
+
role="tablist"
|
|
167
|
+
aria-label="Sidebar panels"
|
|
168
|
+
onKeyDown={handleTablistKeyDown}
|
|
143
169
|
className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
|
144
170
|
style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
|
|
145
171
|
>
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
tab
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
167
|
-
}`}
|
|
168
|
-
>
|
|
169
|
-
Comps
|
|
170
|
-
</button>
|
|
171
|
-
</Tooltip>
|
|
172
|
-
<Tooltip label="Videos, images, audio, fonts" side="bottom">
|
|
173
|
-
<button
|
|
174
|
-
type="button"
|
|
175
|
-
onClick={() => selectTab("assets")}
|
|
176
|
-
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
177
|
-
tab === "assets"
|
|
178
|
-
? "bg-neutral-800 text-white"
|
|
179
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
180
|
-
}`}
|
|
181
|
-
>
|
|
182
|
-
Assets
|
|
183
|
-
</button>
|
|
184
|
-
</Tooltip>
|
|
185
|
-
<Tooltip label="Browse blocks and components" side="bottom">
|
|
186
|
-
<button
|
|
187
|
-
type="button"
|
|
188
|
-
onClick={() => selectTab("blocks")}
|
|
189
|
-
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
190
|
-
tab === "blocks"
|
|
191
|
-
? "bg-neutral-800 text-white"
|
|
192
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
193
|
-
}`}
|
|
194
|
-
>
|
|
195
|
-
Catalog
|
|
196
|
-
</button>
|
|
197
|
-
</Tooltip>
|
|
172
|
+
{SIDEBAR_TABS.map((t) => (
|
|
173
|
+
<Tooltip key={t.id} label={t.tooltip} side="bottom">
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
role="tab"
|
|
177
|
+
data-tab-id={t.id}
|
|
178
|
+
aria-selected={tab === t.id}
|
|
179
|
+
aria-controls={`sidebar-panel-${t.id}`}
|
|
180
|
+
tabIndex={tab === t.id ? 0 : -1}
|
|
181
|
+
onClick={() => selectTab(t.id)}
|
|
182
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all active:scale-[0.97] ${
|
|
183
|
+
tab === t.id
|
|
184
|
+
? "bg-neutral-800 text-white"
|
|
185
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
186
|
+
}`}
|
|
187
|
+
>
|
|
188
|
+
{t.label}
|
|
189
|
+
</button>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
))}
|
|
198
192
|
</div>
|
|
199
193
|
{onToggleCollapse && (
|
|
200
194
|
<button
|
|
@@ -225,29 +219,41 @@ export const LeftSidebar = memo(
|
|
|
225
219
|
|
|
226
220
|
{/* Tab content */}
|
|
227
221
|
{tab === "compositions" && (
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
222
|
+
<div
|
|
223
|
+
id="sidebar-panel-compositions"
|
|
224
|
+
role="tabpanel"
|
|
225
|
+
className="flex flex-col flex-1 min-h-0"
|
|
226
|
+
>
|
|
227
|
+
<CompositionsTab
|
|
228
|
+
projectId={projectId}
|
|
229
|
+
compositions={compositions}
|
|
230
|
+
activeComposition={activeComposition}
|
|
231
|
+
onSelect={onSelectComposition}
|
|
232
|
+
onAddToTimeline={onAddCompositionToTimeline}
|
|
233
|
+
onRenderComposition={onRenderComposition}
|
|
234
|
+
isRendering={isRendering}
|
|
235
|
+
lintFindingsByFile={lintFindingsByFile}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
238
|
)}
|
|
239
239
|
{tab === "assets" && (
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
<div
|
|
241
|
+
id="sidebar-panel-assets"
|
|
242
|
+
role="tabpanel"
|
|
243
|
+
className="flex flex-col flex-1 min-h-0"
|
|
244
|
+
>
|
|
245
|
+
<AssetsTab
|
|
246
|
+
projectId={projectId}
|
|
247
|
+
assets={assets}
|
|
248
|
+
onImport={onImportFiles}
|
|
249
|
+
onDelete={onDeleteFile}
|
|
250
|
+
onRename={onRenameFile}
|
|
251
|
+
onAddAssetToTimeline={onAddAssetToTimeline}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
248
254
|
)}
|
|
249
255
|
{tab === "code" && (
|
|
250
|
-
<div className="flex flex-1 min-h-0">
|
|
256
|
+
<div id="sidebar-panel-code" role="tabpanel" className="flex flex-1 min-h-0">
|
|
251
257
|
{(fileProp?.length ?? 0) > 0 && (
|
|
252
258
|
<div className="w-[160px] flex-shrink-0 border-r border-neutral-800 overflow-y-auto">
|
|
253
259
|
<FileTree
|
|
@@ -276,7 +282,13 @@ export const LeftSidebar = memo(
|
|
|
276
282
|
)}
|
|
277
283
|
|
|
278
284
|
{tab === "blocks" && (
|
|
279
|
-
<
|
|
285
|
+
<div
|
|
286
|
+
id="sidebar-panel-blocks"
|
|
287
|
+
role="tabpanel"
|
|
288
|
+
className="flex flex-col flex-1 min-h-0"
|
|
289
|
+
>
|
|
290
|
+
<BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
|
|
291
|
+
</div>
|
|
280
292
|
)}
|
|
281
293
|
|
|
282
294
|
{/* Lint button pinned at the bottom */}
|
|
@@ -285,7 +297,7 @@ export const LeftSidebar = memo(
|
|
|
285
297
|
<button
|
|
286
298
|
onClick={onLint}
|
|
287
299
|
disabled={linting}
|
|
288
|
-
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 hover:text-amber-300 hover:bg-neutral-800 transition-colors disabled:opacity-40"
|
|
300
|
+
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 enabled:hover:text-amber-300 enabled:hover:bg-neutral-800 enabled:active:scale-[0.98] transition-colors disabled:opacity-40"
|
|
289
301
|
>
|
|
290
302
|
<svg
|
|
291
303
|
width="12"
|