From a12e1b484fdf1a5e2e173668e7c120692b9efa78 Mon Sep 17 00:00:00 2001 From: SankeerthNara Date: Sat, 25 Jul 2026 21:57:25 +0530 Subject: [PATCH 1/2] feat(satellites): add interactive spotlight effect for hover/select (#95) --- frontend/src/components/EarthTwin.tsx | 641 ++++-------------- .../SatelliteSpotlight/GlowEffect.tsx | 166 +++++ .../SatelliteSpotlight/OrbitHighlight.tsx | 113 +++ .../SatelliteSpotlight/SpotlightInfoCard.tsx | 86 +++ .../SatelliteSpotlight/SpotlightManager.tsx | 151 +++++ frontend/src/hooks/useSatelliteHover.ts | 58 ++ frontend/src/hooks/useSatelliteSelection.ts | 102 +++ frontend/src/hooks/useSpotlightEffect.ts | 131 ++++ frontend/src/lib/satelliteVisuals.ts | 36 + frontend/src/styles/spotlight.css | 69 ++ frontend/src/types/satellite.ts | 28 + 11 files changed, 1089 insertions(+), 492 deletions(-) create mode 100644 frontend/src/components/SatelliteSpotlight/GlowEffect.tsx create mode 100644 frontend/src/components/SatelliteSpotlight/OrbitHighlight.tsx create mode 100644 frontend/src/components/SatelliteSpotlight/SpotlightInfoCard.tsx create mode 100644 frontend/src/components/SatelliteSpotlight/SpotlightManager.tsx create mode 100644 frontend/src/hooks/useSatelliteHover.ts create mode 100644 frontend/src/hooks/useSatelliteSelection.ts create mode 100644 frontend/src/hooks/useSpotlightEffect.ts create mode 100644 frontend/src/lib/satelliteVisuals.ts create mode 100644 frontend/src/styles/spotlight.css create mode 100644 frontend/src/types/satellite.ts diff --git a/frontend/src/components/EarthTwin.tsx b/frontend/src/components/EarthTwin.tsx index e3791c3..69b9046 100644 --- a/frontend/src/components/EarthTwin.tsx +++ b/frontend/src/components/EarthTwin.tsx @@ -1,77 +1,43 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useUIStore } from '@/store/uiStore'; import { MaterialIcon } from './MaterialIcon'; +import { SpotlightManager } from './SatelliteSpotlight/SpotlightManager'; +import type { CatalogObject, CollisionRisk } from '@/types/satellite'; +import { CATEGORY_COLORS, getPointSize, getOutlineWidth } from '@/lib/satelliteVisuals'; +import '@/styles/spotlight.css'; import * as Cesium from 'cesium'; -import { isSatelliteSunlit } from '../utils/illumination'; -import { useBookmarks } from '../hooks/useBookmarks'; -import type { Bookmark } from '../hooks/useBookmarkStorage'; -import { BookmarkModal, type BookmarkFormValues, } from './GlobeBookmarks/BookMarkModal'; -import { BookmarkSidebar } from './GlobeBookmarks/BookmarkSidebar'; -import { - createBookmarkShareUrl, - getSharedBookmarkFromUrl, -} from '../utils/bookmarkHelpers'; - - -interface CatalogObject { - id: number; - name: string; - catalog_number: string; - classification: 'PAYLOAD' | 'DEBRIS' | 'ROCKET_BODY' | 'UNKNOWN'; - epoch: string | null; - inclination: number | null; - eccentricity: number | null; - semimajor_axis: number | null; - raan: number | null; - arg_of_perigee: number | null; - mean_anomaly: number | null; - mean_motion: number | null; - period: number | null; - has_tle: boolean; - updated_at: string | null; -} - -interface CollisionRisk { - id: number; - object_a: { name: string; catalog_number: string } | null; - object_b: { name: string; catalog_number: string } | null; - probability: number; - miss_distance_m: number; - relative_velocity_kms: number; - risk_level: string; - tca: string; -} function keplerToLatLonAlt(obj: CatalogObject, timeOffsetSec: number = 0): { lat: number; lon: number; alt: number } | null { if (obj.semimajor_axis == null || obj.inclination == null || obj.raan == null || - obj.arg_of_perigee == null || obj.mean_anomaly == null || obj.mean_motion == null) { + obj.arg_of_perigee == null || obj.mean_anomaly == null || obj.mean_motion == null) { return null; } - const EARTH_RADIUS = 6371; + const EARTH_RADIUS = 6371; const alt = obj.semimajor_axis - EARTH_RADIUS; if (alt < 0 || alt > 100000) return null; - + const epochDate = obj.epoch ? new Date(obj.epoch) : new Date(); const now = new Date(); const elapsedDays = (now.getTime() - epochDate.getTime()) / 86400000 + (timeOffsetSec / 86400); + const meanMotionRadPerSec = (obj.mean_motion * 2 * Math.PI) / 86400; const currentMeanAnomaly = ((obj.mean_anomaly + (elapsedDays * obj.mean_motion * 360)) % 360) * Math.PI / 180; - + const ecc = obj.eccentricity ?? 0; const trueAnomaly = currentMeanAnomaly + 2 * ecc * Math.sin(currentMeanAnomaly); - + const argLat = (obj.arg_of_perigee * Math.PI / 180) + trueAnomaly; - + const raanRad = obj.raan * Math.PI / 180; const incRad = obj.inclination * Math.PI / 180; - + const J2000 = new Date('2000-01-01T12:00:00Z').getTime(); const daysSinceJ2000 = (now.getTime() + timeOffsetSec * 1000 - J2000) / 86400000; const GMST = (280.46061837 + 360.98564736629 * daysSinceJ2000) % 360; @@ -87,34 +53,6 @@ function keplerToLatLonAlt(obj: CatalogObject, timeOffsetSec: number = 0): { lat } -const CATEGORY_COLORS = { - PAYLOAD: { css: '#00E5FF', cesium: Cesium.Color.fromCssColorString('#00E5FF'), label: 'Active Satellites', icon: 'satellite_alt' }, - DEBRIS: { css: '#FFAA00', cesium: Cesium.Color.fromCssColorString('#FFAA00'), label: 'Debris Objects', icon: 'delete_sweep' }, - ROCKET_BODY: { css: '#FF4444', cesium: Cesium.Color.fromCssColorString('#FF4444'), label: 'Rocket Bodies', icon: 'rocket' }, - UNKNOWN: { css: '#888888', cesium: Cesium.Color.fromCssColorString('#888888'), label: 'Unknown Objects', icon: 'help_outline' }, - COLLISION: { css: '#FF0000', cesium: Cesium.Color.fromCssColorString('#FF0000'), label: 'Collision Risk', icon: 'warning' }, - SELECTED: { css: '#00E5FF', cesium: Cesium.Color.fromCssColorString('#00E5FF'), label: 'Selected', icon: 'gps_fixed' }, -} as const; - -function getPointSize(classification: string): number { - switch (classification) { - case 'PAYLOAD': return 5; - case 'DEBRIS': return 4; - case 'ROCKET_BODY': return 6; - default: return 3; - } -} - -function getOutlineWidth(classification: string): number { - switch (classification) { - case 'PAYLOAD': return 1; - case 'DEBRIS': return 1; - case 'ROCKET_BODY': return 2; - default: return 1; - } -} - - const API_BASE = import.meta.env.VITE_API_URL ?? '/api/v1'; async function fetchAllCatalogObjects(): Promise { @@ -137,27 +75,30 @@ export const EarthTwin: React.FC = () => { const containerRef = useRef(null); const viewerRef = useRef(null); const entitiesRef = useRef>(new Map()); + const catalogMapRef = useRef>(new Map()); + const collisionSetRef = useRef>(new Set()); const tooltipRef = useRef(null); const { activeSector, setSelectedSatelliteId } = useUIStore(); const [useFallback, setUseFallback] = useState(true); const [hoveredObject, setHoveredObject] = useState(null); const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 }); + const [viewerInstance, setViewerInstance] = useState(null); + const [containerEl, setContainerEl] = useState(null); + const [datasetVersion, setDatasetVersion] = useState(0); const [showLegend, setShowLegend] = useState(true); const [showDensity, setShowDensity] = useState(false); const [showRiskOverlay, setShowRiskOverlay] = useState(false); const [objectCounts, setObjectCounts] = useState({ payloads: 0, debris: 0, rocketBodies: 0, total: 0, collisions: 0 }); const [dataLoaded, setDataLoaded] = useState(false); - const [isBookmarkModalOpen, setIsBookmarkModalOpen] = useState(false); - const [isBookmarkSidebarOpen, setIsBookmarkSidebarOpen] = useState(false); - const [editingBookmark, setEditingBookmark] = useState(null); + const [stats, setStats] = useState({ totalObjects: 0, lastSync: '', weatherIndex: 'K0', }); - + const populateEntities = useCallback(async (viewer: Cesium.Viewer) => { try { if (!viewer || viewer.isDestroyed()) return; @@ -167,7 +108,7 @@ export const EarthTwin: React.FC = () => { fetchCollisions(), ]); - + const collisionCatNums = new Set(); collisions.forEach(c => { if (c.object_a?.catalog_number) collisionCatNums.add(c.object_a.catalog_number); @@ -177,22 +118,22 @@ export const EarthTwin: React.FC = () => { let payloads = 0, debris = 0, rocketBodies = 0; const entityMap = new Map(); - + if (!viewer || viewer.isDestroyed()) return; - + viewer.entities.removeAll(); - const nowJulian = Cesium.JulianDate.now(); + objects.forEach(obj => { const pos = keplerToLatLonAlt(obj); if (!pos) return; - + if (obj.classification === 'PAYLOAD') payloads++; else if (obj.classification === 'DEBRIS') debris++; else if (obj.classification === 'ROCKET_BODY') rocketBodies++; - + const isCollisionRisk = collisionCatNums.has(obj.catalog_number); const colorConfig = isCollisionRisk ? CATEGORY_COLORS.COLLISION @@ -201,15 +142,12 @@ export const EarthTwin: React.FC = () => { const pixelSize = isCollisionRisk ? 8 : getPointSize(obj.classification); const outlineWidth = isCollisionRisk ? 3 : getOutlineWidth(obj.classification); - const cartPos = Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt * 1000); - const sunlit = isSatelliteSunlit(cartPos, nowJulian, viewer.scene.globe); - const entity = viewer.entities.add({ - position: cartPos, + position: Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt * 1000), point: { pixelSize, - color: sunlit ? colorConfig.cesium.withAlpha(0.85) : colorConfig.cesium.withAlpha(0.3), - outlineColor: sunlit ? colorConfig.cesium.withAlpha(0.4) : colorConfig.cesium.withAlpha(0.1), + color: colorConfig.cesium.withAlpha(0.85), + outlineColor: colorConfig.cesium.withAlpha(0.4), outlineWidth, disableDepthTestDistance: Number.POSITIVE_INFINITY, scaleByDistance: new Cesium.NearFarScalar(1e6, 1.5, 5e7, 0.4), @@ -228,7 +166,7 @@ export const EarthTwin: React.FC = () => { entityMap.set(obj.catalog_number, entity); }); - + collisions.forEach(conj => { if (!conj.object_a || !conj.object_b) return; const entityA = entityMap.get(conj.object_a.catalog_number); @@ -252,6 +190,9 @@ export const EarthTwin: React.FC = () => { }); entitiesRef.current = entityMap; + catalogMapRef.current = new Map(objects.map(obj => [obj.catalog_number, obj])); + collisionSetRef.current = collisionCatNums; + setDatasetVersion(v => v + 1); setObjectCounts({ payloads, debris, @@ -271,179 +212,12 @@ export const EarthTwin: React.FC = () => { } }, []); - - const autoRotateRef = useRef(true); - - const { - bookmarks, - filteredBookmarks, - favoriteBookmarks, - recentBookmarks, - categories, - - searchQuery, - selectedCategory, - - setSearchQuery, - setSelectedCategory, - - addBookmark, - updateBookmark, - deleteBookmark, - toggleFavorite, - markAsRecent, - - exportBookmarks, - importBookmarks, - } = useBookmarks(); - - const [selectedBookmarkId, setSelectedBookmarkId] = - useState(''); - - - const saveCurrentView = useCallback(() => { - const viewer = viewerRef.current; - - if (!viewer || viewer.isDestroyed()) { - return; - } - - const cartographic = - viewer.scene.globe.ellipsoid.cartesianToCartographic( - viewer.camera.positionWC - ); - - if (!cartographic) { - return; - } - - const timestamp = new Intl.DateTimeFormat('en-IN', { - dateStyle: 'medium', - timeStyle: 'short', - }).format(new Date()); - - addBookmark({ - name: `Saved view — ${timestamp}`, - description: 'Saved from the current globe camera position.', - category: 'Custom', - - latitude: Cesium.Math.toDegrees(cartographic.latitude), - longitude: Cesium.Math.toDegrees(cartographic.longitude), - altitude: cartographic.height, - - // Cesium expects these values in radians when restoring. - heading: viewer.camera.heading, - pitch: viewer.camera.pitch, - roll: viewer.camera.roll, - }); - }, [addBookmark]); - - const restoreBookmark = useCallback( - (bookmark: Bookmark) => { - const viewer = viewerRef.current; - - if (!viewer || viewer.isDestroyed()) { - return; - } - - // Prevent the existing automatic globe rotation from immediately - // moving the camera away from the restored bookmark view. - autoRotateRef.current = false; - - markAsRecent(bookmark.id); - - const prefersReducedMotion = - window.matchMedia?.( - '(prefers-reduced-motion: reduce)' - ).matches ?? false; - - viewer.camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees( - bookmark.longitude, - bookmark.latitude, - bookmark.altitude - ), - orientation: { - heading: bookmark.heading, - pitch: bookmark.pitch, - roll: bookmark.roll, - }, - duration: prefersReducedMotion ? 0 : 1.5, - }); - }, - [markAsRecent] - ); - - const handleBookmarkSubmit = useCallback( - (values: BookmarkFormValues) => { - if (editingBookmark) { - updateBookmark(editingBookmark.id, { - name: values.name, - description: values.description, - category: values.category, - }); - - setEditingBookmark(null); - setIsBookmarkModalOpen(false); - return; - } - - const viewer = viewerRef.current; - - if (!viewer || viewer.isDestroyed()) { - return; - } - - const cartographic = - viewer.scene.globe.ellipsoid.cartesianToCartographic( - viewer.camera.positionWC - ); - - if (!cartographic) { - return; - } - - addBookmark({ - name: values.name, - description: values.description, - category: values.category, - - latitude: Cesium.Math.toDegrees(cartographic.latitude), - longitude: Cesium.Math.toDegrees(cartographic.longitude), - altitude: cartographic.height, - - heading: viewer.camera.heading, - pitch: viewer.camera.pitch, - roll: viewer.camera.roll, - }); - - setIsBookmarkModalOpen(false); - }, - [addBookmark, editingBookmark, updateBookmark] - ); - - const handleShareBookmark = useCallback( - async (bookmark: Bookmark) => { - const shareUrl = createBookmarkShareUrl(bookmark); - - try { - await navigator.clipboard.writeText(shareUrl); - } catch { - window.prompt( - 'Copy this bookmark link:', - shareUrl - ); - } - }, - [] - ); - + useEffect(() => { if (!containerRef.current || !Cesium) return; if (viewerRef.current && !viewerRef.current.isDestroyed()) return; let onTick: (() => void) | null = null; - let handler: Cesium.ScreenSpaceEventHandler | null = null; try { const viewer = new Cesium.Viewer(containerRef.current, { @@ -464,18 +238,19 @@ export const EarthTwin: React.FC = () => { }); viewerRef.current = viewer; - // eslint-disable-next-line react-hooks/set-state-in-effect setUseFallback(false); + setViewerInstance(viewer); + setContainerEl(containerRef.current); - - viewer.scene.globe.enableLighting = true; // Enable real-time day/night lighting + + viewer.scene.globe.enableLighting = false; viewer.scene.backgroundColor = Cesium.Color.fromCssColorString('#050710'); viewer.scene.fog.enabled = false; if (viewer.scene.skyAtmosphere) { viewer.scene.skyAtmosphere.show = true; } - + viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(30, 15, 20000000), orientation: { @@ -484,87 +259,17 @@ export const EarthTwin: React.FC = () => { roll: 0.0, }, }); - const sharedBookmark = getSharedBookmarkFromUrl(); - - if (sharedBookmark) { - autoRotateRef.current = false; - - const prefersReducedMotion = - window.matchMedia?.( - '(prefers-reduced-motion: reduce)' - ).matches ?? false; - - viewer.camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees( - sharedBookmark.longitude, - sharedBookmark.latitude, - sharedBookmark.altitude - ), - orientation: { - heading: sharedBookmark.heading, - pitch: sharedBookmark.pitch, - roll: sharedBookmark.roll, - }, - duration: prefersReducedMotion ? 0 : 1.5, - }); - } + onTick = () => { - if (autoRotateRef.current) { - viewer.scene.camera.rotate( - Cesium.Cartesian3.UNIT_Z, - 0.0003 - ); - } + viewer.scene.camera.rotate(Cesium.Cartesian3.UNIT_Z, 0.0003); }; viewer.clock.onTick.addEventListener(onTick); - - handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); - - handler.setInputAction((movement: { endPosition: Cesium.Cartesian2 }) => { - const picked = viewer.scene.pick(movement.endPosition); - if (Cesium.defined(picked) && picked.id && picked.id.properties) { - try { - const rawData = picked.id.properties.catalogData?.getValue(Cesium.JulianDate.now()); - if (rawData) { - const obj = JSON.parse(rawData) as CatalogObject; - setHoveredObject(obj); - setTooltipPos({ x: movement.endPosition.x + 15, y: movement.endPosition.y - 10 }); - } - } catch { /* ignore parse errors */ } - } else { - setHoveredObject(null); - } - }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); - - - handler.setInputAction((click: { position: Cesium.Cartesian2 }) => { - const picked = viewer.scene.pick(click.position); - if (Cesium.defined(picked) && picked.id && picked.id.properties) { - try { - const rawData = picked.id.properties.catalogData?.getValue(Cesium.JulianDate.now()); - if (rawData) { - const obj = JSON.parse(rawData) as CatalogObject; - setSelectedSatelliteId(obj.catalog_number); - - - const pos = keplerToLatLonAlt(obj); - if (pos) { - viewer.camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.alt * 1000 + 2000000), - duration: 1.5, - }); - } - } - } catch { /* ignore parse errors */ } - } - }, Cesium.ScreenSpaceEventType.LEFT_CLICK); - - + populateEntities(viewer); - + const refreshInterval = setInterval(() => { if (viewerRef.current && !viewerRef.current.isDestroyed()) { populateEntities(viewerRef.current); @@ -573,26 +278,32 @@ export const EarthTwin: React.FC = () => { return () => { clearInterval(refreshInterval); - if (handler) handler.destroy(); const v = viewerRef.current; if (v && !v.isDestroyed()) { if (onTick) v.clock.onTick.removeEventListener(onTick); v.destroy(); } viewerRef.current = null; + setViewerInstance(null); + setContainerEl(null); }; } catch (e) { console.warn('Cesium initialization failed, using fallback', e); setUseFallback(true); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, []); return (
{/* 3D Cesium Container */} -
+
{/* High-Fidelity SVG Fallback */} {useFallback && ( @@ -613,6 +324,22 @@ export const EarthTwin: React.FC = () => {
)} + {/* ── Interactive Satellite Spotlight ─────────────────────── */} + keplerToLatLonAlt(obj)} + onHoverChange={(obj, pos) => { + setHoveredObject(obj); + setTooltipPos(pos); + }} + onSelectionChange={(id) => setSelectedSatelliteId(id)} + /> + {/* ── Hover Tooltip (Glassmorphism) ──────────────────────── */} {hoveredObject && (
{ )} {/* ── HUD Overlay ───────────────────────────────────────── */} -
+
{/* Top Row */}
@@ -674,30 +401,30 @@ export const EarthTwin: React.FC = () => {
- {dataLoaded ? ( - <> - {objectCounts.collisions > 0 ? ( -
- - - {objectCounts.collisions} ACTIVE CONJUNCTION{objectCounts.collisions !== 1 ? 'S' : ''} - -
- ) : ( -
- - - ALL CLEAR - -
- )} - + {dataLoaded ? ( + <> + {objectCounts.collisions > 0 ? ( +
+ + + {objectCounts.collisions} ACTIVE CONJUNCTION{objectCounts.collisions !== 1 ? 'S' : ''} + +
) : ( - <> -
-
- +
+ + + ALL CLEAR + +
)} + + ) : ( + <> +
+
+ + ) } @@ -712,69 +439,52 @@ export const EarthTwin: React.FC = () => {
) : ( <> -
-

Objects Tracked

-

- {objectCounts.total.toLocaleString()} -

-
-
-

Debris

-

- {objectCounts.debris.toLocaleString()} -

-
-
-

Collision Risks

-

0 ? 'text-status-emergency animate-pulse drop-shadow-[0_0_8px_rgba(255,59,48,0.6)]' : 'text-status-success drop-shadow-[0_0_8px_rgba(0,255,136,0.4)]'}`}> - {objectCounts.collisions} -

-
- +
+

Objects Tracked

+

+ {objectCounts.total.toLocaleString()} +

+
+
+

Debris

+

+ {objectCounts.debris.toLocaleString()} +

+
+
+

Collision Risks

+

0 ? 'text-status-emergency animate-pulse drop-shadow-[0_0_8px_rgba(255,59,48,0.6)]' : 'text-status-success drop-shadow-[0_0_8px_rgba(0,255,136,0.4)]'}`}> + {objectCounts.collisions} +

+
+ ) - } + }
{/* Controls */}
- - - - - - @@ -786,92 +496,39 @@ export const EarthTwin: React.FC = () => { {showLegend && (
{dataLoaded ? ( -
-
- ORBITAL LEGEND - -
-
- {[ - { color: CATEGORY_COLORS.PAYLOAD.css, label: 'Active Satellites', count: objectCounts.payloads }, - { color: CATEGORY_COLORS.DEBRIS.css, label: 'Debris Objects', count: objectCounts.debris }, - { color: CATEGORY_COLORS.ROCKET_BODY.css, label: 'Rocket Bodies', count: objectCounts.rocketBodies }, - { color: CATEGORY_COLORS.COLLISION.css, label: 'Collision Risks', count: objectCounts.collisions }, - ].map(item => ( -
-
- - {item.label} -
- {item.count.toLocaleString()} +
+
+ ORBITAL LEGEND + +
+
+ {[ + { color: CATEGORY_COLORS.PAYLOAD.css, label: 'Active Satellites', count: objectCounts.payloads }, + { color: CATEGORY_COLORS.DEBRIS.css, label: 'Debris Objects', count: objectCounts.debris }, + { color: CATEGORY_COLORS.ROCKET_BODY.css, label: 'Rocket Bodies', count: objectCounts.rocketBodies }, + { color: CATEGORY_COLORS.COLLISION.css, label: 'Collision Risks', count: objectCounts.collisions }, + ].map(item => ( +
+
+ + {item.label}
- ))} -
-
- All positions from real orbital elements -
+ {item.count.toLocaleString()} +
+ ))} +
+
+ All positions from real orbital elements
- ) : ()} +
+ ) : () }
)} - - {/* Bookmark create/edit dialog */} - {isBookmarkModalOpen && ( - setIsBookmarkModalOpen(false)} - onSubmit={handleBookmarkSubmit} - /> - )} - - - {/* Bookmark sidebar */} - {isBookmarkSidebarOpen && ( - { - setEditingBookmark(null); - setIsBookmarkModalOpen(true); - }} - onShareBookmark={handleShareBookmark} - onEditBookmark={(bookmark) => { - setEditingBookmark(bookmark); - setIsBookmarkModalOpen(true); - }} - onDeleteBookmark={deleteBookmark} - onToggleFavorite={(bookmarkId) => - toggleFavorite(bookmarkId) - } - onExportBookmarks={exportBookmarks} - onImportBookmarks={importBookmarks} - onClose={() => setIsBookmarkSidebarOpen(false)} - /> - )} - - {isBookmarkModalOpen && ( - { - setEditingBookmark(null); - setIsBookmarkModalOpen(false); - }} - onSubmit={handleBookmarkSubmit} - /> - )} {/* ── Inline Styles for Animations ──────────────────────── */}