@zendir/ui 0.1.15 → 0.2.1

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 +56 -6
  7. package/dist/react/astro/UnifiedTimeline.js +628 -428
  8. package/dist/react/astro/UnifiedTimeline.js.map +1 -1
  9. package/dist/react/astro/index.d.ts +2 -2
  10. package/dist/react/charts/GroundTrackMap.d.ts +40 -1
  11. package/dist/react/charts/GroundTrackMap.js +112 -60
  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 +140 -39
  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,8 +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';
54
- export { getTimelineEventDisplayStatus, isTimelineFilterActive, matchesTimelineFilter, } 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';
55
55
  /**
56
56
  * Status type (shared across components)
57
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,24 +68,37 @@ 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 calculateTerminatorContinuous(date, depressionDeg = 0, numPoints = 360) {
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 points = [];
76
+ const depRad = depressionDeg * Math.PI / 180;
77
+ const sunset = [];
78
+ const sunrise = [];
77
79
  for (let i = 0; i <= numPoints; i++) {
78
- const latRad = (i / numPoints * 180 - 90) * (Math.PI / 180);
79
- const cosH = -Math.tan(latRad) * Math.tan(decRad);
80
- let lon;
81
- if (cosH < -1) lon = hourAngle + 180;
82
- else if (cosH > 1) lon = hourAngle;
83
- else lon = hourAngle + Math.acos(cosH) * 180 / Math.PI;
84
- while (lon > 180) lon -= 360;
85
- while (lon < -180) lon += 360;
86
- points.push({ lat: i / numPoints * 180 - 90, lon });
80
+ const lat = -89.999 + i / numPoints * 179.998;
81
+ const latRad = lat * (Math.PI / 180);
82
+ const cosH = -(Math.sin(depRad) + Math.sin(latRad) * Math.sin(decRad)) / (Math.cos(latRad) * Math.cos(decRad));
83
+ if (cosH < -1) ;
84
+ else if (cosH > 1) {
85
+ sunset.push([lat, hourAngle]);
86
+ sunrise.push([lat, hourAngle + 360]);
87
+ } else {
88
+ const H = Math.acos(cosH) * 180 / Math.PI;
89
+ sunset.push([lat, hourAngle + H]);
90
+ sunrise.push([lat, hourAngle + 360 - H]);
91
+ }
87
92
  }
88
- return points;
93
+ return { sunset, sunrise };
94
+ }
95
+ function buildNightPolygon(terminator) {
96
+ const { sunset, sunrise } = terminator;
97
+ if (!sunset.length || !sunrise.length) return [];
98
+ const poly = [];
99
+ poly.push(...sunset);
100
+ poly.push(...[...sunrise].reverse());
101
+ return poly;
89
102
  }
90
103
  function GroundTrackMap({
91
104
  groundTrack,
@@ -94,6 +107,7 @@ function GroundTrackMap({
94
107
  accessMask,
95
108
  teamPaths,
96
109
  showTerminator = true,
110
+ terminatorTime,
97
111
  showGrid = true,
98
112
  showLegend = true,
99
113
  showEquator = true,
@@ -109,7 +123,12 @@ function GroundTrackMap({
109
123
  onSatelliteClick,
110
124
  onStationClick,
111
125
  mapProvider = "leaflet",
112
- tileUrl
126
+ tileUrl,
127
+ pins,
128
+ pinsEditable = false,
129
+ onPinAdd,
130
+ onPinUpdate,
131
+ onPinRemove
113
132
  }) {
114
133
  const { tokens } = useTheme();
115
134
  const canvasRef = useRef(null);
@@ -194,55 +213,54 @@ function GroundTrackMap({
194
213
  ctx.fillStyle = COLORS.background;
195
214
  ctx.fillRect(0, 0, W, H);
196
215
  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
+ const now = terminatorTime ?? /* @__PURE__ */ new Date();
217
+ const BAND_STEP = 2;
218
+ const MAX_DEP = 24;
219
+ const NIGHT_ALPHA = 0.55;
220
+ const bandSteps = [];
221
+ for (let d = 0; d <= MAX_DEP; d += BAND_STEP) {
222
+ const t = Math.min(d / 18, 1);
223
+ const alpha = t * t * (NIGHT_ALPHA * 0.82) + (d > 18 ? (d - 18) / 6 * (NIGHT_ALPHA * 0.18) : 0);
224
+ bandSteps.push({ depression: d, alpha: Math.min(alpha, NIGHT_ALPHA) });
216
225
  }
217
- ctx.closePath();
218
- ctx.fill();
219
- const normLon = (l) => {
220
- let r = l;
221
- while (r > 180) r -= 360;
222
- while (r < -180) r += 360;
223
- return r;
224
- };
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);
226
+ bandSteps.push({ depression: 90, alpha: NIGHT_ALPHA });
227
+ let prevAlpha = 0;
228
+ for (const zone of bandSteps) {
229
+ const dep = Math.min(zone.depression, 89);
230
+ const bandAlpha = zone.alpha - prevAlpha;
231
+ if (bandAlpha < 2e-3) {
232
+ prevAlpha = zone.alpha;
233
+ continue;
234
+ }
235
+ const terminator = calculateTerminatorContinuous(now, dep);
236
+ if (terminator.sunset.length === 0) {
237
+ prevAlpha = zone.alpha;
238
+ continue;
239
+ }
240
+ const poly = buildNightPolygon(terminator);
241
+ if (poly.length < 3) {
242
+ prevAlpha = zone.alpha;
243
+ continue;
244
+ }
245
+ ctx.fillStyle = `rgba(0, 6, 24, ${bandAlpha.toFixed(4)})`;
246
+ for (const offset of [-360, 0, 360]) {
247
+ ctx.beginPath();
248
+ for (let i = 0; i < poly.length; i++) {
249
+ const lat = poly[i][0];
250
+ const lon = poly[i][1] + offset;
251
+ const x = lonToX(lon, W);
252
+ const y = latToY(lat, H);
253
+ if (i === 0) {
254
+ ctx.moveTo(x, y);
255
+ } else {
256
+ ctx.lineTo(x, y);
257
+ }
258
+ }
236
259
  ctx.closePath();
237
260
  ctx.fill();
238
- ctx.beginPath();
239
- ctx.moveTo(0, y);
240
261
  }
241
- ctx.lineTo(x, y);
242
- prevLX = x;
262
+ prevAlpha = zone.alpha;
243
263
  }
244
- ctx.closePath();
245
- ctx.fill();
246
264
  }
247
265
  ctx.fillStyle = COLORS.land;
248
266
  ctx.strokeStyle = "rgba(100, 120, 160, 0.25)";
@@ -739,13 +757,41 @@ function GroundTrackMap({
739
757
  ctx.fillText(item.label, legendX + 20, iy + 7);
740
758
  });
741
759
  }
742
- if (allSatellites.length === 0 && groundStations.length === 0) {
760
+ if (pins && pins.length > 0) {
761
+ pins.forEach((pin) => {
762
+ const px = lonToX(pin.longitude, W);
763
+ const py = latToY(pin.latitude, H);
764
+ const pinColor = pin.color || tokens.colors.accent.primary;
765
+ ctx.save();
766
+ ctx.shadowColor = "rgba(0,0,0,0.4)";
767
+ ctx.shadowBlur = 6;
768
+ ctx.shadowOffsetY = 2;
769
+ ctx.beginPath();
770
+ ctx.arc(px, py - 10, 6, Math.PI, 0);
771
+ ctx.lineTo(px, py);
772
+ ctx.closePath();
773
+ ctx.fillStyle = pinColor;
774
+ ctx.fill();
775
+ ctx.restore();
776
+ ctx.beginPath();
777
+ ctx.arc(px, py - 10, 2.5, 0, Math.PI * 2);
778
+ ctx.fillStyle = "#0d1323";
779
+ ctx.fill();
780
+ if (pin.label) {
781
+ ctx.fillStyle = "rgba(255,255,255,0.9)";
782
+ ctx.font = '10px "Inter", system-ui, sans-serif';
783
+ ctx.textAlign = "center";
784
+ ctx.fillText(pin.label, px, py - 20);
785
+ }
786
+ });
787
+ }
788
+ if (allSatellites.length === 0 && groundStations.length === 0 && (!pins || pins.length === 0)) {
743
789
  ctx.fillStyle = COLORS.text;
744
790
  ctx.font = '12px "Inter", system-ui, sans-serif';
745
791
  ctx.textAlign = "center";
746
792
  ctx.fillText(emptyMessage, W / 2, H / 2);
747
793
  }
748
- }, [allSatellites, groundStations, height, showTerminator, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage]);
794
+ }, [allSatellites, groundStations, height, showTerminator, terminatorTime, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage, pins, tokens.colors.accent.primary]);
749
795
  const handleMouseMove = useCallback((e) => {
750
796
  const canvas = canvasRef.current;
751
797
  if (!canvas) return;
@@ -861,6 +907,7 @@ function GroundTrackMap({
861
907
  allSatellites,
862
908
  groundStations,
863
909
  showTerminator,
910
+ terminatorTime,
864
911
  showGrid,
865
912
  showLegend,
866
913
  showEquator,
@@ -874,7 +921,12 @@ function GroundTrackMap({
874
921
  tileUrl,
875
922
  className,
876
923
  onSatelliteClick,
877
- onStationClick
924
+ onStationClick,
925
+ pins,
926
+ pinsEditable,
927
+ onPinAdd,
928
+ onPinUpdate,
929
+ onPinRemove
878
930
  }
879
931
  )
880
932
  }