@opstel/modern-gauges 0.1.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,110 @@
1
+ # modern-gauge
2
+
3
+ An animated half-circle SVG gauge component for React. Zero runtime dependencies beyond React itself.
4
+
5
+ ## Features
6
+
7
+ - Smooth animated arc fill that interpolates from the previous value on each update
8
+ - Gradient color progression across the arc and tick marks
9
+ - Automatically generated minor tick marks between major labels
10
+ - Responsive — fills its container via SVG `viewBox`
11
+ - Optional card wrapper with shadow, or render bare into any layout
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install modern-gauge
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```jsx
22
+ import { HalfGauge } from 'modern-gauge';
23
+
24
+ function Dashboard() {
25
+ return (
26
+ <HalfGauge
27
+ value={72}
28
+ ticks={[0, 20, 40, 60, 80, 100]}
29
+ colors={['#f44336', '#ffeb3b', '#4caf50']}
30
+ title="CPU (%)"
31
+ />
32
+ );
33
+ }
34
+ ```
35
+
36
+ ## Props
37
+
38
+ | Prop | Type | Default | Description |
39
+ |---|---|---|---|
40
+ | `value` | `number` | — | Current value to display |
41
+ | `ticks` | `number[]` | `[0, 5, 10, 15, 20]` | Ordered scale values. First = min, last = max. Labels are rendered at each tick; minor ticks are inserted automatically between them. |
42
+ | `colors` | `string[]` | `['#f44336', '#9c27b0']` | Two or more CSS colors forming the gradient from min to max. |
43
+ | `title` | `string` | — | Optional label displayed below the value |
44
+ | `withoutBackground` | `boolean` | `false` | When `true`, renders only the gauge SVG with no card wrapper. Use this when embedding inside your own container. |
45
+ | `className` | `string` | — | CSS class applied to the card wrapper (ignored when `withoutBackground` is true) |
46
+ | `style` | `object` | — | Inline styles merged into the card wrapper (ignored when `withoutBackground` is true) |
47
+
48
+ ## Examples
49
+
50
+ ### Minimal
51
+
52
+ ```jsx
53
+ <HalfGauge value={12} />
54
+ ```
55
+
56
+ ### Custom scale and colors
57
+
58
+ ```jsx
59
+ <HalfGauge
60
+ value={3500}
61
+ ticks={[0, 1000, 2000, 3000, 4000, 5000, 6000]}
62
+ colors={['#4caf50', '#ffeb3b', '#f44336']}
63
+ title="RPM"
64
+ />
65
+ ```
66
+
67
+ ### Embedded in a custom container (no card)
68
+
69
+ ```jsx
70
+ <div style={{ width: 200, height: 130 }}>
71
+ <HalfGauge
72
+ value={55}
73
+ ticks={[0, 25, 50, 75, 100]}
74
+ colors={['#2196f3', '#e91e63']}
75
+ withoutBackground
76
+ />
77
+ </div>
78
+ ```
79
+
80
+ ### Custom card size via style prop
81
+
82
+ ```jsx
83
+ <HalfGauge
84
+ value={8}
85
+ ticks={[0, 2, 4, 6, 8, 10]}
86
+ title="Score"
87
+ style={{ width: '12rem', height: '10rem', flexGrow: 0 }}
88
+ />
89
+ ```
90
+
91
+ ## Tick behaviour
92
+
93
+ The `ticks` array defines where major labels appear. Minor ticks are inserted automatically:
94
+
95
+ - **≤ 5 major ticks** → 4 minor ticks per gap
96
+ - **6–16 major ticks** → 2 minor ticks per gap
97
+ - **> 16 major ticks** → no minor ticks; every other label is hidden to avoid crowding
98
+
99
+ ## Peer Dependencies
100
+
101
+ ```json
102
+ {
103
+ "react": ">=17.0.0",
104
+ "react-dom": ">=17.0.0"
105
+ }
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,17 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),y=require("react");(function(){if(typeof document>"u")return;const a="__modern-half-gauge-styles__";if(document.getElementById(a))return;const o=document.createElement("style");o.id=a,o.textContent=`
2
+ @keyframes modern-gauge-fill-arc {
3
+ from { stroke-dasharray: var(--mg-initial-dash, 0) 500; }
4
+ to { stroke-dasharray: var(--mg-final-dash, 0) 500; }
5
+ }
6
+ @keyframes modern-gauge-fill-tick {
7
+ from { stroke: #9e9e9e; }
8
+ to { stroke: var(--mg-tick-color, #9e9e9e); }
9
+ }
10
+ .modern-gauge-arc {
11
+ animation: modern-gauge-fill-arc 1s ease-out forwards;
12
+ }
13
+ .modern-gauge-tick {
14
+ animation: modern-gauge-fill-tick 1s ease-out forwards;
15
+ animation-delay: var(--mg-tick-delay, 0s);
16
+ }
17
+ `,document.head.appendChild(o)})();const C=({value:h,colors:a,title:o,minMax:f})=>{const n=f||[0,5,10,15,20],m=n.at(0)??0,Y=n.at(-1)??1,g=h??m,x=y.useRef(`mg-${Math.random().toString(36).substring(2,9)}`).current,p=y.useRef(m);y.useEffect(()=>{const e=setTimeout(()=>{p.current=g},1e3);return()=>clearTimeout(e)},[g]);const j=Y-m||1,b=Math.min(1,Math.max(0,(p.current-m)/j)),v=Math.min(1,Math.max(0,(g-m)/j)),M=Math.PI*40,L=v*M,w=b*M,S=n.length>8;let c=[];if(n.length<17){const e=n.length<6?4:2,i=e+1;for(let r=0;r<n.length;r++)if(c.push({value:n[r],isMajor:!0}),r<n.length-1){const d=n[r],l=(n[r+1]-d)/i;for(let u=1;u<=e;u++)c.push({value:d+u*l,isMajor:!1})}}else c=n.map(e=>({value:e,isMajor:!0}));const I=c.filter(e=>e.isMajor),X=c.map((e,i)=>{const r=c.length-1,s=(i/r*180-180)*(Math.PI/180),l=36,u=e.isMajor?5:2.5,T=I.indexOf(e),F=e.isMajor&&(!S||T%2===0),R=50+l*Math.cos(s),A=45+l*Math.sin(s),E=50+(l-u)*Math.cos(s),G=45+(l-u)*Math.sin(s),k=l-10,P=50+k*Math.cos(s),W=45+k*Math.sin(s);return{value:e.value,innerX:E,innerY:G,outerX:R,outerY:A,labelX:P,labelY:W,showLabel:F}}),$=a&&a.length>=2?a:["#f44336","#9c27b0"];return t.jsx("div",{style:{height:"100%",width:"100%"},children:t.jsxs("div",{style:{width:"100%",height:"100%",position:"relative"},children:[t.jsxs("svg",{width:"100%",height:"100%",viewBox:"0 0 100 55",children:[t.jsx("defs",{children:t.jsx("linearGradient",{id:`gradient-${x}`,gradientUnits:"userSpaceOnUse",x1:10,y1:45,x2:90,y2:45,children:$.map((e,i)=>t.jsx("stop",{offset:`${i/($.length-1)*100}%`,stopColor:e},i))})}),t.jsx("path",{stroke:"#9e9e9e",strokeWidth:"3",strokeLinecap:"round",fill:"none",d:"M 10 45 A 40 40 0 1 1 90 45"}),t.jsx("path",{className:"modern-gauge-arc",fill:"none",strokeWidth:"3",stroke:`url(#gradient-${x})`,strokeLinecap:"round",strokeDashoffset:"0",style:{"--mg-initial-dash":`${w}`,"--mg-final-dash":`${L}`},d:"M 10 45 A 40 40 0 1 1 90 45"}),X.map((e,i)=>{const r=i/(X.length-1),d=r<=v,s=r;return t.jsxs("g",{children:[t.jsx("line",{x1:e.innerX,y1:e.innerY,x2:e.outerX,y2:e.outerY,strokeWidth:"1",strokeLinecap:"round",stroke:"#9e9e9e"}),t.jsx("line",{className:"modern-gauge-tick",x1:e.innerX,y1:e.innerY,x2:e.outerX,y2:e.outerY,strokeWidth:"1",strokeLinecap:"round",style:{"--mg-tick-color":`url(#gradient-${x})`,"--mg-tick-delay":d?`${s}s`:"0s",animationPlayState:d?"running":"paused"}}),e.showLabel&&t.jsx("text",{x:e.labelX,y:e.labelY,textAnchor:"middle",dominantBaseline:"middle",fontSize:"5",fill:"#9e9e9e",fontFamily:"sans-serif",children:e.value})]},e.value)})]}),t.jsxs("div",{style:{position:"absolute",top:"70%",left:"50%",transform:"translate(-50%, -50%)",textAlign:"center",pointerEvents:"none",whiteSpace:"nowrap"},children:[t.jsx("div",{style:{fontSize:"3rem",fontWeight:500,lineHeight:1.2},children:h}),o&&t.jsx("div",{style:{fontSize:"0.9rem",marginTop:"4px"},children:o})]})]},g)})},O=({value:h,colors:a,title:o,minMax:f})=>t.jsx(C,{value:h,colors:a,title:o,minMax:f});exports.HalfGauge=O;
@@ -0,0 +1,198 @@
1
+ import { jsx as t, jsxs as f } from "react/jsx-runtime";
2
+ import { useRef as j, useEffect as V } from "react";
3
+ (function() {
4
+ if (typeof document > "u") return;
5
+ const o = "__modern-half-gauge-styles__";
6
+ if (document.getElementById(o)) return;
7
+ const s = document.createElement("style");
8
+ s.id = o, s.textContent = `
9
+ @keyframes modern-gauge-fill-arc {
10
+ from { stroke-dasharray: var(--mg-initial-dash, 0) 500; }
11
+ to { stroke-dasharray: var(--mg-final-dash, 0) 500; }
12
+ }
13
+ @keyframes modern-gauge-fill-tick {
14
+ from { stroke: #9e9e9e; }
15
+ to { stroke: var(--mg-tick-color, #9e9e9e); }
16
+ }
17
+ .modern-gauge-arc {
18
+ animation: modern-gauge-fill-arc 1s ease-out forwards;
19
+ }
20
+ .modern-gauge-tick {
21
+ animation: modern-gauge-fill-tick 1s ease-out forwards;
22
+ animation-delay: var(--mg-tick-delay, 0s);
23
+ }
24
+ `, document.head.appendChild(s);
25
+ })();
26
+ const _ = ({ value: m, colors: o, title: s, minMax: p }) => {
27
+ const n = p || [0, 5, 10, 15, 20], h = n.at(0) ?? 0, b = n.at(-1) ?? 1, g = m ?? h, y = j(`mg-${Math.random().toString(36).substring(2, 9)}`).current, x = j(h);
28
+ V(() => {
29
+ const e = setTimeout(() => {
30
+ x.current = g;
31
+ }, 1e3);
32
+ return () => clearTimeout(e);
33
+ }, [g]);
34
+ const v = b - h || 1, L = Math.min(
35
+ 1,
36
+ Math.max(0, (x.current - h) / v)
37
+ ), M = Math.min(
38
+ 1,
39
+ Math.max(0, (g - h) / v)
40
+ ), X = Math.PI * 40, w = M * X, S = L * X, I = n.length > 8;
41
+ let c = [];
42
+ if (n.length < 17) {
43
+ const e = n.length < 6 ? 4 : 2, i = e + 1;
44
+ for (let r = 0; r < n.length; r++)
45
+ if (c.push({ value: n[r], isMajor: !0 }), r < n.length - 1) {
46
+ const d = n[r], l = (n[r + 1] - d) / i;
47
+ for (let u = 1; u <= e; u++)
48
+ c.push({ value: d + u * l, isMajor: !1 });
49
+ }
50
+ } else
51
+ c = n.map((e) => ({ value: e, isMajor: !0 }));
52
+ const F = c.filter((e) => e.isMajor), $ = c.map((e, i) => {
53
+ const r = c.length - 1, a = (i / r * 180 - 180) * (Math.PI / 180), l = 36, u = e.isMajor ? 5 : 2.5, T = F.indexOf(e), A = e.isMajor && (!I || T % 2 === 0), E = 50 + l * Math.cos(a), W = 45 + l * Math.sin(a), C = 50 + (l - u) * Math.cos(a), G = 45 + (l - u) * Math.sin(a), Y = l - 10, P = 50 + Y * Math.cos(a), R = 45 + Y * Math.sin(a);
54
+ return {
55
+ value: e.value,
56
+ innerX: C,
57
+ innerY: G,
58
+ outerX: E,
59
+ outerY: W,
60
+ labelX: P,
61
+ labelY: R,
62
+ showLabel: A
63
+ };
64
+ }), k = o && o.length >= 2 ? o : ["#f44336", "#9c27b0"];
65
+ return /* @__PURE__ */ t("div", { style: { height: "100%", width: "100%" }, children: /* @__PURE__ */ f(
66
+ "div",
67
+ {
68
+ style: { width: "100%", height: "100%", position: "relative" },
69
+ children: [
70
+ /* @__PURE__ */ f("svg", { width: "100%", height: "100%", viewBox: "0 0 100 55", children: [
71
+ /* @__PURE__ */ t("defs", { children: /* @__PURE__ */ t(
72
+ "linearGradient",
73
+ {
74
+ id: `gradient-${y}`,
75
+ gradientUnits: "userSpaceOnUse",
76
+ x1: 10,
77
+ y1: 45,
78
+ x2: 90,
79
+ y2: 45,
80
+ children: k.map((e, i) => /* @__PURE__ */ t(
81
+ "stop",
82
+ {
83
+ offset: `${i / (k.length - 1) * 100}%`,
84
+ stopColor: e
85
+ },
86
+ i
87
+ ))
88
+ }
89
+ ) }),
90
+ /* @__PURE__ */ t(
91
+ "path",
92
+ {
93
+ stroke: "#9e9e9e",
94
+ strokeWidth: "3",
95
+ strokeLinecap: "round",
96
+ fill: "none",
97
+ d: "M 10 45 A 40 40 0 1 1 90 45"
98
+ }
99
+ ),
100
+ /* @__PURE__ */ t(
101
+ "path",
102
+ {
103
+ className: "modern-gauge-arc",
104
+ fill: "none",
105
+ strokeWidth: "3",
106
+ stroke: `url(#gradient-${y})`,
107
+ strokeLinecap: "round",
108
+ strokeDashoffset: "0",
109
+ style: {
110
+ "--mg-initial-dash": `${S}`,
111
+ "--mg-final-dash": `${w}`
112
+ },
113
+ d: "M 10 45 A 40 40 0 1 1 90 45"
114
+ }
115
+ ),
116
+ $.map((e, i) => {
117
+ const r = i / ($.length - 1), d = r <= M, a = r;
118
+ return /* @__PURE__ */ f("g", { children: [
119
+ /* @__PURE__ */ t(
120
+ "line",
121
+ {
122
+ x1: e.innerX,
123
+ y1: e.innerY,
124
+ x2: e.outerX,
125
+ y2: e.outerY,
126
+ strokeWidth: "1",
127
+ strokeLinecap: "round",
128
+ stroke: "#9e9e9e"
129
+ }
130
+ ),
131
+ /* @__PURE__ */ t(
132
+ "line",
133
+ {
134
+ className: "modern-gauge-tick",
135
+ x1: e.innerX,
136
+ y1: e.innerY,
137
+ x2: e.outerX,
138
+ y2: e.outerY,
139
+ strokeWidth: "1",
140
+ strokeLinecap: "round",
141
+ style: {
142
+ "--mg-tick-color": `url(#gradient-${y})`,
143
+ "--mg-tick-delay": d ? `${a}s` : "0s",
144
+ animationPlayState: d ? "running" : "paused"
145
+ }
146
+ }
147
+ ),
148
+ e.showLabel && /* @__PURE__ */ t(
149
+ "text",
150
+ {
151
+ x: e.labelX,
152
+ y: e.labelY,
153
+ textAnchor: "middle",
154
+ dominantBaseline: "middle",
155
+ fontSize: "5",
156
+ fill: "#9e9e9e",
157
+ fontFamily: "sans-serif",
158
+ children: e.value
159
+ }
160
+ )
161
+ ] }, e.value);
162
+ })
163
+ ] }),
164
+ /* @__PURE__ */ f(
165
+ "div",
166
+ {
167
+ style: {
168
+ position: "absolute",
169
+ top: "70%",
170
+ left: "50%",
171
+ transform: "translate(-50%, -50%)",
172
+ textAlign: "center",
173
+ pointerEvents: "none",
174
+ whiteSpace: "nowrap"
175
+ },
176
+ children: [
177
+ /* @__PURE__ */ t("div", { style: { fontSize: "3rem", fontWeight: 500, lineHeight: 1.2 }, children: m }),
178
+ s && /* @__PURE__ */ t(
179
+ "div",
180
+ {
181
+ style: {
182
+ fontSize: "0.9rem",
183
+ marginTop: "4px"
184
+ },
185
+ children: s
186
+ }
187
+ )
188
+ ]
189
+ }
190
+ )
191
+ ]
192
+ },
193
+ g
194
+ ) });
195
+ }, q = ({ value: m, colors: o, title: s, minMax: p }) => /* @__PURE__ */ t(_, { value: m, colors: o, title: s, minMax: p });
196
+ export {
197
+ q as HalfGauge
198
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@opstel/modern-gauges",
3
+ "version": "0.1.0",
4
+ "description": "Animated half-circle gauges React component — zero runtime dependencies",
5
+ "keywords": [
6
+ "react",
7
+ "gauge",
8
+ "chart",
9
+ "svg",
10
+ "animation",
11
+ "meter",
12
+ "dashboard"
13
+ ],
14
+ "author": "Caleb <caleb.pope@opstel.com>",
15
+ "license": "MIT",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "main": "dist/index.cjs.js",
20
+ "module": "dist/index.es.js",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.es.js",
24
+ "require": "./dist/index.cjs.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "vite build",
33
+ "dev": "vite build --watch",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "peerDependencies": {
37
+ "react": ">=17.0.0",
38
+ "react-dom": ">=17.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@vitejs/plugin-react": "^4.0.0",
42
+ "react": "^18.0.0",
43
+ "react-dom": "^18.0.0",
44
+ "vite": "^5.0.0"
45
+ }
46
+ }