use-green-mode 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 +74 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +87 -0
- package/dist/index.mjs +58 -0
- package/package.json +29 -0
- package/src/engine.ts +42 -0
- package/src/index.ts +2 -0
- package/src/useGreenMode.ts +25 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 🌿 use-green-mode
|
|
2
|
+
|
|
3
|
+
A lightweight, eco-friendly React hook that gracefully degrades your UI to save battery and data.
|
|
4
|
+
|
|
5
|
+
Modern web apps are heavy. When a user's device is dying or their connection drops, running 60fps animations and loading 4K images drains their battery faster. `use-green-mode` watches native browser APIs and gives you a simple set of boolean flags to turn off the heavy stuff when your users need it most.
|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
* **Zero React Context:** Powered by an independent Vanilla JS engine (Nano Stores).
|
|
9
|
+
* **Event-Driven:** Listens to the Battery Status API instead of expensive polling.
|
|
10
|
+
* **Network Aware:** Detects `Save-Data` headers and 2G/3G connections.
|
|
11
|
+
* **CPU Aware:** Checks `hardwareConcurrency` for low-end devices.
|
|
12
|
+
* **Tiny & Fast:** Built with `tsup` for maximum tree-shaking.
|
|
13
|
+
|
|
14
|
+
## 📦 Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install use-green-mode
|
|
18
|
+
# or
|
|
19
|
+
yarn add use-green-mode
|
|
20
|
+
```
|
|
21
|
+
# 🚀 Quick Start
|
|
22
|
+
|
|
23
|
+
Drop `useGreenMode` into your top-level component. It automatically calculates an `energyScore` (0 to 100) and toggles features based on safe defaults.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
import { useGreenMode } from 'use-green-mode';
|
|
27
|
+
import { motion } from 'framer-motion';
|
|
28
|
+
|
|
29
|
+
export function App() {
|
|
30
|
+
const { lowRes, stopAnimations, ecoTheme } = useGreenMode();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={ecoTheme ? 'theme-dark-high-contrast' : 'theme-standard'}>
|
|
34
|
+
{/* 1. Serve smaller images when data/battery is low */}
|
|
35
|
+
<img
|
|
36
|
+
src={lowRes ? '/hero-tiny.jpg' : '/hero-4k.jpg'}
|
|
37
|
+
alt="Hero Background"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
{/* 2. Kill heavy animations to save CPU/Battery */}
|
|
41
|
+
{!stopAnimations && (
|
|
42
|
+
<motion.div animate={{ rotate: 360 }} />
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
# ⚙️ Customization
|
|
49
|
+
|
|
50
|
+
You can pass custom thresholds to define exactly when your app goes into "survival mode."
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
const { stopAnimations, ecoTheme } = useGreenMode({
|
|
54
|
+
lowResAt: 80, // Trigger earlier
|
|
55
|
+
stopAnimationsAt: 50, // Default
|
|
56
|
+
ecoThemeAt: 15 // Only on extreme low battery
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
# 🛠️ Vanilla JS Usage
|
|
61
|
+
|
|
62
|
+
Not using React? You can use the core engine directly anywhere in your JavaScript.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
import { energyScore, initGreenObserver } from 'use-green-mode/engine';
|
|
66
|
+
|
|
67
|
+
initGreenObserver();
|
|
68
|
+
|
|
69
|
+
energyScore.subscribe((score) => {
|
|
70
|
+
if (score < 50) {
|
|
71
|
+
document.body.classList.add('stop-animations');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as nanostores from 'nanostores';
|
|
2
|
+
|
|
3
|
+
interface GreenThresholds {
|
|
4
|
+
lowResAt?: number;
|
|
5
|
+
stopAnimationsAt?: number;
|
|
6
|
+
ecoThemeAt?: number;
|
|
7
|
+
}
|
|
8
|
+
declare function useGreenMode(thresholds?: GreenThresholds): {
|
|
9
|
+
score: number;
|
|
10
|
+
lowRes: boolean;
|
|
11
|
+
stopAnimations: boolean;
|
|
12
|
+
ecoTheme: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
declare const energyScore: nanostores.PreinitializedWritableAtom<number> & object;
|
|
16
|
+
declare function initGreenObserver(): void;
|
|
17
|
+
|
|
18
|
+
export { energyScore, initGreenObserver, useGreenMode };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as nanostores from 'nanostores';
|
|
2
|
+
|
|
3
|
+
interface GreenThresholds {
|
|
4
|
+
lowResAt?: number;
|
|
5
|
+
stopAnimationsAt?: number;
|
|
6
|
+
ecoThemeAt?: number;
|
|
7
|
+
}
|
|
8
|
+
declare function useGreenMode(thresholds?: GreenThresholds): {
|
|
9
|
+
score: number;
|
|
10
|
+
lowRes: boolean;
|
|
11
|
+
stopAnimations: boolean;
|
|
12
|
+
ecoTheme: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
declare const energyScore: nanostores.PreinitializedWritableAtom<number> & object;
|
|
16
|
+
declare function initGreenObserver(): void;
|
|
17
|
+
|
|
18
|
+
export { energyScore, initGreenObserver, useGreenMode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
energyScore: () => energyScore,
|
|
24
|
+
initGreenObserver: () => initGreenObserver,
|
|
25
|
+
useGreenMode: () => useGreenMode
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/useGreenMode.ts
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var import_react2 = require("@nanostores/react");
|
|
32
|
+
|
|
33
|
+
// src/engine.ts
|
|
34
|
+
var import_nanostores = require("nanostores");
|
|
35
|
+
var energyScore = (0, import_nanostores.atom)(100);
|
|
36
|
+
var isBrowser = typeof window !== "undefined" && typeof navigator !== "undefined";
|
|
37
|
+
function initGreenObserver() {
|
|
38
|
+
if (!isBrowser) return;
|
|
39
|
+
const calculateScore = () => {
|
|
40
|
+
let score = 100;
|
|
41
|
+
if (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4) score -= 20;
|
|
42
|
+
const connection = navigator.connection;
|
|
43
|
+
if (connection) {
|
|
44
|
+
if (connection.saveData) score -= 40;
|
|
45
|
+
if (["slow-2g", "2g", "3g"].includes(connection.effectiveType)) score -= 30;
|
|
46
|
+
}
|
|
47
|
+
energyScore.set(Math.max(0, score));
|
|
48
|
+
};
|
|
49
|
+
if ("getBattery" in navigator) {
|
|
50
|
+
navigator.getBattery().then((battery) => {
|
|
51
|
+
const updateBatteryImpact = () => {
|
|
52
|
+
let currentScore = energyScore.get();
|
|
53
|
+
if (!battery.charging && battery.level <= 0.2) {
|
|
54
|
+
energyScore.set(Math.max(0, currentScore - 50));
|
|
55
|
+
} else {
|
|
56
|
+
calculateScore();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
updateBatteryImpact();
|
|
60
|
+
battery.addEventListener("levelchange", updateBatteryImpact);
|
|
61
|
+
battery.addEventListener("chargingchange", updateBatteryImpact);
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
calculateScore();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/useGreenMode.ts
|
|
69
|
+
function useGreenMode(thresholds = {}) {
|
|
70
|
+
const { lowResAt = 70, stopAnimationsAt = 50, ecoThemeAt = 30 } = thresholds;
|
|
71
|
+
const score = (0, import_react2.useStore)(energyScore);
|
|
72
|
+
(0, import_react.useEffect)(() => {
|
|
73
|
+
initGreenObserver();
|
|
74
|
+
}, []);
|
|
75
|
+
return {
|
|
76
|
+
score,
|
|
77
|
+
lowRes: score <= lowResAt,
|
|
78
|
+
stopAnimations: score <= stopAnimationsAt,
|
|
79
|
+
ecoTheme: score <= ecoThemeAt
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
energyScore,
|
|
85
|
+
initGreenObserver,
|
|
86
|
+
useGreenMode
|
|
87
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/useGreenMode.ts
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useStore } from "@nanostores/react";
|
|
4
|
+
|
|
5
|
+
// src/engine.ts
|
|
6
|
+
import { atom } from "nanostores";
|
|
7
|
+
var energyScore = atom(100);
|
|
8
|
+
var isBrowser = typeof window !== "undefined" && typeof navigator !== "undefined";
|
|
9
|
+
function initGreenObserver() {
|
|
10
|
+
if (!isBrowser) return;
|
|
11
|
+
const calculateScore = () => {
|
|
12
|
+
let score = 100;
|
|
13
|
+
if (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4) score -= 20;
|
|
14
|
+
const connection = navigator.connection;
|
|
15
|
+
if (connection) {
|
|
16
|
+
if (connection.saveData) score -= 40;
|
|
17
|
+
if (["slow-2g", "2g", "3g"].includes(connection.effectiveType)) score -= 30;
|
|
18
|
+
}
|
|
19
|
+
energyScore.set(Math.max(0, score));
|
|
20
|
+
};
|
|
21
|
+
if ("getBattery" in navigator) {
|
|
22
|
+
navigator.getBattery().then((battery) => {
|
|
23
|
+
const updateBatteryImpact = () => {
|
|
24
|
+
let currentScore = energyScore.get();
|
|
25
|
+
if (!battery.charging && battery.level <= 0.2) {
|
|
26
|
+
energyScore.set(Math.max(0, currentScore - 50));
|
|
27
|
+
} else {
|
|
28
|
+
calculateScore();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
updateBatteryImpact();
|
|
32
|
+
battery.addEventListener("levelchange", updateBatteryImpact);
|
|
33
|
+
battery.addEventListener("chargingchange", updateBatteryImpact);
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
calculateScore();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/useGreenMode.ts
|
|
41
|
+
function useGreenMode(thresholds = {}) {
|
|
42
|
+
const { lowResAt = 70, stopAnimationsAt = 50, ecoThemeAt = 30 } = thresholds;
|
|
43
|
+
const score = useStore(energyScore);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
initGreenObserver();
|
|
46
|
+
}, []);
|
|
47
|
+
return {
|
|
48
|
+
score,
|
|
49
|
+
lowRes: score <= lowResAt,
|
|
50
|
+
stopAnimations: score <= stopAnimationsAt,
|
|
51
|
+
ecoTheme: score <= ecoThemeAt
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
energyScore,
|
|
56
|
+
initGreenObserver,
|
|
57
|
+
useGreenMode
|
|
58
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "use-green-mode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
9
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "UdhayaKumar",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"description": "",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@nanostores/react": "^1.0.0",
|
|
19
|
+
"nanostores": "^1.1.1"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^19.2.4"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "^19.2.14",
|
|
26
|
+
"tsup": "^8.5.1",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { atom } from 'nanostores';
|
|
2
|
+
|
|
3
|
+
export const energyScore = atom<number>(100);
|
|
4
|
+
|
|
5
|
+
const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';
|
|
6
|
+
|
|
7
|
+
export function initGreenObserver() {
|
|
8
|
+
if (!isBrowser) return;
|
|
9
|
+
|
|
10
|
+
const calculateScore = () => {
|
|
11
|
+
let score = 100;
|
|
12
|
+
|
|
13
|
+
if (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4) score -= 20;
|
|
14
|
+
|
|
15
|
+
const connection = (navigator as any).connection;
|
|
16
|
+
if (connection) {
|
|
17
|
+
if (connection.saveData) score -= 40;
|
|
18
|
+
if (['slow-2g', '2g', '3g'].includes(connection.effectiveType)) score -= 30;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
energyScore.set(Math.max(0, score));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if ('getBattery' in navigator) {
|
|
25
|
+
(navigator as any).getBattery().then((battery: any) => {
|
|
26
|
+
const updateBatteryImpact = () => {
|
|
27
|
+
let currentScore = energyScore.get();
|
|
28
|
+
if (!battery.charging && battery.level <= 0.2) {
|
|
29
|
+
energyScore.set(Math.max(0, currentScore - 50));
|
|
30
|
+
} else {
|
|
31
|
+
calculateScore();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
updateBatteryImpact();
|
|
36
|
+
battery.addEventListener('levelchange', updateBatteryImpact);
|
|
37
|
+
battery.addEventListener('chargingchange', updateBatteryImpact);
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
calculateScore();
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { energyScore, initGreenObserver } from './engine';
|
|
4
|
+
|
|
5
|
+
interface GreenThresholds {
|
|
6
|
+
lowResAt?: number;
|
|
7
|
+
stopAnimationsAt?: number;
|
|
8
|
+
ecoThemeAt?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useGreenMode(thresholds: GreenThresholds = {}) {
|
|
12
|
+
const { lowResAt = 70, stopAnimationsAt = 50, ecoThemeAt = 30 } = thresholds;
|
|
13
|
+
const score = useStore(energyScore);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
initGreenObserver();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
score,
|
|
21
|
+
lowRes: score <= lowResAt,
|
|
22
|
+
stopAnimations: score <= stopAnimationsAt,
|
|
23
|
+
ecoTheme: score <= ecoThemeAt,
|
|
24
|
+
};
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"jsx": "react",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"moduleResolution": "node"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|