@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
|
@@ -77,8 +77,8 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
77
77
|
disabled={!isComplete}
|
|
78
78
|
aria-label={isComplete ? `Open ${job.filename} in a new tab` : undefined}
|
|
79
79
|
className={[
|
|
80
|
-
"w-20 h-[45px] rounded-md overflow-hidden bg-panel-input
|
|
81
|
-
"outline-
|
|
80
|
+
"w-20 h-[45px] rounded-md overflow-hidden bg-panel-input shrink-0 relative",
|
|
81
|
+
"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent",
|
|
82
82
|
isComplete ? "cursor-pointer" : "cursor-default",
|
|
83
83
|
].join(" ")}
|
|
84
84
|
>
|
|
@@ -130,7 +130,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
130
130
|
{job.filename}
|
|
131
131
|
</span>
|
|
132
132
|
{job.durationMs && (
|
|
133
|
-
<span className="text-[9px] text-panel-text-5
|
|
133
|
+
<span className="text-[9px] text-panel-text-5 shrink-0">
|
|
134
134
|
{formatDuration(job.durationMs)}
|
|
135
135
|
</span>
|
|
136
136
|
)}
|
|
@@ -171,7 +171,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
171
171
|
</div>
|
|
172
172
|
|
|
173
173
|
{/* Actions — always visible to prevent layout shifts */}
|
|
174
|
-
<div className="flex items-center gap-1.5
|
|
174
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
175
175
|
{isRendering ? (
|
|
176
176
|
<Button
|
|
177
177
|
size="sm"
|
|
@@ -211,7 +211,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
211
211
|
<>
|
|
212
212
|
<button
|
|
213
213
|
onClick={isComplete ? handleDownload : undefined}
|
|
214
|
-
className={`p-1.5 min-w-6 min-h-6 rounded transition-colors outline-
|
|
214
|
+
className={`p-1.5 min-w-6 min-h-6 rounded transition-colors outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent ${
|
|
215
215
|
isComplete
|
|
216
216
|
? "text-panel-text-5 hover:text-panel-accent"
|
|
217
217
|
: "text-panel-text-5/30 cursor-default"
|
|
@@ -240,7 +240,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
240
240
|
e.stopPropagation();
|
|
241
241
|
setConfirmingDelete(true);
|
|
242
242
|
}}
|
|
243
|
-
className="p-1.5 min-w-6 min-h-6 rounded text-panel-text-5 hover:text-red-400 transition-colors outline-
|
|
243
|
+
className="p-1.5 min-w-6 min-h-6 rounded-sm text-panel-text-5 hover:text-red-400 transition-colors outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-studio-accent"
|
|
244
244
|
title="Delete render file"
|
|
245
245
|
aria-label={`Delete ${job.filename}`}
|
|
246
246
|
>
|
|
@@ -30,7 +30,7 @@ function CopyChip({ feedback, asset }: { feedback: CopyFeedback; asset: string }
|
|
|
30
30
|
return (
|
|
31
31
|
<span
|
|
32
32
|
role="status"
|
|
33
|
-
className={`
|
|
33
|
+
className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
|
|
34
34
|
feedback.ok ? "text-panel-accent bg-panel-accent/10" : "text-red-400 bg-red-500/10"
|
|
35
35
|
}`}
|
|
36
36
|
>
|
|
@@ -223,12 +223,12 @@ export function AssetCard({
|
|
|
223
223
|
onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
|
|
224
224
|
onPointerEnter={() => setHovered(true)}
|
|
225
225
|
onPointerLeave={() => setHovered(false)}
|
|
226
|
-
className={`flex flex-col gap-1 cursor-pointer rounded-md p-1 transition-colors outline-
|
|
226
|
+
className={`flex flex-col gap-1 cursor-pointer rounded-md p-1 transition-colors outline-hidden focus-visible:bg-neutral-800/60 ${
|
|
227
227
|
isCopied ? "bg-studio-accent/10" : "hover:bg-neutral-800/40"
|
|
228
228
|
}`}
|
|
229
229
|
>
|
|
230
230
|
{/* Thumbnail */}
|
|
231
|
-
<div className="w-full aspect-video rounded overflow-hidden bg-neutral-900 relative">
|
|
231
|
+
<div className="w-full aspect-video rounded-sm overflow-hidden bg-neutral-900 relative">
|
|
232
232
|
{isImage && !imgError && (
|
|
233
233
|
<img
|
|
234
234
|
src={serveUrl}
|
|
@@ -261,14 +261,14 @@ export function AssetCard({
|
|
|
261
261
|
|
|
262
262
|
{/* "Added" badge — top-left */}
|
|
263
263
|
{used && (
|
|
264
|
-
<span className="absolute top-1 left-1 text-[9px] font-semibold leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-1">
|
|
264
|
+
<span className="absolute top-1 left-1 text-[9px] font-semibold leading-none px-1.5 py-[3px] rounded-sm bg-neutral-950/80 text-panel-text-1">
|
|
265
265
|
Added
|
|
266
266
|
</span>
|
|
267
267
|
)}
|
|
268
268
|
|
|
269
269
|
{/* Duration badge — top-right, media only */}
|
|
270
270
|
{durationLabel && (
|
|
271
|
-
<span className="absolute top-1 right-1 text-[9px] font-medium leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-2 tabular-nums">
|
|
271
|
+
<span className="absolute top-1 right-1 text-[9px] font-medium leading-none px-1.5 py-[3px] rounded-sm bg-neutral-950/80 text-panel-text-2 tabular-nums">
|
|
272
272
|
{durationLabel}
|
|
273
273
|
</span>
|
|
274
274
|
)}
|
|
@@ -348,13 +348,13 @@ export function FontRow({
|
|
|
348
348
|
}}
|
|
349
349
|
onDragStart={(e) => writeAssetDragData(e, asset)}
|
|
350
350
|
onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
|
|
351
|
-
className={`px-2.5 py-1.5 flex items-center gap-2.5 cursor-pointer transition-colors outline-
|
|
351
|
+
className={`px-2.5 py-1.5 flex items-center gap-2.5 cursor-pointer transition-colors outline-hidden focus-visible:bg-neutral-800/60 ${
|
|
352
352
|
isCopied
|
|
353
353
|
? "bg-studio-accent/10 border-l-2 border-studio-accent"
|
|
354
354
|
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
355
355
|
}`}
|
|
356
356
|
>
|
|
357
|
-
<div className="w-[50px] h-[32px] rounded overflow-hidden bg-neutral-900
|
|
357
|
+
<div className="w-[50px] h-[32px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 flex items-center justify-center">
|
|
358
358
|
<span className="text-[9px] font-medium text-neutral-700">{extension}</span>
|
|
359
359
|
</div>
|
|
360
360
|
<div className="min-w-0 flex-1">
|
|
@@ -366,7 +366,7 @@ export function FontRow({
|
|
|
366
366
|
<div className="flex items-center gap-1.5">
|
|
367
367
|
<span className="text-[10px] text-neutral-600 truncate">{extension}</span>
|
|
368
368
|
{used && (
|
|
369
|
-
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded">
|
|
369
|
+
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm">
|
|
370
370
|
in use
|
|
371
371
|
</span>
|
|
372
372
|
)}
|
|
@@ -95,11 +95,11 @@ export function ContextMenu({
|
|
|
95
95
|
}, [renameDraft, asset, onRename, onClose]);
|
|
96
96
|
|
|
97
97
|
const itemCls =
|
|
98
|
-
"w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-
|
|
98
|
+
"w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden active:bg-neutral-700/70 transition-colors";
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
101
|
<div
|
|
102
|
-
className="fixed inset-0 z-
|
|
102
|
+
className="fixed inset-0 z-200"
|
|
103
103
|
onClick={onClose}
|
|
104
104
|
onContextMenu={(e) => {
|
|
105
105
|
e.preventDefault();
|
|
@@ -181,19 +181,19 @@ export function ContextMenu({
|
|
|
181
181
|
}
|
|
182
182
|
}}
|
|
183
183
|
aria-label={`Rename ${filename(asset)}`}
|
|
184
|
-
className="w-full bg-neutral-800 border border-neutral-600 rounded px-1.5 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-
|
|
184
|
+
className="w-full bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
|
|
185
185
|
/>
|
|
186
186
|
{renameError && <span className="text-[10px] text-red-400">{renameError}</span>}
|
|
187
187
|
<div className="flex items-center justify-end gap-1">
|
|
188
188
|
<button
|
|
189
189
|
onClick={() => setMode("menu")}
|
|
190
|
-
className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
190
|
+
className="px-2 py-0.5 text-[10px] rounded-sm text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
191
191
|
>
|
|
192
192
|
Cancel
|
|
193
193
|
</button>
|
|
194
194
|
<button
|
|
195
195
|
onClick={commitRename}
|
|
196
|
-
className="px-2 py-0.5 text-[10px] rounded bg-studio-accent/80 hover:bg-studio-accent text-white transition-colors"
|
|
196
|
+
className="px-2 py-0.5 text-[10px] rounded-sm bg-studio-accent/80 hover:bg-studio-accent text-white transition-colors"
|
|
197
197
|
>
|
|
198
198
|
Rename
|
|
199
199
|
</button>
|
|
@@ -217,16 +217,16 @@ function DeleteConfirm({
|
|
|
217
217
|
return (
|
|
218
218
|
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
|
|
219
219
|
<span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
|
|
220
|
-
<div className="flex items-center gap-1
|
|
220
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
221
221
|
<button
|
|
222
222
|
onClick={onConfirm}
|
|
223
|
-
className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
|
|
223
|
+
className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
|
|
224
224
|
>
|
|
225
225
|
Delete
|
|
226
226
|
</button>
|
|
227
227
|
<button
|
|
228
228
|
onClick={onCancel}
|
|
229
|
-
className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
229
|
+
className="px-2 py-0.5 text-[10px] rounded-sm text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
230
230
|
>
|
|
231
231
|
Cancel
|
|
232
232
|
</button>
|
|
@@ -331,7 +331,7 @@ export const AssetsTab = memo(function AssetsTab({
|
|
|
331
331
|
: [activeFilter as MediaCategory].filter((c) => categorized[c].length > 0);
|
|
332
332
|
return (
|
|
333
333
|
<div
|
|
334
|
-
className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-studio-accent/
|
|
334
|
+
className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-studio-accent/5" : ""}`}
|
|
335
335
|
onDragOver={(e) => {
|
|
336
336
|
e.preventDefault();
|
|
337
337
|
setDragOver(true);
|
|
@@ -340,7 +340,7 @@ export const AssetsTab = memo(function AssetsTab({
|
|
|
340
340
|
onDrop={handleDrop}
|
|
341
341
|
>
|
|
342
342
|
{/* Header — matches design panel Section pattern */}
|
|
343
|
-
<div className="px-4 pt-2.5 pb-1.5
|
|
343
|
+
<div className="px-4 pt-2.5 pb-1.5 shrink-0">
|
|
344
344
|
{/* Scope toggle */}
|
|
345
345
|
<div className="flex gap-1 mb-2.5 p-0.5 rounded-md bg-panel-input">
|
|
346
346
|
{(["local", "global"] as const).map((m) => (
|
|
@@ -227,9 +227,9 @@ export function AudioRow({
|
|
|
227
227
|
e.preventDefault();
|
|
228
228
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
229
229
|
}}
|
|
230
|
-
className={`group w-full text-left px-4 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer outline-
|
|
230
|
+
className={`group w-full text-left px-4 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer outline-hidden focus-visible:bg-neutral-800/60 ${
|
|
231
231
|
playing
|
|
232
|
-
? "bg-panel-accent/
|
|
232
|
+
? "bg-panel-accent/6"
|
|
233
233
|
: isCopied
|
|
234
234
|
? "bg-panel-accent/10"
|
|
235
235
|
: "hover:bg-panel-surface-hover"
|
|
@@ -238,7 +238,7 @@ export function AudioRow({
|
|
|
238
238
|
<button
|
|
239
239
|
aria-label={playing ? `Pause preview of ${name}` : `Play preview of ${name}`}
|
|
240
240
|
aria-pressed={playing}
|
|
241
|
-
className={`w-7 h-7 rounded-md
|
|
241
|
+
className={`w-7 h-7 rounded-md shrink-0 flex items-center justify-center transition-colors active:scale-[0.95] ${
|
|
242
242
|
playing
|
|
243
243
|
? "bg-panel-accent/15 text-panel-accent"
|
|
244
244
|
: "text-panel-text-5 group-hover:text-panel-text-3"
|
|
@@ -267,20 +267,20 @@ export function AudioRow({
|
|
|
267
267
|
{name}
|
|
268
268
|
</span>
|
|
269
269
|
{!playing && (
|
|
270
|
-
<span className="text-[11px] text-panel-text-5
|
|
270
|
+
<span className="text-[11px] text-panel-text-5 shrink-0">
|
|
271
271
|
{meta?.duration ? `${meta.duration}s · ` : ""}
|
|
272
272
|
{subtype}
|
|
273
273
|
</span>
|
|
274
274
|
)}
|
|
275
275
|
{used && (
|
|
276
|
-
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded
|
|
276
|
+
<span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm shrink-0">
|
|
277
277
|
in use
|
|
278
278
|
</span>
|
|
279
279
|
)}
|
|
280
280
|
{(isCopied || copyFailed) && (
|
|
281
281
|
<span
|
|
282
282
|
role="status"
|
|
283
|
-
className={`
|
|
283
|
+
className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
|
|
284
284
|
copyFailed ? "text-red-400 bg-red-500/10" : "text-panel-accent bg-panel-accent/10"
|
|
285
285
|
}`}
|
|
286
286
|
>
|
|
@@ -49,7 +49,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
|
|
|
49
49
|
return (
|
|
50
50
|
<div className="flex flex-col flex-1 min-h-0">
|
|
51
51
|
{/* Search */}
|
|
52
|
-
<div className="px-3 pt-2 pb-1
|
|
52
|
+
<div className="px-3 pt-2 pb-1 shrink-0">
|
|
53
53
|
<SearchInput
|
|
54
54
|
value={search}
|
|
55
55
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -59,7 +59,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
|
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
61
|
{/* Category pills */}
|
|
62
|
-
<div className="px-3 pt-1 pb-2
|
|
62
|
+
<div className="px-3 pt-1 pb-2 shrink-0 overflow-x-auto">
|
|
63
63
|
<div className="flex gap-1">
|
|
64
64
|
<CategoryPill label="All" active={category === null} onClick={() => setCategory(null)} />
|
|
65
65
|
{BLOCK_CATEGORIES.map((cat) => (
|
|
@@ -151,7 +151,7 @@ function CategoryPill({
|
|
|
151
151
|
type="button"
|
|
152
152
|
onClick={onClick}
|
|
153
153
|
aria-pressed={active}
|
|
154
|
-
className={`
|
|
154
|
+
className={`shrink-0 px-2 py-1 rounded-full text-[10px] font-medium transition-colors active:scale-[0.98] ${
|
|
155
155
|
active
|
|
156
156
|
? colors
|
|
157
157
|
? `${colors.bg} ${colors.text}`
|
|
@@ -474,12 +474,12 @@ function BlockCard({
|
|
|
474
474
|
{/* Badges */}
|
|
475
475
|
<div className="absolute top-1 right-1 flex items-center gap-0.5 pointer-events-none">
|
|
476
476
|
{needsWebGL && (
|
|
477
|
-
<span className="px-1 py-px rounded text-[7px] font-semibold text-purple-300 bg-purple-900/70">
|
|
477
|
+
<span className="px-1 py-px rounded-sm text-[7px] font-semibold text-purple-300 bg-purple-900/70">
|
|
478
478
|
WebGL
|
|
479
479
|
</span>
|
|
480
480
|
)}
|
|
481
481
|
{duration != null && (
|
|
482
|
-
<span className="px-1 py-px rounded text-[8px] font-medium text-white/80 bg-black/50">
|
|
482
|
+
<span className="px-1 py-px rounded-sm text-[8px] font-medium text-white/80 bg-black/50">
|
|
483
483
|
{duration}s
|
|
484
484
|
</span>
|
|
485
485
|
)}
|
|
@@ -492,7 +492,7 @@ function BlockCard({
|
|
|
492
492
|
{title}
|
|
493
493
|
</div>
|
|
494
494
|
<div className="flex items-center gap-1 mt-0.5">
|
|
495
|
-
<span className={`w-1.5 h-1.5 rounded-full
|
|
495
|
+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${colors.dot}`} />
|
|
496
496
|
<span className={`text-[8px] ${colors.text}`}>
|
|
497
497
|
{BLOCK_CATEGORIES.find((c) => c.id === category)?.label}
|
|
498
498
|
</span>
|
|
@@ -238,13 +238,13 @@ function CompCard({
|
|
|
238
238
|
}}
|
|
239
239
|
onPointerEnter={handleEnter}
|
|
240
240
|
onPointerLeave={handleLeave}
|
|
241
|
-
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-
|
|
241
|
+
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-hidden focus-visible:bg-neutral-800/60 ${
|
|
242
242
|
isActive
|
|
243
243
|
? "bg-studio-accent/10 border-l-2 border-studio-accent"
|
|
244
244
|
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
245
245
|
}`}
|
|
246
246
|
>
|
|
247
|
-
<div className="w-20 h-[45px] rounded overflow-hidden bg-neutral-900
|
|
247
|
+
<div className="w-20 h-[45px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 relative">
|
|
248
248
|
{thumbnailFailed ? (
|
|
249
249
|
<div className="absolute inset-0 flex items-center justify-center px-1 text-center text-[8px] leading-tight text-neutral-600">
|
|
250
250
|
Preview unavailable
|
|
@@ -314,7 +314,7 @@ function CompCard({
|
|
|
314
314
|
{lintInfo && lintInfo.count > 0 && (
|
|
315
315
|
<span
|
|
316
316
|
aria-label={`${lintInfo.count} lint finding${lintInfo.count === 1 ? "" : "s"}`}
|
|
317
|
-
className="
|
|
317
|
+
className="shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400"
|
|
318
318
|
>
|
|
319
319
|
{lintInfo.count}
|
|
320
320
|
</span>
|
|
@@ -331,7 +331,7 @@ function CompCard({
|
|
|
331
331
|
event.stopPropagation();
|
|
332
332
|
onAddToTimeline();
|
|
333
333
|
}}
|
|
334
|
-
className="flex h-8 w-8
|
|
334
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-neutral-600 opacity-0 transition-[color,background-color,opacity] hover:bg-neutral-800 hover:text-studio-accent group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
|
|
335
335
|
>
|
|
336
336
|
<span aria-hidden="true">+</span>
|
|
337
337
|
</button>
|
|
@@ -349,7 +349,7 @@ function CompCard({
|
|
|
349
349
|
// h-6 w-6 = the 24x24 WCAG 2.2 (2.5.8) minimum target; the 14px glyph
|
|
350
350
|
// is unchanged, only the box grows. The sibling "+" button is h-8 w-8,
|
|
351
351
|
// so the card row already has the room.
|
|
352
|
-
className={`flex h-6 w-6
|
|
352
|
+
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors ${
|
|
353
353
|
isRendering
|
|
354
354
|
? "text-neutral-600 cursor-not-allowed"
|
|
355
355
|
: "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
|
|
@@ -80,7 +80,7 @@ export function GlobalAssetsView({ searchQuery }: { searchQuery: string }) {
|
|
|
80
80
|
className="px-4 py-1.5 flex items-center gap-2.5 border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
81
81
|
title={`${row.id} · ${row.type}`}
|
|
82
82
|
>
|
|
83
|
-
<span className="text-[9px] font-medium text-neutral-600 uppercase w-10
|
|
83
|
+
<span className="text-[9px] font-medium text-neutral-600 uppercase w-10 shrink-0">
|
|
84
84
|
{row.type}
|
|
85
85
|
</span>
|
|
86
86
|
<span className="text-xs text-panel-text-1 truncate">{row.label}</span>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { LeftSidebar } from "./LeftSidebar";
|
|
7
|
+
|
|
8
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
9
|
+
(
|
|
10
|
+
window as unknown as { happyDOM: { settings: { disableIframePageLoading: boolean } } }
|
|
11
|
+
).happyDOM.settings.disableIframePageLoading = true;
|
|
12
|
+
|
|
13
|
+
let root: Root | null = null;
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (root) act(() => root?.unmount());
|
|
17
|
+
root = null;
|
|
18
|
+
document.body.innerHTML = "";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function renderBadge(lintHasError: boolean): HTMLElement | null {
|
|
22
|
+
const host = document.createElement("div");
|
|
23
|
+
document.body.append(host);
|
|
24
|
+
root = createRoot(host);
|
|
25
|
+
act(() => {
|
|
26
|
+
root?.render(
|
|
27
|
+
<LeftSidebar
|
|
28
|
+
projectId="demo"
|
|
29
|
+
compositions={["index.html"]}
|
|
30
|
+
assets={[]}
|
|
31
|
+
fileTree={["index.html"]}
|
|
32
|
+
activeComposition={null}
|
|
33
|
+
onSelectComposition={vi.fn()}
|
|
34
|
+
onLint={vi.fn()}
|
|
35
|
+
lintFindingCount={2}
|
|
36
|
+
lintHasError={lintHasError}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
return host.querySelector<HTMLElement>("[data-lint-badge]");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("LeftSidebar lint badge", () => {
|
|
44
|
+
it("pulses, and stops for reduced motion, while an error finding exists", () => {
|
|
45
|
+
const badge = renderBadge(true);
|
|
46
|
+
expect(badge?.dataset.lintBadge).toBe("error");
|
|
47
|
+
expect(badge?.className).toContain("animate-pulse");
|
|
48
|
+
expect(badge?.className).toContain("motion-reduce:animate-none");
|
|
49
|
+
expect(badge?.textContent).toContain("including errors");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("stays still when the findings are warnings only", () => {
|
|
53
|
+
const badge = renderBadge(false);
|
|
54
|
+
expect(badge?.dataset.lintBadge).toBe("warning");
|
|
55
|
+
expect(badge?.className).not.toContain("animate-pulse");
|
|
56
|
+
expect(badge?.textContent).toContain("warnings only");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -74,6 +74,7 @@ interface LeftSidebarProps {
|
|
|
74
74
|
onLint?: () => void;
|
|
75
75
|
linting?: boolean;
|
|
76
76
|
lintFindingCount?: number;
|
|
77
|
+
lintHasError?: boolean;
|
|
77
78
|
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
78
79
|
onToggleCollapse?: () => void;
|
|
79
80
|
onAddBlock?: (blockName: string) => void | Promise<void>;
|
|
@@ -108,6 +109,7 @@ export const LeftSidebar = memo(
|
|
|
108
109
|
onLint,
|
|
109
110
|
linting,
|
|
110
111
|
lintFindingCount,
|
|
112
|
+
lintHasError,
|
|
111
113
|
lintFindingsByFile,
|
|
112
114
|
onToggleCollapse,
|
|
113
115
|
onAddBlock,
|
|
@@ -165,7 +167,7 @@ export const LeftSidebar = memo(
|
|
|
165
167
|
) : (
|
|
166
168
|
<>
|
|
167
169
|
{/* Tabs — Code first */}
|
|
168
|
-
<div className="border-b border-neutral-800/50 px-3 py-3
|
|
170
|
+
<div className="border-b border-neutral-800/50 px-3 py-3 shrink-0">
|
|
169
171
|
<div className="flex items-center gap-2">
|
|
170
172
|
<div
|
|
171
173
|
ref={tablistRef}
|
|
@@ -200,7 +202,7 @@ export const LeftSidebar = memo(
|
|
|
200
202
|
<button
|
|
201
203
|
type="button"
|
|
202
204
|
onClick={onToggleCollapse}
|
|
203
|
-
className="flex h-8 w-8
|
|
205
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
|
204
206
|
title="Hide sidebar"
|
|
205
207
|
aria-label="Hide sidebar"
|
|
206
208
|
>
|
|
@@ -262,7 +264,7 @@ export const LeftSidebar = memo(
|
|
|
262
264
|
{tab === "code" && (
|
|
263
265
|
<div id="sidebar-panel-code" role="tabpanel" className="flex flex-1 min-h-0">
|
|
264
266
|
{(fileProp?.length ?? 0) > 0 && (
|
|
265
|
-
<div className="w-[160px]
|
|
267
|
+
<div className="w-[160px] shrink-0 border-r border-neutral-800 overflow-y-auto">
|
|
266
268
|
<FileTree
|
|
267
269
|
files={fileProp ?? []}
|
|
268
270
|
activeFile={editingFile?.path ?? null}
|
|
@@ -300,7 +302,7 @@ export const LeftSidebar = memo(
|
|
|
300
302
|
|
|
301
303
|
{/* Lint button pinned at the bottom */}
|
|
302
304
|
{onLint && (
|
|
303
|
-
<div className="border-t border-neutral-800 p-2
|
|
305
|
+
<div className="border-t border-neutral-800 p-2 shrink-0">
|
|
304
306
|
<button
|
|
305
307
|
onClick={onLint}
|
|
306
308
|
disabled={linting}
|
|
@@ -319,8 +321,20 @@ export const LeftSidebar = memo(
|
|
|
319
321
|
</svg>
|
|
320
322
|
{linting ? "Linting…" : "Lint"}
|
|
321
323
|
{!linting && lintFindingCount != null && lintFindingCount > 0 && (
|
|
322
|
-
<span
|
|
324
|
+
<span
|
|
325
|
+
data-lint-badge={lintHasError ? "error" : "warning"}
|
|
326
|
+
className={
|
|
327
|
+
lintHasError
|
|
328
|
+
? "ml-1 min-w-[16px] rounded-full bg-panel-danger/25 px-1 text-[9px] font-bold text-panel-danger animate-pulse motion-reduce:animate-none"
|
|
329
|
+
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-amber-400"
|
|
330
|
+
}
|
|
331
|
+
>
|
|
323
332
|
{lintFindingCount}
|
|
333
|
+
<span className="sr-only">
|
|
334
|
+
{lintHasError
|
|
335
|
+
? " lint findings, including errors"
|
|
336
|
+
: " lint findings, warnings only"}
|
|
337
|
+
</span>
|
|
324
338
|
</span>
|
|
325
339
|
)}
|
|
326
340
|
</button>
|
|
@@ -46,7 +46,7 @@ export function PromptPreviewModal({
|
|
|
46
46
|
|
|
47
47
|
return (
|
|
48
48
|
<div
|
|
49
|
-
className={`fixed inset-0 z-
|
|
49
|
+
className={`fixed inset-0 z-100 flex items-center justify-center bg-black/60 backdrop-blur-xs transition-opacity duration-150 ease-out ${
|
|
50
50
|
entered ? "opacity-100" : "opacity-0"
|
|
51
51
|
}`}
|
|
52
52
|
onClick={requestClose}
|
|
@@ -57,7 +57,7 @@ export function PromptPreviewModal({
|
|
|
57
57
|
aria-modal="true"
|
|
58
58
|
aria-label={`Ask agent — ${title}`}
|
|
59
59
|
tabIndex={-1}
|
|
60
|
-
className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl outline-
|
|
60
|
+
className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl outline-hidden"
|
|
61
61
|
onClick={(e) => e.stopPropagation()}
|
|
62
62
|
>
|
|
63
63
|
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
@@ -96,7 +96,7 @@ export function PromptPreviewModal({
|
|
|
96
96
|
onKeyDown={(e) => {
|
|
97
97
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void handleCopy();
|
|
98
98
|
}}
|
|
99
|
-
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-
|
|
99
|
+
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-hidden focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
|
|
100
100
|
/>
|
|
101
101
|
</div>
|
|
102
102
|
<div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// happy-dom has no CSS engine, so this pins the structure that makes the gap
|
|
4
|
+
// work (children are direct flex items of the gap-bearing button), not the
|
|
5
|
+
// computed pixels. The measured gap is verified in a real browser.
|
|
6
|
+
|
|
7
|
+
import { act } from "react";
|
|
8
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
10
|
+
import { Button } from "./Button";
|
|
11
|
+
|
|
12
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
13
|
+
|
|
14
|
+
let root: Root | null = null;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (root) act(() => root?.unmount());
|
|
18
|
+
root = null;
|
|
19
|
+
document.body.innerHTML = "";
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function mount(element: React.ReactNode): HTMLButtonElement {
|
|
23
|
+
const host = document.createElement("div");
|
|
24
|
+
document.body.append(host);
|
|
25
|
+
root = createRoot(host);
|
|
26
|
+
act(() => root?.render(element));
|
|
27
|
+
const button = host.querySelector("button");
|
|
28
|
+
if (!button) throw new Error("button did not render");
|
|
29
|
+
return button;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Button children layout", () => {
|
|
33
|
+
it("lays label and kbd out as sibling flex items of the gap-bearing button", () => {
|
|
34
|
+
const button = mount(
|
|
35
|
+
<Button size="md">
|
|
36
|
+
<span>Export</span>
|
|
37
|
+
<kbd>⌘E</kbd>
|
|
38
|
+
</Button>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(button.classList.contains("inline-flex")).toBe(true);
|
|
42
|
+
expect(button.classList.contains("gap-1.5")).toBe(true);
|
|
43
|
+
expect(Array.from(button.children).map((child) => child.tagName)).toEqual(["SPAN", "KBD"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("puts children next to the icon as direct flex items too", () => {
|
|
47
|
+
const button = mount(
|
|
48
|
+
<Button icon={<svg />}>
|
|
49
|
+
<span>Export</span>
|
|
50
|
+
<kbd>⌘E</kbd>
|
|
51
|
+
</Button>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(Array.from(button.children).map((child) => child.tagName)).toEqual([
|
|
55
|
+
"SPAN",
|
|
56
|
+
"SPAN",
|
|
57
|
+
"KBD",
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -84,7 +84,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
84
84
|
// so a wrapping Tooltip can explain WHY they're disabled (A5).
|
|
85
85
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
86
86
|
"select-none cursor-pointer",
|
|
87
|
-
"outline-
|
|
87
|
+
"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-studio-accent",
|
|
88
88
|
variantStyles[variant],
|
|
89
89
|
sizeStyles[size],
|
|
90
90
|
className,
|
|
@@ -108,9 +108,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
108
108
|
/>
|
|
109
109
|
</svg>
|
|
110
110
|
) : icon ? (
|
|
111
|
-
<span className="
|
|
111
|
+
<span className="shrink-0">{icon}</span>
|
|
112
112
|
) : null}
|
|
113
|
-
{children
|
|
113
|
+
{children}
|
|
114
114
|
</button>
|
|
115
115
|
);
|
|
116
116
|
},
|
|
@@ -144,7 +144,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
144
144
|
"transition-all duration-press ease-standard",
|
|
145
145
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
146
146
|
"select-none cursor-pointer",
|
|
147
|
-
"outline-
|
|
147
|
+
"outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-studio-accent",
|
|
148
148
|
variantStyles[variant],
|
|
149
149
|
iconSizeStyles[size],
|
|
150
150
|
className,
|
|
@@ -21,7 +21,7 @@ export function SearchInput({ className = "", ...props }: SearchInputProps) {
|
|
|
21
21
|
height="12"
|
|
22
22
|
viewBox="0 0 256 256"
|
|
23
23
|
fill="none"
|
|
24
|
-
className="
|
|
24
|
+
className="shrink-0"
|
|
25
25
|
aria-hidden="true"
|
|
26
26
|
>
|
|
27
27
|
<circle
|
|
@@ -45,7 +45,7 @@ export function SearchInput({ className = "", ...props }: SearchInputProps) {
|
|
|
45
45
|
</svg>
|
|
46
46
|
<input
|
|
47
47
|
type="text"
|
|
48
|
-
className="min-w-0 w-full bg-transparent text-[11px] text-panel-text-1 outline-
|
|
48
|
+
className="min-w-0 w-full bg-transparent text-[11px] text-panel-text-1 outline-hidden placeholder:text-panel-text-5"
|
|
49
49
|
{...props}
|
|
50
50
|
/>
|
|
51
51
|
</div>
|
|
@@ -104,7 +104,7 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
|
|
104
104
|
{visible &&
|
|
105
105
|
createPortal(
|
|
106
106
|
<div
|
|
107
|
-
className="fixed z-
|
|
107
|
+
className="fixed z-200 pointer-events-none"
|
|
108
108
|
style={{
|
|
109
109
|
left: clampedX,
|
|
110
110
|
top: pos.y,
|
|
@@ -189,7 +189,7 @@ export function useAppHotkeys({
|
|
|
189
189
|
// earlier file edit (which would ALSO leave the caption change intact).
|
|
190
190
|
const captionState = useCaptionStore.getState();
|
|
191
191
|
// Only when the caption preview is actually visible: isEditMode stays
|
|
192
|
-
// true while the preview is hidden
|
|
192
|
+
// true while the preview is hidden, and eating ⌘Z
|
|
193
193
|
// there would pop invisible caption edits instead of file history.
|
|
194
194
|
if (captionState.isEditMode && isCaptionPreviewVisible()) {
|
|
195
195
|
const restored = direction === "undo" ? captionState.undo() : captionState.redo();
|