diff --git a/src/app/npg-lite/page.tsx b/src/app/npg-lite/page.tsx index 94067f5..6267f48 100644 --- a/src/app/npg-lite/page.tsx +++ b/src/app/npg-lite/page.tsx @@ -4,6 +4,7 @@ import React, { useRef, useState, useCallback, + useMemo, } from "react"; import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; @@ -41,14 +42,34 @@ import { Eye, BicepsFlexed, Settings, - Loader + Loader, + Battery, + BatteryLow, + BatteryMedium, + BatteryFull, + BatteryWarning } from "lucide-react"; import { lightThemeColors, darkThemeColors, getCustomColor } from '@/components/Colors'; import { useTheme } from "next-themes"; +// Device configuration interface +interface DeviceConfig { + maxChannels: number; + sampleLength: number; + hasBattery: boolean; + name: string; +} + +const defaultConfig: DeviceConfig = { + maxChannels: 3, + sampleLength: 7, + hasBattery: false, + name: "" +}; const NPG_Ble = () => { const isRecordingRef = useRef(false); // Ref to track if the device is recording + const isOldfirmwareRef = useRef(false); // Ref to track if the device has old firmware const [isDisplay, setIsDisplay] = useState(true); // Display state const [isRecord, setIsrecord] = useState(true); // Display state const [isEndTimePopoverOpen, setIsEndTimePopoverOpen] = useState(false); @@ -73,9 +94,22 @@ const NPG_Ble = () => { const linesRef = useRef([]); const sweepPositions = useRef(new Array(6).fill(0)); // Array for sweep positions const currentSweepPos = useRef(new Array(6).fill(0)); // Array for sweep positions - const maxCanvasElementCountRef = useRef(3); - const channelNames = Array.from({ length: maxCanvasElementCountRef.current }, (_, i) => `CH${i + 1}`); - let numChannels = 3; + + // Centralized device configuration + const deviceConfigRef = useRef(defaultConfig); + const [deviceConfig, setDeviceConfig] = useState(defaultConfig); + + // State for UI updates only + const [batteryLevel, setBatteryLevel] = useState(null); // Battery level percentage for UI + const [deviceName, setDeviceName] = useState(""); // Store device name for UI + const [refreshKey, setRefreshKey] = useState(0); // Force re-render when config changes + + // Derive channel names from device config + const channelNames = useMemo(() => + Array.from({ length: deviceConfig.maxChannels }, (_, i) => `CH${i + 1}`), + [deviceConfig.maxChannels] + ); + const [selectedChannels, setSelectedChannels] = useState([1]); const [manuallySelected, setManuallySelected] = useState(false); // New state to track manual selection const { theme } = useTheme(); // Current theme of the app @@ -93,24 +127,22 @@ const NPG_Ble = () => { const fillingindex = useRef(0); // Initialize useRef with 0 const MAX_BUFFER_SIZE = 500; const pauseRef = useRef(true); + const togglePause = () => { const newPauseState = !isDisplay; setIsDisplay(newPauseState); pauseRef.current = newPauseState; }; const samplesReceivedRef = useRef(0); + const createCanvasElements = () => { const container = canvasContainerRef.current; if (!container) { return; // Exit if the ref is null } - // Ensure dataPointCount is calculated from current sampling rate and timeBase - const dpCount = samplingrateref.current * timeBase; - dataPointCountRef.current = dpCount; - - currentSweepPos.current = new Array(numChannels).fill(0); - sweepPositions.current = new Array(numChannels).fill(0); + currentSweepPos.current = new Array(deviceConfig.maxChannels).fill(0); + sweepPositions.current = new Array(deviceConfig.maxChannels).fill(0); // Clear existing child elements while (container.firstChild) { @@ -192,10 +224,10 @@ const NPG_Ble = () => { wglp.gScaleY = Zoom; - const line = new WebglLine(getLineColor(channelNumber, theme), dpCount); + const line = new WebglLine(getLineColor(channelNumber, theme), dataPointCountRef.current); wglp.gOffsetY = 0; line.offsetY = 0; - line.lineSpaceX(-1, 2 / dpCount); + line.lineSpaceX(-1, 2 / dataPointCountRef.current); wglp.addLine(line); newLines.push(line); @@ -222,7 +254,7 @@ const NPG_Ble = () => { const handleSelectAllToggle = () => { - const enabledChannels = Array.from({ length: maxCanvasElementCountRef.current }, (_, i) => i + 1); + const enabledChannels = Array.from({ length: deviceConfig.maxChannels }, (_, i) => i + 1); if (!isAllEnabledChannelSelected) { // Programmatic selection of all channels @@ -231,13 +263,11 @@ const NPG_Ble = () => { } else { // RESET functionality const savedchannels = JSON.parse(localStorage.getItem('savedchannels') || '[]'); - let initialSelectedChannelsRefs: number[] = []; // Default to channel 1 if no saved channels are found + let initialSelectedChannelsRefs: number[] = [1]; // Default to channel 1 // Get the saved channels for the device initialSelectedChannelsRefs = [1]; // Load saved channels or default to [1] - - // Set the channels back to saved values setSelectedChannels(initialSelectedChannelsRefs); // Reset to saved channels } @@ -252,9 +282,11 @@ const NPG_Ble = () => { useEffect(() => { createCanvasElements(); setRefresh(r => r + 1); - }, [numChannels, theme, timeBase, selectedChannels, Zoom, isConnected]); + }, [deviceConfig.maxChannels, theme, timeBase, selectedChannels, Zoom, isConnected, refreshKey]); + useEffect(() => { selectedChannelsRef.current = selectedChannels; + canvasElementCountRef.current = selectedChannels.length; }, [selectedChannels]); //filters @@ -317,9 +349,11 @@ const NPG_Ble = () => { }); forceUpdate(); // Trigger re-render }; + useEffect(() => { dataPointCountRef.current = (samplingrateref.current * timeBase); }, [timeBase]); + const zoomRef = useRef(Zoom); useEffect(() => { @@ -329,31 +363,107 @@ const NPG_Ble = () => { const SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"; const DATA_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8"; const CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"; + const BATTERY_CHAR_UUID = "f633d0ec-46b4-43c1-a39f-1ca06d0602e1"; // Battery characteristic UUID - const SINGLE_SAMPLE_LEN = 7; // Each sample is 10 bytes - const BLOCK_COUNT = 10; // 10 samples batched per notification - const NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT; // 100 bytes + let prevSampleCounter: number | null = null; + let channelData: number[] = []; + // Initialize filters with device config + const notchFiltersRef = useRef(Array.from({ length: deviceConfig.maxChannels }, () => new Notch())); + const exgFiltersRef = useRef(Array.from({ length: deviceConfig.maxChannels }, () => new EXGFilter())); + const pointoneFilterRef = useRef(Array.from({ length: deviceConfig.maxChannels }, () => new HighPassFilter())); + // Update filters when device config changes + useEffect(() => { + const config = deviceConfigRef.current; - let prevSampleCounter: number | null = null; - let channelData: number[] = []; - const notchFiltersRef = useRef(Array.from({ length: maxCanvasElementCountRef.current }, () => new Notch())); - const exgFiltersRef = useRef(Array.from({ length: maxCanvasElementCountRef.current }, () => new EXGFilter())); - const pointoneFilterRef = useRef(Array.from({ length: maxCanvasElementCountRef.current }, () => new HighPassFilter())); - notchFiltersRef.current.forEach((filter) => { - filter.setbits(samplingrateref.current); - }); - exgFiltersRef.current.forEach((filter) => { - filter.setbits("12", samplingrateref.current); - }); - pointoneFilterRef.current.forEach((filter) => { - filter.setSamplingRate(samplingrateref.current); - }); + notchFiltersRef.current = Array.from({ length: config.maxChannels }, () => new Notch()); + exgFiltersRef.current = Array.from({ length: config.maxChannels }, () => new EXGFilter()); + pointoneFilterRef.current = Array.from({ length: config.maxChannels }, () => new HighPassFilter()); + + notchFiltersRef.current.forEach((filter) => { + filter.setbits(samplingrateref.current); + }); + exgFiltersRef.current.forEach((filter) => { + filter.setbits("12", samplingrateref.current); + }); + pointoneFilterRef.current.forEach((filter) => { + filter.setSamplingRate(samplingrateref.current); + }); + }, [deviceConfig]); + + // Function to update device configuration based on name + const updateDeviceConfiguration = (name: string) => { + let newConfig: DeviceConfig; + + if (name.includes("3CH")) { + newConfig = { + maxChannels: 3, + sampleLength: 7, // 3 channels * 2 bytes + 1 byte counter = 7 bytes + hasBattery: true, + name + }; + isOldfirmwareRef.current = false; // Mark as new firmware if 3CH device is detected + } else if (name.includes("6CH")) { + newConfig = { + maxChannels: 6, + sampleLength: 13, // 6 channels * 2 bytes + 1 byte counter = 13 bytes + hasBattery: true, + name + }; + isOldfirmwareRef.current = false; // Mark as new firmware if 6CH device is detected + } else { + newConfig = { + maxChannels: 3, + sampleLength: 7, + hasBattery: false, + name + }; + isOldfirmwareRef.current = true; // Mark as old firmware if device name doesn't match known patterns + } + + // Update both ref and state + deviceConfigRef.current = newConfig; + setDeviceConfig(newConfig); + setDeviceName(name); + + // Reinitialize filters with new channel count + notchFiltersRef.current = Array.from({ length: newConfig.maxChannels }, () => new Notch()); + exgFiltersRef.current = Array.from({ length: newConfig.maxChannels }, () => new EXGFilter()); + pointoneFilterRef.current = Array.from({ length: newConfig.maxChannels }, () => new HighPassFilter()); + + // Initialize filters + notchFiltersRef.current.forEach((filter) => { + filter.setbits(samplingrateref.current); + }); + exgFiltersRef.current.forEach((filter) => { + filter.setbits("12", samplingrateref.current); + }); + pointoneFilterRef.current.forEach((filter) => { + filter.setSamplingRate(samplingrateref.current); + }); + + // Reset UI states + setSelectedChannels([1]); + setManuallySelected(false); + setBatteryLevel(null); + + // Force re-render to update UI with new channel count + setRefreshKey(prev => prev + 1); + }; + + const BLOCK_COUNT = 10; // 10 samples batched per notification + // Dynamic packet length based on current device config + const packetLengthRef = useRef(0); + useEffect(() => { + packetLengthRef.current = deviceConfigRef.current.sampleLength * BLOCK_COUNT; + }, [deviceConfig]); // Inside your component const processSample = useCallback((dataView: DataView): void => { - if (dataView.byteLength !== SINGLE_SAMPLE_LEN) { + const config = deviceConfigRef.current; + + if (dataView.byteLength !== config.sampleLength) { console.log("Unexpected sample length: " + dataView.byteLength); return; } @@ -372,11 +482,16 @@ const NPG_Ble = () => { channelData.push(sampleCounter); - for (let channel = 0; channel < numChannels; channel++) { + // Process the correct number of channels based on device configuration + + for (let channel = 0; channel < config.maxChannels; channel++) { const sample = dataView.getInt16(1 + (channel * 2), false); channelData.push( notchFiltersRef.current[channel].process( - exgFiltersRef.current[channel].process(pointoneFilterRef.current[channel].process(sample), appliedEXGFiltersRef.current[channel]), + exgFiltersRef.current[channel].process( + pointoneFilterRef.current[channel].process(sample), + appliedEXGFiltersRef.current[channel] + ), appliedFiltersRef.current[channel] ) ); @@ -412,7 +527,7 @@ const NPG_Ble = () => { channelData = []; samplesReceivedRef.current += 1; }, [ - canvasElementCountRef.current, selectedChannels, timeBase + canvasElementCountRef.current, selectedChannels, timeBase, deviceConfig ]); interface BluetoothRemoteGATTCharacteristicExtended extends EventTarget { @@ -425,95 +540,209 @@ const NPG_Ble = () => { console.log("Received event with no value."); return; } - if (currentSweepPos.current.length !== numChannels || !pauseRef.current) { - currentSweepPos.current = new Array(numChannels).fill(0); - sweepPositions.current = new Array(numChannels).fill(0); - } + + const config = deviceConfigRef.current; + const currentPacketLength = packetLengthRef.current; const value = target.value; - if (value.byteLength === NEW_PACKET_LEN) { - for (let i = 0; i < NEW_PACKET_LEN; i += SINGLE_SAMPLE_LEN) { - const sampleBuffer = value.buffer.slice(i, i + SINGLE_SAMPLE_LEN); + if (value.byteLength === currentPacketLength) { + for (let i = 0; i < currentPacketLength; i += config.sampleLength) { + const sampleBuffer = value.buffer.slice(i, i + config.sampleLength); const sampleDataView = new DataView(sampleBuffer); processSample(sampleDataView); } - } else if (value.byteLength === SINGLE_SAMPLE_LEN) { + } else if (value.byteLength === config.sampleLength) { processSample(new DataView(value.buffer)); } else { console.log("Unexpected packet length: " + value.byteLength); } } + const connectedDeviceRef = useRef(null); // UseRef for device tracking + const batteryCharacteristicRef = useRef(null); // Ref for battery characteristic + + const disconnectIntervalRef = useRef(null); + + // Update the connectBLE function async function connectBLE(): Promise { try { - setIsLoading(true); const nav = navigator as any; if (!nav.bluetooth) { console.log("Web Bluetooth API is not available in this browser."); + setIsLoading(false); return; } + const device = await nav.bluetooth.requestDevice({ filters: [{ namePrefix: "NPG" }], optionalServices: [SERVICE_UUID], }); + + // Update configuration based on device name + updateDeviceConfiguration(device.name || ""); + const server = await device.gatt?.connect(); if (!server) { + setIsLoading(false); return; } + connectedDeviceRef.current = device; const service = await server.getPrimaryService(SERVICE_UUID); const controlChar = await service.getCharacteristic(CONTROL_CHAR_UUID); const dataChar = await service.getCharacteristic(DATA_CHAR_UUID); + + // Try to get battery characteristic if device supports it + if (deviceConfigRef.current.hasBattery) { + try { + const batteryChar = await service.getCharacteristic(BATTERY_CHAR_UUID); + batteryCharacteristicRef.current = batteryChar; + await batteryChar.startNotifications(); + + batteryChar.addEventListener("characteristicvaluechanged", (event: any) => { + const target = event.target as BluetoothRemoteGATTCharacteristicExtended; + if (target.value && target.value.byteLength === 1) { + const batteryValue = target.value.getUint8(0); + setBatteryLevel(batteryValue); + } + }); + } catch (error) { + console.log("Battery characteristic not available:", error); + } + } + const encoder = new TextEncoder(); await controlChar.writeValue(encoder.encode("START")); await dataChar.startNotifications(); dataChar.addEventListener("characteristicvaluechanged", handleNotification); setIsConnected(true); - setInterval(() => { + setIsLoading(false); + + // Clear any existing interval first + if (disconnectIntervalRef.current) { + clearInterval(disconnectIntervalRef.current); + disconnectIntervalRef.current = null; + } + + // Create new interval for monitoring samples + disconnectIntervalRef.current = setInterval(() => { if (samplesReceivedRef.current === 0) { + console.log("No samples received in 1 second, disconnecting..."); + // Clear the interval before calling disconnect + if (disconnectIntervalRef.current) { + clearInterval(disconnectIntervalRef.current); + disconnectIntervalRef.current = null; + } disconnect(); - window.location.reload(); } samplesReceivedRef.current = 0; }, 1000); } catch (error) { console.log("Error: " + (error instanceof Error ? error.message : error)); setIsLoading(false); - } } + async function disconnect(): Promise { try { + setIsLoading(true); // Show loading while disconnecting + if (disconnectIntervalRef.current) { + clearInterval(disconnectIntervalRef.current); + disconnectIntervalRef.current = null; + } + + // Clear any existing timeouts/intervals + if (samplesReceivedRef.current > 0) { + // Clear the interval that checks for samples + const checkInterval = setInterval(() => { }); + clearInterval(checkInterval); + } + if (!connectedDeviceRef.current) { console.log("No connected device to disconnect."); + setIsLoading(false); return; } const server = connectedDeviceRef.current.gatt; if (!server) { + setIsLoading(false); return; } + try { + // Only try to stop notifications if server is connected + if (server.connected) { + const service = await server.getPrimaryService(SERVICE_UUID); - if (!server.connected) { + // Stop data notifications + try { + const dataChar = await service.getCharacteristic(DATA_CHAR_UUID); + await dataChar.stopNotifications(); + dataChar.removeEventListener("characteristicvaluechanged", handleNotification); + } catch (error) { + console.log("Error stopping data notifications:", error); + } + + // Send stop command if control characteristic exists + try { + const controlChar = await service.getCharacteristic(CONTROL_CHAR_UUID); + const encoder = new TextEncoder(); + await controlChar.writeValue(encoder.encode("STOP")); + } catch (error) { + console.log("Error sending STOP command:", error); + } + + // Disconnect from device + server.disconnect(); + } + } catch (error) { + console.log("Error during cleanup:", error); + } finally { + // Reset all states and refs connectedDeviceRef.current = null; setIsConnected(false); - return; - } + setBatteryLevel(null); + setDeviceName(""); - const service = await server.getPrimaryService(SERVICE_UUID); - const dataChar = await service.getCharacteristic(DATA_CHAR_UUID); - await dataChar.stopNotifications(); - dataChar.removeEventListener("characteristicvaluechanged", handleNotification); + // Reset device configuration to defaults + deviceConfigRef.current = defaultConfig; + setDeviceConfig(defaultConfig); + + // Reset sample tracking + prevSampleCounter = null; + channelData = []; + samplesReceivedRef.current = 0; + + // Reset recording state + isRecordingRef.current = false; + setIsrecord(true); + setRecordingElapsedTime(0); + + // Reset UI states + setIsDisplay(true); + pauseRef.current = true; - server.disconnect(); // Disconnect the device + // Clear buffers + recordingBuffers.forEach(buffer => buffer.length = 0); + activeBufferIndex = 0; + fillingindex.current = 0; - connectedDeviceRef.current = null; // Clear the global reference - setIsConnected(false); + // Clear lines and sweep positions + linesRef.current = []; + sweepPositions.current = new Array(6).fill(0); + currentSweepPos.current = new Array(6).fill(0); + + // Force re-render + setRefreshKey(prev => prev + 1); + + setIsLoading(false); + } } catch (error) { console.log("Error during disconnection: " + (error instanceof Error ? error.message : error)); + setIsLoading(false); } } @@ -527,10 +756,6 @@ const NPG_Ble = () => { } }; - useEffect(() => { - canvasElementCountRef.current = selectedChannels.length; - }, [selectedChannels]); - const setSelectedChannelsInWorker = (selectedChannels: number[]) => { if (!workerRef.current) { initializeWorker(); @@ -704,6 +929,7 @@ const NPG_Ble = () => { currentFileNameRef.current = filename; } }; + const stopRecording = async () => { if (!recordingStartTimeRef.current) { toast.error("Recording start time was not captured."); @@ -723,6 +949,7 @@ const NPG_Ble = () => { // Call fetchData after stopping the recording fetchData(); }; + const getFileCountFromIndexedDB = async (): Promise => { if (!workerRef.current) { initializeWorker(); @@ -767,6 +994,7 @@ const NPG_Ble = () => { // Clear the input field after handling setCustomTimeInput(""); }; + const formatTime = (milliseconds: number): string => { const date = new Date(milliseconds); const hours = String(date.getUTCHours()).padStart(2, '0'); @@ -777,7 +1005,7 @@ const NPG_Ble = () => { useEffect(() => { - const enabledChannels = Array.from({ length: maxCanvasElementCountRef.current }, (_, i) => i + 1); + const enabledChannels = Array.from({ length: deviceConfig.maxChannels }, (_, i) => i + 1); const allSelected = selectedChannels.length === enabledChannels.length; const onlyOneLeft = selectedChannels.length === enabledChannels.length - 1; @@ -786,10 +1014,11 @@ const NPG_Ble = () => { // Update the "Select All" button state setIsAllEnabledChannelSelected(allSelected); - }, [selectedChannels, maxCanvasElementCountRef.current, manuallySelected]); + }, [selectedChannels, deviceConfig.maxChannels, manuallySelected, refreshKey]); const toggleChannel = (channelIndex: number) => { setSelectedChannels((prevSelected) => { + setManuallySelected(true); const updatedChannels = prevSelected.includes(channelIndex) ? prevSelected.filter((ch) => ch !== channelIndex) : [...prevSelected, channelIndex]; @@ -802,12 +1031,8 @@ const NPG_Ble = () => { return sortedChannels; }); - - setManuallySelected(true); }; - - const updatePlots = useCallback( (data: number[], Zoom: number) => { // Access the latest selectedChannels via the ref @@ -829,6 +1054,7 @@ const NPG_Ble = () => { console.warn(`WebglPlot instance at index ${index} is undefined.`); } }); + linesRef.current.forEach((line, i) => { if (!line) { console.warn(`Line at index ${i} is undefined.`); @@ -844,7 +1070,6 @@ const NPG_Ble = () => { const channelData = data[channelNumber]; - // Ensure sweepPositions.current[i] is initialized if (sweepPositions.current[i] === undefined) { sweepPositions.current[i] = 0; @@ -904,14 +1129,69 @@ const NPG_Ble = () => { }, [animate, Zoom]); + // Function to get battery icon based on level + const getBatteryIcon = (level: number | null) => { + if (level === null) return ; + + if (level <= 10) return ; + if (level <= 20.0 && level > 10.0) return ; + if (level <= 70.0 && level > 20.0) return ; + if (level > 70) return ; + return ; + }; + useEffect(() => { + if (batteryLevel === null) return; + + let intervalId: number | undefined; + + // Handle battery level checks + if (batteryLevel <= 10) { + // Show immediate notification + toast.error("Very low battery! Please recharge immediately."); + + // Set up interval to show notification every minute + intervalId = window.setInterval(() => { + toast.error("Very low battery! Please recharge immediately."); + }, 60000); // 60000ms = 1 minute + + } else if (batteryLevel === 20) { + toast.warning("Battery is low at " + batteryLevel + "%. Consider recharging soon."); + } else if (batteryLevel === 70) { + toast.success("Battery level is " + batteryLevel + "%."); + } else if (batteryLevel === 99) { + toast.success("Battery fully charged."); + } + + // Cleanup function to clear interval when batteryLevel changes or component unmounts + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + + }, [batteryLevel]); + + // Function to get battery color based on level + const getBatteryColor = (level: number | null) => { + if (level === null) return "text-red-500"; + + if (level <= 20.0 && level > 10.0) return "text-red-500"; + if (level <= 70.0 && level > 20.0) return "text-orange-500"; + if (level > 70) return "text-green-500"; + return "text-green-500"; + }; return (
-
+
@@ -989,6 +1269,8 @@ const NPG_Ble = () => {
)} + + {/* Center-aligned buttons */} @@ -1029,6 +1311,7 @@ const NPG_Ble = () => { +
@@ -1172,7 +1455,7 @@ const NPG_Ble = () => { - +
{/* Channel Selection */} @@ -1403,7 +1686,7 @@ const NPG_Ble = () => { Channels Count: {selectedChannels.length} { - !(selectedChannels.length === maxCanvasElementCountRef.current && manuallySelected) && ( + !(selectedChannels.length === deviceConfig.maxChannels && manuallySelected) && ( + + + + + {!isConnected ? ( +
+

Device Not Connected!

+

Please connect your device to view battery status.

+
+ ) : ( + <> + {isOldfirmwareRef.current ? ( +
+

Old Firmware Detected

+ +

+ Update firmware using + NPG Lite Flasher + . +

+
+ ) : ( + <> + {batteryLevel === null ? ( +
+

Calibrating...

+
+ ) : ( +
+
+ {getBatteryIcon(batteryLevel)} + {batteryLevel}% +
+ + {batteryLevel < 10 && ( + + Low Battery + + )} +
+ )} + + )} + + )} + + +
+ +
diff --git a/tailwind.config.ts b/tailwind.config.ts index d7a802a..2228735 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -76,6 +76,10 @@ const config = { sm: "calc(var(--radius) - 4px)", }, keyframes: { + blink: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0.5' }, + }, "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, @@ -101,6 +105,8 @@ const config = { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", "typewriter": 'typewriter 5s steps(50) 1s infinite forwards, infinite', + 'blink': 'blink 1s ease-in-out infinite', + 'pulse-fast': 'pulse 0.5s ease-in-out infinite', }, }, }, diff --git a/tsconfig.json b/tsconfig.json index f381505..2ca745a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { @@ -42,4 +42,4 @@ "exclude": [ "node_modules" ] -} +}