@zendir/ui 0.1.14 → 0.2.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/index.js +6 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/react/astro/SimulationControls.js +3 -3
  5. package/dist/react/astro/SimulationControls.js.map +1 -1
  6. package/dist/react/astro/UnifiedTimeline.d.ts +70 -8
  7. package/dist/react/astro/UnifiedTimeline.js +886 -260
  8. package/dist/react/astro/UnifiedTimeline.js.map +1 -1
  9. package/dist/react/astro/index.d.ts +2 -1
  10. package/dist/react/charts/GroundTrackMap.d.ts +40 -1
  11. package/dist/react/charts/GroundTrackMap.js +98 -47
  12. package/dist/react/charts/GroundTrackMap.js.map +1 -1
  13. package/dist/react/charts/GroundTrackMapLeaflet.d.ts +11 -2
  14. package/dist/react/charts/GroundTrackMapLeaflet.js +128 -15
  15. package/dist/react/charts/GroundTrackMapLeaflet.js.map +1 -1
  16. package/dist/react/charts/index.d.ts +1 -1
  17. package/dist/react/charts/unified/theme.d.ts +7 -7
  18. package/dist/react/context/CategoryContext.d.ts +51 -0
  19. package/dist/react/context/CategoryContext.js +36 -0
  20. package/dist/react/context/CategoryContext.js.map +1 -0
  21. package/dist/react/context/index.d.ts +2 -0
  22. package/dist/react/index.d.ts +6 -4
  23. package/dist/react/types.d.ts +26 -0
  24. package/dist/react/types.js.map +1 -1
  25. package/dist/react/utils/categoryPalette.d.ts +43 -0
  26. package/dist/react/utils/categoryPalette.js +104 -0
  27. package/dist/react/utils/categoryPalette.js.map +1 -0
  28. package/dist/react/utils/index.d.ts +1 -0
  29. package/dist/react/utils/index.js.map +1 -1
  30. package/dist/react.js +6 -0
  31. package/dist/react.js.map +1 -1
  32. package/dist/style.css +49 -0
  33. package/package.json +1 -1
