react-time-machine-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # react-time-machine-js
2
+
3
+ A React component package that provides a ready-to-drop-in dev widget for any React app, powered by [time-machine-js](https://www.npmjs.com/package/time-machine-js).
4
+
5
+ It provides a minimal UI to control the global `Date.now()` patch, allowing you to simulate different points in time (flowing or frozen) during development.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install react-time-machine-js
11
+ ```
12
+
13
+ > [!NOTE]
14
+ > `react` and `react-dom` (>=17) are peer dependencies and must be provided by the consumer.
15
+
16
+ ## Usage
17
+
18
+ Render the `<TimeMachine />` component anywhere in your app. It is recommended to wrap it in a development guard.
19
+
20
+ ```tsx
21
+ import { TimeMachine } from 'react-time-machine-js';
22
+
23
+ function App() {
24
+ return (
25
+ <div>
26
+ {/* ... your app ... */}
27
+
28
+ {process.env.NODE_ENV === 'development' && <TimeMachine />}
29
+ </div>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### Props
37
+
38
+ | Prop | Type | Default | Description |
39
+ | :--- | :--- | :--- | :--- |
40
+ | `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Widget corner position. |
41
+ | `storageKey` | `string` | `'__timeMachine__'` | LocalStorage key for persistence. |
42
+ | `onTravel` | `(timestamp: number, mode: 'flowing' | 'frozen') => void` | - | Callback on activation. |
43
+ | `onReturnToPresent` | `() => void` | - | Callback on reset. |
44
+
45
+ ## Behavior
46
+
47
+ - **Auto-Restore**: On mount, the component automatically restores any saved state from `localStorage`.
48
+ - **Auto-Cleanup**: On unmount, the component calls `returnToPresent()` to restore the native `Date.now()`.
49
+ - **Persistence**: Clicking "Activate" saves the state to `localStorage`. Clicking "Reset" clears the saved state.
50
+ - **Sync**: The widget polls the current simulated time every second to keep the status display in sync.
51
+
52
+ ## Styling
53
+
54
+ Styles are fully self-contained using inline CSS. No external stylesheets or configuration required. The widget uses a high z-index (`9999`) to stay on top of your application.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import './TimeMachine.css';
3
+ export interface TimeMachineProps {
4
+ /**
5
+ * Position of the widget on the screen.
6
+ * @default 'bottom-right'
7
+ */
8
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
9
+ /**
10
+ * The localStorage key used to persist the time machine state.
11
+ * @default '__timeMachine__'
12
+ */
13
+ storageKey?: string;
14
+ /**
15
+ * Callback fired when a new time/mode is activated.
16
+ */
17
+ onTravel?: (timestamp: number, mode: 'flowing' | 'frozen') => void;
18
+ /**
19
+ * Callback fired when the time machine is reset.
20
+ */
21
+ onReturnToPresent?: () => void;
22
+ }
23
+ export declare const TimeMachine: React.FC<TimeMachineProps>;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { travel, returnToPresent, isActive, save, restore, getMode } from 'time-machine-js';
4
+ import './TimeMachine.css';
5
+ let isRestored = false;
6
+ export const TimeMachine = ({ position = 'bottom-right', storageKey = '__timeMachine__', onTravel, onReturnToPresent, }) => {
7
+ if (!isRestored) {
8
+ restore(storageKey);
9
+ isRestored = true;
10
+ }
11
+ const [isExpanded, setIsExpanded] = useState(false);
12
+ const [active, setActive] = useState(isActive());
13
+ const [displayTime, setDisplayTime] = useState(Date.now());
14
+ const [mode, setMode] = useState('flowing');
15
+ const [inputTime, setInputTime] = useState('');
16
+ useEffect(() => {
17
+ const update = () => {
18
+ setActive(isActive());
19
+ setDisplayTime(Date.now());
20
+ };
21
+ const interval = setInterval(update, 1000);
22
+ return () => clearInterval(interval);
23
+ }, []);
24
+ useEffect(() => {
25
+ return () => {
26
+ returnToPresent();
27
+ };
28
+ }, []);
29
+ const handleToggleExpand = () => {
30
+ if (!isExpanded) {
31
+ const now = new Date();
32
+ const pad = (n) => n.toString().padStart(2, '0');
33
+ const localStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
34
+ setInputTime(localStr);
35
+ const currentMode = getMode();
36
+ if (currentMode) {
37
+ setMode(currentMode);
38
+ }
39
+ }
40
+ setIsExpanded(!isExpanded);
41
+ };
42
+ const handleActivate = () => {
43
+ const timestamp = new Date(inputTime).getTime();
44
+ if (isNaN(timestamp))
45
+ return;
46
+ travel(timestamp, mode);
47
+ save(storageKey);
48
+ setActive(true);
49
+ if (onTravel)
50
+ onTravel(timestamp, mode);
51
+ };
52
+ const handleReset = () => {
53
+ returnToPresent();
54
+ localStorage.removeItem(storageKey);
55
+ setActive(false);
56
+ if (onReturnToPresent)
57
+ onReturnToPresent();
58
+ };
59
+ const getStatusText = () => {
60
+ if (!active)
61
+ return '● Real time';
62
+ const dateStr = new Date(displayTime).toLocaleString();
63
+ return `● ${mode.charAt(0).toUpperCase() + mode.slice(1)}: ${dateStr}`;
64
+ };
65
+ return (_jsxs("div", { className: `time-machine-widget position-${position}`, children: [_jsx("div", { className: "time-machine-status-bar", onClick: handleToggleExpand, children: _jsx("span", { className: active ? 'time-machine-status-bar-active' : 'time-machine-status-bar-inactive', children: getStatusText() }) }), _jsxs("div", { className: `time-machine-panel ${!isExpanded ? 'time-machine-panel-hidden' : ''}`, children: [_jsxs("div", { className: "time-machine-input-group", children: [_jsx("label", { children: "Target Date/Time:" }), _jsx("input", { type: "datetime-local", className: "time-machine-input", value: inputTime, onChange: (e) => setInputTime(e.target.value) })] }), _jsxs("div", { className: "time-machine-input-group", children: [_jsx("label", { children: "Mode:" }), _jsxs("div", { className: "time-machine-toggle", children: [_jsx("div", { className: `time-machine-toggle-option ${mode === 'flowing' ? 'time-machine-toggle-option-active' : ''}`, onClick: () => setMode('flowing'), children: "Flowing" }), _jsx("div", { className: `time-machine-toggle-option ${mode === 'frozen' ? 'time-machine-toggle-option-active' : ''}`, onClick: () => setMode('frozen'), children: "Frozen" })] })] }), _jsx("button", { className: "time-machine-button", onClick: handleActivate, children: "Activate" }), active && (_jsx("button", { className: "time-machine-button time-machine-button-reset", onClick: handleReset, children: "Reset to Present" }))] })] }));
66
+ };
@@ -0,0 +1 @@
1
+ export * from './TimeMachine';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './TimeMachine';
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "react-time-machine-js",
3
+ "version": "1.0.0",
4
+ "description": "Ready-to-drop-in React dev widget for time-machine-js",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "keywords": [
13
+ "react",
14
+ "time-machine",
15
+ "testing",
16
+ "development",
17
+ "date-monkey-patch"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/WilliamPinto-Olmos/react-time-machine-js.git"
24
+ },
25
+ "dependencies": {
26
+ "time-machine-js": "latest"
27
+ },
28
+ "peerDependencies": {
29
+ "react": ">=17",
30
+ "react-dom": ">=17"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.0.0",
34
+ "@types/react-dom": "^18.0.0",
35
+ "react": "^18.0.0",
36
+ "react-dom": "^18.0.0",
37
+ "typescript": "^5.0.0"
38
+ }
39
+ }
@@ -0,0 +1,105 @@
1
+ .time-machine-widget {
2
+ position: fixed;
3
+ z-index: 9999;
4
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
5
+ font-size: 12px;
6
+ background-color: rgba(0, 0, 0, 0.85);
7
+ color: #fff;
8
+ border-radius: 4px;
9
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
10
+ display: flex;
11
+ flex-direction: column;
12
+ overflow: hidden;
13
+ border: 1px solid rgba(255,255,255,0.1);
14
+ transition: all 0.2s ease-in-out;
15
+ user-select: none;
16
+ }
17
+
18
+ .time-machine-widget.position-bottom-right { bottom: 20px; right: 20px; }
19
+ .time-machine-widget.position-bottom-left { bottom: 20px; left: 20px; }
20
+ .time-machine-widget.position-top-right { top: 20px; right: 20px; }
21
+ .time-machine-widget.position-top-left { top: 20px; left: 20px; }
22
+
23
+ .time-machine-status-bar {
24
+ padding: 8px 12px;
25
+ cursor: pointer;
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 8px;
29
+ white-space: nowrap;
30
+ font-weight: 500;
31
+ }
32
+
33
+ .time-machine-status-bar-active {
34
+ color: #10b981;
35
+ }
36
+
37
+ .time-machine-status-bar-inactive {
38
+ color: #6b7280;
39
+ }
40
+
41
+ .time-machine-panel {
42
+ padding: 12px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 10px;
46
+ border-top: 1px solid rgba(255,255,255,0.1);
47
+ width: 240px;
48
+ }
49
+
50
+ .time-machine-panel-hidden {
51
+ display: none !important;
52
+ }
53
+
54
+ .time-machine-input-group {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 4px;
58
+ }
59
+
60
+ .time-machine-input {
61
+ background-color: #333;
62
+ border: 1px solid #444;
63
+ color: #fff;
64
+ padding: 4px 8px;
65
+ border-radius: 3px;
66
+ font-size: 12px;
67
+ width: 100%;
68
+ box-sizing: border-box;
69
+ }
70
+
71
+ .time-machine-toggle {
72
+ display: flex;
73
+ background-color: #222;
74
+ border-radius: 3px;
75
+ padding: 2px;
76
+ }
77
+
78
+ .time-machine-toggle-option {
79
+ flex: 1;
80
+ padding: 4px;
81
+ text-align: center;
82
+ cursor: pointer;
83
+ border-radius: 2px;
84
+ background-color: transparent;
85
+ transition: background 0.2s;
86
+ }
87
+
88
+ .time-machine-toggle-option-active {
89
+ background-color: #444;
90
+ }
91
+
92
+ .time-machine-button {
93
+ background-color: #3d82f6;
94
+ color: #fff;
95
+ border: none;
96
+ padding: 6px 12px;
97
+ border-radius: 3px;
98
+ cursor: pointer;
99
+ font-size: 12px;
100
+ font-weight: bold;
101
+ }
102
+
103
+ .time-machine-button-reset {
104
+ background-color: #ef4444;
105
+ }
@@ -0,0 +1,147 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { travel, returnToPresent, getOffset, isActive, save, restore, TimeMachineMode, getMode } from 'time-machine-js';
3
+ import './TimeMachine.css';
4
+
5
+ export interface TimeMachineProps {
6
+ /**
7
+ * Position of the widget on the screen.
8
+ * @default 'bottom-right'
9
+ */
10
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
11
+ /**
12
+ * The localStorage key used to persist the time machine state.
13
+ * @default '__timeMachine__'
14
+ */
15
+ storageKey?: string;
16
+ /**
17
+ * Callback fired when a new time/mode is activated.
18
+ */
19
+ onTravel?: (timestamp: number, mode: 'flowing' | 'frozen') => void;
20
+ /**
21
+ * Callback fired when the time machine is reset.
22
+ */
23
+ onReturnToPresent?: () => void;
24
+ }
25
+
26
+ let isRestored = false;
27
+
28
+ export const TimeMachine: React.FC<TimeMachineProps> = ({
29
+ position = 'bottom-right',
30
+ storageKey = '__timeMachine__',
31
+ onTravel,
32
+ onReturnToPresent,
33
+ }) => {
34
+ if (!isRestored) {
35
+ restore(storageKey);
36
+ isRestored = true;
37
+ }
38
+
39
+ const [isExpanded, setIsExpanded] = useState(false);
40
+ const [active, setActive] = useState(isActive());
41
+ const [displayTime, setDisplayTime] = useState(Date.now());
42
+ const [mode, setMode] = useState<TimeMachineMode>('flowing');
43
+ const [inputTime, setInputTime] = useState('');
44
+
45
+ useEffect(() => {
46
+ const update = () => {
47
+ setActive(isActive());
48
+ setDisplayTime(Date.now());
49
+ };
50
+
51
+ const interval = setInterval(update, 1000);
52
+ return () => clearInterval(interval);
53
+ }, []);
54
+
55
+ useEffect(() => {
56
+ return () => {
57
+ returnToPresent();
58
+ };
59
+ }, []);
60
+
61
+ const handleToggleExpand = () => {
62
+ if (!isExpanded) {
63
+ const now = new Date();
64
+ const pad = (n: number) => n.toString().padStart(2, '0');
65
+ const localStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
66
+ setInputTime(localStr);
67
+ const currentMode = getMode();
68
+ if (currentMode) {
69
+ setMode(currentMode);
70
+ }
71
+ }
72
+ setIsExpanded(!isExpanded);
73
+ };
74
+
75
+ const handleActivate = () => {
76
+ const timestamp = new Date(inputTime).getTime();
77
+ if (isNaN(timestamp)) return;
78
+
79
+ travel(timestamp, mode);
80
+ save(storageKey);
81
+ setActive(true);
82
+ if (onTravel) onTravel(timestamp, mode);
83
+ };
84
+
85
+ const handleReset = () => {
86
+ returnToPresent();
87
+ localStorage.removeItem(storageKey);
88
+ setActive(false);
89
+ if (onReturnToPresent) onReturnToPresent();
90
+ };
91
+
92
+ const getStatusText = () => {
93
+ if (!active) return '● Real time';
94
+ const dateStr = new Date(displayTime).toLocaleString();
95
+ return `● ${mode.charAt(0).toUpperCase() + mode.slice(1)}: ${dateStr}`;
96
+ };
97
+
98
+ return (
99
+ <div className={`time-machine-widget position-${position}`}>
100
+ <div className="time-machine-status-bar" onClick={handleToggleExpand}>
101
+ <span className={active ? 'time-machine-status-bar-active' : 'time-machine-status-bar-inactive'}>
102
+ {getStatusText()}
103
+ </span>
104
+ </div>
105
+
106
+ <div className={`time-machine-panel ${!isExpanded ? 'time-machine-panel-hidden' : ''}`}>
107
+ <div className="time-machine-input-group">
108
+ <label>Target Date/Time:</label>
109
+ <input
110
+ type="datetime-local"
111
+ className="time-machine-input"
112
+ value={inputTime}
113
+ onChange={(e) => setInputTime(e.target.value)}
114
+ />
115
+ </div>
116
+
117
+ <div className="time-machine-input-group">
118
+ <label>Mode:</label>
119
+ <div className="time-machine-toggle">
120
+ <div
121
+ className={`time-machine-toggle-option ${mode === 'flowing' ? 'time-machine-toggle-option-active' : ''}`}
122
+ onClick={() => setMode('flowing')}
123
+ >
124
+ Flowing
125
+ </div>
126
+ <div
127
+ className={`time-machine-toggle-option ${mode === 'frozen' ? 'time-machine-toggle-option-active' : ''}`}
128
+ onClick={() => setMode('frozen')}
129
+ >
130
+ Frozen
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <button className="time-machine-button" onClick={handleActivate}>
136
+ Activate
137
+ </button>
138
+
139
+ {active && (
140
+ <button className="time-machine-button time-machine-button-reset" onClick={handleReset}>
141
+ Reset to Present
142
+ </button>
143
+ )}
144
+ </div>
145
+ </div>
146
+ );
147
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './TimeMachine';
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
7
+ "allowJs": true,
8
+ "skipLibCheck": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "strict": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "declaration": true,
15
+ "outDir": "dist",
16
+ "jsx": "react-jsx"
17
+ },
18
+ "include": ["src"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }