@hyperframes/studio 0.8.104 → 0.8.106
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-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +9 -9
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -1,57 +1,11 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { usePlayerStore } from "../../player/store/playerStore";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
7
4
|
import { resolveMasterCompositionPath } from "../../utils/studioUrlState";
|
|
8
|
-
import {
|
|
5
|
+
import { mountCompositionsTab } from "./compositionsTabTestUtils";
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
(
|
|
12
|
-
window as unknown as { happyDOM: { settings: { disableIframePageLoading: boolean } } }
|
|
13
|
-
).happyDOM.settings.disableIframePageLoading = true;
|
|
14
|
-
|
|
15
|
-
let root: Root | null = null;
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
if (root) act(() => root?.unmount());
|
|
19
|
-
root = null;
|
|
20
|
-
document.body.innerHTML = "";
|
|
21
|
-
usePlayerStore.setState({ thumbnailRevisions: {} });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function mount(compositions: string[], masterCompositionPath: string | null) {
|
|
25
|
-
const host = document.createElement("div");
|
|
26
|
-
document.body.append(host);
|
|
27
|
-
root = createRoot(host);
|
|
28
|
-
act(() => {
|
|
29
|
-
root?.render(
|
|
30
|
-
<CompositionsTab
|
|
31
|
-
projectId="demo"
|
|
32
|
-
compositions={compositions}
|
|
33
|
-
activeComposition={null}
|
|
34
|
-
masterCompositionPath={masterCompositionPath}
|
|
35
|
-
onSelect={vi.fn()}
|
|
36
|
-
/>,
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
return host;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("CompositionsTab card thumbnails", () => {
|
|
43
|
-
it("waits for the live preview to boot and keeps them through edit reloads", () => {
|
|
44
|
-
usePlayerStore.getState().reset();
|
|
45
|
-
const host = mount(["index.html"], "index.html");
|
|
46
|
-
expect(host.querySelector("img")).toBeNull();
|
|
47
|
-
|
|
48
|
-
act(() => usePlayerStore.getState().markPreviewBooted());
|
|
49
|
-
expect(host.querySelector("img")?.getAttribute("src")).toContain("/thumbnail/index.html");
|
|
50
|
-
|
|
51
|
-
act(() => usePlayerStore.getState().setTimelineReady(false));
|
|
52
|
-
expect(host.querySelector("img")).not.toBeNull();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
7
|
+
const mount = (compositions: string[], masterCompositionPath: string | null) =>
|
|
8
|
+
mountCompositionsTab({ compositions, masterCompositionPath });
|
|
55
9
|
|
|
56
10
|
describe("CompositionsTab root badge", () => {
|
|
57
11
|
it("marks the composition matching masterCompositionPath as root", () => {
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { thumbnailScheduler } from "../../player/lib/thumbnailScheduler";
|
|
7
|
+
import { TIMELINE_VIEWPORT_BUDGETS } from "../../player/lib/timelineViewportBudgets";
|
|
8
|
+
import { CompositionThumbnail } from "../../player/components/CompositionThumbnail";
|
|
9
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
10
|
+
import { renderPosterForNextOpen } from "../nle/PreviewPoster";
|
|
11
|
+
import { mountCompositionsTab } from "./compositionsTabTestUtils";
|
|
12
|
+
|
|
13
|
+
class DecodingImage {
|
|
14
|
+
onload: (() => void) | null = null;
|
|
15
|
+
onerror: (() => void) | null = null;
|
|
16
|
+
naturalWidth = 16;
|
|
17
|
+
naturalHeight = 9;
|
|
18
|
+
set src(value: string) {
|
|
19
|
+
if (value) queueMicrotask(() => this.onload?.());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let autoIntersect = true;
|
|
24
|
+
const observed: Array<{
|
|
25
|
+
element: Element;
|
|
26
|
+
callback: IntersectionObserverCallback;
|
|
27
|
+
root?: Element | Document | null;
|
|
28
|
+
}> = [];
|
|
29
|
+
class ViewportObserver {
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly callback: IntersectionObserverCallback,
|
|
32
|
+
private readonly options?: IntersectionObserverInit,
|
|
33
|
+
) {}
|
|
34
|
+
observe(element: Element) {
|
|
35
|
+
observed.push({ element, callback: this.callback, root: this.options?.root });
|
|
36
|
+
if (autoIntersect) this.enter();
|
|
37
|
+
}
|
|
38
|
+
enter() {
|
|
39
|
+
const entry = { isIntersecting: true } as IntersectionObserverEntry;
|
|
40
|
+
this.callback([entry], this as unknown as IntersectionObserver);
|
|
41
|
+
}
|
|
42
|
+
disconnect() {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Render {
|
|
46
|
+
url: URL;
|
|
47
|
+
signal: AbortSignal;
|
|
48
|
+
answer: (status?: number) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const real = {
|
|
52
|
+
IntersectionObserver: globalThis.IntersectionObserver,
|
|
53
|
+
fetch: globalThis.fetch,
|
|
54
|
+
Image: globalThis.Image,
|
|
55
|
+
create: URL.createObjectURL,
|
|
56
|
+
revoke: URL.revokeObjectURL,
|
|
57
|
+
};
|
|
58
|
+
let renders: Render[] = [];
|
|
59
|
+
let inFlight = 0;
|
|
60
|
+
let peak = 0;
|
|
61
|
+
let frames = 0;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
renders = [];
|
|
65
|
+
[inFlight, peak, frames] = [0, 0, 0];
|
|
66
|
+
globalThis.fetch = vi.fn(
|
|
67
|
+
(input: RequestInfo | URL, init?: RequestInit) =>
|
|
68
|
+
new Promise<Response>((resolve, reject) => {
|
|
69
|
+
const signal = init?.signal ?? new AbortController().signal;
|
|
70
|
+
peak = Math.max(peak, ++inFlight);
|
|
71
|
+
const end = () => (inFlight -= 1);
|
|
72
|
+
signal.addEventListener("abort", () => {
|
|
73
|
+
end();
|
|
74
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
75
|
+
});
|
|
76
|
+
const answer = (status = 200) => {
|
|
77
|
+
end();
|
|
78
|
+
resolve(new Response(new Blob(["png"]), { status }));
|
|
79
|
+
};
|
|
80
|
+
renders.push({ url: new URL(String(input), window.location.origin), signal, answer });
|
|
81
|
+
}),
|
|
82
|
+
) as typeof fetch;
|
|
83
|
+
globalThis.Image = DecodingImage as unknown as typeof Image;
|
|
84
|
+
globalThis.IntersectionObserver = ViewportObserver as unknown as typeof IntersectionObserver;
|
|
85
|
+
[autoIntersect, observed.length] = [true, 0];
|
|
86
|
+
URL.createObjectURL = vi.fn(() => `blob:frame-${++frames}`);
|
|
87
|
+
URL.revokeObjectURL = vi.fn();
|
|
88
|
+
usePlayerStore.setState({ previewBooted: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
Object.assign(globalThis, {
|
|
93
|
+
fetch: real.fetch,
|
|
94
|
+
Image: real.Image,
|
|
95
|
+
IntersectionObserver: real.IntersectionObserver,
|
|
96
|
+
});
|
|
97
|
+
Object.assign(URL, { createObjectURL: real.create, revokeObjectURL: real.revoke });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const mount = (compositions?: string[]) => mountCompositionsTab(compositions && { compositions });
|
|
101
|
+
const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 5)));
|
|
102
|
+
const shown = (host: HTMLElement) => host.querySelector("img")?.getAttribute("src") ?? null;
|
|
103
|
+
|
|
104
|
+
async function nextRevisionShows(host: HTMLElement, revision: string, frame: string) {
|
|
105
|
+
act(() => usePlayerStore.getState().bumpThumbnailRevisions(null));
|
|
106
|
+
expect(renders.at(-1)!.url.searchParams.get("revision")).toBe(revision);
|
|
107
|
+
renders.at(-1)!.answer();
|
|
108
|
+
await settle();
|
|
109
|
+
expect(shown(host)).toBe(frame);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("composition card thumbnails", () => {
|
|
113
|
+
it("render through the thumbnail scheduler once the preview boots, and never point an image at the route", async () => {
|
|
114
|
+
usePlayerStore.getState().reset();
|
|
115
|
+
const host = mount();
|
|
116
|
+
expect(renders).toHaveLength(0);
|
|
117
|
+
|
|
118
|
+
act(() => usePlayerStore.getState().markPreviewBooted());
|
|
119
|
+
expect(renders.map((r) => [r.url.pathname, r.url.searchParams.get("t")])).toEqual([
|
|
120
|
+
["/api/projects/demo/thumbnail/compositions/headline.html", "3.00"],
|
|
121
|
+
]);
|
|
122
|
+
renders[0]!.answer();
|
|
123
|
+
await settle();
|
|
124
|
+
expect(shown(host)).toBe("blob:frame-1");
|
|
125
|
+
expect(host.querySelector("iframe")).toBeNull();
|
|
126
|
+
|
|
127
|
+
act(() => usePlayerStore.getState().setTimelineReady(false));
|
|
128
|
+
expect(shown(host)).toBe("blob:frame-1");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("wait until a card is near the viewport, so an offscreen card never renders ahead of a visible one", () => {
|
|
132
|
+
autoIntersect = false;
|
|
133
|
+
mount(["compositions/above.html", "compositions/seen.html", "compositions/below.html"]);
|
|
134
|
+
expect(renders).toHaveLength(0);
|
|
135
|
+
|
|
136
|
+
const seen = observed.find((o) =>
|
|
137
|
+
o.element.closest("[draggable]")?.textContent?.includes("seen"),
|
|
138
|
+
)!;
|
|
139
|
+
act(() =>
|
|
140
|
+
seen.callback(
|
|
141
|
+
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
142
|
+
{} as IntersectionObserver,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
expect(renders.map((r) => r.url.pathname)).toEqual([
|
|
146
|
+
"/api/projects/demo/thumbnail/compositions/seen.html",
|
|
147
|
+
]);
|
|
148
|
+
expect(seen.root).toBe(document.querySelector("[data-composition-list]"));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("keep showing the last frame, leased, until the next revision's frame is ready", async () => {
|
|
152
|
+
const host = mount();
|
|
153
|
+
renders[0]!.answer();
|
|
154
|
+
await settle();
|
|
155
|
+
act(() => usePlayerStore.getState().bumpThumbnailRevisions(null));
|
|
156
|
+
expect(shown(host)).toBe("blob:frame-1");
|
|
157
|
+
thumbnailScheduler.invalidateProject("demo");
|
|
158
|
+
expect(URL.revokeObjectURL).not.toHaveBeenCalledWith("blob:frame-1");
|
|
159
|
+
|
|
160
|
+
renders.at(-1)!.answer();
|
|
161
|
+
await settle();
|
|
162
|
+
expect(shown(host)).toBe("blob:frame-2");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keep the last frame through a new revision even when the scheduler has evicted it", async () => {
|
|
166
|
+
const host = mount();
|
|
167
|
+
renders[0]!.answer();
|
|
168
|
+
await settle();
|
|
169
|
+
const fillers = Array.from({ length: 100 }, (_, i) =>
|
|
170
|
+
thumbnailScheduler.acquire(
|
|
171
|
+
{
|
|
172
|
+
key: `filler-${i}`,
|
|
173
|
+
projectId: "demo",
|
|
174
|
+
sessionEpoch: 0,
|
|
175
|
+
kind: "image",
|
|
176
|
+
priority: "visible",
|
|
177
|
+
load: async () => ({
|
|
178
|
+
value: { kind: "image", url: `blob:filler-${i}`, aspect: 1 },
|
|
179
|
+
weight: 1,
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
() => {},
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
while (
|
|
186
|
+
thumbnailScheduler.getDiagnostics().queued + thumbnailScheduler.getDiagnostics().active >
|
|
187
|
+
0
|
|
188
|
+
) {
|
|
189
|
+
await settle();
|
|
190
|
+
}
|
|
191
|
+
for (const filler of fillers) filler.release();
|
|
192
|
+
|
|
193
|
+
act(() => usePlayerStore.getState().bumpThumbnailRevisions(null));
|
|
194
|
+
expect(renders.map((r) => r.url.searchParams.get("revision"))).toEqual(["0", "1"]);
|
|
195
|
+
expect(shown(host)).toBe("blob:frame-1");
|
|
196
|
+
expect(URL.revokeObjectURL).not.toHaveBeenCalledWith("blob:frame-1");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("share one render with the timeline's thumbnail of the same composition", () => {
|
|
200
|
+
mount();
|
|
201
|
+
const timeline = createRoot(document.body.appendChild(document.createElement("div")));
|
|
202
|
+
try {
|
|
203
|
+
act(() =>
|
|
204
|
+
timeline.render(
|
|
205
|
+
<CompositionThumbnail
|
|
206
|
+
previewUrl="/api/projects/demo/preview/comp/compositions/headline.html"
|
|
207
|
+
label=""
|
|
208
|
+
labelColor=""
|
|
209
|
+
projectId="demo"
|
|
210
|
+
sessionEpoch={usePlayerStore.getState().timelineSessionEpoch}
|
|
211
|
+
seekTime={3}
|
|
212
|
+
duration={0}
|
|
213
|
+
/>,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
expect(renders).toHaveLength(1);
|
|
217
|
+
} finally {
|
|
218
|
+
act(() => timeline.unmount());
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("keep the cards and the poster render to the scheduler's cap, so Studio's own requests get a connection", async () => {
|
|
223
|
+
const cap = TIMELINE_VIEWPORT_BUDGETS.concurrentCompositionFetches;
|
|
224
|
+
mount(Array.from({ length: 6 }, (_, i) => `compositions/scene-${i}.html`));
|
|
225
|
+
renderPosterForNextOpen("demo");
|
|
226
|
+
expect(inFlight).toBe(cap);
|
|
227
|
+
|
|
228
|
+
while (renders.some((r) => !r.signal.aborted)) {
|
|
229
|
+
renders.shift()!.answer();
|
|
230
|
+
await settle();
|
|
231
|
+
}
|
|
232
|
+
expect(renders).toHaveLength(0);
|
|
233
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(7);
|
|
234
|
+
expect(peak).toBe(cap);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("abort a stale revision's render, and free its frame when the scheduler evicts it", async () => {
|
|
238
|
+
const host = mount();
|
|
239
|
+
const first = renders[0]!;
|
|
240
|
+
await nextRevisionShows(host, "1", "blob:frame-1");
|
|
241
|
+
expect(first.signal.aborted).toBe(true);
|
|
242
|
+
await nextRevisionShows(host, "2", "blob:frame-2");
|
|
243
|
+
|
|
244
|
+
thumbnailScheduler.invalidateProject("demo");
|
|
245
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:frame-1");
|
|
246
|
+
expect(URL.revokeObjectURL).not.toHaveBeenCalledWith("blob:frame-2");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("show a fallback when the render fails, and retry it at the next content revision", async () => {
|
|
250
|
+
const host = mount();
|
|
251
|
+
renders[0]!.answer(500);
|
|
252
|
+
await settle();
|
|
253
|
+
expect(host.textContent).toContain("Preview unavailable");
|
|
254
|
+
await nextRevisionShows(host, "1", "blob:frame-1");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
2
|
-
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
4
|
+
import {
|
|
5
|
+
thumbnailScheduler,
|
|
6
|
+
type ThumbnailRequest,
|
|
7
|
+
type ThumbnailSnapshot,
|
|
8
|
+
} from "../../player/lib/thumbnailScheduler";
|
|
3
9
|
import {
|
|
4
10
|
buildCompositionThumbnailUrl,
|
|
11
|
+
compositionThumbnailRequest,
|
|
5
12
|
resolveThumbnailSeekTime,
|
|
6
13
|
THUMBNAIL_SEEK_TIME_SECONDS,
|
|
7
14
|
} from "../../player/components/CompositionThumbnail";
|
|
@@ -125,6 +132,38 @@ export function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay:
|
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
const imageUrlOf = (snapshot: ThumbnailSnapshot) =>
|
|
136
|
+
snapshot.status === "ready" && snapshot.value.kind === "image" ? snapshot.value.url : null;
|
|
137
|
+
|
|
138
|
+
// A layout effect takes the old frame's lease before the passive cleanup drops the first one.
|
|
139
|
+
function useLastFrame(request: ThumbnailRequest | null, snapshot: ThumbnailSnapshot) {
|
|
140
|
+
const current = imageUrlOf(snapshot);
|
|
141
|
+
const [kept, setKept] = useState<{ request: ThumbnailRequest; url: string } | null>(null);
|
|
142
|
+
if (request && current && kept?.request !== request) setKept({ request, url: current });
|
|
143
|
+
useLayoutEffect(() => {
|
|
144
|
+
if (!kept || kept.request === request) return;
|
|
145
|
+
const lease = thumbnailScheduler.acquire(kept.request, () => {});
|
|
146
|
+
return () => lease.release();
|
|
147
|
+
}, [kept, request]);
|
|
148
|
+
return current ?? kept?.url ?? null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function useNearViewport<T extends Element>(): [(element: T | null) => void, boolean] {
|
|
152
|
+
const [element, setElement] = useState<T | null>(null);
|
|
153
|
+
const [near, setNear] = useState(false);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!element || near) return;
|
|
156
|
+
if (typeof IntersectionObserver === "undefined") return setNear(true);
|
|
157
|
+
const observer = new IntersectionObserver(
|
|
158
|
+
(entries) => entries.some((entry) => entry.isIntersecting) && setNear(true),
|
|
159
|
+
{ root: element.closest("[data-composition-list]"), rootMargin: "200px" },
|
|
160
|
+
);
|
|
161
|
+
observer.observe(element);
|
|
162
|
+
return () => observer.disconnect();
|
|
163
|
+
}, [element, near]);
|
|
164
|
+
return [setElement, near];
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
function CompCard({
|
|
129
168
|
projectId,
|
|
130
169
|
comp,
|
|
@@ -153,7 +192,6 @@ function CompCard({
|
|
|
153
192
|
const [hovered, setHovered] = useState(false);
|
|
154
193
|
const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
|
|
155
194
|
const [livePreviewLoaded, setLivePreviewLoaded] = useState(false);
|
|
156
|
-
const [failedThumbnailUrl, setFailedThumbnailUrl] = useState<string | null>(null);
|
|
157
195
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
158
196
|
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
159
197
|
const syncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -192,7 +230,21 @@ function CompCard({
|
|
|
192
230
|
const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
|
|
193
231
|
const previewUrl = compositionPreviewUrl(projectId, comp);
|
|
194
232
|
const thumbnailUrl = compositionCardThumbnailUrl(projectId, comp, contentRevision);
|
|
195
|
-
const
|
|
233
|
+
const [thumbnailBox, nearViewport] = useNearViewport<HTMLDivElement>();
|
|
234
|
+
const timelineSessionEpoch = usePlayerStore((state) => state.timelineSessionEpoch);
|
|
235
|
+
const thumbnailRequest = useMemo(
|
|
236
|
+
() =>
|
|
237
|
+
previewBooted && nearViewport
|
|
238
|
+
? compositionThumbnailRequest(thumbnailUrl, projectId, {
|
|
239
|
+
sessionEpoch: timelineSessionEpoch,
|
|
240
|
+
rich: true,
|
|
241
|
+
})
|
|
242
|
+
: null,
|
|
243
|
+
[previewBooted, nearViewport, thumbnailUrl, projectId, timelineSessionEpoch],
|
|
244
|
+
);
|
|
245
|
+
const thumbnail = useThumbnailLease(thumbnailRequest);
|
|
246
|
+
const frameUrl = useLastFrame(thumbnailRequest, thumbnail);
|
|
247
|
+
const thumbnailFailed = thumbnail.status === "error";
|
|
196
248
|
const previewScale = resolveCompositionPreviewScale({
|
|
197
249
|
cardWidth: CARD_W,
|
|
198
250
|
cardHeight: CARD_H,
|
|
@@ -250,19 +302,20 @@ function CompCard({
|
|
|
250
302
|
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
251
303
|
}`}
|
|
252
304
|
>
|
|
253
|
-
<div
|
|
305
|
+
<div
|
|
306
|
+
ref={thumbnailBox}
|
|
307
|
+
className="w-20 h-[45px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 relative"
|
|
308
|
+
>
|
|
254
309
|
{thumbnailFailed ? (
|
|
255
310
|
<div className="absolute inset-0 flex items-center justify-center px-1 text-center text-[8px] leading-tight text-neutral-600">
|
|
256
311
|
Preview unavailable
|
|
257
312
|
</div>
|
|
258
|
-
) : !
|
|
313
|
+
) : !frameUrl ? null : (
|
|
259
314
|
<img
|
|
260
|
-
src={
|
|
315
|
+
src={frameUrl}
|
|
261
316
|
alt=""
|
|
262
317
|
draggable={false}
|
|
263
|
-
loading="lazy"
|
|
264
318
|
decoding="async"
|
|
265
|
-
onError={() => setFailedThumbnailUrl(thumbnailUrl)}
|
|
266
319
|
className={`absolute inset-0 h-full w-full object-contain transition-opacity ${
|
|
267
320
|
livePreviewLoaded ? "opacity-0" : "opacity-100"
|
|
268
321
|
}`}
|
|
@@ -320,7 +373,7 @@ function CompCard({
|
|
|
320
373
|
{lintInfo && lintInfo.count > 0 && (
|
|
321
374
|
<span
|
|
322
375
|
aria-label={`${lintInfo.count} lint finding${lintInfo.count === 1 ? "" : "s"}`}
|
|
323
|
-
className="shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-
|
|
376
|
+
className="shrink-0 min-w-[16px] text-center rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-warning-ink"
|
|
324
377
|
>
|
|
325
378
|
{lintInfo.count}
|
|
326
379
|
</span>
|
|
@@ -337,7 +390,7 @@ function CompCard({
|
|
|
337
390
|
event.stopPropagation();
|
|
338
391
|
onAddToTimeline();
|
|
339
392
|
}}
|
|
340
|
-
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-
|
|
393
|
+
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-accent-ink group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
|
|
341
394
|
>
|
|
342
395
|
<span aria-hidden="true">+</span>
|
|
343
396
|
</button>
|
|
@@ -358,7 +411,7 @@ function CompCard({
|
|
|
358
411
|
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors ${
|
|
359
412
|
isRendering
|
|
360
413
|
? "text-neutral-600 cursor-not-allowed"
|
|
361
|
-
: "text-neutral-600 hover:text-
|
|
414
|
+
: "text-neutral-600 hover:text-accent-ink hover:bg-neutral-800"
|
|
362
415
|
}`}
|
|
363
416
|
>
|
|
364
417
|
<svg
|
|
@@ -405,7 +458,7 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
405
458
|
}
|
|
406
459
|
|
|
407
460
|
return (
|
|
408
|
-
<div className="flex-1 overflow-y-auto">
|
|
461
|
+
<div className="flex-1 overflow-y-auto" data-composition-list>
|
|
409
462
|
{compositions.map((comp) => (
|
|
410
463
|
<CompCard
|
|
411
464
|
key={`${projectId}:${comp}`}
|
|
@@ -106,10 +106,10 @@ export function PromptPreviewModal({
|
|
|
106
106
|
<button
|
|
107
107
|
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-colors active:scale-[0.97] ${
|
|
108
108
|
copyState === "copied"
|
|
109
|
-
? "bg-
|
|
109
|
+
? "bg-accent text-on-accent"
|
|
110
110
|
: copyState === "failed"
|
|
111
|
-
? "bg-
|
|
112
|
-
: "bg-
|
|
111
|
+
? "bg-danger text-on-danger"
|
|
112
|
+
: "bg-accent text-on-accent hover:bg-accent-hover"
|
|
113
113
|
}`}
|
|
114
114
|
onClick={handleCopy}
|
|
115
115
|
>
|
|
@@ -17,7 +17,7 @@ export function SidebarLintButton({
|
|
|
17
17
|
type="button"
|
|
18
18
|
onClick={onLint}
|
|
19
19
|
disabled={linting}
|
|
20
|
-
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-
|
|
20
|
+
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-warning-ink enabled:hover:bg-neutral-800 enabled:active:scale-[0.98] transition-colors disabled:opacity-40"
|
|
21
21
|
>
|
|
22
22
|
<svg
|
|
23
23
|
width="12"
|
|
@@ -38,8 +38,8 @@ export function SidebarLintButton({
|
|
|
38
38
|
style={hasError ? { animationIterationCount: ERROR_PULSES_BEFORE_IDLE } : undefined}
|
|
39
39
|
className={
|
|
40
40
|
hasError
|
|
41
|
-
? "ml-1 min-w-[16px] rounded-full bg-
|
|
42
|
-
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-
|
|
41
|
+
? "ml-1 min-w-[16px] rounded-full bg-danger px-1 text-[9px] font-bold text-on-danger animate-pulse motion-reduce:animate-none"
|
|
42
|
+
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-warning-ink"
|
|
43
43
|
}
|
|
44
44
|
>
|
|
45
45
|
{findingCount}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { act, type ComponentProps } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, vi } from "vitest";
|
|
4
|
+
import { thumbnailScheduler } from "../../player/lib/thumbnailScheduler";
|
|
5
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
6
|
+
import { CompositionsTab } from "./CompositionsTab";
|
|
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
|
+
thumbnailScheduler.invalidateProject("demo");
|
|
19
|
+
document.body.innerHTML = "";
|
|
20
|
+
usePlayerStore.setState({ thumbnailRevisions: {} });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function mountCompositionsTab(props: Partial<ComponentProps<typeof CompositionsTab>> = {}) {
|
|
24
|
+
const host = document.createElement("div");
|
|
25
|
+
document.body.append(host);
|
|
26
|
+
root = createRoot(host);
|
|
27
|
+
act(() => {
|
|
28
|
+
root?.render(
|
|
29
|
+
<CompositionsTab
|
|
30
|
+
projectId="demo"
|
|
31
|
+
compositions={["compositions/headline.html"]}
|
|
32
|
+
activeComposition={null}
|
|
33
|
+
onSelect={vi.fn()}
|
|
34
|
+
{...props}
|
|
35
|
+
/>,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
return host;
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// CapCut-flat icon buttons: a transparent 28px hit area with a subtle hover wash.
|
|
2
2
|
export const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
|
|
3
|
-
export const flatIdle = `${flatBtn} text-
|
|
4
|
-
export const flatActive = `${flatBtn} bg-
|
|
5
|
-
export const flatDisabled = `${flatBtn} text-
|
|
3
|
+
export const flatIdle = `${flatBtn} text-text-2 hover:bg-hover hover:text-text-0 active:bg-press active:scale-[0.98]`;
|
|
4
|
+
export const flatActive = `${flatBtn} bg-on text-text-0 hover:bg-on-hover active:scale-[0.98]`;
|
|
5
|
+
export const flatDisabled = `${flatBtn} text-text-off cursor-not-allowed`;
|
|
@@ -50,8 +50,8 @@ export const buttonBase = cn(
|
|
|
50
50
|
*/
|
|
51
51
|
export const buttonVariants: Record<ButtonVariant, string> = {
|
|
52
52
|
primary: cn(
|
|
53
|
-
"bg-accent text-
|
|
54
|
-
"enabled:hover:
|
|
53
|
+
"bg-accent text-on-accent font-semibold",
|
|
54
|
+
"enabled:hover:bg-accent-hover data-[preview-state=hover]:bg-accent-hover",
|
|
55
55
|
"enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
|
|
56
56
|
),
|
|
57
57
|
secondary: cn(
|
|
@@ -60,8 +60,8 @@ export const buttonVariants: Record<ButtonVariant, string> = {
|
|
|
60
60
|
"enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
|
|
61
61
|
),
|
|
62
62
|
danger: cn(
|
|
63
|
-
"bg-danger text-
|
|
64
|
-
"enabled:hover:brightness-
|
|
63
|
+
"bg-danger text-on-danger font-medium",
|
|
64
|
+
"enabled:hover:brightness-95 data-[preview-state=hover]:brightness-95",
|
|
65
65
|
"enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
|
|
66
66
|
),
|
|
67
67
|
ghost: cn(
|