@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 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.
@@ -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: [_jsxs(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) }), 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" })] }))] }), cameraMenu.open &&
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 = (actions && actions.length > 0) ||
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.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "React components and hooks for kerbcast camera feeds.",
5
- "license": "CC-BY-NC-SA-4.0",
5
+ "license": "MIT",
6
6
  "author": "Jon Pepler",
7
7
  "homepage": "https://github.com/ksp-gonogo/kerbcast#readme",
8
8
  "bugs": {
@@ -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
 
@@ -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
- &#8249;
808
- </OverlayIconButton>
809
- <OverlayIconButton
810
- type="button"
811
- aria-label="Next camera"
812
- disabled={!canStep}
813
- onClick={() => stepCamera(1)}
814
- >
815
- &#8250;
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
+ &#8249;
877
+ </OverlayIconButton>
878
+ <OverlayIconButton
879
+ type="button"
880
+ aria-label="Next camera"
881
+ disabled={!canStep}
882
+ onClick={() => stepCamera(1)}
883
+ >
884
+ &#8250;
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`