@deck.gl/widgets 9.3.0-alpha.6 → 9.3.0-beta.2

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.
Files changed (55) hide show
  1. package/dist/compass-widget.d.ts +12 -0
  2. package/dist/compass-widget.d.ts.map +1 -1
  3. package/dist/compass-widget.js +9 -3
  4. package/dist/compass-widget.js.map +1 -1
  5. package/dist/dist.dev.js +285 -93
  6. package/dist/fullscreen-widget.d.ts +5 -0
  7. package/dist/fullscreen-widget.d.ts.map +1 -1
  8. package/dist/fullscreen-widget.js +18 -8
  9. package/dist/fullscreen-widget.js.map +1 -1
  10. package/dist/geocoder-widget.d.ts +13 -0
  11. package/dist/geocoder-widget.d.ts.map +1 -1
  12. package/dist/geocoder-widget.js +13 -1
  13. package/dist/geocoder-widget.js.map +1 -1
  14. package/dist/gimbal-widget.d.ts +12 -0
  15. package/dist/gimbal-widget.d.ts.map +1 -1
  16. package/dist/gimbal-widget.js +4 -1
  17. package/dist/gimbal-widget.js.map +1 -1
  18. package/dist/index.cjs +278 -90
  19. package/dist/index.cjs.map +3 -3
  20. package/dist/loading-widget.d.ts +5 -0
  21. package/dist/loading-widget.d.ts.map +1 -1
  22. package/dist/loading-widget.js +4 -1
  23. package/dist/loading-widget.js.map +1 -1
  24. package/dist/reset-view-widget.d.ts +9 -0
  25. package/dist/reset-view-widget.d.ts.map +1 -1
  26. package/dist/reset-view-widget.js +4 -1
  27. package/dist/reset-view-widget.js.map +1 -1
  28. package/dist/stats-widget.d.ts +18 -3
  29. package/dist/stats-widget.d.ts.map +1 -1
  30. package/dist/stats-widget.js +29 -12
  31. package/dist/stats-widget.js.map +1 -1
  32. package/dist/theme-widget.d.ts +20 -2
  33. package/dist/theme-widget.d.ts.map +1 -1
  34. package/dist/theme-widget.js +38 -14
  35. package/dist/theme-widget.js.map +1 -1
  36. package/dist/timeline-widget.d.ts +34 -3
  37. package/dist/timeline-widget.d.ts.map +1 -1
  38. package/dist/timeline-widget.js +98 -24
  39. package/dist/timeline-widget.js.map +1 -1
  40. package/dist/zoom-widget.d.ts +22 -1
  41. package/dist/zoom-widget.d.ts.map +1 -1
  42. package/dist/zoom-widget.js +62 -15
  43. package/dist/zoom-widget.js.map +1 -1
  44. package/dist.min.js +2 -2
  45. package/package.json +3 -3
  46. package/src/compass-widget.tsx +23 -3
  47. package/src/fullscreen-widget.tsx +24 -9
  48. package/src/geocoder-widget.tsx +23 -1
  49. package/src/gimbal-widget.tsx +17 -1
  50. package/src/loading-widget.tsx +11 -1
  51. package/src/reset-view-widget.tsx +15 -1
  52. package/src/stats-widget.tsx +43 -14
  53. package/src/theme-widget.tsx +56 -17
  54. package/src/timeline-widget.tsx +123 -24
  55. package/src/zoom-widget.tsx +108 -17
package/dist/dist.dev.js CHANGED
@@ -659,25 +659,65 @@ var __exports__ = (() => {
659
659
  ] });
660
660
  B(ui, rootElement);
661
661
  }
