@hyperframes/studio 0.8.50 → 0.8.52
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-I5YRzB02.js} +3 -3
- package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
- package/dist/assets/index-CxAu_Okg.css +1 -0
- package/dist/assets/index-DocJlW2v.js +376 -0
- package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
- package/dist/index.d.ts +419 -20
- package/dist/index.html +2 -2
- package/dist/index.js +7496 -8288
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +13 -12
- package/src/App.tsx +131 -143
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -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 +8 -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/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.tsx +110 -210
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/StudioRightPanel.tsx +51 -39
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +8 -12
- package/src/components/dock/Dock.test.tsx +260 -0
- package/src/components/dock/Dock.tsx +281 -0
- package/src/components/dock/dock.css +28 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +105 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -0
- package/src/components/dock/dockTestHarness.ts +11 -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.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +7 -5
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.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.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +7 -5
- 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/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- 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 +3 -3
- 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/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +7 -7
- 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/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +252 -0
- package/src/components/ui/Button.tsx +89 -126
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/hooks/useAppHotkeys.ts +21 -80
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- 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/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/index.ts +51 -0
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -3
- 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 +21 -14
- package/src/player/components/TimelineClip.tsx +0 -1
- 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/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- 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/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +243 -0
- package/src/styles/theme.test.ts +183 -0
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -1
- package/src/utils/studioUiPreferences.ts +7 -0
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-n2ml4tUn.js +0 -442
- package/dist/assets/index-pO6p0H27.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- 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
|
@@ -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,5 @@
|
|
|
1
|
+
// CapCut-flat icon buttons: a transparent 28px hit area with a subtle hover wash.
|
|
2
|
+
export const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
|
|
3
|
+
export const flatIdle = `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-neutral-200 active:scale-[0.98]`;
|
|
4
|
+
export const flatActive = `${flatBtn} bg-white/8 text-neutral-100 active:scale-[0.98]`;
|
|
5
|
+
export const flatDisabled = `${flatBtn} text-neutral-700 cursor-not-allowed`;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Button/IconButton: renders every variant and size, then compiles Studio's
|
|
5
|
+
* real stylesheet against the emitted classes so an unstyled class fails.
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import React, { act } from "react";
|
|
11
|
+
import { compile } from "tailwindcss";
|
|
12
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
13
|
+
import { Button, type ButtonSize, type ButtonVariant } from "./Button";
|
|
14
|
+
import { IconButton } from "./IconButton";
|
|
15
|
+
import { Tab, Tabs, TabsList } from "./Tabs";
|
|
16
|
+
import { cleanupMounted, mountHost } from "./mountHost.testHelpers";
|
|
17
|
+
import { isTypingTarget } from "../../utils/typingTarget";
|
|
18
|
+
import { shouldIgnorePlaybackShortcutTarget } from "../../player/lib/playbackShortcuts";
|
|
19
|
+
|
|
20
|
+
const VARIANTS: ButtonVariant[] = ["primary", "secondary", "danger", "ghost"];
|
|
21
|
+
const SIZES: ButtonSize[] = ["sm", "md", "lg"];
|
|
22
|
+
|
|
23
|
+
afterEach(cleanupMounted);
|
|
24
|
+
|
|
25
|
+
function classesOf(host: HTMLElement, selector = "button"): string[] {
|
|
26
|
+
const el = host.querySelector(selector);
|
|
27
|
+
if (!el) throw new Error(`nothing matched ${selector}`);
|
|
28
|
+
return [...el.classList];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// -- the compiled stylesheet, built once for the whole file --
|
|
32
|
+
|
|
33
|
+
const STYLES_DIR = path.resolve(__dirname, "../../styles");
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const TAILWIND_DIR = path.dirname(require.resolve("tailwindcss/package.json"));
|
|
36
|
+
|
|
37
|
+
async function loadStylesheet(id: string, base: string) {
|
|
38
|
+
const file = id === "tailwindcss" ? path.join(TAILWIND_DIR, "index.css") : path.resolve(base, id);
|
|
39
|
+
return { path: file, base: path.dirname(file), content: readFileSync(file, "utf8") };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let compileStudioCss: (candidates: string[]) => string;
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
const compiled = await compile(readFileSync(path.join(STYLES_DIR, "studio.css"), "utf8"), {
|
|
46
|
+
base: STYLES_DIR,
|
|
47
|
+
loadStylesheet,
|
|
48
|
+
});
|
|
49
|
+
compileStudioCss = (candidates) => compiled.build(candidates);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function unresolved(candidates: string[]): string[] {
|
|
53
|
+
const css = compileStudioCss(candidates);
|
|
54
|
+
return candidates.filter((candidate) => !css.includes(CSS.escape(candidate)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("Button classes", () => {
|
|
58
|
+
it("emits only classes Studio's stylesheet defines", () => {
|
|
59
|
+
const emitted = new Set<string>();
|
|
60
|
+
for (const variant of VARIANTS) {
|
|
61
|
+
for (const size of SIZES) {
|
|
62
|
+
const host = mountHost(
|
|
63
|
+
<>
|
|
64
|
+
<Button variant={variant} size={size}>
|
|
65
|
+
Export
|
|
66
|
+
</Button>
|
|
67
|
+
<IconButton variant={variant} size={size} icon={null} aria-label="Zoom out" />
|
|
68
|
+
</>,
|
|
69
|
+
);
|
|
70
|
+
for (const el of host.querySelectorAll("button")) {
|
|
71
|
+
for (const cls of el.classList) emitted.add(cls);
|
|
72
|
+
}
|
|
73
|
+
cleanupMounted();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(emitted.size).toBeGreaterThan(20);
|
|
78
|
+
expect(unresolved([...emitted])).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("fails an undefined class, so the check above is not vacuous", () => {
|
|
82
|
+
// Names in a real namespace with no token behind them; the theme file now defines the
|
|
83
|
+
// `rounded-button` pair this used to name.
|
|
84
|
+
expect(unresolved(["bg-not-a-token", "text-step-999"])).toEqual([
|
|
85
|
+
"bg-not-a-token",
|
|
86
|
+
"text-step-999",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("lets a caller's type size beat the size's own", () => {
|
|
91
|
+
// The Renders Export case: `size="md"` brings `text-step-12`, the caller
|
|
92
|
+
// wants 11px, and with plain concatenation both survived into the class
|
|
93
|
+
// list and the stylesheet's order decided the winner.
|
|
94
|
+
const classes = classesOf(mountHost(<Button size="md" className="text-step-11" />));
|
|
95
|
+
|
|
96
|
+
expect(classes).toContain("text-step-11");
|
|
97
|
+
expect(classes).not.toContain("text-step-12");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps every interactive look reachable without a pointer", () => {
|
|
101
|
+
// `data-preview-state` exists so a gallery can show the hover, active and
|
|
102
|
+
// focus looks. It is worth nothing if it drifts from the real state, so
|
|
103
|
+
// each pair is matched here in both directions.
|
|
104
|
+
const realPrefix = {
|
|
105
|
+
hover: ["enabled:hover:", "hover:"],
|
|
106
|
+
active: ["enabled:active:", "active:"],
|
|
107
|
+
focus: ["focus-visible:"],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const variant of VARIANTS) {
|
|
111
|
+
const classes = classesOf(mountHost(<Button variant={variant}>Export</Button>));
|
|
112
|
+
cleanupMounted();
|
|
113
|
+
|
|
114
|
+
for (const [state, prefixes] of Object.entries(realPrefix)) {
|
|
115
|
+
const previewPrefix = `data-[preview-state=${state}]:`;
|
|
116
|
+
const preview = classes
|
|
117
|
+
.filter((cls) => cls.startsWith(previewPrefix))
|
|
118
|
+
.map((cls) => cls.slice(previewPrefix.length));
|
|
119
|
+
const real = classes
|
|
120
|
+
.filter((cls) => prefixes.some((prefix) => cls.startsWith(prefix)))
|
|
121
|
+
.map((cls) => cls.slice(cls.lastIndexOf(":") + 1));
|
|
122
|
+
|
|
123
|
+
expect([...preview].sort(), `${variant} ${state}`).toEqual([...real].sort());
|
|
124
|
+
expect(preview.length, `${variant} ${state}`).toBeGreaterThan(0);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("uses a motion token that zeroes itself under reduced motion", () => {
|
|
130
|
+
// AE4. The zero-duration case lives in the `duration-press` utility itself
|
|
131
|
+
// (theme.css), not in a `motion-reduce:` class beside it, so a caller
|
|
132
|
+
// cannot use the token and forget the reduced-motion half. The button's
|
|
133
|
+
// job is to name the token; the stylesheet's job is the media query.
|
|
134
|
+
expect(classesOf(mountHost(<Button>Export</Button>))).toContain("duration-press");
|
|
135
|
+
|
|
136
|
+
expect(compileStudioCss(["duration-press"])).toMatch(
|
|
137
|
+
/\.duration-press \{[\s\S]*?prefers-reduced-motion: reduce[\s\S]*?transition-duration: 0ms/,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("Button behaviour", () => {
|
|
143
|
+
it("does not fire a disabled click, and says it is disabled", () => {
|
|
144
|
+
let clicks = 0;
|
|
145
|
+
const host = mountHost(
|
|
146
|
+
<Button disabled onClick={() => (clicks += 1)}>
|
|
147
|
+
Export
|
|
148
|
+
</Button>,
|
|
149
|
+
);
|
|
150
|
+
const button = host.querySelector("button")!;
|
|
151
|
+
|
|
152
|
+
act(() => button.click());
|
|
153
|
+
|
|
154
|
+
expect(clicks).toBe(0);
|
|
155
|
+
expect(button.getAttribute("aria-disabled")).toBe("true");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("still fires a click while a preview state is forced", () => {
|
|
159
|
+
let clicks = 0;
|
|
160
|
+
const host = mountHost(
|
|
161
|
+
<Button data-preview-state="hover" onClick={() => (clicks += 1)}>
|
|
162
|
+
Export
|
|
163
|
+
</Button>,
|
|
164
|
+
);
|
|
165
|
+
const button = host.querySelector("button")!;
|
|
166
|
+
|
|
167
|
+
act(() => button.click());
|
|
168
|
+
|
|
169
|
+
expect(clicks).toBe(1);
|
|
170
|
+
expect(button.getAttribute("data-preview-state")).toBe("hover");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("is classified by the hotkey selectors the way a plain button is", () => {
|
|
174
|
+
// KTD13. Both selector lists gate on roles and element names. A primitive
|
|
175
|
+
// that landed on a different role would leak or swallow global hotkeys with
|
|
176
|
+
// nothing to notice it.
|
|
177
|
+
const host = mountHost(
|
|
178
|
+
<>
|
|
179
|
+
<button type="button" data-testid="plain">
|
|
180
|
+
Export
|
|
181
|
+
</button>
|
|
182
|
+
<Button data-testid="primitive">Export</Button>
|
|
183
|
+
<Tabs defaultValue="code">
|
|
184
|
+
<TabsList aria-label="Sidebar panels">
|
|
185
|
+
<Tab value="code">Code</Tab>
|
|
186
|
+
</TabsList>
|
|
187
|
+
</Tabs>
|
|
188
|
+
</>,
|
|
189
|
+
);
|
|
190
|
+
const plain = host.querySelector('[data-testid="plain"]')!;
|
|
191
|
+
const button = host.querySelector('[data-testid="primitive"]')!;
|
|
192
|
+
const tab = host.querySelector('[role="tab"]')!;
|
|
193
|
+
|
|
194
|
+
for (const el of [plain, button, tab]) {
|
|
195
|
+
expect(isTypingTarget(el)).toBe(false);
|
|
196
|
+
expect(shouldIgnorePlaybackShortcutTarget(el)).toBe(true);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("disabled states", () => {
|
|
202
|
+
it("disables a loading Button", () => {
|
|
203
|
+
const host = mountHost(<Button loading>Save</Button>);
|
|
204
|
+
const button = host.querySelector("button");
|
|
205
|
+
|
|
206
|
+
expect(button?.disabled).toBe(true);
|
|
207
|
+
expect(button?.getAttribute("aria-disabled")).toBe("true");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("marks a disabled IconButton for assistive tech", () => {
|
|
211
|
+
const host = mountHost(<IconButton aria-label="Undo" icon={<svg />} disabled />);
|
|
212
|
+
const button = host.querySelector("button");
|
|
213
|
+
|
|
214
|
+
expect(button?.disabled).toBe(true);
|
|
215
|
+
expect(button?.getAttribute("aria-disabled")).toBe("true");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("children layout", () => {
|
|
220
|
+
it("keeps label and shortcut direct flex items of the gap-bearing button without an icon", () => {
|
|
221
|
+
const host = mountHost(
|
|
222
|
+
<Button size="md">
|
|
223
|
+
<span>Export</span>
|
|
224
|
+
<kbd>⌘E</kbd>
|
|
225
|
+
</Button>,
|
|
226
|
+
);
|
|
227
|
+
const button = host.querySelector("button");
|
|
228
|
+
|
|
229
|
+
expect(button?.classList.contains("inline-flex")).toBe(true);
|
|
230
|
+
expect(button?.classList.contains("gap-1.5")).toBe(true);
|
|
231
|
+
expect(Array.from(button?.children ?? []).map((child) => child.tagName)).toEqual([
|
|
232
|
+
"SPAN",
|
|
233
|
+
"KBD",
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("makes label and shortcut direct flex items so the button gap applies between them", () => {
|
|
238
|
+
const host = mountHost(
|
|
239
|
+
<Button icon={<svg />}>
|
|
240
|
+
<span>Export</span>
|
|
241
|
+
<kbd>⌘E</kbd>
|
|
242
|
+
</Button>,
|
|
243
|
+
);
|
|
244
|
+
const button = host.querySelector("button");
|
|
245
|
+
|
|
246
|
+
expect(Array.from(button?.children ?? []).map((child) => child.tagName)).toEqual([
|
|
247
|
+
"SPAN",
|
|
248
|
+
"SPAN",
|
|
249
|
+
"KBD",
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
});
|