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 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
+ ```
@@ -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 };
@@ -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,2 @@
1
+ export { useGreenMode } from './useGreenMode';
2
+ export { energyScore, initGreenObserver } from './engine';
@@ -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
+ }