662
- handleZoom(viewId, nextZoom) {
662
+ isOrthographicView(viewId) {
663
+ const deck = this.deck;
664
+ const view = deck?.isInitialized && deck.getView(viewId);
665
+ return view instanceof import_core2.OrthographicView;
666
+ }
667
+ handleZoom(viewId, delta) {
663
668
  const viewState = this.getViewState(viewId);
664
- if (viewState) {
665
- const { minZoom, maxZoom } = viewState;
666
- if (Number.isFinite(minZoom)) {
667
- nextZoom = Math.max(minZoom, nextZoom);
668
- }
669
- if (Number.isFinite(maxZoom)) {
670
- nextZoom = Math.min(maxZoom, nextZoom);
669
+ const newViewState = {};
670
+ if (this.isOrthographicView(viewId)) {
671
+ const { zoomAxis } = this.props;
672
+ const { zoomX, minZoomX, maxZoomX, zoomY, minZoomY, maxZoomY } = normalizeOrthographicViewState(
673
+ viewState
674
+ );
675
+ let nextZoom;
676
+ let nextZoomY;
677
+ if (zoomAxis === "X") {
678
+ nextZoom = clamp(zoomX + delta, minZoomX, maxZoomX);
679
+ nextZoomY = zoomY;
680
+ } else if (zoomAxis === "Y") {
681
+ nextZoom = zoomX;
682
+ nextZoomY = clamp(zoomY + delta, minZoomY, maxZoomY);
683
+ } else {
684
+ const clampedDelta = clamp(
685
+ delta,
686
+ Math.max(minZoomX - zoomX, minZoomY - zoomY),
687
+ Math.min(maxZoomX - zoomX, maxZoomY - zoomY)
688
+ );
689
+ nextZoom = zoomX + clampedDelta;
690
+ nextZoomY = zoomY + clampedDelta;
671
691
  }
692
+ newViewState.zoom = [nextZoom, nextZoomY];
693
+ newViewState.zoomX = nextZoom;
694
+ newViewState.zoomY = nextZoomY;
695
+ this.props.onZoom?.({
696
+ viewId,
697
+ delta,
698
+ // `zoom` will not match the new state if using 2D zoom. Deprecated behavior for backward compatibility.
699
+ zoom: zoomAxis === "Y" ? nextZoomY : nextZoom,
700
+ zoomX: nextZoom,
701
+ zoomY: nextZoomY
702
+ });
703
+ } else {
704
+ const { zoom = 0, minZoom, maxZoom } = viewState;
705
+ const nextZoom = clamp(zoom + delta, minZoom, maxZoom);
706
+ newViewState.zoom = nextZoom;
707
+ this.props.onZoom?.({
708
+ viewId,
709
+ delta,
710
+ zoom: nextZoom
711
+ });
672
712
  }
673
713
  const nextViewState = {
674
714
  ...viewState,
675
- zoom: nextZoom
715
+ ...newViewState
676
716
  };
677
717
  if (this.props.transitionDuration > 0) {
678
718
  nextViewState.transitionDuration = this.props.transitionDuration;
679
719
  nextViewState.transitionInterpolator = "latitude" in nextViewState ? new import_core2.FlyToInterpolator() : new import_core2.LinearInterpolator({
680
- transitionProps: ["zoom"]
720
+ transitionProps: "zoomX" in newViewState ? ["zoomX", "zoomY"] : ["zoom"]
681
721
  });
682
722
  }
683
723
  this.setViewState(viewId, nextViewState);
@@ -685,15 +725,13 @@ var __exports__ = (() => {
685
725
  handleZoomIn() {
686
726
  const viewIds = this.viewId ? [this.viewId] : this.deck?.getViews().map((v3) => v3.id) ?? [];
687
727
  for (const viewId of viewIds) {
688
- const viewState = this.getViewState(viewId);
689
- this.handleZoom(viewId, viewState.zoom + 1);
728
+ this.handleZoom(viewId, 1);
690
729
  }
691
730
  }
692
731
  handleZoomOut() {
693
732
  const viewIds = this.viewId ? [this.viewId] : this.deck?.getViews().map((v3) => v3.id) ?? [];
694
733
  for (const viewId of viewIds) {
695
- const viewState = this.getViewState(viewId);
696
- this.handleZoom(viewId, viewState.zoom - 1);
734
+ this.handleZoom(viewId, -1);
697
735
  }
698
736
  }
699
737
  };
@@ -705,8 +743,29 @@ var __exports__ = (() => {
705
743
  transitionDuration: 200,
706
744
  zoomInLabel: "Zoom In",
707
745
  zoomOutLabel: "Zoom Out",
708
- viewId: null
746
+ zoomAxis: "all",
747
+ viewId: null,
748
+ onZoom: () => {
749
+ }
709
750
  };
751
+ function clamp(zoom, minZoom, maxZoom) {
752
+ return zoom < minZoom ? minZoom : zoom > maxZoom ? maxZoom : zoom;
753
+ }
754
+ function normalizeOrthographicViewState({
755
+ zoom = 0,
756
+ zoomX,
757
+ zoomY,
758
+ minZoom = -Infinity,
759
+ maxZoom = Infinity,
760
+ minZoomX = minZoom,
761
+ maxZoomX = maxZoom,
762
+ minZoomY = minZoom,
763
+ maxZoomY = maxZoom
764
+ }) {
765
+ zoomX = zoomX ?? (Array.isArray(zoom) ? zoom[0] : zoom);
766
+ zoomY = zoomY ?? (Array.isArray(zoom) ? zoom[1] : zoom);
767
+ return { zoomX, zoomY, minZoomX, minZoomY, maxZoomX, maxZoomY };
768
+ }
710
769
 
711
770
  // src/reset-view-widget.tsx
712
771
  var import_core3 = __toESM(require_core(), 1);
@@ -748,6 +807,7 @@ var __exports__ = (() => {
748
807
  // transitionDuration: this.props.transitionDuration,
749
808
  // transitionInterpolator: new FlyToInterpolator()
750
809
  };
810
+ this.props.onReset?.({ viewId, viewState: nextViewState });
751
811
  this.setViewState(viewId, nextViewState);
752
812
  }
753
813
  }
@@ -758,7 +818,9 @@ var __exports__ = (() => {
758
818
  placement: "top-left",
759
819
  label: "Reset View",
760
820
  initialViewState: void 0,
761
- viewId: null
821
+ viewId: null,
822
+ onReset: () => {
823
+ }
762
824
  };
763
825
 
764
826
  // src/gimbal-widget.tsx
@@ -857,6 +919,7 @@ var __exports__ = (() => {
857
919
  const viewId = this.viewId || viewport?.id || "OrbitView";
858
920
  const viewState = this.getViewState(viewId);
859
921
  if ("rotationOrbit" in viewState || "rotationX" in viewState) {
922
+ this.props.onReset?.({ viewId, rotationOrbit: 0, rotationX: 0 });
860
923
  const nextViewState = {
861
924
  ...viewState,
862
925
  rotationOrbit: 0,
@@ -891,7 +954,9 @@ var __exports__ = (() => {
891
954
  viewId: null,
892
955
  label: "Gimbal",
893
956
  strokeWidth: 1.5,
894
- transitionDuration: 200
957
+ transitionDuration: 200,
958
+ onReset: () => {
959
+ }
895
960
  };
896
961
  function normalizeAndClampAngle(angle) {
897
962
  let normalized = ((angle + 180) % 360 + 360) % 360 - 180;
@@ -975,10 +1040,14 @@ var __exports__ = (() => {
975
1040
  const viewId = this.viewId || viewport.id;
976
1041
  if (viewport instanceof import_core5.WebMercatorViewport) {
977
1042
  const viewState = this.getViewState(viewId);
1043
+ const resetPitch = this.getRotation(viewport)[0] === 0;
1044
+ const nextBearing = 0;
1045
+ const nextPitch = resetPitch ? 0 : viewport.pitch;
1046
+ this.props.onReset?.({ viewId, bearing: nextBearing, pitch: nextPitch });
978
1047
  const nextViewState = {
979
1048
  ...viewState,
980
- bearing: 0,
981
- ...this.getRotation(viewport)[0] === 0 ? { pitch: 0 } : {},
1049
+ bearing: nextBearing,
1050
+ ...resetPitch ? { pitch: nextPitch } : {},
982
1051
  transitionDuration: this.props.transitionDuration,
983
1052
  transitionInterpolator: new import_core5.FlyToInterpolator()
984
1053
  };
@@ -992,7 +1061,9 @@ var __exports__ = (() => {
992
1061
  placement: "top-left",
993
1062
  viewId: null,
994
1063
  label: "Reset Compass",
995
- transitionDuration: 200
1064
+ transitionDuration: 200,
1065
+ onReset: () => {
1066
+ }
996
1067
  };
997
1068
 
998
1069
  // src/scale-widget.tsx
@@ -1506,6 +1577,16 @@ var __exports__ = (() => {
1506
1577
  flyTo(viewState) {
1507
1578
  const viewIds = this.viewId ? [this.viewId] : this.deck?.getViews().map((v3) => v3.id) ?? [];
1508
1579
  for (const viewId of viewIds) {
1580
+ if ("longitude" in viewState && "latitude" in viewState) {
1581
+ this.props.onGeocode?.({
1582
+ viewId,
1583
+ coordinates: {
1584
+ longitude: viewState.longitude,
1585
+ latitude: viewState.latitude,
1586
+ zoom: viewState.zoom
1587
+ }
1588
+ });
1589
+ }
1509
1590
  const currentViewState = this.getViewState(viewId);
1510
1591
  const nextViewState = {
1511
1592
  ...currentViewState,
@@ -1529,7 +1610,9 @@ var __exports__ = (() => {
1529
1610
  geocoder: "coordinates",
1530
1611
  customGeocoder: CoordinatesGeocoder,
1531
1612
  apiKey: "",
1532
- _geolocation: false
1613
+ _geolocation: false,
1614
+ onGeocode: () => {
1615
+ }
1533
1616
  };
1534
1617
  function getGeocoder(props) {
1535
1618
  switch (props.geocoder) {
@@ -1568,6 +1651,7 @@ var __exports__ = (() => {
1568
1651
  document.removeEventListener("fullscreenchange", this.onFullscreenChange.bind(this));
1569
1652
  }
1570
1653
  onRenderHTML(rootElement) {
1654
+ const isFullscreen = this.getFullscreen();
1571
1655
  B(
1572
1656
  /* @__PURE__ */ u2(
1573
1657
  IconButton,
@@ -1575,8 +1659,8 @@ var __exports__ = (() => {
1575
1659
  onClick: () => {
1576
1660
  this.handleClick().catch((err) => import_core9.log.error(err)());
1577
1661
  },
1578
- label: this.fullscreen ? this.props.exitLabel : this.props.enterLabel,
1579
- className: this.fullscreen ? "deck-widget-fullscreen-exit" : "deck-widget-fullscreen-enter"
1662
+ label: isFullscreen ? this.props.exitLabel : this.props.enterLabel,
1663
+ className: isFullscreen ? "deck-widget-fullscreen-exit" : "deck-widget-fullscreen-enter"
1580
1664
  }
1581
1665
  ),
1582
1666
  rootElement
@@ -1590,21 +1674,24 @@ var __exports__ = (() => {
1590
1674
  getContainer() {
1591
1675
  return this.props.container || this.deck?.props.parent || this.deck?.getCanvas()?.parentElement;
1592
1676
  }
1677
+ getFullscreen() {
1678
+ return this.fullscreen;
1679
+ }
1593
1680
  onFullscreenChange() {
1594
- const prevFullscreen = this.fullscreen;
1595
1681
  const fullscreen = document.fullscreenElement === this.getContainer();
1596
- if (prevFullscreen !== fullscreen) {
1597
- this.fullscreen = !this.fullscreen;
1682
+ if (fullscreen !== this.fullscreen) {
1683
+ this.fullscreen = fullscreen;
1684
+ this.props.onFullscreenChange?.(fullscreen);
1685
+ this.updateHTML();
1598
1686
  }
1599
- this.updateHTML();
1600
1687
  }
1601
1688
  async handleClick() {
1602
- if (this.fullscreen) {
1689
+ const isFullscreen = this.getFullscreen();
1690
+ if (isFullscreen) {
1603
1691
  await this.exitFullscreen();
1604
1692
  } else {
1605
1693
  await this.requestFullscreen();
1606
1694
  }
1607
- this.updateHTML();
1608
1695
  }
1609
1696
  async requestFullscreen() {
1610
1697
  const container = this.getContainer();
@@ -1623,6 +1710,9 @@ var __exports__ = (() => {
1623
1710
  }
1624
1711
  togglePseudoFullscreen() {
1625
1712
  this.getContainer()?.classList.toggle("deck-pseudo-fullscreen");
1713
+ this.fullscreen = !this.fullscreen;
1714
+ this.props.onFullscreenChange?.(this.fullscreen);
1715
+ this.updateHTML();
1626
1716
  }
1627
1717
  };
1628
1718
  FullscreenWidget.defaultProps = {
@@ -1632,7 +1722,9 @@ var __exports__ = (() => {
1632
1722
  viewId: null,
1633
1723
  enterLabel: "Enter Fullscreen",
1634
1724
  exitLabel: "Exit Fullscreen",
1635
- container: void 0
1725
+ container: void 0,
1726
+ onFullscreenChange: () => {
1727
+ }
1636
1728
  };
1637
1729
 
1638
1730
  // src/splitter-widget.tsx
@@ -1885,7 +1977,7 @@ var __exports__ = (() => {
1885
1977
  bottom: "top",
1886
1978
  top: "bottom"
1887
1979
  };
1888
- function clamp(start, value, end) {
1980
+ function clamp2(start, value, end) {
1889
1981
  return max(start, min(value, end));
1890
1982
  }
1891
1983
  function evaluate(value, param) {
@@ -2245,7 +2337,7 @@ var __exports__ = (() => {
2245
2337
  const min$1 = minPadding;
2246
2338
  const max2 = clientSize - arrowDimensions[length] - maxPadding;
2247
2339
  const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
2248
- const offset3 = clamp(min$1, center, max2);
2340
+ const offset3 = clamp2(min$1, center, max2);
2249
2341
  const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset3 && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;
2250
2342
  const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max2 : 0;
2251
2343
  return {
@@ -2483,14 +2575,14 @@ var __exports__ = (() => {
2483
2575
  const maxSide = mainAxis === "y" ? "bottom" : "right";
2484
2576
  const min2 = mainAxisCoord + overflow[minSide];
2485
2577
  const max2 = mainAxisCoord - overflow[maxSide];
2486
- mainAxisCoord = clamp(min2, mainAxisCoord, max2);
2578
+ mainAxisCoord = clamp2(min2, mainAxisCoord, max2);
2487
2579
  }
2488
2580
  if (checkCrossAxis) {
2489
2581
  const minSide = crossAxis === "y" ? "top" : "left";
2490
2582
  const maxSide = crossAxis === "y" ? "bottom" : "right";
2491
2583
  const min2 = crossAxisCoord + overflow[minSide];
2492
2584
  const max2 = crossAxisCoord - overflow[maxSide];
2493
- crossAxisCoord = clamp(min2, crossAxisCoord, max2);
2585
+ crossAxisCoord = clamp2(min2, crossAxisCoord, max2);
2494
2586
  }
2495
2587
  const limitedCoords = limiter.fn({
2496
2588
  ...state,
@@ -3708,7 +3800,7 @@ var __exports__ = (() => {
3708
3800
 
3709
3801
  // src/lib/components/range-input.tsx
3710
3802
  var wheelListenerOptions = { passive: false };
3711
- var clamp2 = (value, min2, max2) => {
3803
+ var clamp3 = (value, min2, max2) => {
3712
3804
  if (value < min2) {
3713
3805
  return min2;
3714
3806
  }
@@ -3764,7 +3856,7 @@ var __exports__ = (() => {
3764
3856
  const range = max2 - min2;
3765
3857
  const rangeSize = Math.max(0, value[1] - value[0]);
3766
3858
  const maxStart = Math.max(0, range - rangeSize);
3767
- const clampedStart = clamp2(value[0], min2, min2 + maxStart);
3859
+ const clampedStart = clamp3(value[0], min2, min2 + maxStart);
3768
3860
  const { thumbLength, thumbOffset } = T2(() => {
3769
3861
  if (trackLength <= 0 || range <= 0) {
3770
3862
  return { thumbLength: 0, thumbOffset: 0 };
@@ -3774,7 +3866,7 @@ var __exports__ = (() => {
3774
3866
  }
3775
3867
  const nextThumbLength = rangeSize / range;
3776
3868
  const travel = Math.max(0, 1 - nextThumbLength);
3777
- const ratio = maxStart <= 0 ? 0 : clamp2((clampedStart - min2) / maxStart, 0, 1);
3869
+ const ratio = maxStart <= 0 ? 0 : clamp3((clampedStart - min2) / maxStart, 0, 1);
3778
3870
  return {
3779
3871
  thumbLength: Math.max(0, Math.min(nextThumbLength, 1)),
3780
3872
  thumbOffset: travel * ratio
@@ -3785,7 +3877,7 @@ var __exports__ = (() => {
3785
3877
  if (!onChange) {
3786
3878
  return;
3787
3879
  }
3788
- const clamped = clamp2(nextStart, min2, min2 + maxStart);
3880
+ const clamped = clamp3(nextStart, min2, min2 + maxStart);
3789
3881
  onChange([clamped, clamped + rangeSize]);
3790
3882
  },
3791
3883
  [onChange, min2, maxStart, rangeSize]
@@ -3823,7 +3915,7 @@ var __exports__ = (() => {
3823
3915
  const coordinate = vertical ? event.clientY - trackStart : event.clientX - trackStart;
3824
3916
  const span = Math.max(1, 1 - thumbLength) * trackLength;
3825
3917
  const thumbCenter = thumbLength / 2 * trackLength;
3826
- const ratio = span <= 0 ? 0 : clamp2((coordinate - thumbCenter) / span, 0, 1);
3918
+ const ratio = span <= 0 ? 0 : clamp3((coordinate - thumbCenter) / span, 0, 1);
3827
3919
  emitRange(min2 + ratio * maxStart);
3828
3920
  },
3829
3921
  [vertical, trackLength, thumbLength, emitRange, min2, maxStart]
@@ -3858,7 +3950,7 @@ var __exports__ = (() => {
3858
3950
  const coordinate = vertical ? event.clientY : event.clientX;
3859
3951
  const delta = coordinate - state.startCoord;
3860
3952
  const ratio = state.startRatio + delta / trackLength2;
3861
- const nextStart = clamp2(ratio * (state.max - state.min), 0, state.maxStart) + state.min;
3953
+ const nextStart = clamp3(ratio * (state.max - state.min), 0, state.maxStart) + state.min;
3862
3954
  emitRange(nextStart);
3863
3955
  event.preventDefault();
3864
3956
  },
@@ -4048,7 +4140,7 @@ var __exports__ = (() => {
4048
4140
  }
4049
4141
 
4050
4142
  // src/scrollbar-widget.tsx
4051
- var clamp3 = (value, min2, max2) => {
4143
+ var clamp4 = (value, min2, max2) => {
4052
4144
  if (value < min2) {
4053
4145
  return min2;
4054
4146
  }
@@ -4177,7 +4269,7 @@ var __exports__ = (() => {
4177
4269
  }
4178
4270
  getClampedOffset() {
4179
4271
  const maxScroll = this.getMaxScroll();
4180
- return clamp3(this.scrollOffset, 0, maxScroll);
4272
+ return clamp4(this.scrollOffset, 0, maxScroll);
4181
4273
  }
4182
4274
  isVertical() {
4183
4275
  return this.props.orientation !== "horizontal";
@@ -4196,7 +4288,7 @@ var __exports__ = (() => {
4196
4288
  }
4197
4289
  emitScroll(next) {
4198
4290
  const maxScroll = this.getMaxScroll();
4199
- const target = clamp3(Math.round(next), 0, maxScroll);
4291
+ const target = clamp4(Math.round(next), 0, maxScroll);
4200
4292
  const viewport = this.viewport;
4201
4293
  if (viewport && target !== this.getClampedOffset()) {
4202
4294
  const pixel = viewport.project(viewport.position);
@@ -4429,20 +4521,27 @@ var __exports__ = (() => {
4429
4521
  this.id = "timeline";
4430
4522
  this.className = "deck-widget-timeline";
4431
4523
  this.placement = "fill";
4432
- this.playing = false;
4524
+ this._playing = false;
4433
4525
  this.timerId = null;
4434
4526
  this.handlePlayPause = () => {
4435
- if (this.playing) {
4436
- this.stop();
4437
- } else {
4438
- this.play();
4527
+ const isPlaying = this.getPlaying();
4528
+ const nextPlaying = !isPlaying;
4529
+ this.props.onPlayingChange?.(nextPlaying);
4530
+ if (this.props.playing === void 0) {
4531
+ if (nextPlaying) {
4532
+ this.play();
4533
+ } else {
4534
+ this.stop();
4535
+ }
4439
4536
  }
4440
4537
  };
4441
4538
  this.handleTimeChange = ([value]) => {
4442
- this.currentTime = value;
4443
- this.props.timeline?.setTime(value);
4444
4539
  this.props.onTimeChange(value);
4445
- this.updateHTML();
4540
+ if (this.props.time === void 0) {
4541
+ this.currentTime = value;
4542
+ this.props.timeline?.setTime(value);
4543
+ this.updateHTML();
4544
+ }
4446
4545
  };
4447
4546
  this.tick = () => {
4448
4547
  const {
@@ -4451,51 +4550,90 @@ var __exports__ = (() => {
4451
4550
  loop
4452
4551
  } = this.props;
4453
4552
  if (step > 0) {
4454
- let next = Math.round(this.currentTime / step) * step + step;
4553
+ const currentTime = this.getTime();
4554
+ let next = Math.round(currentTime / step) * step + step;
4455
4555
  if (next > max2) {
4456
- if (this.currentTime < max2) {
4556
+ if (currentTime < max2) {
4457
4557
  next = max2;
4458
4558
  } else if (loop) {
4459
4559
  next = min2;
4460
4560
  } else {
4461
4561
  next = max2;
4462
- this.playing = false;
4562
+ this._playing = false;
4563
+ this.props.onPlayingChange?.(false);
4463
4564
  }
4464
4565
  }
4465
- this.currentTime = next;
4466
4566
  this.props.onTimeChange(next);
4567
+ if (this.props.time === void 0) {
4568
+ this.currentTime = next;
4569
+ this.props.timeline?.setTime(next);
4570
+ }
4467
4571
  this.updateHTML();
4468
4572
  }
4469
- if (this.playing) {
4573
+ if (this._playing) {
4470
4574
  this.timerId = window.setTimeout(this.tick, this.props.playInterval);
4471
4575
  } else {
4472
4576
  this.timerId = null;
4473
4577
  }
4474
4578
  };
4475
4579
  this.currentTime = this.props.initialTime ?? this.props.timeRange[0];
4476
- this.props.timeline?.setTime(this.currentTime);
4580
+ const syncTime = this.props.time ?? this.currentTime;
4581
+ this.props.timeline?.setTime(syncTime);
4477
4582
  this.setProps(this.props);
4478
4583
  }
4584
+ /**
4585
+ * Returns the current time value.
4586
+ * In controlled mode, returns the time prop.
4587
+ * In uncontrolled mode, returns the internal state.
4588
+ */
4589
+ getTime() {
4590
+ return this.props.time ?? this.currentTime;
4591
+ }
4592
+ /**
4593
+ * Returns the current playing state.
4594
+ * In controlled mode, returns the playing prop.
4595
+ * In uncontrolled mode, returns the internal state.
4596
+ */
4597
+ getPlaying() {
4598
+ return this.props.playing ?? this._playing;
4599
+ }
4479
4600
  setProps(props) {
4601
+ const { playing: prevPlaying, time: prevTime } = this.props;
4480
4602
  this.viewId = props.viewId ?? this.viewId;
4481
4603
  super.setProps(props);
4604
+ if (props.time !== void 0 && props.time !== prevTime) {
4605
+ this.props.timeline?.setTime(props.time);
4606
+ }
4607
+ if (props.playing !== void 0 && props.playing !== prevPlaying) {
4608
+ if (props.playing && !this._playing) {
4609
+ this._startTimer();
4610
+ } else if (!props.playing && this._playing) {
4611
+ this._stopTimer();
4612
+ }
4613
+ }
4482
4614
  }
4483
4615
  onAdd() {
4484
- this.playing = false;
4616
+ this._playing = false;
4485
4617
  this.timerId = null;
4486
- if (this.props.autoPlay)
4487
- this.play();
4618
+ if (this.props.autoPlay) {
4619
+ if (this.props.playing !== void 0) {
4620
+ this.props.onPlayingChange?.(true);
4621
+ } else {
4622
+ this.play();
4623
+ }
4624
+ }
4488
4625
  }
4489
4626
  onRemove() {
4490
4627
  this.stop();
4491
4628
  }
4492
4629
  onRenderHTML(rootElement) {
4493
4630
  const { timeRange, step, formatLabel } = this.props;
4494
- const currentTime = this.currentTime;
4631
+ const isPlaying = this.getPlaying();
4632
+ const currentTime = this.getTime();
4495
4633
  rootElement.dataset.placement = this.props.placement;
4496
4634
  B(
4497
4635
  /* @__PURE__ */ u2("div", { className: "deck-widget-button-group", children: [
4498
- this.playing ? /* @__PURE__ */ u2(
4636
+ isPlaying ? /* @__PURE__ */ u2(
4499
4637
  IconButton,
4500
4638
  {
4501
4639
  label: "Pause",
@@ -4532,24 +4670,34 @@ var __exports__ = (() => {
4532
4670
  );
4533
4671
  }
4534
4672
  play() {
4535
- this.playing = true;
4673
+ this._playing = true;
4536
4674
  const {
4537
4675
  timeRange: [min2, max2]
4538
4676
  } = this.props;
4539
- if (this.currentTime >= max2) {
4677
+ if (this.props.time === void 0 && this.getTime() >= max2) {
4540
4678
  this.currentTime = min2;
4541
4679
  this.props.onTimeChange(min2);
4680
+ this.props.timeline?.setTime(min2);
4542
4681
  }
4543
4682
  this.updateHTML();
4544
4683
  this.tick();
4545
4684
  }
4546
4685
  stop() {
4547
- this.playing = false;
4686
+ this._stopTimer();
4687
+ this.updateHTML();
4688
+ }
4689
+ /** Start the playback timer (used internally) */
4690
+ _startTimer() {
4691
+ this._playing = true;
4692
+ this.tick();
4693
+ }
4694
+ /** Stop the playback timer (used internally) */
4695
+ _stopTimer() {
4696
+ this._playing = false;
4548
4697
  if (this.timerId !== null) {
4549
4698
  window.clearTimeout(this.timerId);
4550
4699
  this.timerId = null;
4551
4700
  }
4552
- this.updateHTML();
4553
4701
  }
4554
4702
  };
4555
4703
  TimelineWidget.defaultProps = {
@@ -4561,11 +4709,15 @@ var __exports__ = (() => {
4561
4709
  timeRange: [0, 100],
4562
4710
  step: 1,
4563
4711
  initialTime: void 0,
4712
+ time: void 0,
4564
4713
  onTimeChange: () => {
4565
4714
  },
4566
4715
  autoPlay: false,
4567
4716
  loop: false,
4568
4717
  playInterval: 1e3,
4718
+ playing: void 0,
4719
+ onPlayingChange: () => {
4720
+ },
4569
4721
  formatLabel: String
4570
4722
  };
4571
4723
 
@@ -4760,53 +4912,72 @@ var __exports__ = (() => {
4760
4912
  }
4761
4913
  // eslint-disable-next-line complexity
4762
4914
  setProps(props) {
4763
- const { lightModeTheme, darkModeTheme } = this.props;
4915
+ const { lightModeTheme, darkModeTheme, themeMode: prevThemeMode } = this.props;
4764
4916
  this.placement = props.placement ?? this.placement;
4765
4917
  this.viewId = props.viewId ?? this.viewId;
4766
4918
  super.setProps(props);
4767
- switch (this.themeMode) {
4919
+ const currentMode = this.getThemeMode();
4920
+ if (props.themeMode !== void 0 && props.themeMode !== prevThemeMode) {
4921
+ this._applyTheme(props.themeMode);
4922
+ return;
4923
+ }
4924
+ switch (currentMode) {
4768
4925
  case "light":
4769
4926
  if (props.lightModeTheme && !(0, import_core21._deepEqual)(props.lightModeTheme, lightModeTheme, 1)) {
4770
- this._setThemeMode("light");
4927
+ this._applyTheme("light");
4771
4928
  }
4772
4929
  break;
4773
4930
  case "dark":
4774
4931
  if (props.darkModeTheme && !(0, import_core21._deepEqual)(props.darkModeTheme, darkModeTheme, 1)) {
4775
- this._setThemeMode("dark");
4932
+ this._applyTheme("dark");
4776
4933
  }
4777
4934
  break;
4778
4935
  default:
4779
- import_core21.log.warn(`Invalid theme mode ${this.themeMode}`)();
4936
+ import_core21.log.warn(`Invalid theme mode ${currentMode}`)();
4780
4937
  }
4781
4938
  }
4782
4939
  onRenderHTML(rootElement) {
4783
4940
  const { lightModeLabel, darkModeLabel } = this.props;
4941
+ const currentMode = this.getThemeMode();
4784
4942
  B(
4785
4943
  /* @__PURE__ */ u2(
4786
4944
  IconButton,
4787
4945
  {
4788
4946
  onClick: this._handleClick.bind(this),
4789
- label: this.themeMode === "dark" ? darkModeLabel : lightModeLabel,
4790
- className: this.themeMode === "dark" ? "deck-widget-moon" : "deck-widget-sun"
4947
+ label: currentMode === "dark" ? darkModeLabel : lightModeLabel,
4948
+ className: currentMode === "dark" ? "deck-widget-moon" : "deck-widget-sun"
4791
4949
  }
4792
4950
  ),
4793
4951
  rootElement
4794
4952
  );
4795
4953
  }
4796
4954
  onAdd() {
4797
- this._setThemeMode(this.themeMode);
4955
+ this._applyTheme(this.getThemeMode());
4956
+ }
4957
+ /**
4958
+ * Returns the current theme mode.
4959
+ * In controlled mode, returns the themeMode prop.
4960
+ * In uncontrolled mode, returns the internal state.
4961
+ */
4962
+ getThemeMode() {
4963
+ return this.props.themeMode ?? this.themeMode;
4798
4964
  }
4799
4965
  _handleClick() {
4800
- const newThemeMode = this.themeMode === "dark" ? "light" : "dark";
4801
- this._setThemeMode(newThemeMode);
4966
+ const currentMode = this.getThemeMode();
4967
+ const nextMode = currentMode === "dark" ? "light" : "dark";
4968
+ this.props.onThemeModeChange?.(nextMode);
4969
+ if (this.props.themeMode === void 0) {
4970
+ this.themeMode = nextMode;
4971
+ this._applyTheme(nextMode);
4972
+ }
4802
4973
  }
4803
- _setThemeMode(themeMode) {
4804
- this.themeMode = themeMode;
4974
+ /** Apply theme styling without changing internal state */
4975
+ _applyTheme(themeMode) {
4805
4976
  const container = this.rootElement?.closest(".deck-widget-container");
4806
4977
  if (container) {
4807
4978
  const themeStyle = themeMode === "dark" ? this.props.darkModeTheme : this.props.lightModeTheme;
4808
4979
  (0, import_core21._applyStyles)(container, themeStyle);
4809
- const label = this.themeMode === "dark" ? this.props.darkModeLabel : this.props.lightModeLabel;
4980
+ const label = themeMode === "dark" ? this.props.darkModeLabel : this.props.lightModeLabel;
4810
4981
  import_core21.log.log(1, `Switched theme to ${label}`, themeStyle)();
4811
4982
  this.updateHTML();
4812
4983
  }
@@ -4832,7 +5003,10 @@ var __exports__ = (() => {
4832
5003
  lightModeTheme: LightGlassTheme,
4833
5004
  darkModeLabel: "Dark Mode",
4834
5005
  darkModeTheme: DarkGlassTheme,
4835
- initialThemeMode: "auto"
5006
+ initialThemeMode: "auto",
5007
+ themeMode: void 0,
5008
+ onThemeModeChange: () => {
5009
+ }
4836
5010
  };
4837
5011
 
4838
5012
  // src/loading-widget.tsx
@@ -4868,6 +5042,7 @@ var __exports__ = (() => {
4868
5042
  const loading = layers.some((layer) => !layer.isLoaded);
4869
5043
  if (loading !== this.loading) {
4870
5044
  this.loading = loading;
5045
+ this.props.onLoadingChange?.(loading);
4871
5046
  this.updateHTML();
4872
5047
  }
4873
5048
  }
@@ -4880,7 +5055,9 @@ var __exports__ = (() => {
4880
5055
  id: "loading",
4881
5056
  placement: "top-left",
4882
5057
  viewId: null,
4883
- label: "Loading layer data"
5058
+ label: "Loading layer data",
5059
+ onLoadingChange: () => {
5060
+ }
4884
5061
  };
4885
5062
 
4886
5063
  // src/stats-widget.tsx
@@ -4907,19 +5084,31 @@ var __exports__ = (() => {
4907
5084
  this.className = "deck-widget-stats";
4908
5085
  this.placement = "top-left";
4909
5086
  this._counter = 0;
4910
- this.collapsed = true;
4911
- this._toggleCollapsed = () => {
4912
- this.collapsed = !this.collapsed;
4913
- this.updateHTML();
5087
+ this._expanded = false;
5088
+ this._toggleExpanded = () => {
5089
+ const nextExpanded = !this.getExpanded();
5090
+ this.props.onExpandedChange?.(nextExpanded);
5091
+ if (this.props.expanded === void 0) {
5092
+ this._expanded = nextExpanded;
5093
+ this.updateHTML();
5094
+ }
4914
5095
  };
4915
5096
  this._getFps = () => {
4916
5097
  return Math.round(this.deck?.metrics.fps ?? 0);
4917
5098
  };
4918
5099
  this._formatters = { ...DEFAULT_FORMATTERS };
4919
5100
  this._resetOnUpdate = { ...this.props.resetOnUpdate };
4920
- this.collapsed = !props.defaultIsExpanded;
5101
+ this._expanded = Boolean(props.initialExpanded);
4921
5102
  this.setProps(props);
4922
5103
  }
5104
+ /**
5105
+ * Returns the current expanded state.
5106
+ * In controlled mode, returns the expanded prop.
5107
+ * In uncontrolled mode, returns the internal state.
5108
+ */
5109
+ getExpanded() {
5110
+ return this.props.expanded ?? this._expanded;
5111
+ }
4923
5112
  setProps(props) {
4924
5113
  this.placement = props.placement ?? this.placement;
4925
5114
  this.viewId = props.viewId ?? this.viewId;
@@ -4940,9 +5129,9 @@ var __exports__ = (() => {
4940
5129
  }
4941
5130
  }
4942
5131
  onRenderHTML(rootElement) {
4943
- const collapsed = this.collapsed;
4944
- if (collapsed) {
4945
- B(/* @__PURE__ */ u2(FpsIcon, { getFps: this._getFps, onClick: this._toggleCollapsed }), rootElement);
5132
+ const isExpanded = this.getExpanded();
5133
+ if (!isExpanded) {
5134
+ B(/* @__PURE__ */ u2(FpsIcon, { getFps: this._getFps, onClick: this._toggleExpanded }), rootElement);
4946
5135
  return;
4947
5136
  }
4948
5137
  const stats = this._getStats();
@@ -4969,7 +5158,7 @@ var __exports__ = (() => {
4969
5158
  {
4970
5159
  className: "deck-widget-stats-header",
4971
5160
  style: { cursor: "pointer", pointerEvents: "auto" },
4972
- onClick: this._toggleCollapsed,
5161
+ onClick: this._toggleExpanded,
4973
5162
  children: [
4974
5163
  /* @__PURE__ */ u2("b", { children: title }),
4975
5164
  deviceLabel && /* @__PURE__ */ u2("span", { className: "deck-widget-stats-device", children: deviceLabel }),
@@ -4983,7 +5172,7 @@ var __exports__ = (() => {
4983
5172
  );
4984
5173
  }
4985
5174
  onRedraw() {
4986
- if (!this.collapsed) {
5175
+ if (this.getExpanded()) {
4987
5176
  const framesPerUpdate = Math.max(1, this.props.framesPerUpdate || 1);
4988
5177
  if (this._counter++ % framesPerUpdate === 0) {
4989
5178
  this.updateHTML();
@@ -5036,13 +5225,16 @@ var __exports__ = (() => {
5036
5225
  type: "deck",
5037
5226
  placement: "top-left",
5038
5227
  viewId: null,
5039
- defaultIsExpanded: false,
5228
+ initialExpanded: false,
5040
5229
  stats: void 0,
5041
5230
  title: "Stats",
5042
5231
  framesPerUpdate: 1,
5043
5232
  formatters: {},
5044
5233
  resetOnUpdate: {},
5045
- id: "stats"
5234
+ id: "stats",
5235
+ expanded: void 0,
5236
+ onExpandedChange: () => {
5237
+ }
5046
5238
  };
5047
5239
  function FpsIcon({ getFps, onClick }) {
5048
5240
  const [fps, setFps] = h2(getFps());