@hyperframes/studio 0.4.14 → 0.4.15
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/index-CVm-zeM9.css +1 -0
- package/dist/assets/index-RzXlAX2g.js +93 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +133 -46
- package/src/player/components/PlayerControls.tsx +13 -3
- package/src/utils/timelineDiscovery.test.ts +90 -0
- package/src/utils/timelineDiscovery.ts +57 -0
- package/dist/assets/index-Dcn0cnE7.js +0 -93
- package/dist/assets/index-DhO5JhHF.css +0 -1
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<title>HyperFrames Studio</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-RzXlAX2g.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CVm-zeM9.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"codemirror": "^6.0.1",
|
|
34
34
|
"motion": "^12.38.0",
|
|
35
|
-
"@hyperframes/
|
|
36
|
-
"@hyperframes/
|
|
35
|
+
"@hyperframes/core": "0.4.15",
|
|
36
|
+
"@hyperframes/player": "0.4.15"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"vite": "^6.4.2",
|
|
48
48
|
"vitest": "^3.2.4",
|
|
49
49
|
"zustand": "^5.0.0",
|
|
50
|
-
"@hyperframes/producer": "0.4.
|
|
50
|
+
"@hyperframes/producer": "0.4.15"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
getNextTimelineZoomPercent,
|
|
28
28
|
getTimelineZoomPercent,
|
|
29
29
|
} from "./player/components/timelineZoom";
|
|
30
|
+
import {
|
|
31
|
+
TIMELINE_TOGGLE_SHORTCUT_LABEL,
|
|
32
|
+
getTimelineEditorHintDismissed,
|
|
33
|
+
getTimelineToggleTitle,
|
|
34
|
+
setTimelineEditorHintDismissed,
|
|
35
|
+
shouldHandleTimelineToggleHotkey,
|
|
36
|
+
} from "./utils/timelineDiscovery";
|
|
30
37
|
|
|
31
38
|
interface EditingFile {
|
|
32
39
|
path: string;
|
|
@@ -196,7 +203,11 @@ export function StudioApp() {
|
|
|
196
203
|
const [globalDragOver, setGlobalDragOver] = useState(false);
|
|
197
204
|
const [uploadToast, setUploadToast] = useState<string | null>(null);
|
|
198
205
|
const [timelineVisible, setTimelineVisible] = useState(true);
|
|
206
|
+
const [timelineEditorHintDismissed, setTimelineEditorHintState] = useState(
|
|
207
|
+
getTimelineEditorHintDismissed,
|
|
208
|
+
);
|
|
199
209
|
const dragCounterRef = useRef(0);
|
|
210
|
+
const previewHotkeyWindowRef = useRef<Window | null>(null);
|
|
200
211
|
const panelDragRef = useRef<{
|
|
201
212
|
side: "left" | "right";
|
|
202
213
|
startX: number;
|
|
@@ -224,6 +235,51 @@ export function StudioApp() {
|
|
|
224
235
|
() => getTimelineZoomPercent(zoomMode, manualZoomPercent),
|
|
225
236
|
[zoomMode, manualZoomPercent],
|
|
226
237
|
);
|
|
238
|
+
const toggleTimelineVisibility = useCallback(() => {
|
|
239
|
+
setTimelineVisible((visible) => !visible);
|
|
240
|
+
}, []);
|
|
241
|
+
const dismissTimelineEditorHint = useCallback(() => {
|
|
242
|
+
setTimelineEditorHintState(true);
|
|
243
|
+
setTimelineEditorHintDismissed(true);
|
|
244
|
+
}, []);
|
|
245
|
+
const handleTimelineToggleHotkey = useCallback(
|
|
246
|
+
(event: KeyboardEvent) => {
|
|
247
|
+
if (!shouldHandleTimelineToggleHotkey(event)) return;
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
toggleTimelineVisibility();
|
|
250
|
+
},
|
|
251
|
+
[toggleTimelineVisibility],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
useMountEffect(() => {
|
|
255
|
+
window.addEventListener("keydown", handleTimelineToggleHotkey);
|
|
256
|
+
return () => {
|
|
257
|
+
window.removeEventListener("keydown", handleTimelineToggleHotkey);
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const syncPreviewTimelineHotkey = useCallback(
|
|
262
|
+
(iframe: HTMLIFrameElement | null) => {
|
|
263
|
+
const nextWindow = iframe?.contentWindow ?? null;
|
|
264
|
+
if (previewHotkeyWindowRef.current === nextWindow) return;
|
|
265
|
+
if (previewHotkeyWindowRef.current) {
|
|
266
|
+
previewHotkeyWindowRef.current.removeEventListener("keydown", handleTimelineToggleHotkey);
|
|
267
|
+
}
|
|
268
|
+
previewHotkeyWindowRef.current = nextWindow;
|
|
269
|
+
nextWindow?.addEventListener("keydown", handleTimelineToggleHotkey);
|
|
270
|
+
},
|
|
271
|
+
[handleTimelineToggleHotkey],
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
useEffect(
|
|
275
|
+
() => () => {
|
|
276
|
+
if (previewHotkeyWindowRef.current) {
|
|
277
|
+
previewHotkeyWindowRef.current.removeEventListener("keydown", handleTimelineToggleHotkey);
|
|
278
|
+
previewHotkeyWindowRef.current = null;
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
[handleTimelineToggleHotkey],
|
|
282
|
+
);
|
|
227
283
|
|
|
228
284
|
const renderClipContent = useCallback(
|
|
229
285
|
(el: TimelineElement, style: { clip: string; label: string }): ReactNode => {
|
|
@@ -323,48 +379,75 @@ export function StudioApp() {
|
|
|
323
379
|
[compIdToSrc, activePreviewUrl, effectiveTimelineDuration],
|
|
324
380
|
);
|
|
325
381
|
const timelineToolbar = (
|
|
326
|
-
<div className="
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
382
|
+
<div className="border-b border-neutral-800/40 bg-neutral-950/96">
|
|
383
|
+
{timelineVisible && timelineElements.length > 0 && !timelineEditorHintDismissed && (
|
|
384
|
+
<div className="px-3 pt-3">
|
|
385
|
+
<div className="flex items-start justify-between gap-3 rounded-xl border border-studio-accent/20 bg-studio-accent/[0.07] px-3 py-3">
|
|
386
|
+
<div className="min-w-0">
|
|
387
|
+
<div className="text-[11px] font-semibold text-neutral-100">Timeline editor</div>
|
|
388
|
+
<p className="mt-1 text-[11px] leading-5 text-neutral-300">
|
|
389
|
+
Drag clips to move timing, and drag clip edges to resize them when handles are
|
|
390
|
+
available. Hide the panel anytime and bring it back with{" "}
|
|
391
|
+
<span className="font-mono text-[10px] text-studio-accent">
|
|
392
|
+
{TIMELINE_TOGGLE_SHORTCUT_LABEL}
|
|
393
|
+
</span>
|
|
394
|
+
.
|
|
395
|
+
</p>
|
|
396
|
+
</div>
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
onClick={dismissTimelineEditorHint}
|
|
400
|
+
className="flex-shrink-0 rounded-md border border-neutral-700 px-2 py-1 text-[10px] font-medium text-neutral-300 transition-colors hover:border-neutral-500 hover:text-neutral-100"
|
|
401
|
+
>
|
|
402
|
+
Dismiss
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
409
|
+
<div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
|
|
410
|
+
Timeline
|
|
411
|
+
</div>
|
|
412
|
+
<div className="flex items-center gap-1">
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
onClick={() => setZoomMode("fit")}
|
|
416
|
+
className={`h-7 px-2.5 rounded-md border text-[11px] font-medium transition-colors ${
|
|
417
|
+
zoomMode === "fit"
|
|
418
|
+
? "border-studio-accent/30 bg-studio-accent/10 text-studio-accent"
|
|
419
|
+
: "border-neutral-800 text-neutral-400 hover:border-neutral-700 hover:text-neutral-200"
|
|
420
|
+
}`}
|
|
421
|
+
title="Fit timeline to width"
|
|
422
|
+
>
|
|
423
|
+
Fit
|
|
424
|
+
</button>
|
|
425
|
+
<button
|
|
426
|
+
type="button"
|
|
427
|
+
onClick={() => {
|
|
428
|
+
setZoomMode("manual");
|
|
429
|
+
setManualZoomPercent(getNextTimelineZoomPercent("out", zoomMode, manualZoomPercent));
|
|
430
|
+
}}
|
|
431
|
+
className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
|
|
432
|
+
title="Zoom out"
|
|
433
|
+
>
|
|
434
|
+
-
|
|
435
|
+
</button>
|
|
436
|
+
<div className="min-w-[58px] text-center text-[10px] font-medium tabular-nums text-neutral-500">
|
|
437
|
+
{`${displayedTimelineZoomPercent}%`}
|
|
438
|
+
</div>
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
onClick={() => {
|
|
442
|
+
setZoomMode("manual");
|
|
443
|
+
setManualZoomPercent(getNextTimelineZoomPercent("in", zoomMode, manualZoomPercent));
|
|
444
|
+
}}
|
|
445
|
+
className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
|
|
446
|
+
title="Zoom in"
|
|
447
|
+
>
|
|
448
|
+
+
|
|
449
|
+
</button>
|
|
356
450
|
</div>
|
|
357
|
-
<button
|
|
358
|
-
type="button"
|
|
359
|
-
onClick={() => {
|
|
360
|
-
setZoomMode("manual");
|
|
361
|
-
setManualZoomPercent(getNextTimelineZoomPercent("in", zoomMode, manualZoomPercent));
|
|
362
|
-
}}
|
|
363
|
-
className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
|
|
364
|
-
title="Zoom in"
|
|
365
|
-
>
|
|
366
|
-
+
|
|
367
|
-
</button>
|
|
368
451
|
</div>
|
|
369
452
|
</div>
|
|
370
453
|
);
|
|
@@ -948,13 +1031,15 @@ export function StudioApp() {
|
|
|
948
1031
|
</svg>
|
|
949
1032
|
</button>
|
|
950
1033
|
<button
|
|
951
|
-
|
|
952
|
-
|
|
1034
|
+
type="button"
|
|
1035
|
+
onClick={toggleTimelineVisibility}
|
|
1036
|
+
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
|
|
953
1037
|
timelineVisible
|
|
954
1038
|
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
955
|
-
: "
|
|
1039
|
+
: "text-neutral-300 border-neutral-700 hover:border-neutral-500 hover:bg-neutral-800"
|
|
956
1040
|
}`}
|
|
957
|
-
title={timelineVisible
|
|
1041
|
+
title={getTimelineToggleTitle(timelineVisible)}
|
|
1042
|
+
aria-label={timelineVisible ? "Hide timeline editor" : "Show timeline editor"}
|
|
958
1043
|
>
|
|
959
1044
|
<svg
|
|
960
1045
|
width="14"
|
|
@@ -969,6 +1054,7 @@ export function StudioApp() {
|
|
|
969
1054
|
<line x1="3" y1="9" x2="21" y2="9" />
|
|
970
1055
|
<line x1="3" y1="5" x2="21" y2="5" />
|
|
971
1056
|
</svg>
|
|
1057
|
+
<span>Timeline</span>
|
|
972
1058
|
</button>
|
|
973
1059
|
<button
|
|
974
1060
|
onClick={() => setRightCollapsed((v) => !v)}
|
|
@@ -1079,6 +1165,7 @@ export function StudioApp() {
|
|
|
1079
1165
|
}}
|
|
1080
1166
|
onIframeRef={(iframe) => {
|
|
1081
1167
|
previewIframeRef.current = iframe;
|
|
1168
|
+
syncPreviewTimelineHotkey(iframe);
|
|
1082
1169
|
consoleErrorsRef.current = [];
|
|
1083
1170
|
setConsoleErrors(null);
|
|
1084
1171
|
if (!iframe) return;
|
|
@@ -1143,7 +1230,7 @@ export function StudioApp() {
|
|
|
1143
1230
|
) : undefined
|
|
1144
1231
|
}
|
|
1145
1232
|
timelineVisible={timelineVisible}
|
|
1146
|
-
onToggleTimeline={
|
|
1233
|
+
onToggleTimeline={toggleTimelineVisibility}
|
|
1147
1234
|
/>
|
|
1148
1235
|
</div>
|
|
1149
1236
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useRef, useState, useCallback, useEffect, memo } from "react";
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
|
+
import {
|
|
4
|
+
TIMELINE_TOGGLE_SHORTCUT_LABEL,
|
|
5
|
+
getTimelineToggleTitle,
|
|
6
|
+
} from "../../utils/timelineDiscovery";
|
|
3
7
|
import { formatTime } from "../lib/time";
|
|
4
8
|
import { usePlayerStore, liveTime } from "../store/playerStore";
|
|
5
9
|
|
|
@@ -328,13 +332,15 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
328
332
|
{/* Timeline toggle */}
|
|
329
333
|
{onToggleTimeline !== undefined && (
|
|
330
334
|
<button
|
|
335
|
+
type="button"
|
|
331
336
|
onClick={onToggleTimeline}
|
|
332
|
-
className={`
|
|
337
|
+
className={`h-7 flex items-center gap-1.5 rounded-md border px-2.5 text-[11px] font-medium transition-colors ${
|
|
333
338
|
timelineVisible
|
|
334
339
|
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
335
|
-
: "border-neutral-700 text-neutral-
|
|
340
|
+
: "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800"
|
|
336
341
|
}`}
|
|
337
|
-
title={timelineVisible
|
|
342
|
+
title={getTimelineToggleTitle(Boolean(timelineVisible))}
|
|
343
|
+
aria-label={timelineVisible ? "Hide timeline editor" : "Show timeline editor"}
|
|
338
344
|
>
|
|
339
345
|
<svg
|
|
340
346
|
width="13"
|
|
@@ -349,6 +355,10 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
349
355
|
<line x1="3" y1="9" x2="21" y2="9" />
|
|
350
356
|
<line x1="3" y1="5" x2="21" y2="5" />
|
|
351
357
|
</svg>
|
|
358
|
+
<span>Timeline</span>
|
|
359
|
+
<span className="hidden md:inline rounded bg-black/20 px-1 py-0.5 text-[9px] font-mono opacity-70">
|
|
360
|
+
{TIMELINE_TOGGLE_SHORTCUT_LABEL}
|
|
361
|
+
</span>
|
|
352
362
|
</button>
|
|
353
363
|
)}
|
|
354
364
|
</div>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
TIMELINE_TOGGLE_SHORTCUT_LABEL,
|
|
4
|
+
getTimelineToggleTitle,
|
|
5
|
+
shouldHandleTimelineToggleHotkey,
|
|
6
|
+
} from "./timelineDiscovery";
|
|
7
|
+
|
|
8
|
+
describe("shouldHandleTimelineToggleHotkey", () => {
|
|
9
|
+
it("accepts Shift+T when focus is not inside an editor", () => {
|
|
10
|
+
expect(
|
|
11
|
+
shouldHandleTimelineToggleHotkey({
|
|
12
|
+
key: "T",
|
|
13
|
+
shiftKey: true,
|
|
14
|
+
metaKey: false,
|
|
15
|
+
ctrlKey: false,
|
|
16
|
+
altKey: false,
|
|
17
|
+
target: {
|
|
18
|
+
tagName: "DIV",
|
|
19
|
+
isContentEditable: false,
|
|
20
|
+
closest: () => null,
|
|
21
|
+
},
|
|
22
|
+
} as KeyboardEvent),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("ignores the shortcut inside text inputs", () => {
|
|
27
|
+
expect(
|
|
28
|
+
shouldHandleTimelineToggleHotkey({
|
|
29
|
+
key: "t",
|
|
30
|
+
shiftKey: true,
|
|
31
|
+
metaKey: false,
|
|
32
|
+
ctrlKey: false,
|
|
33
|
+
altKey: false,
|
|
34
|
+
target: {
|
|
35
|
+
tagName: "TEXTAREA",
|
|
36
|
+
isContentEditable: false,
|
|
37
|
+
closest: () => null,
|
|
38
|
+
},
|
|
39
|
+
} as KeyboardEvent),
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("ignores the shortcut inside contenteditable editors", () => {
|
|
44
|
+
expect(
|
|
45
|
+
shouldHandleTimelineToggleHotkey({
|
|
46
|
+
key: "t",
|
|
47
|
+
shiftKey: true,
|
|
48
|
+
metaKey: false,
|
|
49
|
+
ctrlKey: false,
|
|
50
|
+
altKey: false,
|
|
51
|
+
target: {
|
|
52
|
+
tagName: "DIV",
|
|
53
|
+
isContentEditable: true,
|
|
54
|
+
closest: () => null,
|
|
55
|
+
},
|
|
56
|
+
} as KeyboardEvent),
|
|
57
|
+
).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("requires Shift without other modifiers", () => {
|
|
61
|
+
expect(
|
|
62
|
+
shouldHandleTimelineToggleHotkey({
|
|
63
|
+
key: "t",
|
|
64
|
+
shiftKey: false,
|
|
65
|
+
metaKey: false,
|
|
66
|
+
ctrlKey: false,
|
|
67
|
+
altKey: false,
|
|
68
|
+
target: null,
|
|
69
|
+
} as KeyboardEvent),
|
|
70
|
+
).toBe(false);
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
shouldHandleTimelineToggleHotkey({
|
|
74
|
+
key: "t",
|
|
75
|
+
shiftKey: true,
|
|
76
|
+
metaKey: true,
|
|
77
|
+
ctrlKey: false,
|
|
78
|
+
altKey: false,
|
|
79
|
+
target: null,
|
|
80
|
+
} as KeyboardEvent),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("getTimelineToggleTitle", () => {
|
|
86
|
+
it("includes the shortcut in both show and hide titles", () => {
|
|
87
|
+
expect(getTimelineToggleTitle(true)).toContain(TIMELINE_TOGGLE_SHORTCUT_LABEL);
|
|
88
|
+
expect(getTimelineToggleTitle(false)).toContain(TIMELINE_TOGGLE_SHORTCUT_LABEL);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const TIMELINE_TOGGLE_SHORTCUT_LABEL = "Shift+T";
|
|
2
|
+
const TIMELINE_EDITOR_HINT_STORAGE_KEY = "hf-studio-timeline-editor-hint-dismissed";
|
|
3
|
+
|
|
4
|
+
type TimelineToggleHotkeyEvent = Pick<
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
"key" | "shiftKey" | "metaKey" | "ctrlKey" | "altKey" | "target"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
interface EditableTargetLike {
|
|
10
|
+
tagName?: string;
|
|
11
|
+
isContentEditable?: boolean;
|
|
12
|
+
closest?: (selector: string) => unknown;
|
|
13
|
+
getAttribute?: (name: string) => string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
17
|
+
if (!target || typeof target !== "object") return false;
|
|
18
|
+
|
|
19
|
+
const element = target as EditableTargetLike;
|
|
20
|
+
const tagName = element.tagName?.toLowerCase();
|
|
21
|
+
if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
|
|
22
|
+
if (element.isContentEditable) return true;
|
|
23
|
+
|
|
24
|
+
const role = element.getAttribute?.("role");
|
|
25
|
+
if (role === "textbox" || role === "searchbox" || role === "combobox") return true;
|
|
26
|
+
|
|
27
|
+
return Boolean(
|
|
28
|
+
element.closest?.(
|
|
29
|
+
"input, textarea, select, [contenteditable='true'], [role='textbox'], .cm-editor",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shouldHandleTimelineToggleHotkey(event: TimelineToggleHotkeyEvent): boolean {
|
|
35
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
36
|
+
if (!event.shiftKey) return false;
|
|
37
|
+
if (event.key.toLowerCase() !== "t") return false;
|
|
38
|
+
return !isEditableTarget(event.target);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getTimelineToggleTitle(timelineVisible: boolean): string {
|
|
42
|
+
return `${timelineVisible ? "Hide" : "Show"} timeline editor (${TIMELINE_TOGGLE_SHORTCUT_LABEL})`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getTimelineEditorHintDismissed(): boolean {
|
|
46
|
+
if (typeof window === "undefined") return false;
|
|
47
|
+
return window.localStorage.getItem(TIMELINE_EDITOR_HINT_STORAGE_KEY) === "1";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function setTimelineEditorHintDismissed(dismissed: boolean): void {
|
|
51
|
+
if (typeof window === "undefined") return;
|
|
52
|
+
if (dismissed) {
|
|
53
|
+
window.localStorage.setItem(TIMELINE_EDITOR_HINT_STORAGE_KEY, "1");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
window.localStorage.removeItem(TIMELINE_EDITOR_HINT_STORAGE_KEY);
|
|
57
|
+
}
|