+
{props.name}
{props.description}
- Good:
+ Good:
{`< ${props.score[0]}`}
- Needs Improvement:
+ Needs Improvement:
{`< ${props.score[1]}`}
- Poor:
+ Poor:
{`> ${props.score[1]}`}
@@ -146,4 +148,4 @@ const radialGraph = (props) => {
);
};
-export default radialGraph;
+export default WebMetrics;
diff --git a/src/app/components/StateRoute/WebMetrics/WebMetricsContainer.tsx b/src/app/components/StateRoute/WebMetrics/WebMetricsContainer.tsx
index ae0e34e74..1ffd8efeb 100644
--- a/src/app/components/StateRoute/WebMetrics/WebMetricsContainer.tsx
+++ b/src/app/components/StateRoute/WebMetrics/WebMetricsContainer.tsx
@@ -1,99 +1,109 @@
import React from 'react';
import WebMetrics from './WebMetrics';
-/*
- Used to render a container for all of the 'Web Metrics' tab
-*/
-
const WebMetricsContainer = (props) => {
const { webMetrics } = props;
+ // Add default values for all color variables
+ let LCPColor = '#fc2000'; // Default to poor/red
+ let FIDColor = '#fc2000';
+ let FCPColor = '#fc2000';
+ let TTFBColor = '#fc2000';
+ let CLSColor = '#fc2000';
+ let INPColor = '#fc2000';
- let LCPColor: String;
- let FIDColor: String;
- let FCPColor: String;
- let TTFBColor: String;
- let CLSColor: String;
- let INPColor: String;
-
- // adjust the strings that represent colors of the webmetrics performance bar for 'Largest Contentful Paint (LCP)', 'First Input Delay (FID)', 'First Contentfuly Paint (FCP)', 'Time to First Byte (TTFB)', 'Cumulative Layout Shift (CLS)', and 'Interaction to Next Paint (INP)' based on webMetrics outputs.
- if (webMetrics.LCP <= 2500) LCPColor = '#0bce6b';
- if (webMetrics.LCP > 2500 && webMetrics.LCP <= 4000) LCPColor = '#fc5a03';
- if (webMetrics.LCP > 4000) LCPColor = '#fc2000';
+ // Ensure webMetrics exists and has valid values
+ if (webMetrics) {
+ // LCP color logic
+ if (typeof webMetrics.LCP === 'number') {
+ if (webMetrics.LCP <= 2500) LCPColor = '#0bce6b';
+ else if (webMetrics.LCP <= 4000) LCPColor = '#fc5a03';
+ }
- if (webMetrics.FID <= 100) FIDColor = '#0bce6b';
- if (webMetrics.FID > 100 && webMetrics.FID <= 300) FIDColor = '#fc5a03';
- if (webMetrics.FID > 300) FIDColor = '#fc2000';
+ // FID color logic
+ if (typeof webMetrics.FID === 'number') {
+ if (webMetrics.FID <= 100) FIDColor = '#0bce6b';
+ else if (webMetrics.FID <= 300) FIDColor = '#fc5a03';
+ }
- if (webMetrics.FCP <= 1800) FCPColor = '#0bce6b';
- if (webMetrics.FCP > 1800 && webMetrics.FCP <= 3000) FCPColor = '#fc5a03';
- if (webMetrics.FCP > 3000) FCPColor = '#fc2000';
+ // FCP color logic
+ if (typeof webMetrics.FCP === 'number') {
+ if (webMetrics.FCP <= 1800) FCPColor = '#0bce6b';
+ else if (webMetrics.FCP <= 3000) FCPColor = '#fc5a03';
+ }
- if (webMetrics.TTFB <= 800) TTFBColor = '#0bce6b';
- if (webMetrics.TTFB > 800 && webMetrics.TTFB <= 1800) TTFBColor = '#fc5a03';
- if (webMetrics.TTFB > 1800) TTFBColor = '#fc2000';
+ // TTFB color logic
+ if (typeof webMetrics.TTFB === 'number') {
+ if (webMetrics.TTFB <= 800) TTFBColor = '#0bce6b';
+ else if (webMetrics.TTFB <= 1800) TTFBColor = '#fc5a03';
+ }
- if (webMetrics.CLS <= 0.1) CLSColor = '#0bce6b';
- if (webMetrics.CLS > 0.1 && webMetrics.CLS <= 0.25) CLSColor = '#fc5a03';
- if (webMetrics.CLS > 0.25) CLSColor = '#fc2000';
+ // CLS color logic
+ if (typeof webMetrics.CLS === 'number') {
+ if (webMetrics.CLS <= 0.1) CLSColor = '#0bce6b';
+ else if (webMetrics.CLS <= 0.25) CLSColor = '#fc5a03';
+ }
- if (webMetrics.INP <= 200) INPColor = '#0bce6b';
- if (webMetrics.INP > 200 && webMetrics.INP <= 500) INPColor = '#fc5a03';
- if (webMetrics.INP > 500) INPColor = '#fc2000';
+ // INP color logic
+ if (typeof webMetrics.INP === 'number') {
+ if (webMetrics.INP <= 200) INPColor = '#0bce6b';
+ else if (webMetrics.INP <= 500) INPColor = '#fc5a03';
+ }
+ }
return (
+ series={webMetrics.LCP ? [webMetrics.LCP / 70 < 100 ? webMetrics.LCP / 70 : 100] : [0]}
+ formatted={(value) =>
typeof webMetrics.LCP !== 'number' ? '- ms' : `${webMetrics.LCP.toFixed(2)} ms`
}
score={['2500 ms', '4000 ms']}
- overLimit={webMetrics.LCP > 7000}
+ overLimit={webMetrics.LCP ? webMetrics.LCP > 7000 : false}
label='Largest Contentful Paint'
name='Largest Contentful Paint'
description='Measures loading performance.'
/>
+ series={webMetrics.FID ? [webMetrics.FID / 5 < 100 ? webMetrics.FID / 5 : 100] : [0]}
+ formatted={(value) =>
typeof webMetrics.FID !== 'number' ? '- ms' : `${webMetrics.FID.toFixed(2)} ms`
}
score={['100 ms', '300 ms']}
- overLimit={webMetrics.FID > 500}
+ overLimit={webMetrics.FID ? webMetrics.FID > 500 : false}
label='First Input Delay'
name='First Input Delay'
description='Measures interactivity.'
/>
+ series={webMetrics.FCP ? [webMetrics.FCP / 50 < 100 ? webMetrics.FCP / 50 : 100] : [0]}
+ formatted={(value) =>
typeof webMetrics.FCP !== 'number' ? '- ms' : `${webMetrics.FCP.toFixed(2)} ms`
}
score={['1800 ms', '3000 ms']}
- overLimit={webMetrics.FCP > 5000}
+ overLimit={webMetrics.FCP ? webMetrics.FCP > 5000 : false}
label='First Contentful Paint'
name='First Contentful Paint'
description='Measures the time it takes the browser to render the first piece of DOM content.'
/>
+ series={webMetrics.TTFB ? [webMetrics.TTFB / 30 < 100 ? webMetrics.TTFB / 30 : 100] : [0]}
+ formatted={(value) =>
typeof webMetrics.TTFB !== 'number' ? '- ms' : `${webMetrics.TTFB.toFixed(2)} ms`
}
score={['800 ms', '1800 ms']}
- overLimit={webMetrics.TTFB > 3000}
+ overLimit={webMetrics.TTFB ? webMetrics.TTFB > 3000 : false}
label='Time To First Byte'
name='Time to First Byte'
description='Measures the time it takes for a browser to receive the first byte of page content.'
/>
+ series={webMetrics.CLS ? [webMetrics.CLS * 200 < 100 ? webMetrics.CLS * 200 : 100] : [0]}
+ formatted={(value) =>
`CLS Score: ${
typeof webMetrics.CLS !== 'number'
? 'N/A'
@@ -101,19 +111,19 @@ const WebMetricsContainer = (props) => {
}`
}
score={['0.1', '0.25']}
- overLimit={webMetrics.CLS > 0.5}
+ overLimit={webMetrics.CLS ? webMetrics.CLS > 0.5 : false}
label='Cumulative Layout Shift'
name='Cumulative Layout Shift'
description={`Quantifies the visual stability of a web page by measuring layout shifts during the application's loading and interaction.`}
/>
+ series={webMetrics.INP ? [webMetrics.INP / 7 < 100 ? webMetrics.INP / 7 : 100] : [0]}
+ formatted={(value) =>
typeof webMetrics.INP !== 'number' ? '- ms' : `${webMetrics.INP.toFixed(2)} ms`
}
score={['200 ms', '500 ms']}
- overLimit={webMetrics.INP > 700}
+ overLimit={webMetrics.INP ? webMetrics.INP > 700 : false}
label='Interaction to Next Paint'
name='Interaction to Next Paint'
description={`Assesses a page's overall responsiveness to user interactions by observing the latency of all click, tap, and keyboard interactions that occur throughout the lifespan of a user's visit to a page`}
diff --git a/src/app/components/TimeTravel/MainSlider.tsx b/src/app/components/TimeTravel/VerticalSlider.tsx
similarity index 71%
rename from src/app/components/TimeTravel/MainSlider.tsx
rename to src/app/components/TimeTravel/VerticalSlider.tsx
index 7af7c7673..df41058f6 100644
--- a/src/app/components/TimeTravel/MainSlider.tsx
+++ b/src/app/components/TimeTravel/VerticalSlider.tsx
@@ -1,12 +1,12 @@
import React, { useState, useEffect } from 'react';
import Slider from 'rc-slider';
import Tooltip from 'rc-tooltip';
-import { changeSlider, pause } from '../../slices/mainSlice';
+import { changeSlider } from '../../slices/mainSlice';
import { useDispatch, useSelector } from 'react-redux';
import { HandleProps, MainSliderProps, MainState, RootState } from '../../FrontendTypes';
const { Handle } = Slider; // component constructor of Slider that allows customization of the handle
-
+//takes in snapshot length
const handle = (props: HandleProps): JSX.Element => {
const { value, dragging, index, ...restProps } = props;
@@ -27,9 +27,9 @@ const handle = (props: HandleProps): JSX.Element => {
);
};
-function MainSlider(props: MainSliderProps): JSX.Element {
+function VerticalSlider(props: MainSliderProps): JSX.Element {
const dispatch = useDispatch();
- const { snapshotsLength } = props; // destructure props to get our total number of snapshots
+ const { snapshots } = props; // destructure props to get our total number of snapshots
const [sliderIndex, setSliderIndex] = useState(0); // create a local state 'sliderIndex' and set it to 0.
const { tabs, currentTab }: MainState = useSelector((state: RootState) => state.main);
const { currLocation } = tabs[currentTab]; // we destructure the currentTab object
@@ -37,8 +37,15 @@ function MainSlider(props: MainSliderProps): JSX.Element {
useEffect(() => {
if (currLocation) {
// if we have a 'currLocation'
- //@ts-ignore
- setSliderIndex(currLocation.index); // set our slider thumb position to the 'currLocation.index'
+ let correctedSliderIndex;
+
+ for (let i = 0; i < snapshots.length; i++) {
+ //@ts-ignore -- ignores the errors on the next line
+ if (snapshots[i].props.index === currLocation.index) {
+ correctedSliderIndex = i;
+ }
+ }
+ setSliderIndex(correctedSliderIndex);
} else {
setSliderIndex(0); // just set the thumb position to the beginning
}
@@ -47,22 +54,20 @@ function MainSlider(props: MainSliderProps): JSX.Element {
return (
{
// when the slider position changes
setSliderIndex(index); // update the sliderIndex
- }}
- onAfterChange={() => {
- // after updating the sliderIndex
- dispatch(changeSlider(sliderIndex)); // updates currentTab's 'sliderIndex' and replaces our snapshot with the appropriate snapshot[sliderIndex]
- dispatch(pause()); // pauses playing and sets currentTab object'a intervalId to null
+ dispatch(changeSlider(snapshots[index].props.index));
}}
handle={handle}
/>
);
}
-export default MainSlider;
+export default VerticalSlider;
diff --git a/src/app/containers/ActionContainer.tsx b/src/app/containers/ActionContainer.tsx
index d81e995a1..147d53bf9 100644
--- a/src/app/containers/ActionContainer.tsx
+++ b/src/app/containers/ActionContainer.tsx
@@ -1,57 +1,39 @@
/* eslint-disable max-len */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import Action from '../components/Actions/Action';
-import SwitchAppDropdown from '../components/Actions/SwitchApp';
-import { emptySnapshots, changeView, changeSlider } from '../slices/mainSlice';
+import { emptySnapshots, changeSlider } from '../slices/mainSlice';
import { useDispatch, useSelector } from 'react-redux';
import RouteDescription from '../components/Actions/RouteDescription';
+import DropDown from '../components/Actions/DropDown';
+import ProvConContainer from './ProvConContainer';
import { ActionContainerProps, CurrentTab, MainState, Obj, RootState } from '../FrontendTypes';
-import { Button, Switch } from '@mui/material';
+import { Button } from '@mui/material';
+import RecordButton from '../components/Actions/RecordButton';
/*
This file renders the 'ActionContainer'. The action container is the leftmost column in the application. It includes the button that shrinks and expands the action container, a dropdown to select the active site, a clear button, the current selected Route, and a list of selectable snapshots with timestamps.
*/
-// resetSlider locates the rc-slider elements on the document and resets it's style attributes
-const resetSlider = () => {
- const slider = document.querySelector('.rc-slider-handle');
- const sliderTrack = document.querySelector('.rc-slider-track');
- if (slider && sliderTrack) {
- slider.setAttribute('style', 'left: 0');
- sliderTrack.setAttribute('style', 'width: 0');
- }
-};
-
function ActionContainer(props: ActionContainerProps): JSX.Element {
+ const [dropdownSelection, setDropdownSelection] = useState('Time Jump');
+ const actionsEndRef = useRef(null as unknown as HTMLDivElement);
+
const dispatch = useDispatch();
const { currentTab, tabs, port }: MainState = useSelector((state: RootState) => state.main);
const { currLocation, hierarchy, sliderIndex, viewIndex }: Partial = tabs[currentTab]; // we destructure the currentTab object
- const {
- toggleActionContainer, // function that handles Time Jump Sidebar view from MainContainer
- actionView, // local state declared in MainContainer
- setActionView, // function to update actionView state declared in MainContainer
- } = props;
+ const { snapshots } = props;
const [recordingActions, setRecordingActions] = useState(true); // We create a local state 'recordingActions' and set it to true
let actionsArr: JSX.Element[] = []; // we create an array 'actionsArr' that will hold elements we create later on
// we create an array 'hierarchyArr' that will hold objects and numbers
const hierarchyArr: (number | {})[] = [];
- /*
- function to traverse state from hierarchy and also getting information on display name and component name
-
- the obj parameter is an object with the following structure:
- {
- stateSnapshot: {
- route: any;
- children: any[];
- };
- name: number;
- branch: number;
- index: number;
- children?: [];
+ // Auto scroll when snapshots change
+ useEffect(() => {
+ if (actionsEndRef.current) {
+ actionsEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
- */
+ }, [snapshots]);
const displayArray = (obj: Obj): void => {
if (
@@ -65,7 +47,7 @@ function ActionContainer(props: ActionContainerProps): JSX.Element {
//This utility can be used to map the properties of a type to another type) and populate it's properties with
//relevant values from our argument 'obj'.
index: obj.index,
- displayName: `${obj.name}.${obj.branch}`,
+ displayName: `${obj.index + 1}`,
state: obj.stateSnapshot.children[0].state,
componentName: obj.stateSnapshot.children[0].name,
routePath: obj.stateSnapshot.route.url,
@@ -80,6 +62,7 @@ function ActionContainer(props: ActionContainerProps): JSX.Element {
if (obj.children) {
// if argument has a 'children' array, we iterate through it and run 'displayArray' on each element
obj.children.forEach((element): void => {
+ //recursive call
displayArray(element);
});
}
@@ -88,28 +71,6 @@ function ActionContainer(props: ActionContainerProps): JSX.Element {
// the hierarchy gets set on the first click in the page
if (hierarchy) displayArray(hierarchy); // when page is refreshed we may not have a hierarchy so we need to check if hierarchy was initialized. If it was initialized, invoke displayArray to display the hierarchy
- // This function allows us to use our arrow keys to jump between snapshots. It passes an event and the index of each action-component. Using the arrow keys allows us to highligh snapshots and the enter key jumps to the selected snapshot
- function handleOnKeyDown(e: Partial, i: number): void {
- let currIndex = i;
-
- if (e.key === 'ArrowUp') {
- // up arrow key pressed
- currIndex--;
- if (currIndex < 0) return;
- dispatch(changeView(currIndex));
- } else if (e.key === 'ArrowDown') {
- // down arrow key pressed
- currIndex++;
- if (currIndex > hierarchyArr.length - 1) return;
- dispatch(changeView(currIndex));
- } else if (e.key === 'Enter') {
- // enter key pressed
- e.stopPropagation(); // prevents further propagation of the current event in the capturing and bubbling phases
- e.preventDefault(); // needed or will trigger onClick right after
- dispatch(changeSlider(currIndex));
- }
- }
-
// Sort hierarchyArr by index property of each object. This will be useful when later when we build our components so that our components will be displayed in index/chronological order
hierarchyArr.sort((a: Obj, b: Obj): number => a.index - b.index);
@@ -127,6 +88,7 @@ function ActionContainer(props: ActionContainerProps): JSX.Element {
const selected = index === viewIndex; // boolean on whether the current index is the same as the viewIndex
const last = viewIndex === -1 && index === hierarchyArr.length - 1; // boolean on whether the view index is less than 0 and if the index is the same as the last snapshot's index value in hierarchyArr
const isCurrIndex = index === currLocation.index;
+
return (
{
- // setActionView(true);
- // }, [setActionView]);
-
// Function sends message to background.js which sends message to the content script
const toggleRecord = (): void => {
port.postMessage({
@@ -180,67 +136,43 @@ function ActionContainer(props: ActionContainerProps): JSX.Element {
// UNLESS actionView={true} is passed into in the beforeEach() call in ActionContainer.test.tsx
return (
);
}
diff --git a/src/app/containers/ButtonsContainer.tsx b/src/app/containers/ButtonsContainer.tsx
index 2eadc0e76..b74edeba4 100644
--- a/src/app/containers/ButtonsContainer.tsx
+++ b/src/app/containers/ButtonsContainer.tsx
@@ -1,59 +1,42 @@
import * as React from 'react';
-//importing useState from react to handle local state for button reconnect functionality
-import { useState, useEffect } from 'react';
import { Button } from '@mui/material';
-//importing necesary material UI components for dialogue popup
-import { Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import Tutorial from '../components/Buttons/Tutorial';
-import LockIcon from '@mui/icons-material/Lock';
-import LockOpenIcon from '@mui/icons-material/LockOpen';
-import FileDownloadIcon from '@mui/icons-material/FileDownload';
-import FileUploadIcon from '@mui/icons-material/FileUpload';
import { toggleMode, importSnapshots, startReconnect } from '../slices/mainSlice';
import { useDispatch, useSelector } from 'react-redux';
import StatusDot from '../components/Buttons/StatusDot';
-import LoopIcon from '@mui/icons-material/Loop';
-import CloseIcon from '@mui/icons-material/Close';
-import WarningIcon from '@mui/icons-material/Warning';
import { MainState, RootState } from '../FrontendTypes';
+import { Lock, Unlock, Download, Upload, RefreshCw } from 'lucide-react';
+import { toast } from 'react-hot-toast';
function exportHandler(snapshots: []): void {
- // function that takes in our tabs[currentTab] object to be exported as a JSON file. NOTE: TypeScript needs to be updated
- const fileDownload: HTMLAnchorElement = document.createElement('a'); // invisible HTML element that will hold our tabs[currentTab] object
-
+ const fileDownload: HTMLAnchorElement = document.createElement('a');
fileDownload.href = URL.createObjectURL(
- // href is the reference to the URL object created from the Blob
- new Blob([JSON.stringify(snapshots)], { type: 'application/json' }), // Blob obj is raw data. The tabs[currentTab] object is stringified so the Blob can access the raw data
+ new Blob([JSON.stringify(snapshots)], { type: 'application/json' }),
);
-
- fileDownload.setAttribute('download', 'snapshot.json'); // We set a download attribute with snapshots.json as the file name. This allows us to download the file when the element is 'clicked.' The file will be named snapshots.json once the file is downloaded locally
- fileDownload.click(); // click is a method on all HTML elements that simulates a mouse click, triggering the element's click event
-
- URL.revokeObjectURL(fileDownload.href); // after file is downloaded, remove the href
+ fileDownload.setAttribute('download', 'snapshot.json');
+ fileDownload.click();
+ URL.revokeObjectURL(fileDownload.href);
}
function importHandler(dispatch: (a: unknown) => void): void {
- // function handles the importing of a tabs[currentTab] object when the upload button is selected
- const fileUpload = document.createElement('input'); // invisible HTML element that will hold our uploaded tabs[currentTab] object
- fileUpload.setAttribute('type', 'file'); // Attributes added to HTML element
+ const fileUpload = document.createElement('input');
+ fileUpload.setAttribute('type', 'file');
fileUpload.onchange = (e: Event) => {
- // onChange is when value of HTML element is changed
- const reader = new FileReader(); // FileReader is an object that reads contents of local files in async. It can use file or blob objects
- const eventFiles = e.target as HTMLInputElement; // uploaded tabs[currentTab] object is stored as the event.target
+ const reader = new FileReader();
+ const eventFiles = e.target as HTMLInputElement;
if (eventFiles) {
- // if the fileUpload element has an eventFiles
- reader.readAsText(eventFiles.files[0]); // the reader parses the file into a string and stores it within the reader object
+ reader.readAsText(eventFiles.files[0]);
}
reader.onload = () => {
- const test = reader.result.toString(); // once the local file has been loaded, result property on FileReader object returns the file's contents and then converts the file contents to a string
- return dispatch(importSnapshots(JSON.parse(test))); // dispatch sends the result of of converting our tabs[currentTab] object => string => JSON Object. This updates the current tab
+ const test = reader.result.toString();
+ return dispatch(importSnapshots(JSON.parse(test)));
};
};
- fileUpload.click(); // click is a method on all HTML elements that simulates a mouse click, triggering the element's click event
+ fileUpload.click();
}
function ButtonsContainer(): JSX.Element {
@@ -61,145 +44,72 @@ function ButtonsContainer(): JSX.Element {
const { currentTab, tabs, currentTabInApp, connectionStatus }: MainState = useSelector(
(state: RootState) => state.main,
);
+
//@ts-ignore
const {
//@ts-ignore
mode: { paused },
} = tabs[currentTab];
- //adding a local state using useState for the reconnect button functionality
- const [reconnectDialogOpen, setReconnectDialogOpen] = useState(false);
-
- //logic for handling dialog box opening and closing
- const handleReconnectClick = () => {
- setReconnectDialogOpen(true);
- };
-
- const handleReconnectConfirm = () => {
- handleReconnectCancel();
+ const handleReconnect = () => {
dispatch(startReconnect());
+ toast.success('Successfully reconnected', {
+ duration: 2000,
+ position: 'top-right',
+ style: {
+ background: 'var(--bg-primary)',
+ color: 'var(--text-primary)',
+ },
+ iconTheme: {
+ primary: 'var(--color-primary)',
+ secondary: 'var(--text-primary)',
+ },
+ });
};
- const handleReconnectCancel = () => {
- //closing the dialog
- setReconnectDialogOpen(false);
- };
-
- useEffect(() => {
- if (!connectionStatus) setReconnectDialogOpen(true);
- }, [connectionStatus]);
-
return (
-
dispatch(toggleMode('paused'))}
- >
- {paused ? (
-
- ) : (
-
- )}
- {paused ? 'Locked' : 'Unlocked'}
-
-
exportHandler(tabs[currentTab])}
- >
-
- Download
-
-
importHandler(dispatch)}>
-
- Upload
-
- {/* The component below renders a button for the tutorial walkthrough of Reactime */}
-
- {/* adding a button for reconnection functionality 10/5/2023 */}
-
-
-
- }
- >
-
- Reconnect
-
-
-
- handleReconnectCancel()}
- className='close-icon-pop-up'
- />
-
-
-
- WARNING
-
-
- Status: {connectionStatus ? 'Connected' : 'Disconnected'}
- {connectionStatus ? (
- <>
- Reconnecting while Reactime is still connected to the application may cause unforeseen
- issues. Are you sure you want to proceed with the reconnection?
- >
+
+ dispatch(toggleMode('paused'))}
+ >
+ {paused ? (
+
) : (
- <>
- Reactime has unexpectedly disconnected from your application. To continue using
- Reactime, please reconnect.
-
-
- WARNING: Reconnecting can sometimes cause unforeseen issues, consider downloading the
- data before proceeding with the reconnection, if needed.
- >
- )}
-
-
-
- handleReconnectCancel()}
- className='cancel-button-pop-up'
- type='button'
- variant='contained'
- style={{ backgroundColor: '#474c55' }}
- >
- Cancel
-
- {!connectionStatus && (
- exportHandler(tabs[currentTab])}
- type='button'
- className='download-button-pop-up'
- variant='contained'
- color='primary'
- >
- Download
-
+
)}
- handleReconnectConfirm()}
- type='button'
- className='reconnect-button-pop-up'
- variant='contained'
- style={{ backgroundColor: '#F00008' }}
- >
- Reconnect
-
-
-
+ {paused ? 'Locked' : 'Unlocked'}
+
+ exportHandler(tabs[currentTab])}
+ >
+
+ Download
+
+ importHandler(dispatch)}>
+
+ Upload
+
+
+
+
+
+ }
+ >
+
+ Reconnect
+
+
);
}
diff --git a/src/app/containers/ErrorContainer.tsx b/src/app/containers/ErrorContainer.tsx
index 4725d2d9a..7bd786104 100644
--- a/src/app/containers/ErrorContainer.tsx
+++ b/src/app/containers/ErrorContainer.tsx
@@ -6,6 +6,8 @@ import ErrorMsg from '../components/ErrorHandling/ErrorMsg';
import { useDispatch, useSelector } from 'react-redux';
import { MainState, RootState, ErrorContainerProps } from '../FrontendTypes';
import { current } from '@reduxjs/toolkit';
+import { RefreshCw, Github, PlayCircle } from 'lucide-react';
+
/*
This is the loading screen that a user may get when first initalizing the application. This page checks:
@@ -107,38 +109,61 @@ function ErrorContainer(props: ErrorContainerProps): JSX.Element {
return (
-
-
-
Launching Reactime on tab: {currentTitle}
-
-
-
Checking if content script has been launched on current tab
-
-
-
Checking if React Dev Tools has been installed
-
-
-
Checking if target is a compatible React app
-
-
-
-
-
-
+
+
+
+
+
+
+ Welcome to Reactime
+
+
+
+
Checking if content script has been launched on current tab
+
+
+
Checking if React Dev Tools has been installed
+
+
+
Checking if target is a compatible React app
+
+
+
+
+ To ensure Reactime works correctly with your React application, please refresh your
+ development page. This allows Reactime to properly connect with your app and start
+ monitoring state changes.
+
+
+ Important: Reactime requires React Developer Tools to be installed. If you haven't
+ already, please{' '}
+
+ install React Developer Tools
+ {' '}
+ first.
+
+
+
+
+ Note: Reactime only works with React applications and by default only launches on URLs
+ starting with localhost.
+
+
+
+
+ Visit Reactime Github for more information
+
-
-
- Please visit the Reactime Github for more info.
-
);
}
diff --git a/src/app/containers/MainContainer.tsx b/src/app/containers/MainContainer.tsx
index 9f91bd980..52eda6d6e 100644
--- a/src/app/containers/MainContainer.tsx
+++ b/src/app/containers/MainContainer.tsx
@@ -1,5 +1,5 @@
/* eslint-disable max-len */
-import React, { useEffect, useState } from 'react';
+import React, { useState } from 'react';
import ActionContainer from './ActionContainer';
import TravelContainer from './TravelContainer';
import ButtonsContainer from './ButtonsContainer';
@@ -12,14 +12,13 @@ import {
setTab,
deleteTab,
noDev,
- aReactApp, // JR added 12.20.23 9.53pm
+ aReactApp,
setCurrentLocation,
disconnected,
endConnect,
} from '../slices/mainSlice';
import { useDispatch, useSelector } from 'react-redux';
import { MainState, RootState } from '../FrontendTypes';
-import HeatMapLegend from '../components/StateRoute/ComponentMap/heatMapLegend';
/*
This is the main container where everything in our application is rendered
@@ -29,34 +28,6 @@ function MainContainer(): JSX.Element {
const dispatch = useDispatch();
const { currentTab, tabs, port }: MainState = useSelector((state: RootState) => state.main);
- //JR: check connection status
- const { connectionStatus }: MainState = useSelector((state: RootState) => state.main);
-
- // JR 12.22.23: so far this log always returns true
- // console.log('MainContainer connectionStatus at initialization: ', connectionStatus);
-
- const [actionView, setActionView] = useState(true); // We create a local state 'actionView' and set it to true
-
- // this function handles Time Jump sidebar view
- const toggleActionContainer = () => {
- setActionView(!actionView); // sets actionView to the opposite boolean value
-
- const toggleElem = document.querySelector('aside'); // aside is like an added text that appears "on the side" aside some text.
- toggleElem.classList.toggle('no-aside'); // toggles the addition or the removal of the 'no-aside' class
-
- //JR: added for collapse label
- const collapse = document.querySelector('.collapse');
- collapse.classList.toggle('hidden');
-
- const recordBtn = document.getElementById('recordBtn');
-
- if (recordBtn.style.display === 'none') {
- // switches whether to display the record toggle button by changing the display property between none and flex
- recordBtn.style.display = 'flex';
- } else {
- recordBtn.style.display = 'none';
- }
- };
// Function handles when Reactime unexpectedly disconnects
const handleDisconnect = (msg): void => {
@@ -73,7 +44,6 @@ function MainContainer(): JSX.Element {
payload: Record
;
sourceTab: number;
}) => {
- // const { action, payload, sourceTab } = message;
let maxTab: number;
if (!sourceTab && action !== 'keepAlive') {
@@ -93,7 +63,6 @@ function MainContainer(): JSX.Element {
dispatch(noDev(payload));
break;
}
- // JR 12.20.23 9.53pm added a listener case for sending aReactApp to frontend
case 'aReactApp': {
dispatch(aReactApp(payload));
break;
@@ -120,7 +89,6 @@ function MainContainer(): JSX.Element {
}
};
- // useEffect(() => {
async function awaitPortConnection() {
if (port) return; // only open port once so if it exists, do not run useEffect again
@@ -147,10 +115,7 @@ function MainContainer(): JSX.Element {
dispatch(endConnect());
}
awaitPortConnection();
- // });
- // Error Page launch IF(Content script not launched OR RDT not installed OR Target not React app)
- // setTimeout(() => {
if (
!tabs[currentTab] ||
//@ts-ignore
@@ -162,8 +127,6 @@ function MainContainer(): JSX.Element {
return ;
}
- // }, 5000);
-
const { axSnapshots, currLocation, viewIndex, sliderIndex, snapshots, hierarchy, webMetrics } =
tabs[currentTab]; // we destructure the currentTab object which is passed in from background.js
//@ts-ignore
@@ -208,11 +171,7 @@ function MainContainer(): JSX.Element {
return (
-
+
{/* @ts-ignore */}
{snapshots.length ? (
@@ -231,9 +190,11 @@ function MainContainer(): JSX.Element {
/>
) : null}
- {/* @ts-ignore */}
-
-
+
+ {/* @ts-ignore */}
+
+
+
);
diff --git a/src/app/containers/ProvConContainer.tsx b/src/app/containers/ProvConContainer.tsx
new file mode 100644
index 000000000..c2e6b7044
--- /dev/null
+++ b/src/app/containers/ProvConContainer.tsx
@@ -0,0 +1,196 @@
+import React from 'react';
+import { ProvConContainerProps, FilteredNode } from '../FrontendTypes';
+import { JSONTree } from 'react-json-tree';
+
+const ProvConContainer = (props: ProvConContainerProps): JSX.Element => {
+ const jsonTheme = {
+ scheme: 'custom',
+ base00: 'transparent',
+ base0B: '#14b8a6', // dark navy for strings
+ base0D: '#60a5fa', // Keys
+ base09: '#f59e0b', // Numbers
+ base0C: '#EF4444', // Null values
+ };
+
+ //deconstruct props
+ const { currentSnapshot } = props;
+
+ //parse through node
+ const keepContextAndProviderNodes = (node) => {
+ if (!node) return null;
+
+ // Check if this node should be kept
+ const hasContext =
+ node?.componentData?.context && Object.keys(node.componentData.context).length > 0;
+ const isProvider = node?.name && node.name.endsWith('Provider');
+ const shouldKeepNode = hasContext || isProvider;
+
+ // Process children first
+ let processedChildren = [];
+ if (node.children) {
+ processedChildren = node.children
+ .map((child) => keepContextAndProviderNodes(child))
+ .filter(Boolean); // Remove null results
+ }
+
+ // If this node should be kept or has kept children, return it
+ if (shouldKeepNode || processedChildren.length > 0) {
+ return {
+ ...node,
+ children: processedChildren,
+ };
+ }
+
+ // If neither the node should be kept nor it has kept children, filter it out
+ return null;
+ };
+ const contextProvidersOnly = keepContextAndProviderNodes(currentSnapshot);
+
+ const filterComponentProperties = (node: any): FilteredNode | null => {
+ if (!node) return null;
+
+ // Helper function to check if an object is empty (including nested objects)
+ const isEmptyObject = (obj: any): boolean => {
+ if (!obj) return true;
+ if (Array.isArray(obj)) return obj.length === 0;
+ if (typeof obj !== 'object') return false;
+
+ // Check each property recursively
+ for (const key in obj) {
+ const value = obj[key];
+ if (typeof value === 'object') {
+ if (!isEmptyObject(value)) return false;
+ } else if (value !== undefined && value !== null) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // Create a new object for the filtered node
+ const filteredNode: FilteredNode = {};
+
+ // Flatten root level props if they exist
+ if (node.props && !isEmptyObject(node.props)) {
+ Object.entries(node.props).forEach(([key, value]) => {
+ if (!isEmptyObject(value)) {
+ filteredNode[`${key}`] = value;
+ }
+ });
+ }
+
+ // Flatten componentData properties into root level if they exist
+ if (node.componentData.context && !isEmptyObject(node.componentData.context)) {
+ // Add context directly if it exists
+ Object.entries(node.componentData.context).forEach(([key, value]) => {
+ if (!isEmptyObject(value)) {
+ filteredNode[`${key}`] = value;
+ }
+ });
+
+ // Flatten componentData.props if they exist
+ if (node.componentData.props && !isEmptyObject(node.componentData.props)) {
+ Object.entries(node.componentData.props).forEach(([key, value]) => {
+ if (!isEmptyObject(value)) {
+ filteredNode[`${key}`] = value;
+ }
+ });
+ }
+
+ // Flatten componentData.hooksState if it exists
+ if (node.componentData.hooksState && !isEmptyObject(node.componentData.hooksState)) {
+ Object.entries(node.componentData.hooksState).forEach(([key, value]) => {
+ if (!isEmptyObject(value)) {
+ filteredNode[`${key}`] = value;
+ }
+ });
+ }
+ }
+
+ // Flatten root level hooksState if it exists
+ if (node.componentData.hooksState && !isEmptyObject(node.componentData.hooksState)) {
+ filteredNode['State'] = node.componentData.hooksState;
+ }
+
+ // Process children and add them using the node's name as the key
+ if (node.hasOwnProperty('children') && Array.isArray(node.children)) {
+ for (const child of node.children) {
+ const filteredChild = filterComponentProperties(child);
+ if (filteredChild && !isEmptyObject(filteredChild) && child.name) {
+ filteredNode[child.name] = filteredChild;
+ }
+ }
+ }
+
+ // Only return the node if it has at least one non-empty property
+ return isEmptyObject(filteredNode) ? null : filteredNode;
+ };
+
+ const filteredProviders = filterComponentProperties(contextProvidersOnly);
+
+ const parseStringifiedValues = (obj) => {
+ if (!obj || typeof obj !== 'object') return obj;
+
+ const parsed = { ...obj };
+ for (const key in parsed) {
+ if (typeof parsed[key] === 'string') {
+ try {
+ // Check if the string looks like JSON
+ if (parsed[key].startsWith('{') || parsed[key].startsWith('[')) {
+ const parsedValue = JSON.parse(parsed[key]);
+ parsed[key] = parsedValue;
+ }
+ } catch (e) {
+ // If parsing fails, keep original value
+ continue;
+ }
+ } else if (typeof parsed[key] === 'object') {
+ parsed[key] = parseStringifiedValues(parsed[key]);
+ }
+ }
+ return parsed;
+ };
+
+ const renderSection = (title, content, isReducer = false) => {
+ if (
+ !content ||
+ (Array.isArray(content) && content.length === 0) ||
+ Object.keys(content).length === 0
+ ) {
+ return null;
+ }
+
+ // Parse any stringified JSON before displaying
+ const parsedContent = parseStringifiedValues(content);
+
+ return (
+
+
{title}
+
+ true}
+ shouldExpandNodeInitially={() => true}
+ />
+
+
+ );
+ };
+
+ return (
+
+
Providers / Consumers
+ {filteredProviders ? (
+
{renderSection(null, filteredProviders)}
+ ) : (
+
+
No providers or consumers found in the current component tree.
+
+ )}
+
+ );
+};
+
+export default ProvConContainer;
diff --git a/src/app/containers/StateContainer.tsx b/src/app/containers/StateContainer.tsx
index 9dc9136bd..06f1a58b7 100644
--- a/src/app/containers/StateContainer.tsx
+++ b/src/app/containers/StateContainer.tsx
@@ -8,7 +8,6 @@ import StateRoute from '../components/StateRoute/StateRoute';
import DiffRoute from '../components/DiffRoute/DiffRoute';
import { StateContainerProps } from '../FrontendTypes';
import { Outlet } from 'react-router';
-import HeatMapLegend from '../components/StateRoute/ComponentMap/heatMapLegend';
// eslint-disable-next-line react/prop-types
const StateContainer = (props: StateContainerProps): JSX.Element => {
@@ -19,48 +18,17 @@ const StateContainer = (props: StateContainerProps): JSX.Element => {
viewIndex, // from 'tabs[currentTab]' object in 'MainContainer'
webMetrics, // from 'tabs[currentTab]' object in 'MainContainer'
currLocation, // from 'tabs[currentTab]' object in 'MainContainer'
- axSnapshots,// from 'tabs[currentTab]' object in 'MainContainer'
+ axSnapshots, // from 'tabs[currentTab]' object in 'MainContainer'
} = props;
return (
<>
-
-
-
-
-
- navData.isActive ? 'is-active main-router-link' : 'main-router-link'
- }
- to='/'
- >
- State
-
-
- navData.isActive ? 'is-active main-router-link' : 'main-router-link'
- }
- to='/diff'
- >
- Diff
-
-
-
-
-
-
-
- {/* */}
-
- }
- />
-
+
+
+
+ {
snapshots={snapshots}
currLocation={currLocation}
/>
- {/* */}
- {/* */}
-
- }
- />
-
-
+ }
+ />
+
+
>
);
};
diff --git a/src/app/containers/TravelContainer.tsx b/src/app/containers/TravelContainer.tsx
index 7fc2506a2..428a96918 100644
--- a/src/app/containers/TravelContainer.tsx
+++ b/src/app/containers/TravelContainer.tsx
@@ -1,20 +1,10 @@
/* eslint-disable max-len */
import React, { useState } from 'react';
-import MainSlider from '../components/TimeTravel/MainSlider';
import Dropdown from '../components/TimeTravel/Dropdown';
-import {
- playForward,
- pause,
- startPlaying,
- moveForward,
- moveBackward,
- resetSlider,
-} from '../slices/mainSlice';
+import { playForward, pause, startPlaying, resetSlider, changeSlider } from '../slices/mainSlice';
import { useDispatch, useSelector } from 'react-redux';
import { MainState, RootState, TravelContainerProps } from '../FrontendTypes';
-import { Button } from '@mui/material';
-import FastRewindIcon from '@mui/icons-material/FastRewind';
-import FastForwardIcon from '@mui/icons-material/FastForward';
+import { Play, Pause } from 'lucide-react';
/*
This container renders the time-travel play button, seek bar, playback controls, and the playback speed dropdown, located towards the bottom of the application, above the locked, download, upload, and tutorial buttons
@@ -54,6 +44,7 @@ function play( // function that will start/pause slider movement
// as long as we're not the last snapshot, increment slider up through our dispatch and increment index
dispatch(playForward(true));
currentIndex += 1;
+ dispatch(changeSlider(currentIndex));
} else {
dispatch(pause()); // pause the slider when we reach the end
}
@@ -72,40 +63,16 @@ function TravelContainer(props: TravelContainerProps): JSX.Element {
return (
-
play(selectedSpeed.value, playing, dispatch, snapshotsLength, sliderIndex)}
>
- {playing ? 'Pause' : 'Play'}
-
-
-
dispatch(moveBackward(false))}
- type='button'
- sx={{ height: 25, minWidth: 30, p: 0, mr: 1 }}
- aria-label='Backward'
- >
-
-
-
dispatch(moveForward(false))}
- type='button'
- sx={{ height: 25, minWidth: 30, p: 0 }}
- aria-label='Forward'
- >
-
-
+
+ {playing ?
:
}
+
{playing ? 'Pause' : 'Play'}
+
+
);
diff --git a/src/app/index.tsx b/src/app/index.tsx
index 99145962e..b24b57132 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -6,13 +6,9 @@ import './styles/main.scss';
import { store } from './store'; //imported RTK Store
import { Provider } from 'react-redux'; //imported Provider
-//Updated rendering sytax for React 18
const root = createRoot(document.getElementById('root'));
root.render(
- // Strict mode is for developers to better track best practices
- //
,
- //
);
diff --git a/src/app/slices/AxSlices/axLegendSlice.tsx b/src/app/slices/AxSlices/axLegendSlice.tsx
deleted file mode 100644
index 08e29e2ff..000000000
--- a/src/app/slices/AxSlices/axLegendSlice.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-
-export const axLegendSlice = createSlice({
- name: 'axLegend',
- initialState: {
- axLegendButtonClicked: false,
- },
- reducers: {
- renderAxLegend: (state) => {
- if (!state.axLegendButtonClicked) state.axLegendButtonClicked = true;
- else state.axLegendButtonClicked = false;
- }
- }
-})
-
-export const { renderAxLegend } = axLegendSlice.actions;
-
-export default axLegendSlice.reducer;
\ No newline at end of file
diff --git a/src/app/slices/mainSlice.ts b/src/app/slices/mainSlice.ts
index 98fd12d6e..3409618ed 100644
--- a/src/app/slices/mainSlice.ts
+++ b/src/app/slices/mainSlice.ts
@@ -88,7 +88,9 @@ export const mainSlice = createSlice({
const { snapshots: newSnaps } = payload[tab];
tabs[tab] = {
...payload[tab],
- sliderIndex: newSnaps.length - 1,
+ intervalId: tabs[tab].intervalId,
+ playing: tabs[tab].playing,
+ sliderIndex: tabs[tab].sliderIndex,
seriesSavedStatus: false,
};
}
@@ -202,6 +204,7 @@ export const mainSlice = createSlice({
},
changeSlider: (state, action) => {
+ //should really be called jump to snapshot
const { port, currentTab, tabs } = state;
const { hierarchy, snapshots } = tabs[currentTab] || {};
@@ -279,59 +282,6 @@ export const mainSlice = createSlice({
tabs[currentTab].intervalId = action.payload;
},
- moveForward: (state, action) => {
- const { port, tabs, currentTab } = state;
- const { hierarchy, snapshots, sliderIndex, intervalId } = tabs[currentTab] || {};
-
- if (sliderIndex < snapshots.length - 1) {
- const newIndex = sliderIndex + 1;
- // eslint-disable-next-line max-len
- // finds the name by the newIndex parsing through the hierarchy to send to background.js the current name in the jump action
- const nameFromIndex = findName(newIndex, hierarchy);
-
- port.postMessage({
- action: 'jumpToSnap',
- payload: snapshots[newIndex],
- index: newIndex,
- name: nameFromIndex,
- tabId: currentTab,
- });
-
- tabs[currentTab].sliderIndex = newIndex;
-
- // message is coming from the user
- if (!action.payload) {
- clearInterval(intervalId);
- tabs[currentTab].playing = false;
- }
- }
- },
-
- moveBackward: (state, action) => {
- const { port, tabs, currentTab } = state;
- const { hierarchy, snapshots, sliderIndex, intervalId } = tabs[currentTab] || {};
-
- if (sliderIndex > 0) {
- const newIndex = sliderIndex - 1;
- // eslint-disable-next-line max-len
- // finds the name by the newIndex parsing through the hierarchy to send to background.js the current name in the jump action
- const nameFromIndex = findName(newIndex, hierarchy);
-
- port.postMessage({
- action: 'jumpToSnap',
- payload: snapshots[newIndex],
- index: newIndex,
- name: nameFromIndex,
- tabId: currentTab,
- newProp: 'newPropFromReducer',
- });
- clearInterval(intervalId);
-
- tabs[currentTab].sliderIndex = newIndex;
- tabs[currentTab].playing = false;
- }
- },
-
resetSlider: (state) => {
const { port, tabs, currentTab } = state;
const { snapshots, sliderIndex } = tabs[currentTab] || {};
@@ -426,30 +376,17 @@ export const mainSlice = createSlice({
});
},
- save: (state, action) => {
- const { currentTab, tabs } = state;
+ toggleExpanded: (state, action) => {
+ const { tabs, currentTab } = state;
+ const snapshot = tabs[currentTab].currLocation.stateSnapshot;
- const { newSeries, newSeriesName } = action.payload;
- if (!tabs[currentTab].seriesSavedStatus) {
- tabs[currentTab] = { ...tabs[currentTab], seriesSavedStatus: 'inputBoxOpen' };
- return;
- }
- // Runs if series name input box is active.
- // Updates chrome local storage with the newly saved series. Console logging the seriesArray grabbed from local storage may be helpful.
- if (tabs[currentTab].seriesSavedStatus === 'inputBoxOpen') {
- let seriesArray: any = localStorage.getItem('project');
- seriesArray = seriesArray === null ? [] : JSON.parse(seriesArray);
- newSeries.name = newSeriesName;
- seriesArray.push(newSeries);
- localStorage.setItem('project', JSON.stringify(seriesArray));
- tabs[currentTab] = { ...tabs[currentTab], seriesSavedStatus: 'saved' };
+ // Special case for root node
+ if (action.payload.name === 'root' && snapshot.name === 'root') {
+ snapshot.isExpanded = !snapshot.isExpanded;
return;
}
- },
- toggleExpanded: (state, action) => {
- const { tabs, currentTab } = state;
- // find correct node from currLocation and toggle isExpanded
+ // Regular case for other nodes
const checkChildren = (node) => {
if (_.isEqual(node, action.payload)) {
node.isExpanded = !node.isExpanded;
@@ -459,25 +396,7 @@ export const mainSlice = createSlice({
});
}
};
- checkChildren(tabs[currentTab].currLocation.stateSnapshot);
- },
-
- deleteSeries: (state) => {
- const { tabs, currentTab } = state;
- const allStorage = () => {
- const keys = Object.keys(localStorage);
- let i = keys.length;
- while (i--) {
- localStorage.removeItem(keys[i]);
- }
- };
- allStorage();
- Object.keys(tabs).forEach((tab) => {
- tabs[tab] = {
- ...tabs[tab],
- };
- });
- tabs[currentTab] = { ...tabs[currentTab], seriesSavedStatus: false };
+ checkChildren(snapshot);
},
disconnected: (state) => {
@@ -522,17 +441,13 @@ export const {
launchContentScript,
playForward,
startPlaying,
- moveForward,
- moveBackward,
resetSlider,
toggleMode,
importSnapshots,
tutorialSaveSeriesToggle,
onHover,
onHoverExit,
- save,
toggleExpanded,
- deleteSeries,
disconnected,
startReconnect,
endConnect,
diff --git a/src/app/store.ts b/src/app/store.ts
index 459bf1fbb..9ed9da56e 100644
--- a/src/app/store.ts
+++ b/src/app/store.ts
@@ -1,13 +1,11 @@
//Import store from redux tool kit
import { configureStore } from '@reduxjs/toolkit';
import { mainSlice } from './slices/mainSlice';
-import { axLegendSlice } from './slices/AxSlices/axLegendSlice';
//Export Store
export const store = configureStore({
reducer: {
main: mainSlice.reducer,
- axLegend: axLegendSlice.reducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }),
});
diff --git a/src/app/styles/abstracts/_variablesLM.scss b/src/app/styles/abstracts/_variablesLM.scss
deleted file mode 100644
index 75501f4af..000000000
--- a/src/app/styles/abstracts/_variablesLM.scss
+++ /dev/null
@@ -1,181 +0,0 @@
-/// fontFamily: 'monaco, Consolas, Lucida Console, monospace'
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-
-/// @type List
-$text-font-stack: 'Outfit', sans-serif !default;
-
-/// @type Color
-/// LIGHT MODE (LM) THEME COLORS
-$primary-color: #284b63;
-$primary-color-weak: #81929e;
-$primary-color-strong: #3c6e71;
-$secondary-color: #df6f37;
-
-//gradient used for some buttons
-$primary-strong-gradient: linear-gradient(
- 145deg,
- lighten($primary-color-strong, 20%),
- $primary-color-strong
-);
-
-/// GRAYSCALE THEME COLORS
-$background-color: #f0f0f0;
-$background-color-strong: #ffffff;
-$background-color-weak: #d9d9d9;
-$contrasting-color: #161617;
-$contrasting-color-weak: #353536;
-
-/// HEATMAP COLORS
-$heat-level-1: #F1B476;
-$heat-level-2: #E4765B;
-$heat-level-3: #C64442;
-$heat-level-4: #8C2743;
-
-/////////////////////////////////////////////////////////////////////
-// ALL OF THE FOLLOWING COLORS SHOULD REFERENCE A COLOR FROM ABOVE //
-/////////////////////////////////////////////////////////////////////
-
-/// @type Color
-
-//general text color theme
-$dark-text: $contrasting-color;
-$light-text: $background-color;
-//general icon color theme
-$icon-primary: $primary-color-strong;
-$icon-secondary: lighten($primary-color-strong, 50%);
-$icon-bg: $primary-color-weak;
-
-//the first window that pops up that checks for react devtools being installed
-$loader-background: $background-color;
-$loader-text: $dark-text;
-$loader-checks-border: $primary-color-strong;
-$launch-button: $primary-color-strong;
-$launch-button-hover: $primary-color;
-$launch-button-text: $light-text;
-
-//Collapse and Toggle Record labels in the top left of the screen
-$collapse-text: $dark-text;
-$toggle-record-text: $dark-text;
-
-//ACTION TAB COLORS (the main column on the left side)
-
-//drop-down menu for selecting which tab Reactime is observing
-$tab-select-background: $background-color-weak;
-$tab-select-text: $dark-text;
-$tab-dropdown-background: $background-color-weak;
-$tab-dropdown-hover: darken($background-color-weak, 10%);
-$tab-vertical-line: lighten($contrasting-color, 30%);
-$tab-arrow-indicator: lighten($contrasting-color, 30%);
-
-//SNAPSHOTS AND ROUTES LIST
-$action-tab-background: $background-color;
-$action-cont-border: $contrasting-color;
-//$action-clear-button: $primary-color; //currently being handled by mui theme.ts file
-//$action-clear-button-text: $background-color; //currently being handled by mui theme.ts file
-$route-bar: $primary-color-strong;
-$route-bar-text: $light-text;
-$indiv-action-input-bg: $background-color-strong;
-$indiv-action-selected: darken($background-color, 15%);
-$indiv-action-selected-text: $light-text;
-$indiv-action-filler-text: lighten($contrasting-color,60%);
-$indiv-action-custom-text: $dark-text;
-$indiv-action-border: $contrasting-color-weak;
-$indiv-action-time: $primary-color-weak;
-$indiv-action-time-text: $light-text;
-$time-button-height: 20px;
-$current: $contrasting-color-weak;
-$current-text: $light-text;
-$jump: $contrasting-color-weak;
-$jump-hover: $primary-color;
-$jump-text: $light-text;
-
-//the State + Diff toggle // THIS FEATURE IS CURRENTLY COMMENTED OUT
-$header-background: $background-color;
-$header-button-active: $primary-color;
-$header-button-active-text: $light-text;
-$header-button-inactive: $primary-color-weak;
-$header-button-inactive-text: $dark-text;
-$header-button-hover: lighten($header-button-inactive, 20%);
-
-//the buttons/tabs controlling what is displayed in STATECONTAINER (Map, Performance, History, Web Metrics, Tree)
-$navbar-background: $background-color; //this color only shows up in Diff mode when buttons don't fill the whole bar //DIFF FEATURE IS CURRENTLY COMMENTED OUT
-$navbar-selected: $primary-color-strong;
-$navbar-selected-text: $light-text;
-$navbar-unselected: $primary-color-weak;
-$navbar-unselected-text: $dark-text;
-$navbar-hover: darken($navbar-unselected,15%);
-
-$state-background: $background-color-strong;
-$state-cont-border: $contrasting-color;
-
-//MAP TAB
-$map-options-label: $dark-text;
-//the dropdown colors are determined in the dropDownStyle object in LinkControls.tsx
-$map-link: $contrasting-color;
-// $map-root-fill: $primary-color; //root fill is currently a visx LinearGradient defined in ComponentMap.tsx
-$map-root-stroke: $primary-color;
-$map-root-text: $light-text;
-//$map-parent-fill: $primary-color-strong; //parent fill is currently a visx LinearGradient defined in ComponentMap.tsx
-$map-parent-stroke: darken($primary-color-strong, 20%);
-$map-parent-text: $light-text;
-$map-child-fill: $primary-color-weak;
-$map-child-stroke: lighten($primary-color, 50%);
-$map-child-text: $dark-text;
-
-//PERFORMANCE TAB
-$performance-subtab-border: 1px;
-$performance-subtab-text: $contrasting-color;
-$performance-subtab-hover: $background-color-weak;
-$performance-options-dropdown: $primary-color-strong;
-$performance-options-label: $dark-text;
-$performance-save-series-button: $primary-color;
-$performance-save-series-button-text: $light-text;
-$perf-background: $background-color-strong;
-//axis tick colors and labels determined in BarGraph.tsx on line 29 and BarGraphComparison.tsx on line 39
-//on comparison view, button colors are in BarGraphComparison.tsx on line 220, 240
-
-//HISTORY TAB
-$history-node: $primary-color; //this is currently set in History.tsx line 291
-$history-link: $contrasting-color; // this is currently set in d3graph.css line 26
-//$history-text: $light-text;
-
-//WEB METRICS TAB
-//web metrics colors are selected in WebMetrics.tsx (text color line 69)
-$web-circles: $contrasting-color;
-$web-text: $dark-text;
-
-$tree-background: $background-color;
-
-//CONTORL BAR for time travel
-$travel-background: $background-color;
-$travel-top-border: lighten($contrasting-color, 50%);
-$play-button: $primary-strong-gradient;
-$play-button-text: $light-text;
-$slider-handle: $primary-strong-gradient;
-$slider-rail-left: $primary-color;
-$slider-rail-right: $background-color-weak;
-$scrub-button: $primary-strong-gradient;
-$scrub-icon: $light-text;
-$speed-dropdown: $primary-color-strong;
-$speed-dropdown-text: $light-text;
-$speed-dropdown-expanded: lighten($primary-color-strong, 20%);
-$speed-dropdown-expanded-hover: $primary-color;
-$speed-dropdown-expanded-text: $light-text;
-
-//the buttons at the very bottom of the screen (locked, download, upload, tutorial, reconnect)
-$function-bar-background: $background-color;
-//outline color currently handled in mui theme.ts file
-$function-bar-button-outlines: $primary-color;
-$function-bar-text: $primary-color-strong;
-$function-bar-text-hover: $primary-color;
-$function-bar-text-highlight: $primary-color-strong;
-$function-bar-icons: $primary-color-strong;
-
-//colors for pop-up window that opens when clicking the reconnect button
-$pop-up-window-background: $background-color;
-$pop-up-window-text: $dark-text;
-$reconnect-button: $primary-color-strong;
-$reconnect-button-text: $light-text;
-
-/// @type Font Size
-$button-text-size: 16px;
diff --git a/src/app/styles/base/_base.scss b/src/app/styles/base/_base.scss
deleted file mode 100644
index 3dc53f1cc..000000000
--- a/src/app/styles/base/_base.scss
+++ /dev/null
@@ -1,65 +0,0 @@
-html {
- margin: 0;
- padding: 0;
- height: 100%;
-}
-
-body {
- margin: 0;
- height: 100%;
-}
-
-#root {
- height: 100%;
-}
-
-.travel-container {
- grid-area: travel;
-}
-.action-container {
- grid-area: actions;
- transition: 0.5s;
-}
-
-.perf-rect {
- fill: $perf-background;
-}
-
-.state-container-container {
- grid-area: states;
-}
-
-.buttons-container {
- //color: #ff0000;
- grid-area: buttons;
- //border-color: #ff0000;
-}
-
-.action-container {
- border-style: solid;
- border-color: $action-cont-border;
- border-width: 0px;
-}
-
-.state-container {
- border-style: solid;
- border-color: $state-cont-border;
- border-width: 0px;
-}
-
-.travel-container {
- border-style: solid;
- border-color: $travel-top-border;
- border-left: 0px;
- border-right: 0px;
- border-bottom: 0px;
-}
-
-.saveSeriesContainer {
- padding-bottom: 15px;
- padding-top: 10px;
-}
-
-.performance-dropdown {
- background-color: #f5b9b9;
-}
diff --git a/src/app/styles/base/_typography.scss b/src/app/styles/base/_typography.scss
deleted file mode 100644
index c550ea0d2..000000000
--- a/src/app/styles/base/_typography.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-body {
- color: $contrasting-color;
- font: normal 16px $text-font-stack;
-}
diff --git a/src/app/styles/components/_actionComponent.scss b/src/app/styles/components/_actionComponent.scss
index b28f8fcfc..4cdf0ceaa 100644
--- a/src/app/styles/components/_actionComponent.scss
+++ b/src/app/styles/components/_actionComponent.scss
@@ -1,53 +1,128 @@
-.action-component {
- padding: 3px 10px;
- display: grid;
- grid-template-columns: none;
- align-items: center;
- height: 24px;
- // background-color: $brand-color;
- // border-bottom-style: solid;
- // border-bottom-width: 1px;
- // background-color: none;
- // border-color: #292929;
- // border-color: $border-color;
- cursor: pointer;
- overflow: hidden;
- @extend %disable-highlight;
+.route-container {
+ width: 100%;
}
-.clear-button {
- background-color:#187924;
- color:#187924;
+.route-header {
+ background-color: var(--button-primary-bg);
+ color: var(--button-primary-text);
+ padding: 10px;
+ font-size: 14px;
}
-.action-component-text {
- margin-bottom: 8px;
- color: $indiv-action-custom-text
+.route-content {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 50px;
+}
+
+/* Container for individual action */
+.individual-action {
+ margin: 0;
+ padding: 0;
+}
+
+.action-component {
+ display: flex;
+ padding: 6px 16px;
+ background: var(--bg-primary);
+ border-bottom: 1px solid var(--border-color);
+ transition: background-color 200ms ease;
+}
+
+.action-component:hover {
+ background-color: var(--hover-bg);
}
.action-component.selected {
- //font-size: 16px;
- background-color: $indiv-action-selected;
- color: $indiv-action-selected-text
+ background-color: var(--selected-bg);
}
-.action-component.exclude {
- color: #ff0000;
+.action-component-trigger {
+ width: 100%;
display: flex;
- justify-content: center;
- margin-top: 10px;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.action-component-text {
+ flex: 1;
+}
+
+/* Input styling */
+.actionname {
+ border: none;
+ background: transparent;
+ font-size: 1rem;
+ color: var(--text-primary);
+ width: 100%;
+ padding: 2px 0;
+}
+
+.actionname::placeholder {
+ color: var(--text-secondary);
}
-.action-component:focus {
+.actionname:focus {
outline: none;
}
-.action-component-trigger {
- display: grid;
- grid-template-columns: 4fr 1fr;
+/* Button styling */
+.time-button,
+.jump-button,
+.current-location {
+ min-width: 90px;
+ height: 28px;
+ padding: 0 8px;
+ border-radius: 6px;
+ font-size: 0.9375rem;
+ font-weight: 500;
+ display: flex;
align-items: center;
- //height: 20px;
+ justify-content: center;
+ border: none;
+ transition: all 200ms ease;
+ margin: 0;
+}
+
+.time-button {
+ color: var(--text-secondary);
+ background: transparent;
+}
+
+.jump-button {
+ color: var(--button-primary-text);
+ background: var(--button-primary-bg);
+ display: none;
+ transition: all 200ms ease;
+}
+
+.jump-button:hover {
+ background: var(--text-primary);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
- overflow: hidden;
- @extend %disable-highlight;
+}
+
+.current-location {
+ color: var(--color-primary);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--color-primary);
+}
+
+/* Hide/show button transitions */
+.action-component:hover .time-button {
+ display: none;
+}
+
+.action-component:hover .jump-button {
+ display: block;
+}
+
+.current-snap {
+ font-weight: 700;
+ border: none;
+ background: transparent;
+ font-size: 1rem;
+ color: var(--text-primary);
+ width: 100%;
+ padding: 2px 0px;
}
diff --git a/src/app/styles/components/_ax.scss b/src/app/styles/components/_ax.scss
index 1185b8c24..db9b06c4b 100644
--- a/src/app/styles/components/_ax.scss
+++ b/src/app/styles/components/_ax.scss
@@ -1,6 +1,80 @@
-// Ax.tsx
-#axControls {
- display: inline-flex;
- position: sticky;
- left: 0;
-}
\ No newline at end of file
+/* Container for the radio controls */
+.accessibility-controls {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 12px 24px;
+}
+
+/* Hide the default radio inputs */
+.accessibility-controls input[type='radio'] {
+ display: none;
+}
+
+/* Style the labels as buttons */
+.accessibility-controls label {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 60px;
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--bg-primary);
+ background-color: var(--color-primary);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 200ms ease;
+ user-select: none;
+}
+
+.accessibility-controls label:hover {
+ background-color: var(--color-primary-dark);
+}
+
+.accessibility-disable input[type='radio'] {
+ display: none;
+}
+
+.accessibility-disable label {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 60px;
+ padding: 6px 12px;
+ font-size: 14px;
+ font-weight: 500;
+ background-color: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 200ms ease;
+ user-select: none;
+}
+
+.accessibility-disable label:hover {
+ border-color: var(--border-color-dark);
+ background-color: var(--bg-tertiary);
+}
+
+.accessibility-text {
+ padding: 12px 24px;
+ max-width: 800px;
+ line-height: 1.5;
+}
+
+.accessibility-text p {
+ margin: 0;
+ color: var(--text-primary);
+ font-size: 16px;
+}
+
+.tooltipData-JSONTree {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border-color);
+ background-color: var(--bg-primary);
+}
diff --git a/src/app/styles/components/_buttons.scss b/src/app/styles/components/_buttons.scss
index 5eb55191e..ff875dfc8 100644
--- a/src/app/styles/components/_buttons.scss
+++ b/src/app/styles/components/_buttons.scss
@@ -1,464 +1,182 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-
-.save-series-button {
- padding: 3px;
- outline: transparent;
- color: $performance-save-series-button-text;
- font-size: 16px;
+.play-button {
display: flex;
- justify-content: center;
align-items: center;
- border-style: solid;
- border-color: transparent;
- border-radius: 5px;
- cursor: pointer;
- line-height: 1.5em;
- font: 400 16px 'Outfit', sans-serif;
- width: 120px;
- background: $performance-save-series-button;
- height: 30px;
- position: absolute;
- right: 3rem;
-
- &.animate {
- background: rgb(41, 164, 41);
- }
-}
-
-.delete-button {
- padding: 3px;
- outline: transparent;
- color: white;
- display: flex;
justify-content: center;
- align-items: center;
- border-style: solid;
- border-color: transparent;
- border-radius: 3px;
+ gap: 8px;
+ width: 100px;
+ height: 40px;
+ padding: 8px 12x;
+ border-radius: 8px;
+ border: none;
+ background-color: var(--button-primary-bg);
+ color: var(--button-primary-text);
+ font-size: 14px;
+ font-weight: 500;
cursor: pointer;
- line-height: 1.5em;
- font: 300 16px 'Outfit', sans-serif;
- font-size: $button-text-size;
- width: 120px;
- height: 30px;
+ transition: all 200ms ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ position: relative;
+ overflow: hidden;
- &.animate {
- background-color: rgb(41, 164, 41);
+ &:hover {
+ background-color: var(--text-primary);
}
-}
-.empty-button {
- padding: 3px;
- outline: transparent;
- color: black;
- display: flex;
- justify-content: center;
- align-items: center;
- border-style: solid;
- border-color: transparent;
- border-radius: 3px;
- cursor: pointer;
- line-height: 1.5em;
- font: 500 16px 'Roboto', sans-serif;
- width: 120px;
- //background: #ff0000;
-}
-
-.clear-button {
- background: #3256f1;
- background-color: #050787;
- color: #3256f1;
-}
-
-.empty-button:hover {
- color: black;
- box-shadow: #ff0001;
-}
+ &:focus {
+ outline: none;
+ box-shadow:
+ 0 0 0 2px white,
+ 0 0 0 4px #1f2937;
+ }
-.state-dropdown {
- width: 240px;
- margin-left: 20px;
-}
+ &-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ position: relative;
+ z-index: 1;
+ }
-.action-component:hover .time-button {
- display: none;
-}
+ &::after {
+ content: '';
+ position: absolute;
+ width: 5px;
+ height: 5px;
+ background: rgba(255, 255, 255, 0.5);
+ opacity: 1;
+ border-radius: 50%;
+ transform: scale(1);
+ animation: ripple 600ms linear;
+ display: none;
+ }
-.action-component:hover .jump-button {
- opacity: 1;
- transform: rotateX(0deg);
- transition: opacity 300ms, transform 0.15s linear;
+ &:active::after {
+ display: block;
+ }
}
-.time-button {
- outline: none;
- height: $time-button-height;
- margin-bottom: 8px;
- width: 100px;
- border: none;
- background: $indiv-action-time;
- color: $indiv-action-time-text;
+/* sidebar button open and closing functionality */
+.record-button-container {
display: flex;
- justify-content: center;
align-items: center;
- padding: 5px;
+ padding: 4px 16px;
+ background: transparent;
+ justify-content: space-between;
}
-.jump-button {
- outline: none;
- height: $time-button-height;
- margin-bottom: 8px;
- width: 100px;
- border: transparent;
- border-radius: 3px;
- background-color: #232529;
- color: #ffffff;
- transform: rotateX(90deg);
- transition: opacity 300ms, transform 0.15s linear;
- opacity: 0;
+.record-controls {
display: flex;
align-items: center;
- justify-content: center;
- padding: 2px;
- padding-bottom: 5px;
-}
-
-.jump-button:hover {
- // remove the blue border when button is clicked
- color: $jump-text;
- background-color: $jump-hover;
- cursor: pointer;
-}
-
-.current-location {
- outline: none;
- height: $time-button-height;
- margin-bottom: 8px;
- width: 100px;
- border: transparent;
- border-radius: 3px;
-}
-
-.empty-button:hover {
- color: white;
- background-color: #ff0001;
-}
-
-.play-button {
- width: 100px;
- height: 25px;
- margin: 0 1% 0 2%;
- color: $play-button-text;
- border-color: transparent;
- background: $play-button;
- border-radius: 5px;
- font-weight: 500;
- font-size: 16px;
- justify-content: center;
-}
-
-.play-button:hover {
- transform: translate3d(0, -3px, 0);
- cursor: pointer;
-}
-
-.backward-button,
-.forward-button {
- color: $scrub-icon;
- border-color: transparent;
- background: $scrub-button;
- border-radius: 5px;
-}
-
-.backward-button-icon,
-.forward-button-icon {
- color: $scrub-icon;
-}
-
-.forward-button:hover,
-.backward-button:hover {
- transform: translate3d(0, -3px, 0);
- cursor: pointer;
-}
-
-// .pause-button {
-// color: #ff0000;
-// background-color: #ff0000;
-// }
-
-.import-button:hover,
-.howToUse-button:hover,
-.export-button:hover,
-.pause-button:hover,
-.reconnect-button:hover {
- //@extend %button-shared;
- color: $function-bar-text-hover;
- box-shadow: 1px 1px 2px 1px rgba(30, 86, 171, 0.25);
- transform: translate3d(0, -3px, 0);
}
-.svg-inline--fa {
- //color: $blue-brand;
- margin-right: 0.75em;
- display: inline-block;
- font-size: inherit;
- height: 0.75em;
- overflow: visible;
- vertical-align: -0.125em;
-}
-
-%button-shared {
- padding: 5px;
- outline: none;
+.record-label {
display: flex;
- justify-content: center;
align-items: center;
- cursor: pointer;
- line-height: 1.5em;
- font: 300 16px 'Outfit', sans-serif;
- font-size: $button-text-size;
-
- background: #ff0001;
- border-radius: 5px;
- position: relative;
- border: 1px solid #b8c4c240;
-
- @extend %disable-highlight;
-}
-
-// Save button
-.dropdown-dark {
- background: #444;
- border-color: #111111 #0a0a0a black;
- background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.4));
- background-image: -moz-linear-gradient(top, transparent, rgba(0, 0, 0, 0.4));
- background-image: -o-linear-gradient(top, transparent, rgba(0, 0, 0, 0.4));
- background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.4));
- -webkit-box-shadow: inset 0 1px rgba(255, 255, 255, 0.1), 0 1px 1px rgba(0, 0, 0, 0.2);
- box-shadow: inset 0 1px rgba(255, 255, 255, 0.1), 0 1px 1px rgba(0, 0, 0, 0.2);
-}
-
-.dropdown-dark:before {
- border-bottom-color: #aaa;
+ gap: 8px;
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 500;
}
-.dropdown-dark:after {
- border-top-color: #aaa;
+.record-icon {
+ width: 10px;
+ height: 10px;
+ background-color: #ef4444;
+ border-radius: 50%;
+ transition: opacity 0.3s ease;
}
-.dropdown-dark .dropdown-select {
- color: #aaa;
- text-shadow: 0 1px black;
- background: #444;
- /* Fallback for IE 8 */
+.record-icon.recording {
+ animation: pulse 2s infinite;
}
-.dropdown-dark .dropdown-select:focus {
- color: #ccc;
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
}
-.dropdown-dark .dropdown-select > option {
- background: #444;
- text-shadow: 0 1px rgba(0, 0, 0, 0.4);
+.clear-button-modern {
+ width: 100% !important;
+ background-color: var(--bg-tertiary) !important;
+ color: var(--text-primary) !important;
+ font-size: 0.875rem !important;
+ font-weight: 500 !important;
+ text-transform: uppercase !important;
+ padding: 0.375rem 1rem !important;
+ border-radius: 0.375rem !important;
+ transition: background-color 200ms ease-in-out !important;
}
-.MuiSelect-icon {
- color: lightgrey !important;
+.clear-button-modern:hover {
+ background-color: var(--border-color) !important;
}
-.series-options-container {
- margin: 0 1rem;
+/* Theme toggle button styling */
+.theme-toggle {
+ position: relative;
+ width: 64px;
+ height: 32px;
+ border-radius: 16px;
+ border: 2px solid var(--border-color);
+ background-color: var(--bg-tertiary);
+ cursor: pointer;
+ transition: all 300ms ease;
+ padding: 2px;
}
-.snapshotId-header {
- font-size: 1rem;
+.theme-toggle.dark {
+ background-color: var(--bg-primary);
+ border-color: var(--border-color-dark);
}
-.dropdown-and-delete-series-container {
-
+.theme-toggle-slider {
+ position: relative;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background-color: var(--bg-primary);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
- justify-content: space-around;
- margin: 16px 0 24px 0;
-}
-
-.close-button-container {
- display: flex;
- justify-content: flex-end;
-}
-
-.closebtn {
- font-size: 20px;
- color: #818181;
- background-color: transparent;
- border: none;
- box-shadow: none;
-}
-
-.closebtn:hover {
- color: #f1f1f1;
-}
-
-.side-bar-button {
- width: 80px;
- height: 30px;
- padding: 0 2rem;
-}
-
-/* sidebar button open and closing functionality */
-aside {
- //background: #242529;
- color: #fff;
- transition: width 1s;
- width: 100%; //JR
+ justify-content: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
-.no-aside {
- width: 45px;
- // margin-right: 15px;
+.theme-toggle.dark .theme-toggle-slider {
+ transform: translateX(32px);
+ background-color: var(--color-primary);
}
-//JR added for collapse label
-.collapse {
- color: $collapse-text;
+.theme-toggle-icons {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
display: flex;
align-items: center;
- justify-content: flex-start;
-}
-
-.hidden {
- display: none;
-}
-
-.toggle:hover {
- cursor: pointer;
-}
-
-.toggle {
- // display: block;
- // position: relative;
- // margin-top: 1rem;
- height: 100%; //JR
-}
-
-/* toggle i handles arrow animation */
-.toggle i,
-.toggle i::after,
-.toggle i::before {
- position: absolute;
- width: 27px;
- height: 4px;
- border-radius: 4px;
- transition: transform 0.15s;
- background-color: $icon-primary;
-}
-
-.toggle i {
- top: 16px;
- left: 10px;
- display: block;
- background: $icon-primary;
-}
-
-.toggle i::before {
- top: -6px;
-}
-
-.toggle i::after {
- bottom: -6px;
-}
-
-.toggle i::after,
-.toggle i::before {
- content: '';
- display: block;
- // top: -5px;
-}
-
-.toggle i::before {
- transform: translate3d(-8px, 0, 0) rotate(-45deg) scale(0.7, 1);
-}
-
-.toggle i::after {
- transform: translate3d(-8px, 0, 0) rotate(45deg) scale(0.7, 1);
-}
-
-.no-aside .toggle i::before,
-.no-aside .toggle i::after {
- transform: none;
-}
-
-#arrow {
- // margin-bottom: 40px;
- // display: flex;
- // justify-content: flex-start;
-
- //JR
- height: 100%;
- display: grid;
- grid-template-columns: 45px 1fr;
-}
-
-/* ^ sidebar button open and closing functionality */
-.reconnect-button-pop-up {
- padding: 3px;
- outline: transparent;
- background-color: #050787;
- color: $reconnect-button-text !important;
- border: none;
- border-radius: 3px;
- cursor: pointer;
- line-height: 1.5em;
- font: 500 $button-text-size $text-font-stack;
- width: 120px;
- &:hover {
- color: black !important;
- background-color: #ff0001;
- }
-}
-
-.cancel-button-pop-up {
- padding: 3px;
- outline: transparent;
- color: black;
- border: none;
- border-radius: 3px;
- cursor: pointer;
- line-height: 1.5em;
- font: 500 $button-text-size $text-font-stack;
- width: 120px;
- &:hover {
- color: $contrasting-color;
- background-color: #ff0001;
- }
+ justify-content: space-between;
+ padding: 4px 8px;
+ pointer-events: none;
}
-.close-icon-pop-up {
- cursor: pointer;
- color: #ff0001;
- font-size: 20px;
- padding-right: 8px;
- color: black;
+.theme-toggle-icon {
+ width: 16px;
+ height: 16px;
+ transition: color 300ms ease;
}
-.download-button-pop-up {
- cursor: pointer;
- border: none;
- font: 500 $button-text-size $text-font-stack;
- width: 120px;
- border-radius: 3px;
- line-height: 1.5em;
- outline: transparent;
- //background: $blue-color-gradient;
- //box-shadow: $box-shadow-blue;
- &:hover {
- color: $contrasting-color;
- background-color: #ff0001;
- }
+.theme-toggle.dark .theme-toggle-icon.moon {
+ color: var(--color-primary);
}
-.warning-icon-pop-up {
- color: #d72828;
- margin-left: 5px;
- margin-right: -22px;
+.theme-toggle .theme-toggle-icon.sun {
+ color: var(--color-primary);
}
diff --git a/src/app/styles/components/_componentMap.scss b/src/app/styles/components/_componentMap.scss
index 565c14b66..7167fee01 100644
--- a/src/app/styles/components/_componentMap.scss
+++ b/src/app/styles/components/_componentMap.scss
@@ -1,46 +1,148 @@
-// .compMapLink {
-// // stroke: $map-link;
-// stroke: $secondary-color;
-// }
-.compMapLink:hover {
- box-shadow: 10px 10px rgb(163, 205, 24);
-};
+/* Component Map Container */
+.componentMapContainer {
+ fill: var(--bg-secondary);
+ transition: all 0.3s ease;
+ overflow: auto;
+}
+.componentMapContainer svg {
+ background-color: var(--bg-secondary);
+}
-.comp-map-options {
- color: $map-options-label;
+#root {
+ height: 100%;
}
-//this was supposed to control the dropDownStyle object in LinkControls.tsx (the dropdown menus in ComponentMap)
-// .comp-map-dropdown {
-// backgroundColor: #ff0008;
-// color: #ff0007;
-// background: #ff0006;
-// }
+/* Node Styling */
+.compMapParent,
+.compMapChild {
+ fill: var(--bg-primary);
+ stroke: var(--border-color);
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
+ transition: all 0.2s ease;
+}
-// .compMapParent {
-// //fill: $map-parent-fill;
-// //stroke: $map-parent-stroke;
-// }
+.compMapParent:hover,
+.compMapChild:hover {
+ stroke: var(--color-primary);
+ stroke-width: 2px;
+ cursor: pointer;
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
+}
-.compMapParentText {
- fill: $map-parent-text
+/* Root Node Styling */
+.compMapRoot {
+ fill: var(--color-primary);
+ stroke: var(--color-primary-dark);
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
+ transition: all 0.2s ease;
}
-.compMapChild {
- fill: $map-child-fill;
- stroke: $map-child-stroke;
+.compMapRoot:hover {
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
+ cursor: pointer;
+ transform: scale(1.05);
}
+/* Text Styling */
+.compMapRootText {
+ fill: var(--bg-primary);
+ font-weight: 500;
+ user-select: none;
+}
+
+.compMapParentText,
.compMapChildText {
- fill: $map-child-text;
+ fill: var(--text-primary);
+ font-weight: 500;
+ user-select: none;
}
-.compMapRoot {
- //fill: $map-root-fill;
- stroke: $map-root-stroke;
+/* Link Styling */
+.compMapLink {
+ stroke-linecap: round;
+ transition: all 0.3s ease;
}
-.compMapRootText {
- fill: $map-root-text;
+.compMapLink:hover {
+ stroke-width: 2;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
+.link-controls {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 12px 16px;
+ background: var(--bg-primary);
+ border-bottom: 1px solid var(--border-color);
+ justify-content: space-between;
+ max-width: 1200px;
+}
+
+.control-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.control-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ user-select: none;
+}
+
+.control-select {
+ background-color: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-size: 14px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ min-width: 100px;
+ cursor: pointer;
+ transition: all 200ms ease;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 8px center;
+ background-size: 16px;
+}
+
+.control-select:hover {
+ border-color: var(--border-color-dark);
+ background-color: var(--bg-tertiary);
+}
+
+.control-select:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 2px rgba(20, 184, 166, 0.1);
+}
+
+.control-range {
+ appearance: none;
+ width: 120px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--border-color);
+ outline: none;
+ margin: 0;
+ cursor: pointer;
+
+ &::-webkit-slider-thumb {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ cursor: pointer;
+ transition: all 200ms ease;
+ border: none;
+
+ &:hover {
+ background: var(--color-primary-dark);
+ transform: scale(1.1);
+ }
+ }
+}
diff --git a/src/app/styles/components/_jsonTree.scss b/src/app/styles/components/_jsonTree.scss
deleted file mode 100644
index 95bdf5e4c..000000000
--- a/src/app/styles/components/_jsonTree.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.json-tree {
- font-size: 13px;
- overflow: auto;
- margin: 10px;
- padding: 0;
- width: 900px;
- background-color: $tree-background;
- list-style: none;
-}
diff --git a/src/app/styles/components/_performanceVisx.scss b/src/app/styles/components/_performanceVisx.scss
index e631e27d1..7442dd944 100644
--- a/src/app/styles/components/_performanceVisx.scss
+++ b/src/app/styles/components/_performanceVisx.scss
@@ -1,75 +1,75 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-
-#RenderContainer {
+.saveSeriesContainer {
display: flex;
- justify-content: center;
+ align-items: center;
+ padding: 12px 16px;
+ background-color: var(--bg-primary);
+ max-width: 1200px;
+ justify-content: space-around;
+ border-bottom: 1px solid var(--border-color);
}
-.MuiSwitch-colorPrimary.Mui-checked {
- color: $icon-primary !important;
-}
-
-.MuiSwitch-switchBase {
- color: $icon-secondary !important;
-}
-
-.MuiSwitch-track {
- background-color: $icon-bg !important;
-}
-
-.MuiTypography-body1 {
- font-size: 2em !important;
+.routesForm {
+ display: flex;
+ align-items: center;
+ gap: 10px;
}
-#routes-formcontrol {
- padding: 3px;
- margin-left: 50px;
- font: 400 16px 'Outfit', sans-serif;
- text-align: left;
+#routes-dropdown {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ user-select: none;
+ white-space: nowrap;
}
-#routes-dropdown {
- color: $performance-options-label !important;
- font: 400 16px 'Outfit', sans-serif;
- text-align: left;
+.performance-dropdown,
+#routes-select,
+#snapshot-select,
+.control-select {
+ background-color: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-size: 14px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ min-width: 140px;
+ cursor: pointer;
+ transition: all 200ms ease;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 8px center;
+ background-size: 16px;
}
-.saveSeriesContainer {
- #routes-select,
- #snapshot-select {
- background-color: $performance-options-dropdown !important;
- }
+.performance-dropdown:hover,
+#routes-select:hover,
+#snapshot-select:hover,
+.control-select:hover {
+ border-color: var(--border-color-dark);
+ background-color: var(--bg-tertiary);
}
-.saveSeriesContainer {
- #routes-select,
- #snapshot-select {
- &:hover {
- cursor: pointer;
- }
- }
+.performance-dropdown:focus,
+#routes-select:focus,
+#snapshot-select:focus,
+.control-select:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 2px rgba(20, 184, 166, 0.1);
}
-#routes-select,
-#snapshot-select {
- color: white !important;
- font: 400 16px 'Outfit', sans-serif;
- text-align: left;
- width: 120px;
- height: 30px;
- border-radius: 5px;
- text-align: center;
+.routes,
+.control-select option {
+ padding: 0.5rem;
+ color: var(--text-primary);
}
-#seriesname {
- float: right;
- width: 220px;
- margin-right: 165px;
- height: 24px;
+// bar graph background
+.perf-rect {
+ fill: var(--bg-secondary);
}
-input:focus,
-textarea:focus,
-select:focus {
- outline: none;
+.bargraph-position {
+ background-color: var(--bg-secondary);
}
diff --git a/src/app/styles/components/_rc-slider.scss b/src/app/styles/components/_rc-slider.scss
index e50d273a2..c868f3349 100644
--- a/src/app/styles/components/_rc-slider.scss
+++ b/src/app/styles/components/_rc-slider.scss
@@ -1,250 +1,54 @@
.rc-slider {
- position: relative;
- width: 100%;
- margin: 20px;
- border-radius: 6px;
- -ms-touch-action: none;
- touch-action: none;
- box-sizing: border-box;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ position: relative;
+ width: 100%;
+ margin: 12px;
+ border-radius: 6px;
}
-.rc-slider * {
- box-sizing: border-box;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-}
.rc-slider-rail {
- position: absolute;
- width: 100%;
- background-color: $slider-rail-right;
- height: 4px;
- border-radius: 6px;
+ position: absolute;
+ width: 100%;
+ background-color: #d9d9d9;
+ height: 4px;
+ border-radius: 6px;
}
+
.rc-slider-track {
- position: absolute;
- left: 0;
- height: 4px;
- border-radius: 6px;
- background-color: $slider-rail-left;
+ position: absolute;
+ left: 0;
+ height: 4px;
+ border-radius: 6px;
+ background-color: #284b63;
}
+
.rc-slider-handle {
- position: absolute;
- margin-left: -10px;
- margin-top: -10px;
- width: 25px;
- height: 25px;
- cursor: pointer;
- cursor: -webkit-grab;
- cursor: grab;
- border-radius: 50%;
- background: $slider-handle;
- -ms-touch-action: pan-x;
- touch-action: pan-x;
-}
-.rc-slider-handle:focus {
- border-color: #ff0001;
- outline: none;
-}
-.rc-slider-handle-click-focused:focus {
- border-color: #ff0001;
-}
-.rc-slider-handle:hover {
- border-color: #ff0001;
-}
-.rc-slider-handle:active {
- //background: #f00004;
- border-color: #ff0001;
- cursor: -webkit-grabbing;
- cursor: grabbing;
-}
-.rc-slider-mark {
- position: absolute;
- top: 18px;
- left: 0;
- width: 100%;
- font-size: 16px;
-}
-.rc-slider-mark-text {
- position: absolute;
- display: inline-block;
- vertical-align: middle;
- text-align: center;
- cursor: pointer;
- color: #999;
-}
-.rc-slider-mark-text-active {
- color: #666;
-}
-.rc-slider-step {
- position: absolute;
- width: 100%;
- height: 4px;
- background: transparent;
-}
-.rc-slider-dot {
- position: absolute;
- bottom: -2px;
- margin-left: -4px;
- width: 8px;
- height: 8px;
- border: 2px solid #ff0001;
- background-color: #fff;
- cursor: pointer;
- border-radius: 50%;
- vertical-align: middle;
-}
-.rc-slider-dot-active {
- border-color: #96dbfa;
-}
-.rc-slider-disabled {
- background-color: #ff0001;
-}
-.rc-slider-disabled .rc-slider-track {
- background-color: #ccc;
-}
-.rc-slider-disabled .rc-slider-handle,
-.rc-slider-disabled .rc-slider-dot {
- border-color: #ccc;
- box-shadow: none;
- background-color: #fff;
- cursor: not-allowed;
-}
-.rc-slider-disabled .rc-slider-mark-text,
-.rc-slider-disabled .rc-slider-dot {
- cursor: not-allowed !important;
+ position: absolute;
+ margin-left: -8px;
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+ cursor: -webkit-grab;
+ cursor: grab;
+ border-radius: 50%;
+ background: #374151;
+ border: none;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
}
+
.rc-slider-vertical {
- width: 14px;
- height: 100%;
- padding: 0 5px;
+ height: 100%;
+ padding: 0 5px;
}
+
.rc-slider-vertical .rc-slider-rail {
- height: 100%;
- width: 4px;
+ height: 100%;
+ width: 4px;
}
+
.rc-slider-vertical .rc-slider-track {
- left: 5px;
- bottom: 0;
- width: 4px;
-}
-.rc-slider-vertical .rc-slider-handle {
- margin-left: -5px;
- margin-bottom: -7px;
- -ms-touch-action: pan-y;
- touch-action: pan-y;
-}
-.rc-slider-vertical .rc-slider-mark {
- top: 0;
- left: 18px;
- height: 100%;
-}
-.rc-slider-vertical .rc-slider-step {
- height: 100%;
- width: 4px;
-}
-.rc-slider-vertical .rc-slider-dot {
- left: 2px;
- margin-bottom: -4px;
-}
-.rc-slider-vertical .rc-slider-dot:first-child {
- margin-bottom: -4px;
-}
-.rc-slider-vertical .rc-slider-dot:last-child {
- margin-bottom: -4px;
-}
-.rc-slider-tooltip-zoom-down-enter,
-.rc-slider-tooltip-zoom-down-appear {
- animation-duration: 0.3s;
- animation-fill-mode: both;
- display: block !important;
- animation-play-state: paused;
-}
-.rc-slider-tooltip-zoom-down-leave {
- animation-duration: 0.3s;
- animation-fill-mode: both;
- display: block !important;
- animation-play-state: paused;
-}
-.rc-slider-tooltip-zoom-down-enter.rc-slider-tooltip-zoom-down-enter-active,
-.rc-slider-tooltip-zoom-down-appear.rc-slider-tooltip-zoom-down-appear-active {
- animation-name: rcSliderTooltipZoomDownIn;
- animation-play-state: running;
-}
-.rc-slider-tooltip-zoom-down-leave.rc-slider-tooltip-zoom-down-leave-active {
- animation-name: rcSliderTooltipZoomDownOut;
- animation-play-state: running;
-}
-.rc-slider-tooltip-zoom-down-enter,
-.rc-slider-tooltip-zoom-down-appear {
- transform: scale(0, 0);
- animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
-}
-.rc-slider-tooltip-zoom-down-leave {
- animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
-}
-@keyframes rcSliderTooltipZoomDownIn {
- 0% {
- opacity: 0;
- transform-origin: 50% 100%;
- transform: scale(0, 0);
- }
- 100% {
- transform-origin: 50% 100%;
- transform: scale(1, 1);
- }
-}
-@keyframes rcSliderTooltipZoomDownOut {
- 0% {
- transform-origin: 50% 100%;
- transform: scale(1, 1);
- }
- 100% {
- opacity: 0;
- transform-origin: 50% 100%;
- transform: scale(0, 0);
- }
-}
-.rc-slider-tooltip {
- position: absolute;
- left: -9999px;
- top: -9999px;
- visibility: visible;
- box-sizing: border-box;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-}
-.rc-slider-tooltip * {
- box-sizing: border-box;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-}
-.rc-slider-tooltip-hidden {
- display: none;
-}
-.rc-slider-tooltip-placement-top {
- padding: 4px 0 8px 0;
-}
-.rc-slider-tooltip-inner {
- padding: 6px 2px;
- min-width: 24px;
- height: 24px;
- font-size: 16px;
- line-height: 1;
- color: #fff;
- text-align: center;
- text-decoration: none;
- background-color: #6c6c6c;
- border-radius: 6px;
-}
-.rc-slider-tooltip-arrow {
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
-}
-.rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow {
- bottom: 4px;
- left: 50%;
- margin-left: -4px;
- border-width: 4px 4px 0;
- border-top-color: #6c6c6c;
+ left: 5px;
+ width: 4px;
}
diff --git a/src/app/styles/components/_renderingFrequency.scss b/src/app/styles/components/_renderingFrequency.scss
deleted file mode 100644
index 3786192d2..000000000
--- a/src/app/styles/components/_renderingFrequency.scss
+++ /dev/null
@@ -1,115 +0,0 @@
-.borderStyling {
- border-radius: 5px;
- border: 1px solid rgba(184, 196, 194, 0.25);
- box-shadow: 2px 3px 4px 2px rgba(0, 0, 0, 0.2);
- width: 53vw;
-}
-
-.borderCheck {
- border: 1px solid rgba(184, 196, 194, 0.25);
- box-shadow: 2px 3px 4px 2px rgba(0, 0, 0, 0.2);
- padding: 5px;
- width: 10vw;
- height: 25vw;
- overflow-y: scroll;
- overflow-wrap: break-word;
- overscroll-behavior: contain;
-}
-
-.DataComponent {
- padding-left: 30px;
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- width: 50vw;
- height: 40vw;
- overflow-y: scroll;
- overflow-wrap: break-word;
- overscroll-behavior: contain;
-}
-
-.StyledGridElement {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: $background-color-strong;
- padding: 20px;
- height: 5vh;
- margin: 20px 10px;
- font-family: 'Roboto', sans-serif;
-
- h3 {
- color: $primary-color;
- margin-bottom: 5px;
- margin-top: 5px;
- text-transform: uppercase;
- font-size: 16px;
- font-weight: 500;
- }
-
- h4 {
- color: $primary-color-strong;
- margin-bottom: 5px;
- margin-top: 5px;
- font-weight: 300;
- }
-
- p {
- color: $primary-color-strong;
- line-height: 20px;
- text-align: center;
- font-size: 18px;
- line-height: 18px;
- }
-}
-
-.ExpandStyledGridElement {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- background: $background-color-strong;
- padding: 20px;
- margin: 20px 10px;
- font-family: 'Roboto', sans-serif;
-
- h3 {
- color: $primary-color;
- margin-bottom: 5px;
- margin-top: 5px;
- text-transform: uppercase;
- font-size: 16px;
- font-weight: 500;
- }
-
- h4 {
- color: $primary-color-strong;
- margin-bottom: 5px;
- margin-top: 5px;
- font-weight: 300;
- }
-
- p {
- color: $primary-color-strong;
- line-height: 20px;
- text-align: center;
- font-size: 18px;
- line-height: 18px;
- }
-}
-
-.RenderRight {
- cursor: pointer;
- padding-right: 5px;
- border-right: 5px;
- //background: $blue-color-gradient;
- width: 50px;
- padding: 0 0.5em;
- right: 10%;
- opacity: 70%;
- transition: 0.3s;
-}
-
-.RenderRight:hover {
- opacity: 100%;
-}
diff --git a/src/app/styles/components/_sliderHandle.scss b/src/app/styles/components/_sliderHandle.scss
deleted file mode 100644
index 5806235cd..000000000
--- a/src/app/styles/components/_sliderHandle.scss
+++ /dev/null
@@ -1,170 +0,0 @@
-.rc-tooltip.rc-tooltip-zoom-enter,
-.rc-tooltip.rc-tooltip-zoom-leave {
- display: block;
-}
-.rc-tooltip-zoom-enter,
-.rc-tooltip-zoom-appear {
- opacity: 0;
- animation-duration: 0.3s;
- animation-fill-mode: both;
- animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
- animation-play-state: paused;
-}
-.rc-tooltip-zoom-leave {
- animation-duration: 0.3s;
- animation-fill-mode: both;
- animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05);
- animation-play-state: paused;
-}
-.rc-tooltip-zoom-enter.rc-tooltip-zoom-enter-active,
-.rc-tooltip-zoom-appear.rc-tooltip-zoom-appear-active {
- animation-name: rcToolTipZoomIn;
- animation-play-state: running;
-}
-.rc-tooltip-zoom-leave.rc-tooltip-zoom-leave-active {
- animation-name: rcToolTipZoomOut;
- animation-play-state: running;
-}
-@keyframes rcToolTipZoomIn {
- 0% {
- opacity: 0;
- transform-origin: 50% 50%;
- transform: scale(0, 0);
- }
- 100% {
- opacity: 1;
- transform-origin: 50% 50%;
- transform: scale(1, 1);
- }
-}
-@keyframes rcToolTipZoomOut {
- 0% {
- opacity: 1;
- transform-origin: 50% 50%;
- transform: scale(1, 1);
- }
- 100% {
- opacity: 0;
- transform-origin: 50% 50%;
- transform: scale(0, 0);
- }
-}
-.rc-tooltip {
- position: absolute;
- z-index: 1070;
- display: block;
- visibility: visible;
- font-size: 16px;
- line-height: 1.5;
- opacity: 0.9;
-}
-.rc-tooltip-hidden {
- display: none;
-}
-.rc-tooltip-placement-top,
-.rc-tooltip-placement-topLeft,
-.rc-tooltip-placement-topRight {
- padding: 5px 0 9px 0;
-}
-.rc-tooltip-placement-right,
-.rc-tooltip-placement-rightTop,
-.rc-tooltip-placement-rightBottom {
- padding: 0 5px 0 9px;
-}
-.rc-tooltip-placement-bottom,
-.rc-tooltip-placement-bottomLeft,
-.rc-tooltip-placement-bottomRight {
- padding: 9px 0 5px 0;
-}
-.rc-tooltip-placement-left,
-.rc-tooltip-placement-leftTop,
-.rc-tooltip-placement-leftBottom {
- padding: 0 9px 0 5px;
-}
-.rc-tooltip-inner {
- padding: 8px 10px;
- color: #fff;
- text-align: left;
- text-decoration: none;
- background-color: #ff0001;
- border-radius: 6px;
- box-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
- min-height: 34px;
-}
-.rc-tooltip-arrow {
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
-}
-.rc-tooltip-placement-top .rc-tooltip-arrow,
-.rc-tooltip-placement-topLeft .rc-tooltip-arrow,
-.rc-tooltip-placement-topRight .rc-tooltip-arrow {
- bottom: 4px;
- margin-left: -5px;
- border-width: 5px 5px 0;
- border-top-color: #ff0001;
-}
-.rc-tooltip-placement-top .rc-tooltip-arrow {
- left: 50%;
-}
-.rc-tooltip-placement-topLeft .rc-tooltip-arrow {
- left: 15%;
-}
-.rc-tooltip-placement-topRight .rc-tooltip-arrow {
- right: 15%;
-}
-.rc-tooltip-placement-right .rc-tooltip-arrow,
-.rc-tooltip-placement-rightTop .rc-tooltip-arrow,
-.rc-tooltip-placement-rightBottom .rc-tooltip-arrow {
- left: 4px;
- margin-top: -5px;
- border-width: 5px 5px 5px 0;
- border-right-color: #ff0001;
-}
-.rc-tooltip-placement-right .rc-tooltip-arrow {
- top: 50%;
-}
-.rc-tooltip-placement-rightTop .rc-tooltip-arrow {
- top: 15%;
- margin-top: 0;
-}
-.rc-tooltip-placement-rightBottom .rc-tooltip-arrow {
- bottom: 15%;
-}
-.rc-tooltip-placement-left .rc-tooltip-arrow,
-.rc-tooltip-placement-leftTop .rc-tooltip-arrow,
-.rc-tooltip-placement-leftBottom .rc-tooltip-arrow {
- right: 4px;
- margin-top: -5px;
- border-width: 5px 0 5px 5px;
- border-left-color: #ff0001;
-}
-.rc-tooltip-placement-left .rc-tooltip-arrow {
- top: 50%;
-}
-.rc-tooltip-placement-leftTop .rc-tooltip-arrow {
- top: 15%;
- margin-top: 0;
-}
-.rc-tooltip-placement-leftBottom .rc-tooltip-arrow {
- bottom: 15%;
-}
-.rc-tooltip-placement-bottom .rc-tooltip-arrow,
-.rc-tooltip-placement-bottomLeft .rc-tooltip-arrow,
-.rc-tooltip-placement-bottomRight .rc-tooltip-arrow {
- top: 4px;
- margin-left: -5px;
- border-width: 0 5px 5px;
- border-bottom-color: #ff0001;
-}
-.rc-tooltip-placement-bottom .rc-tooltip-arrow {
- left: 50%;
-}
-.rc-tooltip-placement-bottomLeft .rc-tooltip-arrow {
- left: 15%;
-}
-.rc-tooltip-placement-bottomRight .rc-tooltip-arrow {
- right: 15%;
-}
diff --git a/src/app/styles/components/d3graph.css b/src/app/styles/components/d3graph.css
index d98105b1d..319d14ddd 100644
--- a/src/app/styles/components/d3graph.css
+++ b/src/app/styles/components/d3graph.css
@@ -1,105 +1,122 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-body {
- background-color: black;
+/* Base node styling */
+.node {
+ cursor: pointer;
+ fill-opacity: 1;
+ transition: all 200ms ease;
}
-.node {
- cursor: pointer;
- fill-opacity: 0.8;
+/* Node rectangle styling */
+.node rect {
+ fill: var(--bg-primary);
+ stroke: var(--border-color);
+ stroke-width: 1px;
+ transition: all 200ms ease;
+}
+
+.node:hover rect {
+ stroke: var(--color-primary);
+ stroke-width: 2px;
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
}
-/* this represents leaf nodes aka nodes with no children */
+/* Node text styling */
.node text {
- fill: #fae6e4;
- background-color: red;
- font-size: 10px;
- font-family: 'Outfit', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ fill: var(--text-primary);
+}
+
+/* Parent node specific styling */
+.node--internal rect {
+ fill: var(--bg-primary);
+ stroke: var(--border-color);
+}
+
+.node--internal:hover rect {
+ stroke: var(--color-primary);
+ stroke-width: 2px;
}
-/* modifies text of parent nodes (has children) */
.node--internal text {
- fill: white;
- font-size: 10px;
+ font-size: 14px;
+ font-weight: 500;
+ fill: var(--text-primary);
+}
+
+/* Current/active node styling */
+.node.active rect {
+ stroke: var(--color-primary);
+ stroke-width: 2px;
}
+
+/* Link styling */
.link {
- fill: none;
- stroke: #161617;
- stroke-opacity: 0.4;
- stroke-width: 3px;
+ fill: none;
+ stroke: var(--border-color);
+ stroke-width: 2px;
+ transition: stroke 200ms ease;
+}
+
+.link:hover {
+ stroke: var(--border-color-dark);
+}
+
+/* Current path highlight */
+.link.current-link {
+ stroke: var(--color-primary);
+ stroke-opacity: 0.6;
}
+/* Tooltip styling */
div.tooltip {
- position: absolute;
- padding: 0.5rem 1rem;
- color: white;
- z-index: 100;
- font-size: 14px;
- font-family: 'Outfit', sans-serif;
- background: rgb(17, 17, 17, 0.9);
- box-shadow: rgb(33 33 33 / 20%) 0px 1px 2px;
- border-radius: 5px;
- max-width: 300px;
-}
-
-.d3-tip {
- line-height: 1;
- padding: 6px;
- background: #679dca;
- color: #2b2f39;
- border-radius: 4px;
- font-size: 13px;
- max-width: 400px;
- overflow-wrap: break-word;
- font-family: 'Overpass Mono', monospace;
-}
-
-/* Creates a small triangle extender for the tooltip */
-.d3-tip:after {
- box-sizing: border-box;
- display: inline;
- font-size: 15px;
- line-height: 1;
- color: #679dca;
- content: '\25BC';
- position: absolute;
- text-align: center;
-}
-
-/* Style northward tooltips specifically */
-.d3-tip.n:after {
- margin: -2px 0 0 0;
- top: 100%;
- left: 0;
-}
-
-.history-d3-container {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: calc(100% - 70px);
-}
-
-.perf-d3-container {
- height: calc(100% - 70px);
-}
-
-.perf-d3-svg {
- display: block;
-}
-
-.perf-chart-labels {
- font: 1.3em sans-serif;
- fill: #2a2f3a;
- pointer-events: none;
- text-anchor: middle;
-}
-
-/* .link {
- stroke: #ccc;
- stroke-width: 1.5;
-} */
-
-/* .current-link {
- stroke: red;
- stroke-width: 2.5; /* Adjust the width as needed */
-/* } */
+ position: absolute;
+ padding: 12px;
+ color: var(--text-primary);
+ z-index: 100;
+ font-size: 14px;
+ font-weight: 500;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ box-shadow:
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ border-radius: 8px;
+ max-width: 250px;
+}
+
+/* Container styling */
+.display {
+ background-color: var(--bg-secondary);
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+}
+
+/* State changes text container styling */
+.node foreignObject div {
+ max-height: 100%; /* Fixed height for scroll container */
+ overflow-y: scroll;
+ overflow-x: hidden;
+ scrollbar-width: thin;
+ padding-right: 6px;
+ scrollbar-color: var(--border-color-dark) var(--bg-secondary);
+}
+
+/* Custom scrollbar styling for Webkit browsers */
+.node foreignObject div::-webkit-scrollbar {
+ width: 6px;
+}
+
+.node foreignObject div::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+ border-radius: 3px;
+}
+
+.node foreignObject div::-webkit-scrollbar-thumb {
+ background: var(--border-color-dark);
+ border-radius: 3px;
+}
+
+.node foreignObject div::-webkit-scrollbar-thumb:hover {
+ background: var(--text-tertiary);
+}
diff --git a/src/app/styles/components/diff.css b/src/app/styles/components/diff.css
index caf3d4087..17a164469 100644
--- a/src/app/styles/components/diff.css
+++ b/src/app/styles/components/diff.css
@@ -1,150 +1,96 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
.jsondiffpatch-delta {
- font-family: 'Outfit', sans-serif;
- font-size: 16px;
+ font-size: 14px;
margin: 0;
- padding: 0 0 0 12px;
+ padding: 0;
display: inline-block;
+ color: var(--text-primary);
}
.jsondiffpatch-delta pre {
- font-family: 'Outfit', sans-serif;
- font-size: 16px;
+ font-size: 14px;
margin: 0;
padding: 0;
display: inline-block;
+ color: var(--text-primary);
}
ul.jsondiffpatch-delta {
list-style-type: none;
- padding: 0 0 0 20px;
+ padding: 0;
margin: 0;
+ color: var(--text-primary);
}
.jsondiffpatch-delta ul {
list-style-type: none;
- padding: 0 0 0 20px;
+ padding: 0;
margin: 0;
+ color: var(--text-primary);
+}
+
+.node foreignObject div .initial-state,
+.node foreignObject div .no-changes {
+ color: var(--text-secondary);
+ font-style: italic;
+ padding: 4px 0;
+ display: block;
}
+
.jsondiffpatch-added .jsondiffpatch-property-name,
.jsondiffpatch-added .jsondiffpatch-value pre,
.jsondiffpatch-modified .jsondiffpatch-right-value pre,
.jsondiffpatch-textdiff-added {
- background: #5a6c46;
+ color: #14b8a6;
}
.jsondiffpatch-deleted .jsondiffpatch-property-name,
.jsondiffpatch-deleted pre,
.jsondiffpatch-modified .jsondiffpatch-left-value pre,
.jsondiffpatch-textdiff-deleted {
- background: #fccdda;
text-decoration: line-through;
+ color: #dc2626;
}
-.jsondiffpatch-unchanged,
-.jsondiffpatch-movedestination {
- color: #81929e;
-}
-.jsondiffpatch-unchanged,
-.jsondiffpatch-movedestination > .jsondiffpatch-value {
- transition: all 0.5s;
- -webkit-transition: all 0.5s;
- overflow-y: hidden;
-}
-.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged,
-.jsondiffpatch-unchanged-showing .jsondiffpatch-movedestination > .jsondiffpatch-value {
- max-height: 100px;
-}
-.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged,
-.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value {
- max-height: 0;
-}
-.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value,
-.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value {
- display: block;
-}
-.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged,
-.jsondiffpatch-unchanged-visible .jsondiffpatch-movedestination > .jsondiffpatch-value {
- max-height: 100px;
-}
-.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged,
-.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value {
- max-height: 0;
-}
-.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow,
-.jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow {
- display: none;
-}
+
.jsondiffpatch-value {
display: inline-block;
}
.jsondiffpatch-property-name {
display: inline-block;
- padding-right: 5px;
- vertical-align: top;
+ padding-right: 4px;
}
+
.jsondiffpatch-property-name:after {
content: ': ';
}
-.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after {
- content: ': [';
-}
-.jsondiffpatch-child-node-type-array:after {
- content: '],';
-}
-div.jsondiffpatch-child-node-type-array:before {
- content: '[';
-}
-div.jsondiffpatch-child-node-type-array:after {
- content: ']';
-}
-.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after {
- content: ': {';
-}
-.jsondiffpatch-child-node-type-object:after {
- content: '},';
-}
-div.jsondiffpatch-child-node-type-object:before {
- content: '{';
-}
-div.jsondiffpatch-child-node-type-object:after {
- content: '}';
-}
-.jsondiffpatch-value pre:after {
- content: ',';
-}
+
li:last-child > .jsondiffpatch-value pre:after,
.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after {
content: '';
}
+
.jsondiffpatch-modified .jsondiffpatch-value {
display: inline-block;
}
+
.jsondiffpatch-modified .jsondiffpatch-right-value {
- margin-left: 5px;
-}
-.jsondiffpatch-moved .jsondiffpatch-value {
- display: none;
+ margin-left: 6px;
}
+
.jsondiffpatch-moved .jsondiffpatch-moved-destination {
display: inline-block;
- background: #ffffbb;
- color: #888;
+ color: #ffffbb;
}
+
.jsondiffpatch-moved .jsondiffpatch-moved-destination:before {
content: ' => ';
}
-ul.jsondiffpatch-textdiff {
- padding: 0;
-}
+
.jsondiffpatch-textdiff-location {
- color: #bbb;
+ color: #ffffbb;
display: inline-block;
min-width: 60px;
}
+
.jsondiffpatch-textdiff-line {
display: inline-block;
}
+
.jsondiffpatch-textdiff-line-number:after {
content: ',';
}
-.jsondiffpatch-error {
- background: red;
- color: white;
- font-weight: bold;
-}
diff --git a/src/app/styles/layout/_actionContainer.scss b/src/app/styles/layout/_actionContainer.scss
index 57a5655fe..08a46b361 100644
--- a/src/app/styles/layout/_actionContainer.scss
+++ b/src/app/styles/layout/_actionContainer.scss
@@ -1,47 +1,24 @@
.action-container {
- // overflow: auto;
+ background: var(--bg-primary);
+ border-right: 1px solid var(--border-color);
+ transition: width 0.3s ease;
overflow-x: hidden;
- background-color: $action-tab-background;
-}
-
-.actionname {
- background-color: $indiv-action-input-bg;
- border: 1px solid $indiv-action-border;
- color: $indiv-action-custom-text;
-}
-
-::placeholder {
- color: $indiv-action-filler-text;
-}
-
-#recordBtn {
- color: #ffb3b3;
+ overflow-y: auto;
height: 100%;
- display: flex;
- margin-left: 10px;
-}
-.actionToolContainer {
- color: #873b3b;
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 35px;
+ .action-button-wrapper {
+ opacity: 1;
+ visibility: visible;
+ transition:
+ opacity 0.2s ease,
+ visibility 0.2s ease;
+ }
}
-#recordBtn .fa-regular {
- height: 100%;
- width: 28px;
+.clear-button-container {
+ padding: 16px;
}
-.route {
- background-color: $route-bar;
- color: $route-bar-text;
- padding-left: 10px;
- padding-top: 5px;
- padding-bottom: 5px;
+.dropdown-container {
+ padding: 4px 16px;
}
-
-.toggle-record {
- color: $toggle-record-text;
-}
\ No newline at end of file
diff --git a/src/app/styles/layout/_bodyContainer.scss b/src/app/styles/layout/_bodyContainer.scss
index c0f95df21..5e030eb8f 100644
--- a/src/app/styles/layout/_bodyContainer.scss
+++ b/src/app/styles/layout/_bodyContainer.scss
@@ -2,23 +2,17 @@
height: 100%;
overflow: hidden;
display: grid;
- grid-template-columns: min-content 1fr;
- grid-template-rows: 1fr minmax(min-content, 5%) minmax(min-content, 5%);
+ grid-template-columns: 280px 1fr;
+ grid-template-rows: 1fr auto;
grid-template-areas:
'actions states'
- 'travel travel'
- 'buttons buttons';
+ 'bottom bottom';
+ transition: grid-template-columns 0.3s ease;
}
-/* if extension width is less than 500px, stack the body containers */
-@media (max-width: 500px) {
- .body-container {
- grid-template-rows: 3fr 5fr 1fr;
- grid-template-columns: auto;
- grid-template-areas:
- 'actions'
- 'states'
- 'travel'
- 'buttons';
- }
+.bottom-controls {
+ grid-area: bottom;
+ display: flex;
+ width: 100%;
+ border-top: 1px solid var(--border-color);
}
diff --git a/src/app/styles/layout/_buttonsContainer.scss b/src/app/styles/layout/_buttonsContainer.scss
index c0ed67b3a..c4f3cb9ab 100644
--- a/src/app/styles/layout/_buttonsContainer.scss
+++ b/src/app/styles/layout/_buttonsContainer.scss
@@ -1,117 +1,104 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-* {
- font-family: 'Outfit', sans-serif;
-}
.buttons-container {
- //color: #ff0000;
- margin: 0 1% 0 1%;
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- grid-gap: 1%;
- padding: 1% 0 1% 0;
+ display: flex;
+ align-items: center;
+ background: var(--bg-primary);
+ border-top: 1px solid var(--border-color);
+ width: 100%;
}
-.introjs-tooltip {
- color: black;
- background-color: white;
- min-width: 20rem;
+.buttons-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0 24px;
+ gap: 16px;
}
-.introjs-tooltiptext ul {
- padding-left: 2px;
+
+.introjs-button {
+ padding: 8px 16px;
+ font-size: 14px;
+ border-radius: 8px;
}
-// .introjs-helperLayer{
-// // border: 2px solid yellow
-// }
+.introjs-tooltip-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--text-primary);
+}
-.tools-container {
- display: flex;
- justify-content: space-between;
- border: 0.5px solid grey;
- background-color: #35383e;
- padding: 3px;
- margin-bottom: 1rem;
+.introjs-progressbar {
+ background-color: var(--color-primary);
}
-#seriesname {
- background-color: #333;
- color: white;
+.introjs-tooltip {
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ min-width: 20rem;
}
-@media (max-width: 500px) {
- .buttons-container {
- //color: #ff0000;
- grid-template-columns: repeat(2, 1fr);
- }
+.introjs-tooltiptext ul {
+ padding-left: 20px;
}
.introjs-nextbutton {
- background-color: none;
- color: #3256f1;
- border: 1px solid;
- outline: none;
+ background-color: var(--color-primary);
+ color: var(--button-primary-text);
}
.introjs-prevbutton {
- background-color: none;
- color: #3256f1;
- border: 1px solid;
- outline: none;
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
}
.introjs-skipbutton {
color: #d72828;
- border: 1px solid;
- margin-top: 2px;
- font-size: 16px;
- outline: none;
+ margin-right: 8px;
+ font-size: 14px;
}
-.introjs-button {
- background: none;
- outline: none;
-}
-
-.dialog-pop-up {
- border: 1px solid #ff0001;
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
- padding: 13px;
-}
-
-.dialog-pop-up-header {
- color: #ff0001;
- padding: 8px 16px;
- font-size: 20px;
- font-family: $text-font-stack;
+.buttons-container button {
+ display: flex;
+ align-items: center;
+ color: var(--text-secondary);
+ font-size: 1rem;
+ font-weight: 500;
+ background: transparent;
+ border: none;
+ border-radius: 0.375rem;
+ transition: all 200ms ease;
+ gap: 8px;
}
-.dialog-pop-up-contents {
- background-color: $pop-up-window-background;
- color: $pop-up-window-text;
- font-family: $text-font-stack;
- line-height: 1.4;
- padding: 0px 15px 15px 15px !important;
+.status-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
}
-.dialog-pop-up-actions {
- padding: 15px;
- background-color: #ff0001;
+.status-dot.active {
+ background-color: var(--color-primary);
+ animation: pulse 2s infinite;
}
-
-.close-icon-pop-up-div {
- height: 40px;
- width: 100%;
- background-color: #ff0001;
- display: flex;
- justify-content: flex-end;
- align-items: center;
+.status-dot.inactive {
+ background-color: var(--text-tertiary);
}
-.warning-header-container {
- background-color: $pop-up-window-background;
- display: flex;
- align-items: center;
- height: 20%;
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.7;
+ transform: scale(1.1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
}
-
diff --git a/src/app/styles/layout/_errorContainer.scss b/src/app/styles/layout/_errorContainer.scss
index 4f2057094..aaf7defa3 100644
--- a/src/app/styles/layout/_errorContainer.scss
+++ b/src/app/styles/layout/_errorContainer.scss
@@ -1,83 +1,116 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-* {
- font-family: 'Outfit', sans-serif;
-}
.error-container {
- height: 100%;
- margin: 0 auto;
- background-color: $loader-background;
- color: $loader-text;
- overflow: hidden;
- font: 'Outfit', sans-serif;
+ height: 100%;
+ background-color: var(--bg-secondary);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.error-logo {
+ height: 50px;
+ margin-bottom: 2rem;
+}
+
+.error-content {
+ max-width: 600px;
+ width: 100%;
+}
+
+.error-alert {
+ background-color: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.error-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
- display: flex;
- flex-direction: column;
- align-items: center;
- a {
- color: rgb(91, 54, 54);
- margin-top: 1%;
- height: 3%;
- }
- img {
- text-align: center;
- padding-top: 5%;
- padding-bottom: 1.5%;
- }
- p {
- padding: 0;
- text-align: center;
- margin: 2% 0;
- }
- .loaderChecks {
- padding: 1%;
- border: 1px solid;
- border-style: solid;
- border-color: loader-checks-border;
- display: grid;
- grid-template-columns: 4fr 1fr;
- grid-template-rows: repeat(3, 1fr);
- grid-column-gap: .5%;
- grid-row-gap: .5%;
- justify-content: center;
- align-items: center;
- }
- .loaderChecks span {
- margin: 0 auto;
- display: inline;
- }
- h2 {
- padding-left: 2%;
- padding-right: 2%;
- }
- .check, .fail{
- font-size: 3em;
- color: green;
- margin: 0;
- align-self: center;
- justify-self: center;
- }
- .fail{
- color: red;
- }
- .errorMsg{
- text-align: center;
- font-weight: bold;
- }
+.error-description {
+ color: var(--text-secondary);
+ line-height: 1.5;
+ margin-bottom: 1rem;
+}
- .launchContentButton {
- background: $launch-button;
- color: $launch-button-text;
- margin: 3px;
- padding: 5px 10px;
- border-radius: 5px;
- border: 1px solid rgb(36, 37, 41);
- }
+.loaderChecks {
+ margin: 1.5rem 0;
+}
+
+.loaderChecks p {
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ line-height: 1.5;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
- .launchContentButton:hover {
- background: $launch-button-hover;
- }
+.error-note {
+ text-align: center;
+ color: var(--text-tertiary);
+ font-size: 0.875rem;
+ margin-bottom: 1rem;
+}
+
+.launch-button {
+ background-color: var(--button-primary-bg);
+ color: var(--button-primary-text);
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ font-size: 1.125rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin: 0 auto;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.launch-button:hover {
+ background-color: var(--text-primary);
+}
+
+.launch-button:active {
+ background-color: var(--button-primary-bg);
+}
+
+.github-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-top: 2rem;
+ color: var(--color-primary);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.github-link:hover {
+ color: var(--color-primary-dark);
+ text-decoration: underline;
+}
+
+.devtools-link {
+ color: var(--color-primary);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
- .launchContentButton:active {
- box-shadow: 1px 1px 10px black;
- }
+.devtools-link:hover {
+ color: var(--color-primary-dark);
+ text-decoration: underline;
}
diff --git a/src/app/styles/layout/_headContainer.scss b/src/app/styles/layout/_headContainer.scss
deleted file mode 100644
index a8317d0d7..000000000
--- a/src/app/styles/layout/_headContainer.scss
+++ /dev/null
@@ -1,118 +0,0 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
-* {
- font-family: 'Outfit', sans-serif;
-}
-
-.head-container {
- height: 5%;
- background: linear-gradient(
- 90deg,
- rgba(41, 41, 41, 1) 0%,
- rgba(51, 51, 51, 1) 50%,
- rgba(41, 41, 41, 1) 100%
- );
-}
-
-.head-container {
- display: flex;
- flex-direction: row-reverse;
- align-items: center;
- justify-content: center;
-}
-
-div .tab-select-container {
- //background-color: #ff0002;
- font-size: 14px;
- height: 40px;
- width: 100%;
- margin-bottom: 0;
-}
-
-.tab-select-container:focus {
- background-color: #ff0006;
-
- outline: none;
-}
-
-.tab-select-container:active {
- outline: none;
- border-color: transparent;
-}
-
-.tab-select-container {
- //background-color: #ff0004;
- height: 70%;
-
- .tab-select__control:focus {
- outline: none;
- }
- div.tab-select-container.css-2b097c-container {
- background-color: #ff0003;
- margin: 0;
- }
-
- .tab-select__control,
- .tab-select__menu {
- outline: none;
- font-size: 14px;
- border-style: none;
- background-color: $tab-dropdown-background;
- z-index: 2;
- margin-bottom: 0;
- @extend %disable-highlight;
- }
- .tab-select__single-value {
- color: $tab-select-text;
- }
- .tab-select__value-container {
- background-color: $tab-select-background;
- margin: 0;
- padding: 0px;
- }
- .tab-select__value-container:focus {
- outline: none;
- }
- .tab-select__option:hover {
- margin-top: 0;
- background-color: $tab-dropdown-hover;
- color: black;
- }
- .tab-select__option--is-selected,
- .tab-select__option--is-focused {
- background-color: transparent;
- outline: transparent;
- }
- .tab-select__indicator {
- color:$tab-arrow-indicator;
- padding: 0;
- }
- .tab-select__indicator-separator {
- margin-top: 3px;
- margin-bottom: 3px;
- background-color: $tab-vertical-line;
- }
-
- .css-1uccc91-singleValue {
- margin-left: 8px;
- }
- // removes the cursor from blinking
- .css-w8afj7-Input {
- color: transparent;
- }
-
- // removes min-height of dropdown and change it to 100%
- .css-yk16xz-control,
- .css-1pahdxg-control {
- min-height: initial;
- height: 100%;
- border: none;
- outline: none;
- margin-bottom: 0;
- border-radius: 0;
- }
- .css-yk16xz-control:focus,
- .css-1pahdxg-control:focus {
- outline: none;
- border-radius: 0;
- }
-}
diff --git a/src/app/styles/layout/_mainContainer.scss b/src/app/styles/layout/_mainContainer.scss
index 8e85a3ba5..b8da1cd86 100644
--- a/src/app/styles/layout/_mainContainer.scss
+++ b/src/app/styles/layout/_mainContainer.scss
@@ -1,10 +1,25 @@
.main-container {
height: 100%;
margin: 0 auto;
- background-color: $function-bar-background;
+ background-color: var(--bg-secondary);
overflow: hidden;
}
.state-container-container {
display: contents;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+}
+
+.history-view {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.history-end-anchor {
+ height: 1px;
+ width: 100%;
}
diff --git a/src/app/styles/layout/_stateContainer.scss b/src/app/styles/layout/_stateContainer.scss
index 16e5e3795..674f1ec13 100644
--- a/src/app/styles/layout/_stateContainer.scss
+++ b/src/app/styles/layout/_stateContainer.scss
@@ -1,292 +1,169 @@
.state-container {
- font-size: 10px;
overflow: auto;
- background-color: $state-background;
-}
-
-.toggleAC {
- background: #ff0001;
- height: 50%;
- text-decoration: none;
- border: none;
-}
-
-.toggleAC:focus {
- outline: none;
- box-shadow: none;
-}
-
-.state-container .main-navbar {
- background-color: $state-background;
- color: #ff0000;
+ height: 100%;
display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- height: 35px;
- margin: 6px;
+ flex-direction: column;
}
-.state-container .componentMapContainer {
- height: 95% !important;
- fill: $state-background;
+.app-body {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
}
-.state-container .main-navbar-container {
- position: sticky;
- top: 0px;
- left: 0px;
- z-index: 1;
- background-color: $state-background;
-
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- height: 35px;
+.app-content {
+ height: 100%;
+ min-height: 0;
}
-.no-data-message {
- color: #ff0001;
- font: normal 13px $text-font-stack;
- padding: 10px;
+.main-navbar-container--structural {
+ height: 0;
+ padding: 0;
+ border: none;
+ overflow: hidden;
+ visibility: hidden;
+ pointer-events: none;
}
.state-container {
- .main-navbar-text {
- margin: 6px;
- }
-
- .main-router-link {
- font-size: 14px;
- height: 75%;
- width: 75px;
- display: flex;
- justify-content: center;
- align-items: center;
- text-decoration: none;
- color: $header-button-inactive-text;
-
- background: $header-button-inactive;
- border-radius: 5px;
- border: 1px solid rgba(184, 196, 194, 0.25);
- }
-
- .main-router-link:hover {
- background: $header-button-hover;
- }
-
- .main-router-link.is-active {
- background: $header-button-active;
- color: $header-button-active-text;
- margin: 3px;
- border-radius: 5px;
- border: 1px solid rgb(36, 37, 41);
- }
-
.router-link {
- height: 100%;
- // width: 34%;
- width: 10%;
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: $navbar-unselected;
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--text-secondary);
text-decoration: none;
- color: $navbar-unselected-text;
-
- font-size: 75%;
- // border-top-right-radius: 10px;
- // border-top-left-radius: 10px;
- overflow: hidden;
- }
-
- .map-tab1 {
- border-top-left-radius: 5px;
- border-bottom-left-radius: 5px;
- }
-
- .accessibility-tab {
- border-top-right-radius: 5px;
- border-bottom-right-radius: 5px;
- }
-
- .router-link:hover {
- background-color: $navbar-hover;
- }
-
- .router-link.is-active {
- background-color: $navbar-selected;
- color: $navbar-selected-text;
- }
-
- .navbar {
- // background-color: $navbar-background;
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- height: 30px;
- position: fixed;
- top: 4px; //should revisit to figure out a more dynamic way of sticking the navbar to the bottom of the main-navbar // <- tried placing the main nav on the same line to save space but ran into problems with the svg div of the tree + animation. Will need closer inspection
- left: 50%;
- transform: translateX(-50%);
- width: 65vw;
- z-index: 1;
-
-
- @extend %disable-highlight;
+ background-color: transparent;
+ cursor: pointer;
+ transition:
+ color 200ms ease,
+ border-color 200ms ease;
+ margin: 0;
+ position: relative;
+ white-space: nowrap;
+
+ &:hover {
+ color: var(--text-primary);
+ background-color: transparent;
+ }
+
+ &.is-active {
+ border-bottom: 2px solid var(--color-primary);
+ color: var(--color-primary);
+ background-color: transparent;
+ }
}
.main-navbar {
- background-color: $header-background;
display: flex;
flex-direction: row;
- justify-content: flex-start;
+ justify-content: space-between;
align-items: center;
- height: 35px;
- margin: 6px;
+ gap: 8px;
+ width: 100%;
}
.main-navbar-container {
- top: 0px;
- left: 0px;
- z-index: 1;
- background-color: $header-background;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- height: 40px;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-primary);
+ padding: 4.5px 24px;
}
}
-.no-data-message {
- color: #ff0001;
- font: normal 13px $text-font-stack;
- padding: 10px;
+// tool tip styles
+.tooltip-header {
+ padding: 8px 8px 8px 12px;
+ background-color: var(--color-primary);
+ border: 1px solid var(--color-primary-dark);
+ border-radius: 8px;
}
-.performance-nav-bar-container {
- background-color: #ff0002;
- display: flex;
- border: $performance-subtab-border;
- height: 30px;
+.tooltip-title {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--bg-primary);
}
-.router-link-performance {
- height: 100%;
- width: 34%;
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: $state-background;
- text-decoration: none;
- color: $performance-subtab-text;
+.tooltip-container {
+ width: 250px;
+ background-color: var(--bg-primary);
+ border-radius: 8px;
+ overflow: hidden;
}
-.router-link-performance:hover {
- background-color: $performance-subtab-hover;
+.tooltip-section {
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border-color);
+ transition: background-color 150ms ease;
}
-.router-link-performance.is-active {
- font-weight: 600;
+.tooltip-section:last-child {
+ border-bottom: none;
}
-// Web Metrics Container
-.web-metrics-container {
- display: grid;
- grid-template-columns: auto auto;
- justify-content: center;
-}
-
-//container for metrics
-.metric {
- min-height: 200px;
- min-width: 200px;
+.tooltip-section-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-primary);
}
-.bargraph {
- position: relative;
- margin-top: 1rem;
+.tooltip-content {
+ max-height: 400px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: #cbd5e1 transparent;
}
-.bar-graph-axis {
- stroke: cornflowerblue;
+.tooltip-item {
+ padding: 8px 0;
+ border-bottom: 1px solid rgba(107, 114, 128, 0.1);
}
-#hover-box {
- max-width: 230px;
- background-color: #51565e;
- border-radius: 5px;
- color: white;
+.tooltip-item:last-child {
+ border-bottom: none;
}
-.bargraph-position {
- position: relative;
+.tooltip-data {
+ border-left: 2px solid var(--color-primary-dark);
}
-// tool tip styles
-
-.visx-tooltip {
- overflow-y: auto;
- overflow-wrap: break-word;
- pointer-events: all !important;
+.tooltip-container {
+ animation: tooltipFade 150ms ease-out;
}
-.tooltipKey {
- margin-bottom: -1px;
+// Web Metrics Container
+.web-metrics-container {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
}
-.historyToolTip {
- z-index: 2;
+.metric {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
-// .state-container .router-link {
-// border: 0.5px solid rgba(0, 0, 0, 0.5);
-// border-bottom: none;
-// }
-
-.tooltipData {
- background-color: #373737;
- margin-top: 4px;
- margin-bottom: 4px;
- padding: 2px;
- max-height: 30vh;
+.hover-box {
+ max-width: 250px;
+ background-color: #51565e;
+ border-radius: 8px;
+ color: white;
+ padding: 2px 8px;
+ line-height: 16px;
}
-.tooltipData-JSONTree {
+/* Tree styling */
+.json-tree {
+ overflow: auto;
list-style: none;
- margin-top: 1px;
- margin-bottom: 1px;
- margin-left: 2px;
- margin-right: 2px;
- padding: 0px;
- overflow-y: auto;
- max-height: 30vh;
- scrollbar-color: #919499 #51565e;
-}
-
-.tooltipData-JSONTree::-webkit-scrollbar-thumb {
- background: #919499;
+ padding: 16px;
+ margin: 0;
}
-.tooltipData-JSONTree::-webkit-scrollbar-track {
- background: #51565e;
-}
-
-.app-body {
- height: 100%;
-}
-
-.app-content {
+.tree-component {
height: 100%;
+ overflow: auto;
}
-
-.heat-map-legend-container {
- display: flex;
- height: 3px;
- width: 200px;
- background-image: linear-gradient(to right, $heat-level-1, $heat-level-2, $heat-level-3, $heat-level-4);
-}
-
diff --git a/src/app/styles/layout/_travelContainer.scss b/src/app/styles/layout/_travelContainer.scss
index 9938c8027..58f53a23b 100644
--- a/src/app/styles/layout/_travelContainer.scss
+++ b/src/app/styles/layout/_travelContainer.scss
@@ -1,63 +1,129 @@
.travel-container {
- background: $travel-background;
- // background: linear-gradient(
- // 90deg,
- // rgba(41, 41, 41, 1) 0%,
- // rgba(51, 51, 51, 1) 50%,
- // rgba(41, 41, 41, 1) 100%
- // );
- // border-color: $border-color;
- // display: flex;
- // flex-direction: row;
- // align-items: center;
- // justify-content: space-around;
-
+ background: var(--bg-primary);
display: flex;
flex-direction: row;
align-items: center;
- justify-content: space-around;
-}
-
-.visx-group {
- margin-top: 10px;
+ border-top: 1px solid var(--border-color);
+ padding-left: 16px;
+ gap: 8px;
}
.react-select-container {
font-size: 16px;
min-width: 90px;
margin: 8px;
- .react-select__control {
- background-color: $speed-dropdown;
- border-color: transparent;
- @extend %disable-highlight;
- }
- .react-select__control:hover {
- cursor: pointer;
- }
- .react-select__menu {
- background-color: $speed-dropdown-expanded;
- color: $speed-dropdown-text;
- @extend %disable-highlight;
- }
- .react-select__single-value {
- color: $speed-dropdown-text;
- }
- .react-select__option:hover {
- background-color: $speed-dropdown-expanded-hover;
- cursor: pointer;
- }
- .react-select__option--is-selected,
- .react-select__option--is-focused {
- background-color: transparent;
- cursor: pointer;
- }
-
- // removes the cursor from blinking
- .css-w8afj7-Input {
- color: transparent;
- }
-}
-
-.button-icon {
- color: $icon-primary;
+}
+
+.react-select__control {
+ background-color: var(--bg-secondary) !important;
+ border: 1px solid var(--border-color) !important;
+ border-radius: 6px !important;
+ min-height: 36px !important;
+ box-shadow: none !important;
+ cursor: pointer !important;
+ transition: all 200ms ease !important;
+}
+
+.react-select__control:hover {
+ border-color: var(--border-color-dark) !important;
+ background-color: var(--bg-tertiary) !important;
+}
+
+.react-select__control--is-focused {
+ border-color: var(--color-primary) !important;
+ box-shadow: 0 0 0 2px rgba(20, 184, 166, 0.1) !important;
+}
+
+.react-select__menu {
+ background-color: var(--bg-primary) !important;
+ border: 1px solid var(--border-color) !important;
+ border-radius: 6px !important;
+ box-shadow:
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
+ margin-top: 4px !important;
+ z-index: 100 !important;
+}
+
+.react-select__option {
+ background-color: var(--bg-primary) !important;
+ color: var(--text-primary) !important;
+ cursor: pointer !important;
+ padding: 8px 12px !important;
+ font-size: 14px !important;
+ transition: all 200ms ease !important;
+}
+
+.react-select__option:hover {
+ background-color: var(--bg-tertiary) !important;
+}
+
+.react-select__option--is-selected {
+ background-color: var(--color-primary) !important;
+ color: white !important;
+}
+
+.react-select__option--is-focused {
+ background-color: var(--bg-tertiary) !important;
+}
+
+.react-select__single-value {
+ color: var(--text-primary) !important;
+ font-size: 14px !important;
+}
+
+.react-select__indicator-separator {
+ background-color: var(--border-color) !important;
+}
+
+.react-select__dropdown-indicator {
+ color: var(--text-secondary) !important;
+ transition: transform 200ms ease !important;
+}
+
+.react-select__dropdown-indicator:hover {
+ color: var(--text-primary) !important;
+}
+
+.react-select__control--menu-is-open .react-select__dropdown-indicator {
+ transform: rotate(180deg) !important;
+}
+
+.react-select__placeholder {
+ color: var(--text-secondary) !important;
+ font-size: 14px !important;
+}
+
+.react-select__multi-value {
+ background-color: var(--bg-tertiary) !important;
+ border-radius: 4px !important;
+}
+
+.react-select__multi-value__label {
+ color: var(--text-primary) !important;
+ font-size: 14px !important;
+ padding: 2px 6px !important;
+}
+
+.react-select__multi-value__remove {
+ color: var(--text-secondary) !important;
+ cursor: pointer !important;
+ padding: 0 4px !important;
+ transition: all 200ms ease !important;
+}
+
+.react-select__multi-value__remove:hover {
+ background-color: var(--border-color) !important;
+ color: var(--text-primary) !important;
+}
+
+.react-select__clear-indicator {
+ color: var(--text-secondary) !important;
+ cursor: pointer !important;
+ padding: 0 8px !important;
+ transition: all 200ms ease !important;
+}
+
+.react-select__clear-indicator:hover {
+ color: var(--text-primary) !important;
}
diff --git a/src/app/styles/main.scss b/src/app/styles/main.scss
index f6ea13579..a63383eaf 100644
--- a/src/app/styles/main.scss
+++ b/src/app/styles/main.scss
@@ -1,4 +1,23 @@
-@charset 'UTF-8';
+@use 'base/helpers';
+
+@use 'layout/mainContainer';
+@use 'layout/bodyContainer';
+@use 'layout/actionContainer';
+@use 'layout/errorContainer';
+@use 'layout/stateContainer';
+@use 'layout/travelContainer';
+@use 'layout/buttonsContainer';
+
+@use 'components/buttons';
+@use 'components/actionComponent';
+@use 'components/performanceVisx';
+@use 'components/componentMap';
+@use 'components/ax';
+
+@use './components/rc-slider';
+@use './components/d3graph';
+@use './components/diff';
+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
* {
@@ -6,50 +25,66 @@
font-size: 14px;
}
-/* width */
-::-webkit-scrollbar {
- width: 5px;
- height: 8px;
+html {
+ margin: 0;
+ padding: 0;
+ height: 100%;
}
-/* Track */
-::-webkit-scrollbar-track {
- // background: rgb(20, 20, 20);
- background: none;
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
}
-/* Handle */
-::-webkit-scrollbar-thumb {
- background: rgb(67, 67, 71);
-}
+:root {
+ // Base colors
+ --color-primary: #14b8a6;
+ --color-primary-dark: #0d9488;
+ --color-primary-light: #2dd4bf;
-/* Handle on hover */
-::-webkit-scrollbar-thumb:hover {
- background: rgb(97, 97, 97);
-}
+ // Background colors
+ --bg-primary: #ffffff;
+ --bg-secondary: #f9fafb;
+ --bg-tertiary: #f3f4f6;
+
+ // Border colors
+ --border-color: #e5e7eb;
+ --border-color-dark: #d1d5db;
-// 1. Configuration and helpers
-@import 'abstracts/variablesLM';
-//@import 'abstracts/variables';
+ // Text colors
+ --text-primary: #374151;
+ --text-secondary: #6b7280;
+ --text-tertiary: #9ca3af;
-// 3. Base stuff
-@import 'base/base', 'base/helpers', 'base/typography';
+ // Interactive colors
+ --hover-bg: #f9fafb;
+ --selected-bg: #f3f4f6;
+ --button-primary-bg: #111827;
+ --button-primary-text: #ffffff;
-// 4. Layout-related sections
-@import 'layout/mainContainer', 'layout/bodyContainer', 'layout/actionContainer',
-'layout/errorContainer', 'layout/stateContainer', 'layout/travelContainer',
-'layout/buttonsContainer', 'layout/headContainer.scss';
+ // Transitions
+ --theme-transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
+}
-// 5. Components
-@import 'components/buttons', 'components/actionComponent',
-'components/jsonTree', 'components/renderingFrequency',
-'components/performanceVisx', 'components/componentMap', 'components/ax';
+:root.dark {
+ // Background colors
+ --bg-primary: #1f2937;
+ --bg-secondary: #2d3748;
+ --bg-tertiary: #374151;
-// slider component
-@import './components/rc-slider', './components/sliderHandle';
+ // Border colors
+ --border-color: #374151;
+ --border-color-dark: #4b5563;
-// d3 chart component
-@import './components/d3graph.css';
+ // Text colors
+ --text-primary: #f3f4f6;
+ --text-secondary: #9ca3af;
+ --text-tertiary: #6b7280;
-// diff component
-@import './components/diff';
\ No newline at end of file
+ // Interactive colors
+ --hover-bg: #2d3748;
+ --selected-bg: #374151;
+ --button-primary-bg: #0f172a;
+ --button-primary-text: #ffffff;
+}
diff --git a/src/app/styles/theme.ts b/src/app/styles/theme.ts
deleted file mode 100644
index d3e93e2bc..000000000
--- a/src/app/styles/theme.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { createTheme } from '@mui/material/styles';
-const theme = createTheme({
- // typography: {
- // fontSize: 2,
- // },
- palette: {
- primary: {
- main: '#3c6e71',
- },
- secondary: {
- main: '#3c6e71',
- },
- },
- components: {
- // Name of the component
-
- MuiButton: {
- styleOverrides: {
- // Name of the slot
- root: {
- // Some CSS
- fontSize: '0.8rem',
- letterSpacing: '0.1rem',
- lineHeight: '0.8rem',
- },
- },
- },
- },
-});
-
-export default theme;
diff --git a/src/backend/__tests__/masterTree.test.tsx b/src/backend/__tests__/masterTree.test.tsx
index 298100c7f..5b3d404f1 100644
--- a/src/backend/__tests__/masterTree.test.tsx
+++ b/src/backend/__tests__/masterTree.test.tsx
@@ -76,6 +76,7 @@ describe('master tree tests', () => {
// clear the saved component actions record
componentActionsRecord.clear();
});
+
describe('createTree Unit test', () => {
describe('Filter components that are from NextJS, Remix or not from allowed component types', () => {
it('should return a Tree if we pass in a empty fiber node', () => {
@@ -111,7 +112,7 @@ describe('master tree tests', () => {
}
}
});
- xit('should filter out NextJS & Remix default components with children and/or siblings', () => {
+ it('should filter out NextJS & Remix default components with children and/or siblings', () => {
(mockChildTree.componentData as ComponentData).index = 0;
(mockSiblingTree.componentData as ComponentData).hooksIndex = [1];
treeRoot.children.push(mockChildTree);
@@ -161,14 +162,26 @@ describe('master tree tests', () => {
const tree = createTree(mockChildNode);
expect(tree).toEqual(treeRoot);
});
- xit('should display class props information', () => {
- mockSiblingNode.memoizedProps = memoizedProps;
- (mockSiblingTree.componentData as ComponentData).props = props;
- treeRoot.children.push(mockSiblingTree);
+ it('should display class props information', () => {
+ // Assign mock properties to the child node
+ mockChildNode.memoizedProps = memoizedProps;
+ (mockChildTree.componentData as ComponentData).props = props;
- const tree = createTree(mockSiblingNode);
- expect(tree).toEqual(treeRoot);
+ // Set up an isolated copy of the root tree
+ const isolatedTreeRoot = deepCopy(treeRoot);
+ isolatedTreeRoot.children.push(mockChildTree);
+
+ // Generate the tree
+ const tree = createTree(mockChildNode);
+
+ // Debugging: Log actual and expected tree for comparison
+ console.log('Generated Tree:', JSON.stringify(tree, null, 2));
+ console.log('Expected Tree:', JSON.stringify(isolatedTreeRoot, null, 2));
+
+ // Perform the assertion
+ expect(tree).toEqual(isolatedTreeRoot);
});
+
it('should display React router props information', () => {
(mockSiblingTree.componentData as ComponentData) = {
...(mockSiblingTree.componentData as ComponentData),
@@ -225,8 +238,8 @@ describe('master tree tests', () => {
expect(tree).toEqual(treeRoot);
});
- xit('should display props information of multiple components', () => {
- // Construct Fiber Node (root => FiberNode => child1 => child2 & sibling1)
+ it('should display props information of multiple components', () => {
+ // Set up Fiber Node tree (root => child1 => child2 & sibling1)
mockChildNode.memoizedProps = memoizedProps;
const child1 = deepCopy(mockChildNode);
child1.memoizedProps.name = 'child1';
@@ -235,32 +248,39 @@ describe('master tree tests', () => {
mockSiblingNode.memoizedProps = memoizedProps;
const sibling1 = deepCopy(mockSiblingNode);
sibling1.memoizedProps.name = 'sibling1';
+
+ // Link nodes
mockFiberNode.child = child1;
child1.child = child2;
child2.sibling = sibling1;
- const tree = createTree(mockFiberNode);
- // Construct result tree (root => FiberTree => childTree1 => childTree2 & siblingTree1)
+ // Set up expected tree structure
+ const isolatedMockFiberTree = deepCopy(mockFiberTree);
(mockChildTree.componentData as ComponentData).props = props;
+
const childTree1 = deepCopy(mockChildTree);
childTree1.name = 'IncrementClass1';
(childTree1.componentData as ComponentData).props.name = 'child1';
(childTree1.componentData as ComponentData).index = 0;
+
const childTree2 = deepCopy(mockChildTree);
childTree2.name = 'IncrementClass2';
(childTree2.componentData as ComponentData).props.name = 'child2';
(childTree2.componentData as ComponentData).index = 1;
- (mockSiblingTree.componentData as ComponentData).props = props;
+
const siblingTree1 = deepCopy(mockSiblingTree);
siblingTree1.name = 'IncrementFunc';
- (siblingTree1.componentData as ComponentData).hooksIndex = [2];
(siblingTree1.componentData as ComponentData).props.name = 'sibling1';
+ (siblingTree1.componentData as ComponentData).hooksIndex = [2];
- mockFiberTree.children[0].children = [childTree1];
+ isolatedMockFiberTree.children[0].children = [childTree1];
childTree1.children.push(childTree2, siblingTree1);
- // Compare the two trees:
- expect(tree).toEqual(mockFiberTree);
+ // Generate the actual tree
+ const tree = createTree(mockFiberNode);
+
+ // Assertions
+ expect(tree).toEqual(isolatedMockFiberTree);
});
});
describe('Display component states information', () => {
@@ -311,18 +331,25 @@ describe('master tree tests', () => {
expect(tree).toEqual(treeRoot);
});
- it('should display class state information', () => {
- // Construct Fiber Node (root => childNode)
- mockChildNode.stateNode = stateNode;
- const tree = createTree(mockChildNode);
+ it('should display functional state information', () => {
+ // Set up mock Fiber node for functional state
+ mockSiblingNode.memoizedState = {
+ memoizedState: { dummy: 'dummy' },
+ queue: {}, // Required for useState hooks
+ next: null,
+ };
- // Construct Result Tree (root => childTree)
- mockChildTree.state = classState;
- (mockChildTree.componentData as ComponentData).state = classState;
- treeRoot.children.push(mockChildTree);
+ // Generate the tree
+ const tree = createTree(mockSiblingNode);
- // Compare the two trees:
- expect(tree).toEqual(treeRoot);
+ // Set up expected tree structure
+ mockSiblingTree.state = functionalState;
+ (mockSiblingTree.componentData as ComponentData).hooksState = functionalState;
+ (mockSiblingTree.componentData as ComponentData).hooksIndex = [0];
+ treeRoot.children.push(mockSiblingTree);
+
+ // Assertions
+ expect(tree).toBe(treeRoot);
});
it('should keep track of class state index', () => {
@@ -370,22 +397,29 @@ describe('master tree tests', () => {
expect(tree).toEqual(treeRoot);
});
- xit('should display functional state information', () => {
- // Construct Fiber Node (root => siblingNode)
- mockSiblingNode.memoizedState = memoizedState;
- const tree = createTree(mockSiblingNode);
+ it('should display functional state information', () => {
+ // Set up mock Fiber node for functional state
+ mockSiblingNode.memoizedState = {
+ memoizedState: { dummy: 'dummy' },
+ queue: {}, // Required for useState hooks
+ next: null,
+ };
- // Construct Result Tree (root => siblingTree)
+ // Create tree
+ const tree = createTree(mockSiblingNode);
+ // Set up expected tree structure
mockSiblingTree.state = functionalState;
(mockSiblingTree.componentData as ComponentData).hooksState = functionalState;
+ (mockSiblingTree.componentData as ComponentData).hooksIndex = [0]; // Single hook index
+ mockSiblingTree.name = 'IncrementFunc'; // Ensure name matches
treeRoot.children.push(mockSiblingTree);
- // Compare the two trees:
+ // Compare the actual tree with the expected tree
expect(tree).toEqual(treeRoot);
});
- xit('should keep track of functional state index', () => {
+ it('should keep track of functional state index', () => {
// Construct Fiber Node (root => FiberNode => sibling1 => sibling 2 & 3)
// sibling 3 will have 2 states
mockSiblingNode.memoizedState = memoizedState;
@@ -421,10 +455,6 @@ describe('master tree tests', () => {
expect(tree).toEqual(mockFiberTree);
});
});
-
- describe('Replace fromLinkFiber class value', () => {
- xit('NEED UNDERSTANDING THE PURPOSE OF FROMLINKFIBER FOR FRONTEND, currently unable to replicate DOMTokenList instance', () => {});
- });
});
describe('Tree unit test', () => {
@@ -529,77 +559,50 @@ describe('master tree tests', () => {
expect(nextChild1.name).toBe('child2');
expect(nextChild2.name).toBe('child3');
});
-
- xit('should be able to add multiple children and sibilings', () => {});
- });
- });
-
- describe('createComponentActionsRecord unit test', () => {
- it('should save a new component action record if the Fiber node is a stateful class component', () => {
- mockFiberNode.tag = ClassComponent;
- mockFiberNode.stateNode = {
- state: { counter: 0 }, // a mock state object
- setState: jest.fn(), // a mock setState method
- };
- createComponentActionsRecord(mockFiberNode);
- expect(componentActionsRecord.getComponentByIndex(0)).toBe(mockFiberNode.stateNode);
});
- it('should save a new component action record if the Fiber node is a stateful class component with props', () => {
- mockFiberNode.tag = ClassComponent;
- // a mock state object
- mockFiberNode.stateNode = {
- state: { counter: 0 },
- props: { start: 0 },
- setState: jest.fn(), // a mock setState method
- };
- createComponentActionsRecord(mockFiberNode);
- expect(componentActionsRecord.getComponentByIndex(0)).toMatchObject({
- props: mockFiberNode.stateNode.props,
- state: mockFiberNode.stateNode.state,
+ describe('createComponentActionsRecord unit test', () => {
+ it('should save a new component action record if the Fiber node is a stateful class component', () => {
+ mockFiberNode.tag = ClassComponent;
+ mockFiberNode.stateNode = {
+ state: { counter: 0 }, // a mock state object
+ setState: jest.fn(), // a mock setState method
+ };
+ createComponentActionsRecord(mockFiberNode);
+ expect(componentActionsRecord.getComponentByIndex(0)).toBe(mockFiberNode.stateNode);
});
- });
- it('should save a new component action record if the Fiber node is a functional component with state', () => {
- mockFiberNode.tag = FunctionComponent;
- mockFiberNode.memoizedState = {
- queue: [{}, { state: { value: 'test' } }], // a mock memoizedState object
- };
- createComponentActionsRecord(mockFiberNode);
- expect(componentActionsRecord.getComponentByIndex(0)).toBe(mockFiberNode.memoizedState.queue);
- });
+ it('should save a new component action record if the Fiber node is a stateful class component with props', () => {
+ mockFiberNode.tag = ClassComponent;
+ // a mock state object
+ mockFiberNode.stateNode = {
+ state: { counter: 0 },
+ props: { start: 0 },
+ setState: jest.fn(), // a mock setState method
+ };
+ createComponentActionsRecord(mockFiberNode);
+ expect(componentActionsRecord.getComponentByIndex(0)).toMatchObject({
+ props: mockFiberNode.stateNode.props,
+ state: mockFiberNode.stateNode.state,
+ });
+ });
- // WE DONT HAVE PROPS IN STATENODE AND WE DON"T STORE PROPS IN COMPONENT ACTIONS RECORD
- // it('should save multiple component action records when called multiple times with different Fiber nodes', () => {
- // mockFiberNode.tag = 1; // ClassComponent
- // mockFiberNode.stateNode = {
- // state: { counter: 0 },
- // props: { start: 0 }, // a mock state object
- // setState: jest.fn(), // a mock setState method
- // };
- // createComponentActionsRecord(mockFiberNode);
- // expect(componentActionsRecord.getComponentByIndex(0)).toMatchObject({
- // state: mockFiberNode.stateNode.state,
- // props: mockFiberNode.stateNode.props,
- // });
-
- // const mockFiberNode2: Fiber = { ...mockFiberNode };
- // mockFiberNode2.stateNode.props = { start: 1 }; // a different mock memoizedProps object
- // createComponentActionsRecord(mockFiberNode2);
- // expect(componentActionsRecord.getComponentByIndex(1)).toMatchObject({
- // state: mockFiberNode2.stateNode.state,
- // props: mockFiberNode2.stateNode.props,
- // });
- // });
-
- it('should return the correct hooks array for a given component index', () => {
- // create a mock component action record
- });
+ it('should save a new component action record if the Fiber node is a functional component with state', () => {
+ mockFiberNode.tag = FunctionComponent;
+ mockFiberNode.memoizedState = {
+ queue: [{}, { state: { value: 'test' } }], // a mock memoizedState object
+ };
+ createComponentActionsRecord(mockFiberNode);
+ expect(componentActionsRecord.getComponentByIndex(0)).toBe(
+ mockFiberNode.memoizedState.queue,
+ );
+ });
- it('should not save a new component action record if the Fiber node is not a relevant component type', () => {
- mockFiberNode.tag = 4; // HostRoot
- createComponentActionsRecord(mockFiberNode);
- expect(componentActionsRecord.getAllComponents()).toHaveLength(0);
+ it('should not save a new component action record if the Fiber node is not a relevant component type', () => {
+ mockFiberNode.tag = 4; // HostRoot
+ createComponentActionsRecord(mockFiberNode);
+ expect(componentActionsRecord.getAllComponents()).toHaveLength(0);
+ });
});
});
});
diff --git a/src/backend/controllers/createTree.ts b/src/backend/controllers/createTree.ts
index 86f6aee4c..d49d134fe 100644
--- a/src/backend/controllers/createTree.ts
+++ b/src/backend/controllers/createTree.ts
@@ -193,11 +193,8 @@ export default function createTree(currentFiberNode: Fiber): Tree {
// If user use setState to define/manage state, the state object will be stored in stateNode.state => grab the state object stored in the stateNode.state
// Example: for tic-tac-toe demo-app: Board is a stateful component that use setState to store state data.
if ((tag === ClassComponent || tag === IndeterminateComponent) && stateNode?.state) {
- // Save component's state and setState() function to our record for future time-travel state changing. Add record index to snapshot so we can retrieve.
componentData.index = componentActionsRecord.saveNew(stateNode);
- // Save state information in componentData.
componentData.state = stateNode.state;
- // Pass to front end
newState = componentData.state;
}
@@ -205,47 +202,54 @@ export default function createTree(currentFiberNode: Fiber): Tree {
// Check if currentFiberNode is a stateful functional component when user use useState hook.
// If user use useState to define/manage state, the state object will be stored in memoizedState => grab the state object & its update method (dispatch) from memoizedState
// Example: for Stateful buttons demo-app: Increment is a stateful component that use useState hook to store state data.
- if (
- (tag === FunctionComponent ||
- tag === IndeterminateComponent ||
- //TODO: Need reasoning for why we evaluate context provider
- /**
- * So far I haven't seen a case where hook data is stored for ContextProviders in memoized state. So far
- * I've seen some data a non-null memoize state on browser router, but queue is null. Routes has some good info on memoized props,
- * but that's not being addressed here. useContext providers also have null for memoized state.
- */
- tag === ContextProvider) &&
- memoizedState
- ) {
+ // Inside the _createTree function where we handle functional components
+ if ((tag === FunctionComponent || tag === IndeterminateComponent) && memoizedState) {
if (memoizedState.queue) {
try {
- // Obtain all hooksStates & the corresponding udpate method from memoizedState
const hooksStates = getHooksStateAndUpdateMethod(memoizedState);
- // Obtain variable names by parsing the function definition stored in elementType.
const hooksNames = getHooksNames(elementType.toString());
- // Intialize state & index:
componentData.hooksState = {};
+ componentData.reducerStates = []; // New array to store reducer states
componentData.hooksIndex = [];
- hooksStates.forEach(({ state, component }, i) => {
- // Save component's state and dispatch() function to our record for future time-travel state changing. Add record index to snapshot so we can retrieve.
+ hooksStates.forEach(({ state, component, isReducer, lastAction, reducer }, i) => {
componentData.hooksIndex.push(componentActionsRecord.saveNew(component));
- // Save state information in componentData.
- componentData.hooksState[hooksNames[i].varName] = state;
+
+ if (isReducer) {
+ // Store reducer-specific information
+ componentData.reducerStates.push({
+ state,
+ lastAction,
+ reducerIndex: i,
+ hookName: hooksNames[i]?.hookName || `Reducer ${i}`,
+ });
+ } else {
+ // Regular useState hook
+ componentData.hooksState[hooksNames[i]?.varName || `State: ${i}`] = state;
+ }
});
- // Pass to front end
- newState = componentData.hooksState;
+ newState = {
+ ...componentData.hooksState,
+ reducers: componentData.reducerStates,
+ };
} catch (err) {
- // COMMENT OUT TO AVOID PRINTING ON THE CONSOLE OF USER - KEEP IT FOR DEBUGGING PURPOSE
- // console.log({
- // Message: 'Error in createTree during obtaining state from functionalComponent',
- // componentName,
- // err,
- // });
+ console.log('Error extracting component state:', {
+ componentName,
+ memoizedState,
+ error: err,
+ });
}
}
}
+ if (tag === ContextProvider && !elementType._context.displayName) {
+ let stateData = memoizedProps.value;
+ if (stateData === null || typeof stateData !== 'object') {
+ stateData = { CONTEXT: stateData };
+ }
+ componentData.context = filterAndFormatData(stateData);
+ componentName = 'Context';
+ }
// -----------------ADD COMPONENT DATA TO THE OUTPUT TREE-------------------
diff --git a/src/backend/controllers/statePropExtractors.ts b/src/backend/controllers/statePropExtractors.ts
index c01adfe21..0f1a3980f 100644
--- a/src/backend/controllers/statePropExtractors.ts
+++ b/src/backend/controllers/statePropExtractors.ts
@@ -1,4 +1,5 @@
-import parse from 'html-react-parser';
+import { parse } from '@babel/parser';
+import { Node, CallExpression, MemberExpression, Identifier } from '@babel/types';
import { HookStateItem, Fiber } from '../types/backendTypes';
import { exclude } from '../models/filterConditions';
@@ -69,10 +70,29 @@ export function getHooksStateAndUpdateMethod(
const hooksStates: Array
= [];
while (memoizedState) {
if (memoizedState.queue) {
- hooksStates.push({
- component: memoizedState.queue,
- state: memoizedState.memoizedState,
- });
+ // Check if this is a reducer hook by looking at the lastRenderedReducer
+ const isReducer = memoizedState.queue.lastRenderedReducer?.name !== 'basicStateReducer';
+
+ if (isReducer) {
+ // For useReducer hooks, we want to store:
+ // 1. The current state
+ // 2. The last action that was dispatched (if available)
+ // 3. The reducer function itself
+ hooksStates.push({
+ component: memoizedState.queue,
+ state: memoizedState.memoizedState,
+ isReducer: true,
+ lastAction: memoizedState.queue.lastRenderedAction || null,
+ reducer: memoizedState.queue.lastRenderedReducer || null,
+ });
+ } else {
+ // Regular useState hook
+ hooksStates.push({
+ component: memoizedState.queue,
+ state: memoizedState.memoizedState,
+ isReducer: false,
+ });
+ }
}
memoizedState = memoizedState.next;
}
@@ -86,62 +106,125 @@ export function getHooksStateAndUpdateMethod(
* @returns - An array of objects with key: hookName (the name of setState method) | value: varName (the state variable name)
*/
export function getHooksNames(elementType: string): { hookName: string; varName: string }[] {
- // Initialize empty object to store the setters and getter
- // Abstract Syntax Tree
- let AST: any;
try {
- AST = parse(elementType).body;
- // Begin search for hook names, only if ast has a body property.
- // Statements get all the names of the hooks. For example: useCount, useWildcard, ...
+ const AST = parse(elementType, {
+ sourceType: 'module',
+ plugins: ['jsx', 'typescript'],
+ });
+
const statements: { hookName: string; varName: string }[] = [];
- /** All module exports always start off as a single 'FunctionDeclaration' type
- * Other types: "BlockStatement" / "ExpressionStatement" / "ReturnStatement"
- * Iterate through AST of every functional component declaration
- * Check within each functional component declaration if there are hook declarations & variable name declaration */
- AST.forEach((functionDec: any) => {
- let declarationBody: any;
- if (functionDec.expression?.body) declarationBody = functionDec.expression.body.body;
- // check if functionDec.expression.body exists, then set declarationBody to functionDec's body
- else declarationBody = functionDec.body?.body ?? [];
- // Traverse through the function's funcDecs and Expression Statements
- declarationBody.forEach((elem: any) => {
- // Hooks will always be contained in a variable declaration
- if (elem.type === 'VariableDeclaration') {
- // Obtain the declarations array from elem.
- const { declarations } = elem;
- // Obtain the reactHook:
- // Due to difference in babel transpilation in browser vs for jest test, expression is stored in differen location
- const expression =
- declarations[0]?.init?.callee?.expressions || //work for browser
- declarations[0]?.init?.arguments?.[0]?.callee?.expressions; //work for jest test;
-
- //For a functional definition that isn't a hook, it won't have the callee being searched for above. This line will cause this forEach execution to stop here in this case.
- if (expression === undefined) return;
- let reactHook: string;
- reactHook = expression[1].property?.name;
- if (reactHook === 'useState') {
- // Obtain the variable being set:
- let varName: string =
- // Points to second to last element of declarations because webpack adds an extra variable when converting files that use ES6
- declarations[declarations.length - 2]?.id?.name || // work react application;
- (Array.isArray(declarations[0]?.id?.elements)
- ? declarations[0]?.id?.elements[0]?.name
- : undefined); //work for nextJS application
- // Obtain the setState method:
- let hookName: string =
- //Points to last element of declarations because webpack adds an extra variable when converting files that use ES6
- declarations[declarations.length - 1]?.id?.name || // work react application;
- (Array.isArray(declarations[0]?.id?.elements)
- ? declarations[0]?.id?.elements[0]?.name
- : undefined); //work for nextJS & Remix
- // Push reactHook & varName to statements array
- statements.push({ hookName, varName });
+
+ const isIdentifierWithName = (node: any, name: string): boolean => {
+ return node?.type === 'Identifier' && node.name === name;
+ };
+
+ const processArrayPattern = (pattern: any): { setter: string; getter: string } | null => {
+ if (pattern.type === 'ArrayPattern' && pattern.elements.length === 2) {
+ const result = {
+ getter: pattern.elements[0].name,
+ setter: pattern.elements[1].name,
+ };
+ return result;
+ }
+ return null;
+ };
+
+ const extractReducerName = (node: any): string | null => {
+ if (node.type === 'Identifier') {
+ return node.name;
+ }
+
+ if (node.type === 'ArrowFunctionExpression' && node.body.type === 'CallExpression') {
+ if (node.body.callee.type === 'Identifier') {
+ return node.body.callee.name;
+ }
+ }
+
+ return null;
+ };
+
+ function traverse(node: Node) {
+ if (!node) return;
+
+ if (node.type === 'VariableDeclaration') {
+ node.declarations.forEach((declaration) => {
+ if (declaration.init?.type === 'CallExpression') {
+ // Check for Webpack transformed pattern
+ const isWebpackPattern =
+ declaration.init.callee?.type === 'SequenceExpression' &&
+ declaration.init.callee.expressions?.[1]?.type === 'MemberExpression';
+
+ // Get the hook name for Webpack pattern
+ let webpackHookName: string | null = null;
+ if (isWebpackPattern && declaration.init.callee?.type === 'SequenceExpression') {
+ const memberExpr = declaration.init.callee.expressions[1] as any;
+ if (memberExpr?.property?.type === 'Identifier') {
+ webpackHookName = memberExpr.property.name;
+ }
+ }
+
+ // Check for direct pattern
+ const directCallee = declaration.init.callee as any;
+ const isDirectPattern =
+ directCallee?.type === 'Identifier' &&
+ (directCallee.name === 'useState' || directCallee.name === 'useReducer');
+
+ // Check for namespaced pattern
+ const isNamespacedPattern =
+ declaration.init.callee?.type === 'MemberExpression' &&
+ (declaration.init.callee as any).property?.type === 'Identifier' &&
+ ((declaration.init.callee as any).property.name === 'useState' ||
+ (declaration.init.callee as any).property.name === 'useReducer');
+
+ if (isWebpackPattern || isDirectPattern || isNamespacedPattern) {
+ const arrayPattern = processArrayPattern(declaration.id);
+ if (arrayPattern) {
+ const isReducer =
+ webpackHookName === 'useReducer' ||
+ (isDirectPattern && directCallee?.name === 'useReducer') ||
+ (isNamespacedPattern &&
+ (declaration.init.callee as any).property?.name === 'useReducer');
+
+ if (isReducer) {
+ // Handle useReducer
+ if (declaration.init.arguments?.length > 0) {
+ const reducerName = extractReducerName(declaration.init.arguments[0]);
+ if (reducerName) {
+ statements.push({
+ hookName: reducerName,
+ varName: arrayPattern.getter,
+ });
+ }
+ }
+ } else {
+ // Handle useState
+ statements.push({
+ hookName: arrayPattern.setter,
+ varName: arrayPattern.getter,
+ });
+ }
+ }
+ }
+ }
+ });
+ }
+
+ // Recursively traverse
+ for (const key in node) {
+ if (node[key] && typeof node[key] === 'object') {
+ if (Array.isArray(node[key])) {
+ node[key].forEach((child: Node) => traverse(child));
+ } else {
+ traverse(node[key] as Node);
}
}
- });
- });
+ }
+ }
+
+ traverse(AST);
return statements;
} catch (err) {
- throw new Error('getHooksNameError' + err.message);
+ console.error('AST Parsing Error:', err);
+ throw new Error('getHooksNameError: ' + err.message);
}
}
diff --git a/src/backend/controllers/timeJump.ts b/src/backend/controllers/timeJump.ts
index 1f0918b12..bf7a1e85c 100644
--- a/src/backend/controllers/timeJump.ts
+++ b/src/backend/controllers/timeJump.ts
@@ -3,102 +3,89 @@ import { Status } from '../types/backendTypes';
import Tree from '../models/tree';
import { update } from 'lodash';
-// THIS FILE CONTAINS NECCESSARY FUNCTIONALITY FOR TIME-TRAVEL FEATURE
-
-/**
- *
- * This is a closure function to keep track of mode (jumping or not jumping)
- * @param mode - The current mode (i.e. jumping)
- * @returns an async function that takes an `targetSnapshot`, then invokes `updateReactFiberTree` based on the state provided within that target snapshot
- *
- */
export default function timeJumpInitiation(mode: Status) {
- /**
- * This function is to reset jumping mode to false when user hover the mouse over the browser body
- */
const resetJumpingMode = (): void => {
mode.jumping = false;
};
- /**
- * This function that takes a `targetSnapshot` then invokes `updateReactFiberTree` to update the React Application on the browser to match states provided by the `targetSnapshot`
- * @param targetSnapshot - The target snapshot to re-render. The payload from index.ts is assigned to targetSnapshot
- */
+
return async function timeJump(targetSnapshot: Tree): Promise {
mode.jumping = true;
- // Reset mode.navigating
delete mode.navigating;
- // Traverse the snapshotTree to update ReactFiberTree
updateReactFiberTree(targetSnapshot).then(() => {
- // Remove Event listener for mouse over
document.removeEventListener('mouseover', resetJumpingMode);
- // Since in order to change state, user will need to navigate to browser
- // => set an event listener to resetJumpingMode when mouse is over the browser
document.addEventListener('mouseover', resetJumpingMode, { once: true });
});
};
}
-/**
- * This recursive function receives the target snapshot from front end and will update the state of the fiber tree if the component is stateful
- * @param targetSnapshot - Target snapshot portrays some past state we want to travel to.
- * @param circularComponentTable - A table contains visited components
- *
- */
async function updateReactFiberTree(
- //TypeScript Note: Adding a tree type to targetSnapshot throws errors for destructuring componentData below. Not sure how precisely to fix
targetSnapshot,
circularComponentTable: Set = new Set(),
): Promise {
if (!targetSnapshot) return;
- // Base Case: if has visited, return
- if (circularComponentTable.has(targetSnapshot)) {
- return;
- } else {
- circularComponentTable.add(targetSnapshot);
- }
- // ------------------------STATELESS/ROOT COMPONENT-------------------------
- // Since stateless component has no data to update, continue to traverse its child nodes:
+ if (circularComponentTable.has(targetSnapshot)) return;
+
+ circularComponentTable.add(targetSnapshot);
+
if (targetSnapshot.state === 'stateless' || targetSnapshot.state === 'root') {
targetSnapshot.children.forEach((child) => updateReactFiberTree(child, circularComponentTable));
return;
}
- // Destructure component data:
- const { index, state, hooksIndex, hooksState } = targetSnapshot.componentData;
- // ------------------------STATEFUL CLASS COMPONENT-------------------------
- // Check if it is a stateful class component
- // Index can be zero => falsy value => DO NOT REMOVE NULL
+ const { index, state, hooksIndex, hooksState, reducerStates } = targetSnapshot.componentData;
+
+ // Handle class components
if (index !== null) {
- // Obtain the BOUND update method at the given index
const classComponent = componentActionsRecord.getComponentByIndex(index);
- // This conditional avoids the error that occurs when classComponent is undefined
if (classComponent !== undefined) {
- // Update component state
- await classComponent.setState(
- // prevState contains the states of the snapshots we are jumping FROM, not jumping TO
- (prevState) => state,
- );
+ await classComponent.setState(() => state);
}
- // Iterate through new children after state has been set
targetSnapshot.children.forEach((child) => updateReactFiberTree(child, circularComponentTable));
return;
}
- // ----------------------STATEFUL FUNCTIONAL COMPONENT----------------------
- // Check if it is a stateful functional component
- // if yes, grab all relevant components for this snapshot by its index
- // call dispatch on each component passing in the corresponding currState value
- //index can be zero => falsy value => DO NOT REMOVE NULL
+ // Handle hooks
if (hooksIndex !== null) {
- // Obtain the array of BOUND update methods at the given indexes.
- // NOTE: each useState will be a separate update method. So if a component have 3 useState, we will obtain an array of 3 update methods.
const functionalComponent = componentActionsRecord.getComponentByIndexHooks(hooksIndex);
- // Update component state
- for (let i in functionalComponent) {
- await functionalComponent[i].dispatch(Object.values(hooksState)[i]);
+
+ // Handle regular useState hooks
+ if (hooksState) {
+ const stateEntries = Object.entries(hooksState);
+ for (let i = 0; i < stateEntries.length; i++) {
+ const [key, value] = stateEntries[i];
+ if (functionalComponent[i]?.dispatch) {
+ await functionalComponent[i].dispatch(value);
+ }
+ }
}
- // Iterate through new children after state has been set
- targetSnapshot.children.forEach((child) => updateReactFiberTree(child));
+
+ // Handle reducer hooks
+ if (reducerStates && reducerStates.length > 0) {
+ for (const reducerState of reducerStates) {
+ const { state: targetState, reducerIndex, hookName } = reducerState;
+ const reducer = functionalComponent[reducerIndex];
+
+ if (reducer?.dispatch) {
+ try {
+ // Use SET_STATE action to update the state
+ const setStateAction = {
+ type: 'SET_STATE',
+ payload: targetState,
+ };
+
+ await reducer.dispatch(setStateAction);
+ } catch (error) {
+ console.error('Error updating reducer state:', {
+ hookName,
+ error,
+ componentName: targetSnapshot.name,
+ });
+ }
+ }
+ }
+ }
+
+ targetSnapshot.children.forEach((child) => updateReactFiberTree(child, circularComponentTable));
return;
}
}
diff --git a/src/backend/types/backendTypes.ts b/src/backend/types/backendTypes.ts
index 844e18e2d..863a4d38d 100644
--- a/src/backend/types/backendTypes.ts
+++ b/src/backend/types/backendTypes.ts
@@ -67,7 +67,12 @@ export interface ComponentData {
index: number | null;
/** {functional component only} - An object contains all states of the current functional component */
hooksState: {} | null;
- /** {functional component only} - An array of index of the bound dispatch method stored in `componentActionsRecord` */
+ reducerStates?: Array<{
+ state: any;
+ lastAction: any;
+ reducerIndex: number;
+ hookName: string;
+ }> /** {functional component only} - An array of index of the bound dispatch method stored in `componentActionsRecord` */;
hooksIndex: number[] | null;
/** An object contains all props of the current component */
props: { [key: string]: any };
@@ -84,10 +89,11 @@ export interface ComponentData {
* @member component - contains bound dispatch method to update state of the current functional component
*/
export interface HookStateItem {
- /** states within the current functional component */
- state: any;
- /** an object contains bound dispatch method to update state of the current functional component */
component: any;
+ state: any;
+ isReducer: boolean;
+ lastAction?: any;
+ reducer?: Function;
}
export type WorkTag =
diff --git a/src/extension/background.js b/src/extension/background.js
index 01d3353dc..5a0dab50a 100644
--- a/src/extension/background.js
+++ b/src/extension/background.js
@@ -1,7 +1,3 @@
-// import 'core-js';
-
-import { invoke } from 'lodash';
-
// Store ports in an array.
const portsArr = [];
const reloaded = {};
@@ -16,6 +12,25 @@ let activeTab;
const tabsObj = {};
// Will store Chrome web vital metrics and their corresponding values.
const metrics = {};
+function setupKeepAlive() {
+ //ellie
+ // Create an alarm that triggers every 4.9 minutes (under the 5-minute limit)
+ chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 });
+
+ chrome.alarms.onAlarm.addListener((alarm) => {
+ if (alarm.name === 'keepAlive') {
+ console.log('Keep-alive alarm triggered.');
+ pingServiceWorker();
+ }
+ });
+}
+// Ping the service worker to keep it alive
+function pingServiceWorker() {
+ // Use a lightweight API call to keep the service worker active
+ chrome.runtime.getPlatformInfo(() => {
+ console.log('Service worker pinged successfully');
+ });
+}
// function pruning the chrome ax tree and pulling the relevant properties
const pruneAxTree = (axTree) => {
@@ -267,14 +282,14 @@ function changeCurrLocation(tabObj, rootNode, index, name) {
async function getActiveTab() {
return new Promise((resolve, reject) => {
- chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
+ chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs.length > 0) {
resolve(tabs[0].id);
} else {
- reject(new Error('No active tab'))
+ reject(new Error('No active tab'));
}
});
- })
+ });
}
/*
@@ -307,19 +322,29 @@ chrome.runtime.onConnect.addListener(async (port) => {
portsArr.push(port); // push each Reactime communication channel object to the portsArr
// sets the current Title of the Reactime panel
-/**
- * Sends messages to ports in the portsArr array, triggering a tab change action.
- */
+ /**
+ * Sends messages to ports in the portsArr array, triggering a tab change action.
+ */
function sendMessagesToPorts() {
portsArr.forEach((bg, index) => {
- bg.postMessage({
- action: 'changeTab',
- payload: { tabId: activeTab.id, title: activeTab.title },
- });
+ bg.postMessage({
+ action: 'changeTab',
+ payload: { tabId: activeTab.id, title: activeTab.title },
+ });
});
-}
+ }
+ if (port.name === 'keepAlivePort') {
+ console.log('Keep-alive port connected:', port);
-
+ // Keep the port open by responding to any message
+ port.onMessage.addListener((msg) => {
+ console.log('Received message from content script:', msg);
+ });
+
+ port.onDisconnect.addListener(() => {
+ console.warn('Keep-alive port disconnected.');
+ });
+ }
if (portsArr.length > 0 && Object.keys(tabsObj).length > 0) {
//if the activeTab is not set during the onActivate API, run a query to get the tabId and set activeTab
if (!activeTab) {
@@ -330,25 +355,26 @@ chrome.runtime.onConnect.addListener(async (port) => {
activeTab = tab;
sendMessagesToPorts();
}
- });
- };
+ });
+ }
}
if (Object.keys(tabsObj).length > 0) {
+ console.log('Sending initial snapshots to devtools:', tabsObj);
port.postMessage({
action: 'initialConnectSnapshots',
payload: tabsObj,
});
+ } else {
+ console.log('No snapshots to send to devtools on reconnect.');
}
- // every time devtool is closed, remove the port from portsArr
- port.onDisconnect.addListener((e) => {
- for (let i = 0; i < portsArr.length; i += 1) {
- if (portsArr[i] === e) {
- portsArr.splice(i, 1);
- chrome.runtime.sendMessage({ action: 'portDisconnect', port: e.name });
- break;
- }
+ // Handles port disconnection by removing the disconnected port -ellie
+ port.onDisconnect.addListener(() => {
+ const index = portsArr.indexOf(port);
+ if (index !== -1) {
+ console.warn(`Port at index ${index} disconnected. Removing it.`);
+ portsArr.splice(index, 1);
}
});
@@ -356,15 +382,6 @@ chrome.runtime.onConnect.addListener(async (port) => {
// listen for message containing a snapshot from devtools and send it to contentScript -
// (i.e. they're all related to the button actions on Reactime)
port.onMessage.addListener(async (msg) => {
- // msg is action denoting a time jump in devtools
- // ---------------------------------------------------------------
- // message incoming from devTools should look like this:
- // {
- // action: 'emptySnap',
- // payload: tabsObj,
- // tabId: 101
- // }
- // ---------------------------------------------------------------
const { action, payload, tabId } = msg;
switch (action) {
@@ -428,7 +445,6 @@ chrome.runtime.onConnect.addListener(async (port) => {
toggleAxRecord = !toggleAxRecord;
await replaceEmptySnap(tabsObj, tabId, toggleAxRecord);
-
// sends new tabs obj to devtools
if (portsArr.length > 0) {
portsArr.forEach((bg) =>
@@ -599,13 +615,26 @@ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
break;
}
- // DUPLICATE SNAPSHOT CHECK
- // This may be where the bug is coming from that when Reactime fails to collect
- // state. If they happen to take the same actual duration, it won't record the snapshot.
- const previousSnap =
- tabsObj[tabId]?.currLocation?.stateSnapshot?.children[0]?.componentData?.actualDuration;
- const incomingSnap = request.payload.children[0].componentData.actualDuration;
- if (previousSnap === incomingSnap) {
+ // DUPLICATE SNAPSHOT CHECK -ellie
+ const isDuplicateSnapshot = (previous, incoming) => {
+ if (!previous || !incoming) return false;
+ const prevData = previous?.componentData;
+ const incomingData = incoming?.componentData;
+
+ // Check if both snapshots have required data
+ if (!prevData || !incomingData) return false;
+
+ const timeDiff = Math.abs(
+ (incomingData.timestamp || Date.now()) - (prevData.timestamp || Date.now()),
+ );
+ return prevData.actualDuration === incomingData.actualDuration && timeDiff < 1000;
+ };
+
+ const previousSnap = tabsObj[tabId]?.currLocation?.stateSnapshot?.children[0];
+ const incomingSnap = request.payload.children[0];
+
+ if (isDuplicateSnapshot(previousSnap, incomingSnap)) {
+ console.warn('Duplicate snapshot detected, skipping');
break;
}
@@ -626,7 +655,6 @@ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
addedAxSnap = 'emptyAxSnap';
tabsObj[tabId].axSnapshots.push(addedAxSnap);
}
-
sendToHierarchy(
tabsObj[tabId],
new HistoryNode(tabsObj[tabId], request.payload, addedAxSnap),
@@ -636,15 +664,21 @@ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
// sends new tabs obj to devtools
if (portsArr.length > 0) {
- portsArr.forEach((bg) =>
- bg.postMessage({
- action: 'sendSnapshots',
- payload: tabsObj,
- sourceTab,
- }),
- );
+ portsArr.forEach((bg, index) => {
+ try {
+ bg.postMessage({
+ action: 'sendSnapshots',
+ payload: tabsObj,
+ sourceTab,
+ });
+ console.log(`Sent snapshots to port at index ${index}`);
+ } catch (error) {
+ console.warn(`Failed to send snapshots to port at index ${index}:`, error);
+ }
+ });
+ } else {
+ console.warn('No active ports to send snapshots to.');
}
- break;
}
default:
break;
@@ -697,21 +731,15 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
}
});
-// when tab view is changed, put the tabid as the current tab
+// When tab view is changed, put the tabId as the current tab
chrome.tabs.onActivated.addListener((info) => {
- // get info about tab information from tabId
+ // Get info about the tab information from tabId
chrome.tabs.get(info.tabId, (tab) => {
- // never set a reactime instance to the active tab
+ // Never set a reactime instance to the active tab
if (!tab.pendingUrl?.match('^chrome-extension')) {
activeTab = tab;
- /**this setInterval is here to make sure that the app does not stop working even
- * if chrome pauses to save energy. There is probably a better solution, but v25 did
- * not have time to complete.
- */
- setInterval(() => {
- console.log(activeTab)
- }, 10000);
+ // Send messages to active ports about the tab change
if (portsArr.length > 0) {
portsArr.forEach((bg) =>
bg.postMessage({
@@ -724,55 +752,6 @@ chrome.tabs.onActivated.addListener((info) => {
});
});
-// when reactime is installed
-// create a context menu that will open our devtools in a new window
-chrome.runtime.onInstalled.addListener(() => {
- chrome.contextMenus.create({
- id: 'reactime',
- title: 'Reactime',
- contexts: ['page', 'selection', 'image', 'link'],
- });
-});
-
-// when context menu is clicked, listen for the menuItemId,
-// if user clicked on reactime, open the devtools window
-
-// JR 12.19.23
-// As of V22, if multiple monitors are used, it would open the reactime panel on the other screen, which was inconvenient when opening repeatedly for debugging.
-// V23 fixes this by making use of chrome.windows.getCurrent to get the top and left of the screen which invoked the extension.
-// As of chrome manifest V3, background.js is a 'service worker', which does not have access to the DOM or to the native 'window' method, so we use chrome.windows.getCurrent(callback)
-// chrome.windows.getCurrent returns a promise (asynchronous), so all resulting functionality must happen in the callback function, or it will run before 'invokedScreen' variables have been captured.
-chrome.contextMenus.onClicked.addListener(({ menuItemId }) => {
- // // this was a test to see if I could dynamically set the left property to be the 0 origin of the invoked DISPLAY (as opposed to invoked window).
- // // this would allow you to split your screen, keep the browser open on the right side, and reactime always opens at the top left corner.
- // // however it does not tell you which display is the one that invoked it, just gives the array of all available displays. Depending on your monitor setup, it may differ. Leaving for future iterators
- // chrome.system.display.getInfo((displayUnitInfo) => {
- // console.log(displayUnitInfo);
- // });
- chrome.windows.getCurrent((window) => {
- const invokedScreenTop = 75; // window.top || 0;
- const invokedScreenLeft = window.width < 1000 ? window.left + window.width - 1000 : window.left;
- const invokedScreenWidth = 1000;
- const invokedScreenHeight = window.height - invokedScreenTop || 1000;
- const options = {
- type: 'panel',
- left: invokedScreenLeft,
- top: invokedScreenTop,
- width: invokedScreenWidth,
- height: invokedScreenHeight,
- url: chrome.runtime.getURL('panel.html'),
- };
- if (menuItemId === 'reactime') chrome.windows.create(options);
- });
-
- // JR 1.9.23: this code fixes the no target error on load by triggering chrome tab reload before the panel spins up.
- // It does not solve the root issue, which was deeply researched during v23 but we ran out of time to solve. Please see the readme for more information.
- chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
- if (tabs.length) {
- const invokedTab = tabs[0];
- const invokedTabId = invokedTab.id;
- const invokedTabTitle = invokedTab.title;
- chrome.tabs.reload(invokedTabId);
- }
- });
+chrome.runtime.onStartup.addListener(() => {
+ setupKeepAlive();
});
diff --git a/src/extension/build/devtools.html b/src/extension/build/devtools.html
index bc851747d..de554fb40 100644
--- a/src/extension/build/devtools.html
+++ b/src/extension/build/devtools.html
@@ -4,7 +4,7 @@
- Reactime v23
+ Reactime v26.0
diff --git a/src/extension/build/manifest.json b/src/extension/build/manifest.json
index d0fbf3db8..ab6604514 100644
--- a/src/extension/build/manifest.json
+++ b/src/extension/build/manifest.json
@@ -1,8 +1,8 @@
{
"name": "Reactime",
- "version": "25.0.0",
+ "version": "26.0",
"devtools_page": "devtools.html",
- "description": "A Chrome extension that helps debug React applications by memorizing the state of components with every render.",
+ "description": "A Chrome extension for time travel debugging and performance monitoring in React applications.",
"manifest_version": 3,
"background": {
"service_worker": "bundles/background.bundle.js"
@@ -24,6 +24,14 @@
"matches": [""]
}
],
- "permissions": ["contextMenus", "tabs", "activeTab", "scripting", "system.display", "debugger"],
+ "permissions": [
+ "contextMenus",
+ "tabs",
+ "activeTab",
+ "scripting",
+ "system.display",
+ "debugger",
+ "alarms"
+ ],
"host_permissions": [""]
}
diff --git a/src/extension/build/panel.html b/src/extension/build/panel.html
index b97ee64aa..9c475132a 100644
--- a/src/extension/build/panel.html
+++ b/src/extension/build/panel.html
@@ -1,10 +1,10 @@
-
+
- Reactime 23.0
+ Reactime v26.0
diff --git a/src/extension/contentScript.ts b/src/extension/contentScript.ts
index b1ae376bf..a287b048f 100644
--- a/src/extension/contentScript.ts
+++ b/src/extension/contentScript.ts
@@ -2,6 +2,21 @@
// in Web Metrics tab of Reactime app.
import { onTTFB, onLCP, onFID, onFCP, onCLS, onINP } from 'web-vitals';
+function establishConnection() {
+ const port = chrome.runtime.connect({ name: 'keepAlivePort' }); // Reconnect if we lose connection to the port at any time
+ port.onMessage.addListener((msg) => {
+ console.log('Received message from background:', msg);
+ });
+
+ port.onDisconnect.addListener(() => {
+ console.warn('Port disconnected, attempting to reconnect...');
+ setTimeout(establishConnection, 1000); // Retry after 1 second
+ });
+}
+
+// Initially establish connection
+establishConnection();
+
// Reactime application starts off with this file, and will send
// first message to background.js for initial tabs object set up.
// A "tabs object" holds the information of the current tab,
@@ -27,6 +42,8 @@ window.addEventListener('message', (msg) => {
const { action }: { action: string } = msg.data;
if (action === 'recordSnap') {
if (isRecording) {
+ // add timestamp to payload for the purposes of duplicate screenshot check in backgroundscript -ellie
+ msg.data.payload.children[0].componentData.timestamp = Date.now();
chrome.runtime.sendMessage(msg.data);
}
}