@ksp-gonogo/kerbcast-react 1.6.3 → 1.8.0
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/CHANGELOG.md +54 -0
- package/README.md +19 -0
- package/dist/CameraFeed.d.ts +63 -19
- package/dist/CameraFeed.js +329 -202
- package/dist/FeedActionBar.d.ts +133 -0
- package/dist/FeedActionBar.js +225 -0
- package/dist/KerbalFaceFeed.d.ts +82 -0
- package/dist/KerbalFaceFeed.js +111 -0
- package/dist/RecordingsTray.d.ts +37 -0
- package/dist/RecordingsTray.js +279 -0
- package/dist/context.d.ts +27 -1
- package/dist/context.js +68 -2
- package/dist/hooks/useRecordings.d.ts +35 -0
- package/dist/hooks/useRecordings.js +33 -0
- package/dist/hooks/useReportDisplaySize.d.ts +23 -0
- package/dist/hooks/useReportDisplaySize.js +71 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +13 -1
- package/dist/menuPositioning.d.ts +20 -0
- package/dist/menuPositioning.js +98 -0
- package/dist/timing.d.ts +4 -0
- package/dist/timing.js +19 -0
- package/package.json +2 -2
- package/src/CameraFeed.test.tsx +719 -10
- package/src/CameraFeed.tsx +586 -334
- package/src/FeedActionBar.test.ts +196 -0
- package/src/FeedActionBar.test.tsx +215 -0
- package/src/FeedActionBar.tsx +391 -0
- package/src/KerbalFaceFeed.test.tsx +203 -0
- package/src/KerbalFaceFeed.tsx +214 -0
- package/src/RecordingsTray.test.tsx +288 -0
- package/src/RecordingsTray.tsx +475 -0
- package/src/context.test.tsx +94 -0
- package/src/context.tsx +98 -1
- package/src/hooks/useRecordings.sharedState.test.tsx +117 -0
- package/src/hooks/useRecordings.test.tsx +217 -0
- package/src/hooks/useRecordings.ts +110 -0
- package/src/hooks/useReportDisplaySize.test.tsx +166 -0
- package/src/hooks/useReportDisplaySize.ts +91 -0
- package/src/index.ts +27 -1
- package/src/menuPositioning.ts +126 -0
- package/src/timing.test.ts +34 -0
- package/src/timing.ts +21 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared geometry + behaviour for a dropdown portaled to document.body.
|
|
3
|
+
* Used by CameraFeed's camera / quality / tracking menus and by
|
|
4
|
+
* FeedActionBar's overflow menu, so the anchoring, dismissal, and Escape
|
|
5
|
+
* handling live in exactly one place.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
const MENU_GAP = 4; // trigger-to-menu spacing
|
|
9
|
+
const MENU_EDGE = 8; // minimum inset from the viewport edge
|
|
10
|
+
/*
|
|
11
|
+
* Fixed-position style for a portaled menu, anchored to its trigger button.
|
|
12
|
+
* Opens downward by default; flips above the trigger when there is not
|
|
13
|
+
* enough room below but there is above, otherwise clamps to the viewport.
|
|
14
|
+
*/
|
|
15
|
+
export function computeMenuPosition(trigger, anchor) {
|
|
16
|
+
const rect = trigger.getBoundingClientRect();
|
|
17
|
+
const vw = window.innerWidth;
|
|
18
|
+
const vh = window.innerHeight;
|
|
19
|
+
const width = Math.min(anchor.maxWidth, vw - 2 * MENU_EDGE);
|
|
20
|
+
/* Inline offset from the aligned viewport edge, clamped so a full-width
|
|
21
|
+
menu still fits inside the opposite edge. */
|
|
22
|
+
const inset = Math.max(MENU_EDGE, vw - width - MENU_EDGE);
|
|
23
|
+
const inline = anchor.align === "start"
|
|
24
|
+
? { left: Math.min(Math.max(rect.left, MENU_EDGE), inset) }
|
|
25
|
+
: { right: Math.min(Math.max(vw - rect.right, MENU_EDGE), inset) };
|
|
26
|
+
const menuH = Math.min(0.4 * vh, anchor.maxHeight);
|
|
27
|
+
const fitsBelow = rect.bottom + MENU_GAP + menuH <= vh - MENU_EDGE;
|
|
28
|
+
const fitsAbove = rect.top - MENU_GAP - menuH >= MENU_EDGE;
|
|
29
|
+
if (!fitsBelow && fitsAbove) {
|
|
30
|
+
return { ...inline, bottom: vh - rect.top + MENU_GAP };
|
|
31
|
+
}
|
|
32
|
+
const top = Math.min(rect.bottom + MENU_GAP, Math.max(MENU_EDGE, vh - MENU_EDGE - menuH));
|
|
33
|
+
return { ...inline, top };
|
|
34
|
+
}
|
|
35
|
+
/*
|
|
36
|
+
* Shared behaviour for a dropdown portaled to document.body: fixed position
|
|
37
|
+
* computed from the trigger's rect (so tile overflow cannot clip it),
|
|
38
|
+
* re-anchored on window resize/scroll while open, Escape to close with focus
|
|
39
|
+
* returned to the trigger, and portal-aware outside-pointer-down dismissal
|
|
40
|
+
* (menuRef points at the portaled menu, so clicks inside it stay "inside").
|
|
41
|
+
* One menu at a time falls out of the dismissal: opening another menu's
|
|
42
|
+
* trigger is an outside press for this one.
|
|
43
|
+
*/
|
|
44
|
+
export function usePortalMenu({ maxWidth, maxHeight, align }) {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [position, setPosition] = useState(null);
|
|
47
|
+
const menuRef = useRef(null);
|
|
48
|
+
const triggerRef = useRef(null);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!open) {
|
|
51
|
+
setPosition(null);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const update = () => {
|
|
55
|
+
const trigger = triggerRef.current;
|
|
56
|
+
if (trigger) {
|
|
57
|
+
setPosition(computeMenuPosition(trigger, { maxWidth, maxHeight, align }));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
update();
|
|
61
|
+
window.addEventListener("resize", update);
|
|
62
|
+
window.addEventListener("scroll", update, true);
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener("resize", update);
|
|
65
|
+
window.removeEventListener("scroll", update, true);
|
|
66
|
+
};
|
|
67
|
+
}, [open, maxWidth, maxHeight, align]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!open)
|
|
70
|
+
return;
|
|
71
|
+
const onKeyDown = (e) => {
|
|
72
|
+
if (e.key === "Escape") {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
setOpen(false);
|
|
75
|
+
triggerRef.current?.focus();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const onPointerDown = (e) => {
|
|
79
|
+
if (!menuRef.current?.contains(e.target) &&
|
|
80
|
+
!triggerRef.current?.contains(e.target)) {
|
|
81
|
+
setOpen(false);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
document.addEventListener("keydown", onKeyDown);
|
|
85
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
88
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
89
|
+
};
|
|
90
|
+
}, [open]);
|
|
91
|
+
const toggle = useCallback(() => setOpen((v) => !v), []);
|
|
92
|
+
/** Close after an item pick, returning focus to the trigger. */
|
|
93
|
+
const close = useCallback(() => {
|
|
94
|
+
setOpen(false);
|
|
95
|
+
triggerRef.current?.focus();
|
|
96
|
+
}, []);
|
|
97
|
+
return { open, toggle, close, position, menuRef, triggerRef };
|
|
98
|
+
}
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Monotonic clock for elapsed-timer math. Falls back to Date.now under jsdom. */
|
|
2
|
+
export declare function nowMs(): number;
|
|
3
|
+
/** Formats a millisecond duration as m:ss (e.g. "REC 1:05", "Recording 3 feeds - 0:42"). */
|
|
4
|
+
export declare function formatElapsed(ms: number): string;
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared elapsed-timer helpers for REC-style live-duration displays: the
|
|
3
|
+
* per-feed REC badge (CameraFeed) and the REC+ grouped-recording toolbar
|
|
4
|
+
* (a host's own RecGroupBar). Framework-free so both places read the same
|
|
5
|
+
* clock and format the same way instead of drifting apart.
|
|
6
|
+
*/
|
|
7
|
+
/** Monotonic clock for elapsed-timer math. Falls back to Date.now under jsdom. */
|
|
8
|
+
export function nowMs() {
|
|
9
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
10
|
+
? performance.now()
|
|
11
|
+
: Date.now();
|
|
12
|
+
}
|
|
13
|
+
/** Formats a millisecond duration as m:ss (e.g. "REC 1:05", "Recording 3 feeds - 0:42"). */
|
|
14
|
+
export function formatElapsed(ms) {
|
|
15
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
16
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
17
|
+
const seconds = totalSeconds % 60;
|
|
18
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksp-gonogo/kerbcast-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "React components and hooks for kerbcast camera feeds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jon Pepler",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@ksp-gonogo/kerbcast": "^1.
|
|
48
|
+
"@ksp-gonogo/kerbcast": "^1.8.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": "^18 || ^19",
|