@@ -50,7 +50,8 @@ export type { NotificationProps, NotificationStatus } from './Notification';
50
50
  * ```
51
51
  */
52
52
  export { UnifiedTimeline as Timeline } from './UnifiedTimeline';
53
- export type { UnifiedTimelineProps as TimelineProps, TimelineEvent, TimelineTrackDef, TimelineViewMode, TimeFormat, TimelineFilter, } from './UnifiedTimeline';
53
+ export type { UnifiedTimelineProps as TimelineProps, TimelineEvent, TimelineTeam, TimelineTrackDef, TimelineViewMode, TimeFormat, TimelineFilter, } from './UnifiedTimeline';
54
+ export { TIMELINE_FILTER_TEAM_LEGACY, TIMELINE_FILTER_TEAM_NONE, getTimelineEventDisplayStatus, getTimelineTeamAccent, getTimelineTeamDisplayLabel, getTimelineTeamFilterKey, isTimelineFilterActive, matchesTimelineFilter, } from './UnifiedTimeline';
54
55
  /**
55
56
  * Status type (shared across components)
56
57
  * Follows Astro UX 6-level status semantics
@@ -62,6 +62,27 @@ export interface SROGroundStation {
62
62
  longitude: number;
63
63
  altitude?: number | null;
64
64
  }
65
+ /**
66
+ * User-placed point-of-interest pin on the map.
67
+ * Designed for collaborative scenarios where operators share annotated locations
68
+ * across terminals in real time (e.g. targets, waypoints, hazard markers).
69
+ */
70
+ export interface MapPin {
71
+ /** Unique identifier (used as key and for update/remove callbacks) */
72
+ id: string;
73
+ /** Latitude in degrees (−90 … 90) */
74
+ latitude: number;
75
+ /** Longitude in degrees (−180 … 180) */
76
+ longitude: number;
77
+ /** Short label displayed next to the pin (e.g. "Target Alpha") */
78
+ label?: string;
79
+ /** Pin color — any CSS color string. Defaults to accent primary. */
80
+ color?: string;
81
+ /** Optional description shown in tooltip on hover */
82
+ description?: string;
83
+ /** Who placed the pin (display-only; not enforced) */
84
+ createdBy?: string;
85
+ }
65
86
  export interface GroundTrackMapProps {
66
87
  /** Single satellite ground track (legacy API) */
67
88
  groundTrack?: GroundTrackPoint[];
@@ -75,6 +96,10 @@ export interface GroundTrackMapProps {
75
96
  teamPaths?: TeamPath[];
76
97
  /** Show day/night terminator */
77
98
  showTerminator?: boolean;
99
+ /** Override the time used for the day/night terminator calculation.
100
+ * Defaults to wall-clock `new Date()` when omitted.
101
+ * Pass a simulation/mission UTC to keep the terminator in sync with a sim clock. */
102
+ terminatorTime?: Date;
78
103
  /** Show grid lines */
79
104
  showGrid?: boolean;
80
105
  /** Show legend */
@@ -107,6 +132,20 @@ export interface GroundTrackMapProps {
107
132
  mapProvider?: 'leaflet' | 'canvas';
108
133
  /** Tile URL for Leaflet (default: CartoDB dark). Ignored when mapProvider is 'canvas'. */
109
134
  tileUrl?: string;
135
+ /** Array of user-placed pins displayed on the map */
136
+ pins?: MapPin[];
137
+ /**
138
+ * Allow operators to place new pins by clicking the map.
139
+ * When true, a single-click on an empty area opens a pin creation flow.
140
+ * Requires `onPinAdd` to be set. Default false.
141
+ */
142
+ pinsEditable?: boolean;
143
+ /** Called when the user places a new pin (provides lat/lon; consumer assigns id and persists) */
144
+ onPinAdd?: (pin: Omit<MapPin, 'id'>) => void;
145
+ /** Called when the user edits an existing pin (label, color, description change) */
146
+ onPinUpdate?: (pin: MapPin) => void;
147
+ /** Called when the user removes a pin */
148
+ onPinRemove?: (pinId: string) => void;
110
149
  }
111
- export declare function GroundTrackMap({ groundTrack, satellites, groundStations, accessMask, teamPaths, showTerminator, showGrid, showLegend, showEquator, showRecenterButton, isLoading, emptyMessage, width, height, minHeight, defaultCenter, defaultZoom, className, onSatelliteClick, onStationClick, mapProvider, tileUrl, }: GroundTrackMapProps): React.ReactElement;
150
+ export declare function GroundTrackMap({ groundTrack, satellites, groundStations, accessMask, teamPaths, showTerminator, terminatorTime, showGrid, showLegend, showEquator, showRecenterButton, isLoading, emptyMessage, width, height, minHeight, defaultCenter, defaultZoom, className, onSatelliteClick, onStationClick, mapProvider, tileUrl, pins, pinsEditable, onPinAdd, onPinUpdate, onPinRemove, }: GroundTrackMapProps): React.ReactElement;
112
151
  export default GroundTrackMap;
@@ -68,15 +68,16 @@ function drawCoverageEllipse(ctx, centerLat, centerLon, radiusDeg, W, H, lonToX,
68
68
  }
69
69
  ctx.closePath();
70
70
  }
71
- function calculateTerminator(date, numPoints = 72) {
71
+ function calculateTerminator(date, depressionDeg = 0, numPoints = 72) {
72
72
  const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 864e5);
73
73
  const declination = -23.44 * Math.cos(2 * Math.PI / 365 * (dayOfYear + 10));
74
74
  const decRad = declination * Math.PI / 180;
75
75
  const hourAngle = (date.getUTCHours() + date.getUTCMinutes() / 60) / 24 * 360 - 180;
76
+ const depRad = depressionDeg * Math.PI / 180;
76
77
  const points = [];
77
78
  for (let i = 0; i <= numPoints; i++) {
78
79
  const latRad = (i / numPoints * 180 - 90) * (Math.PI / 180);
79
- const cosH = -Math.tan(latRad) * Math.tan(decRad);
80
+ const cosH = -(Math.sin(depRad) + Math.sin(latRad) * Math.sin(decRad)) / (Math.cos(latRad) * Math.cos(decRad));
80
81
  let lon;
81
82
  if (cosH < -1) lon = hourAngle + 180;
82
83
  else if (cosH > 1) lon = hourAngle;
@@ -94,6 +95,7 @@ function GroundTrackMap({
94
95
  accessMask,
95
96
  teamPaths,
96
97
  showTerminator = true,
98
+ terminatorTime,
97
99
  showGrid = true,
98
100
  showLegend = true,
99
101
  showEquator = true,
@@ -109,7 +111,12 @@ function GroundTrackMap({
109
111
  onSatelliteClick,
110
112
  onStationClick,
111
113
  mapProvider = "leaflet",
112
- tileUrl
114
+ tileUrl,
115
+ pins,
116
+ pinsEditable = false,
117
+ onPinAdd,
118
+ onPinUpdate,
119
+ onPinRemove
113
120
  }) {
114
121
  const { tokens } = useTheme();
115
122
  const canvasRef = useRef(null);
@@ -194,55 +201,65 @@ function GroundTrackMap({
194
201
  ctx.fillStyle = COLORS.background;
195
202
  ctx.fillRect(0, 0, W, H);
196
203
  if (showTerminator) {
197
- const terminator = calculateTerminator(/* @__PURE__ */ new Date());
198
- ctx.fillStyle = COLORS.night;
199
- ctx.beginPath();
200
- ctx.moveTo(W, 0);
201
- ctx.lineTo(W, H);
202
- let prevRX = W;
203
- for (let i = terminator.length - 1; i >= 0; i--) {
204
- const x = lonToX(terminator[i].lon, W);
205
- const y = latToY(terminator[i].lat, H);
206
- if (Math.abs(x - prevRX) > W * 0.5 && i < terminator.length - 1) {
207
- const edgeX = prevRX > W / 2 ? W : 0;
208
- ctx.lineTo(edgeX, y);
209
- ctx.closePath();
210
- ctx.fill();
211
- ctx.beginPath();
212
- ctx.moveTo(W, y);
213
- }
214
- ctx.lineTo(x, y);
215
- prevRX = x;
216
- }
217
- ctx.closePath();
218
- ctx.fill();
204
+ const now = terminatorTime ?? /* @__PURE__ */ new Date();
205
+ const twilightZones = [
206
+ { depression: 6, alpha: 0.08 },
207
+ // civil twilight
208
+ { depression: 12, alpha: 0.1 },
209
+ // nautical twilight
210
+ { depression: 18, alpha: 0.12 },
211
+ // astronomical twilight
212
+ { depression: 90, alpha: 0.22 }
213
+ // full night (beyond 18°)
214
+ ];
219
215
  const normLon = (l) => {
220
216
  let r = l;
221
217
  while (r > 180) r -= 360;
222
218
  while (r < -180) r += 360;
223
219
  return r;
224
220
  };
225
- ctx.beginPath();
226
- ctx.moveTo(0, 0);
227
- ctx.lineTo(0, H);
228
- let prevLX = 0;
229
- for (let i = terminator.length - 1; i >= 0; i--) {
230
- const sunriseLon = normLon(terminator[i].lon - 180);
231
- const x = lonToX(sunriseLon, W);
232
- const y = latToY(terminator[i].lat, H);
233
- if (Math.abs(x - prevLX) > W * 0.5 && i < terminator.length - 1) {
234
- const edgeX = prevLX < W / 2 ? 0 : W;
235
- ctx.lineTo(edgeX, y);
236
- ctx.closePath();
237
- ctx.fill();
238
- ctx.beginPath();
239
- ctx.moveTo(0, y);
221
+ const fillNightSide = (terminator, isSunrise) => {
222
+ ctx.beginPath();
223
+ if (isSunrise) {
224
+ ctx.moveTo(0, 0);
225
+ ctx.lineTo(0, H);
226
+ } else {
227
+ ctx.moveTo(W, 0);
228
+ ctx.lineTo(W, H);
240
229
  }
241
- ctx.lineTo(x, y);
242
- prevLX = x;
230
+ let prevX = isSunrise ? 0 : W;
231
+ for (let i = terminator.length - 1; i >= 0; i--) {
232
+ const tLon = isSunrise ? normLon(terminator[i].lon - 180) : terminator[i].lon;
233
+ const x = lonToX(tLon, W);
234
+ const y = latToY(terminator[i].lat, H);
235
+ if (Math.abs(x - prevX) > W * 0.5 && i < terminator.length - 1) {
236
+ const edgeX = isSunrise ? prevX < W / 2 ? 0 : W : prevX > W / 2 ? W : 0;
237
+ ctx.lineTo(edgeX, y);
238
+ ctx.closePath();
239
+ ctx.fill();
240
+ ctx.beginPath();
241
+ ctx.moveTo(isSunrise ? 0 : W, y);
242
+ }
243
+ ctx.lineTo(x, y);
244
+ prevX = x;
245
+ }
246
+ ctx.closePath();
247
+ ctx.fill();
248
+ };
249
+ let prevTerminator = null;
250
+ for (const zone of twilightZones) {
251
+ const dep = Math.min(zone.depression, 89);
252
+ const terminator = calculateTerminator(now, dep);
253
+ ctx.fillStyle = `rgba(0, 6, 24, ${zone.alpha})`;
254
+ if (prevTerminator) {
255
+ fillNightSide(terminator, false);
256
+ fillNightSide(terminator, true);
257
+ } else {
258
+ fillNightSide(terminator, false);
259
+ fillNightSide(terminator, true);
260
+ }
261
+ prevTerminator = terminator;
243
262
  }
244
- ctx.closePath();
245
- ctx.fill();
246
263
  }
247
264
  ctx.fillStyle = COLORS.land;
248
265
  ctx.strokeStyle = "rgba(100, 120, 160, 0.25)";
@@ -739,13 +756,41 @@ function GroundTrackMap({
739
756
  ctx.fillText(item.label, legendX + 20, iy + 7);
740
757
  });
741
758
  }
742
- if (allSatellites.length === 0 && groundStations.length === 0) {
759
+ if (pins && pins.length > 0) {
760
+ pins.forEach((pin) => {
761
+ const px = lonToX(pin.longitude, W);
762
+ const py = latToY(pin.latitude, H);
763
+ const pinColor = pin.color || tokens.colors.accent.primary;
764
+ ctx.save();
765
+ ctx.shadowColor = "rgba(0,0,0,0.4)";
766
+ ctx.shadowBlur = 6;
767
+ ctx.shadowOffsetY = 2;
768
+ ctx.beginPath();
769
+ ctx.arc(px, py - 10, 6, Math.PI, 0);
770
+ ctx.lineTo(px, py);
771
+ ctx.closePath();
772
+ ctx.fillStyle = pinColor;
773
+ ctx.fill();
774
+ ctx.restore();
775
+ ctx.beginPath();
776
+ ctx.arc(px, py - 10, 2.5, 0, Math.PI * 2);
777
+ ctx.fillStyle = "#0d1323";
778
+ ctx.fill();
779
+ if (pin.label) {
780
+ ctx.fillStyle = "rgba(255,255,255,0.9)";
781
+ ctx.font = '10px "Inter", system-ui, sans-serif';
782
+ ctx.textAlign = "center";
783
+ ctx.fillText(pin.label, px, py - 20);
784
+ }
785
+ });
786
+ }
787
+ if (allSatellites.length === 0 && groundStations.length === 0 && (!pins || pins.length === 0)) {
743
788
  ctx.fillStyle = COLORS.text;
744
789
  ctx.font = '12px "Inter", system-ui, sans-serif';
745
790
  ctx.textAlign = "center";
746
791
  ctx.fillText(emptyMessage, W / 2, H / 2);
747
792
  }
748
- }, [allSatellites, groundStations, height, showTerminator, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage]);
793
+ }, [allSatellites, groundStations, height, showTerminator, terminatorTime, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage, pins, tokens.colors.accent.primary]);
749
794
  const handleMouseMove = useCallback((e) => {
750
795
  const canvas = canvasRef.current;
751
796
  if (!canvas) return;
@@ -861,6 +906,7 @@ function GroundTrackMap({
861
906
  allSatellites,
862
907
  groundStations,
863
908
  showTerminator,
909
+ terminatorTime,
864
910
  showGrid,
865
911
  showLegend,
866
912
  showEquator,
@@ -874,7 +920,12 @@ function GroundTrackMap({
874
920
  tileUrl,
875
921
  className,
876
922
  onSatelliteClick,
877
- onStationClick
923
+ onStationClick,
924
+ pins,
925
+ pinsEditable,
926
+ onPinAdd,
927
+ onPinUpdate,
928
+ onPinRemove
878
929
  }
879
930
  )
880
931
  }