@ksp-gonogo/kerbcast-react 1.5.0 → 1.6.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/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/CameraFeed.js +8 -4
- package/package.json +2 -2
- package/src/CameraFeed.test.tsx +33 -0
- package/src/CameraFeed.tsx +25 -21
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jon Pepler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -138,8 +138,12 @@ const useDelayedStream: CameraStreamHook = (flightId) => {
|
|
|
138
138
|
|
|
139
139
|
## License
|
|
140
140
|
|
|
141
|
+
[MIT](./LICENSE). This SDK is the interface to kerbcast, so it is permissive:
|
|
142
|
+
build what you like against it. The kerbcast mod itself is
|
|
141
143
|
[CC BY-NC-SA 4.0](https://github.com/ksp-gonogo/kerbcast/blob/main/LICENSE).
|
|
142
144
|
|
|
145
|
+
Versions published before 1.6.0 carry the older CC BY-NC-SA 4.0 licence.
|
|
146
|
+
|
|
143
147
|
## Versioning
|
|
144
148
|
|
|
145
149
|
Tracks `@ksp-gonogo/kerbcast` exactly; use matching versions of both.
|
package/dist/CameraFeed.js
CHANGED
|
@@ -512,7 +512,7 @@ const CameraFeedInner = forwardRef(function CameraFeedInner({ flightId: requeste
|
|
|
512
512
|
void client.camera(flightId).setQuality(preset);
|
|
513
513
|
closeQualityMenu();
|
|
514
514
|
}, [client, flightId, closeQualityMenu]);
|
|
515
|
-
const topOverlay = (_jsxs(TopOverlay, { children: [
|
|
515
|
+
const topOverlay = (_jsxs(TopOverlay, { children: [_jsx(TitleRow, { children: _jsx(TopTitle, { children: hasCameras ? (_jsxs(TitleButton, { ref: cameraMenu.triggerRef, type: "button", "aria-haspopup": "menu", "aria-expanded": cameraMenu.open, "aria-controls": menuId, onClick: cameraMenu.toggle, children: [_jsx(TitleButton__Text, { children: title }), _jsx(ChevronDownIcon, { "aria-hidden": "true" })] })) : (title) }) }), cameraMenu.open &&
|
|
516
516
|
hasCameras &&
|
|
517
517
|
cameraMenu.position &&
|
|
518
518
|
createPortal(_jsx(CameraMenu, { ref: cameraMenu.menuRef, id: menuId, role: "menu", "aria-label": "Camera", style: cameraMenu.position, children: cameras.map((c) => (_jsxs(CameraMenuItem, { type: "button", role: "menuitemradio", "aria-checked": c.flightId === flightId, "$selected": c.flightId === flightId, onClick: () => {
|
|
@@ -522,10 +522,11 @@ const CameraFeedInner = forwardRef(function CameraFeedInner({ flightId: requeste
|
|
|
522
522
|
(camera ? (_jsxs(TopMeta, { children: [camera.vesselName, " \u00B7 ", camera.renderWidth, "\u00D7", camera.renderHeight, bitrateLabel, adaptiveLabel] })) : (_jsx(TopMeta, { children: "no cameras on this vessel" })))] }));
|
|
523
523
|
const renderAction = (a) => (_jsx(OverlayIconButton, { type: "button", "aria-label": a.label, "aria-pressed": a.active ?? undefined, title: a.label, "$active": a.active ?? false, onClick: a.onClick, children: a.icon }, a.id));
|
|
524
524
|
const builtInActions = flightId !== null && (pipAvailable || fullscreenAvailable || qualityAvailable);
|
|
525
|
-
const hasActionBar =
|
|
525
|
+
const hasActionBar = hasCameras ||
|
|
526
|
+
(actions && actions.length > 0) ||
|
|
526
527
|
(trailingActions && trailingActions.length > 0) ||
|
|
527
528
|
builtInActions;
|
|
528
|
-
const actionBar = hasActionBar ? (_jsxs(ActionBar, { children: [actions?.map(renderAction), qualityAvailable && (_jsxs(OverlayIconButton, { ref: qualityMenu.triggerRef, type: "button", "aria-label": "Quality", "aria-haspopup": "menu", "aria-expanded": qualityMenu.open, "aria-controls": qualityMenuId, title: qualityThrottled
|
|
529
|
+
const actionBar = hasActionBar ? (_jsxs(ActionBar, { children: [hasCameras && (_jsxs(StepButtons, { children: [_jsx(OverlayIconButton, { type: "button", "aria-label": "Previous camera", disabled: !canStep, onClick: () => stepCamera(-1), children: "\u2039" }), _jsx(OverlayIconButton, { type: "button", "aria-label": "Next camera", disabled: !canStep, onClick: () => stepCamera(1), children: "\u203A" })] })), actions?.map(renderAction), qualityAvailable && (_jsxs(OverlayIconButton, { ref: qualityMenu.triggerRef, type: "button", "aria-label": "Quality", "aria-haspopup": "menu", "aria-expanded": qualityMenu.open, "aria-controls": qualityMenuId, title: qualityThrottled
|
|
529
530
|
? "Quality (throttled by adaptive performance)"
|
|
530
531
|
: "Quality", "$active": qualityMenu.open, onClick: qualityMenu.toggle, children: [_jsx(QualityIcon, {}), qualityThrottled && _jsx(ThrottledDot, { "aria-hidden": "true" })] })), flightId !== null && pipAvailable && (_jsx(OverlayIconButton, { type: "button", "aria-label": isPip ? "Exit picture in picture" : "Picture in picture", "aria-pressed": isPip, title: isPip ? "Exit picture in picture" : "Picture in picture", "$active": isPip, onClick: togglePip, children: _jsx(PictureInPictureIcon, {}) })), flightId !== null && fullscreenAvailable && (_jsx(OverlayIconButton, { type: "button", "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen", "aria-pressed": isFullscreen, title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen", "$active": isFullscreen, onClick: toggleFullscreen, children: isFullscreen ? _jsx(FullscreenExitIcon, {}) : _jsx(FullscreenEnterIcon, {}) })), trailingActions?.map(renderAction)] })) : null;
|
|
531
532
|
return (_jsx(Stage, { ref: wrapRef, "$pinned": chromePinned, children: flightId === null ? (_jsxs(_Fragment, { children: [_jsx(Empty, { children: emptyMessage }), topOverlay, actionBar] })) : (_jsxs(_Fragment, { children: [_jsx(StyledVideo, { ref: videoRef, autoPlay: true, playsInline: true, muted: true, controls: false, onClick: () => setChromePinned((v) => !v) }), topOverlay, actionBar, qualityAvailable &&
|
|
@@ -746,8 +747,11 @@ const TopOverlay = styled.div `
|
|
|
746
747
|
const TitleRow = styled.div `
|
|
747
748
|
display: flex;
|
|
748
749
|
align-items: flex-start;
|
|
749
|
-
justify-content: space-between;
|
|
750
750
|
gap: 8px;
|
|
751
|
+
/* Reserve the top-right corner where the ActionBar overlay sits (now home
|
|
752
|
+
to the camera step buttons too) so a long title ellipsizes rather than
|
|
753
|
+
sliding underneath the controls. */
|
|
754
|
+
padding-right: 96px;
|
|
751
755
|
`;
|
|
752
756
|
const TopTitle = styled.h3 `
|
|
753
757
|
margin: 0;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksp-gonogo/kerbcast-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "React components and hooks for kerbcast camera feeds.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "Jon Pepler",
|
|
7
7
|
"homepage": "https://github.com/ksp-gonogo/kerbcast#readme",
|
|
8
8
|
"bugs": {
|
package/src/CameraFeed.test.tsx
CHANGED
|
@@ -445,6 +445,39 @@ describe("CameraFeed - camera selection", () => {
|
|
|
445
445
|
).toBe(true);
|
|
446
446
|
});
|
|
447
447
|
|
|
448
|
+
it("renders step buttons inside the action bar alongside injected actions", async () => {
|
|
449
|
+
// Regression: the step buttons used to sit in the title row and were
|
|
450
|
+
// covered by the separately-positioned action bar (and any consumer
|
|
451
|
+
// action buttons) in the top-right corner. They now share the action
|
|
452
|
+
// bar so the two lay out in one flex row instead of overlapping.
|
|
453
|
+
const { client } = await buildConnectedSource(THREE_CAMERAS);
|
|
454
|
+
|
|
455
|
+
render(
|
|
456
|
+
<KerbcastProvider client={client}>
|
|
457
|
+
<CameraFeed
|
|
458
|
+
flightId={42}
|
|
459
|
+
actions={[
|
|
460
|
+
{
|
|
461
|
+
id: "custom",
|
|
462
|
+
label: "Custom action",
|
|
463
|
+
icon: <span>x</span>,
|
|
464
|
+
onClick: () => {},
|
|
465
|
+
},
|
|
466
|
+
]}
|
|
467
|
+
/>
|
|
468
|
+
</KerbcastProvider>,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const next = screen.getByRole("button", { name: /next camera/i });
|
|
472
|
+
const custom = screen.getByRole("button", { name: /custom action/i });
|
|
473
|
+
|
|
474
|
+
// The injected action's parent is the action bar; the step buttons live
|
|
475
|
+
// in a wrapper within that same action bar, so it contains both.
|
|
476
|
+
const actionBar = custom.parentElement;
|
|
477
|
+
expect(actionBar).not.toBeNull();
|
|
478
|
+
expect(actionBar?.contains(next)).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
448
481
|
it("Escape closes the open camera menu", async () => {
|
|
449
482
|
const { client } = await buildConnectedSource(THREE_CAMERAS);
|
|
450
483
|
|
package/src/CameraFeed.tsx
CHANGED
|
@@ -796,26 +796,6 @@ const CameraFeedInner = forwardRef<CameraFeedHandle, CameraFeedProps>(
|
|
|
796
796
|
title
|
|
797
797
|
)}
|
|
798
798
|
</TopTitle>
|
|
799
|
-
{hasCameras && (
|
|
800
|
-
<StepButtons>
|
|
801
|
-
<OverlayIconButton
|
|
802
|
-
type="button"
|
|
803
|
-
aria-label="Previous camera"
|
|
804
|
-
disabled={!canStep}
|
|
805
|
-
onClick={() => stepCamera(-1)}
|
|
806
|
-
>
|
|
807
|
-
‹
|
|
808
|
-
</OverlayIconButton>
|
|
809
|
-
<OverlayIconButton
|
|
810
|
-
type="button"
|
|
811
|
-
aria-label="Next camera"
|
|
812
|
-
disabled={!canStep}
|
|
813
|
-
onClick={() => stepCamera(1)}
|
|
814
|
-
>
|
|
815
|
-
›
|
|
816
|
-
</OverlayIconButton>
|
|
817
|
-
</StepButtons>
|
|
818
|
-
)}
|
|
819
799
|
</TitleRow>
|
|
820
800
|
|
|
821
801
|
{cameraMenu.open &&
|
|
@@ -879,11 +859,32 @@ const CameraFeedInner = forwardRef<CameraFeedHandle, CameraFeedProps>(
|
|
|
879
859
|
const builtInActions =
|
|
880
860
|
flightId !== null && (pipAvailable || fullscreenAvailable || qualityAvailable);
|
|
881
861
|
const hasActionBar =
|
|
862
|
+
hasCameras ||
|
|
882
863
|
(actions && actions.length > 0) ||
|
|
883
864
|
(trailingActions && trailingActions.length > 0) ||
|
|
884
865
|
builtInActions;
|
|
885
866
|
const actionBar = hasActionBar ? (
|
|
886
867
|
<ActionBar>
|
|
868
|
+
{hasCameras && (
|
|
869
|
+
<StepButtons>
|
|
870
|
+
<OverlayIconButton
|
|
871
|
+
type="button"
|
|
872
|
+
aria-label="Previous camera"
|
|
873
|
+
disabled={!canStep}
|
|
874
|
+
onClick={() => stepCamera(-1)}
|
|
875
|
+
>
|
|
876
|
+
‹
|
|
877
|
+
</OverlayIconButton>
|
|
878
|
+
<OverlayIconButton
|
|
879
|
+
type="button"
|
|
880
|
+
aria-label="Next camera"
|
|
881
|
+
disabled={!canStep}
|
|
882
|
+
onClick={() => stepCamera(1)}
|
|
883
|
+
>
|
|
884
|
+
›
|
|
885
|
+
</OverlayIconButton>
|
|
886
|
+
</StepButtons>
|
|
887
|
+
)}
|
|
887
888
|
{actions?.map(renderAction)}
|
|
888
889
|
{qualityAvailable && (
|
|
889
890
|
<OverlayIconButton
|
|
@@ -1393,8 +1394,11 @@ const TopOverlay = styled.div`
|
|
|
1393
1394
|
const TitleRow = styled.div`
|
|
1394
1395
|
display: flex;
|
|
1395
1396
|
align-items: flex-start;
|
|
1396
|
-
justify-content: space-between;
|
|
1397
1397
|
gap: 8px;
|
|
1398
|
+
/* Reserve the top-right corner where the ActionBar overlay sits (now home
|
|
1399
|
+
to the camera step buttons too) so a long title ellipsizes rather than
|
|
1400
|
+
sliding underneath the controls. */
|
|
1401
|
+
padding-right: 96px;
|
|
1398
1402
|
`;
|
|
1399
1403
|
|
|
1400
1404
|
const TopTitle = styled.h3`
|