gaugeit.js 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.
Files changed (43) hide show
  1. package/CONTRIBUTING.md +36 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +5 -0
  4. package/README.md +894 -0
  5. package/SECURITY.md +24 -0
  6. package/adapters/react/Gaugeit.d.ts +11 -0
  7. package/adapters/react/Gaugeit.js +66 -0
  8. package/adapters/react/Gaugeit.jsx +1 -0
  9. package/dist/gaugeit.cjs +4686 -0
  10. package/dist/gaugeit.cjs.map +7 -0
  11. package/dist/gaugeit.css +300 -0
  12. package/dist/gaugeit.d.ts +442 -0
  13. package/dist/gaugeit.esm.js +4683 -0
  14. package/dist/gaugeit.esm.js.map +7 -0
  15. package/dist/gaugeit.esm.min.js +2 -0
  16. package/dist/gaugeit.esm.min.js.map +7 -0
  17. package/dist/gaugeit.min.cjs +2 -0
  18. package/dist/gaugeit.min.cjs.map +7 -0
  19. package/dist/gaugeit.min.css +1 -0
  20. package/dist/gaugeit.standalone.js +4720 -0
  21. package/dist/gaugeit.standalone.js.map +7 -0
  22. package/dist/gaugeit.standalone.min.js +302 -0
  23. package/dist/gaugeit.standalone.min.js.map +7 -0
  24. package/dist/gaugeit.umd.js +4713 -0
  25. package/dist/gaugeit.umd.js.map +7 -0
  26. package/dist/gaugeit.umd.min.js +4 -0
  27. package/dist/gaugeit.umd.min.js.map +7 -0
  28. package/dist/manifest.json +24 -0
  29. package/docs/api.md +456 -0
  30. package/docs/architecture.md +224 -0
  31. package/docs/assets/arc_zones.png +0 -0
  32. package/docs/assets/center_zero.png +0 -0
  33. package/docs/assets/classic_instrument.png +0 -0
  34. package/docs/assets/classic_linear_center_zero.png +0 -0
  35. package/docs/assets/classic_linear_instrument.png +0 -0
  36. package/docs/assets/heritage_rolling_tape.png +0 -0
  37. package/docs/assets/heritage_round.png +0 -0
  38. package/docs/assets/line_scale.png +0 -0
  39. package/docs/assets/rose_balance.png +0 -0
  40. package/docs/creating-gauge-types.md +286 -0
  41. package/docs/development.md +56 -0
  42. package/docs/framework-integration.md +84 -0
  43. package/package.json +114 -0
@@ -0,0 +1,4713 @@
1
+ /*! Gaugeit.js v0.1.0 | MIT License | https://github.com/kasperikoski/gaugeit.js#readme */
2
+ (function (root, factory) {
3
+ if (typeof define === "function" && define.amd) define([], factory);
4
+ else if (typeof module === "object" && module.exports) module.exports = factory();
5
+ else root.Gaugeit = factory();
6
+ })(typeof globalThis !== "undefined" ? globalThis : this, function () {
7
+ var Gaugeit = (() => {
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/index.js
27
+ var index_exports = {};
28
+ __export(index_exports, {
29
+ Gauge: () => Gauge,
30
+ GaugeTypeRegistry: () => GaugeTypeRegistry,
31
+ VERSION: () => VERSION,
32
+ autoMount: () => autoMount,
33
+ createGauge: () => createGauge,
34
+ deepMerge: () => deepMerge,
35
+ default: () => index_default,
36
+ defineGaugeitElement: () => defineGaugeitElement,
37
+ easingNames: () => easingNames,
38
+ gaugeTypeRegistry: () => gaugeTypeRegistry,
39
+ geometry: () => geometry,
40
+ getMountedGauge: () => getMountedGauge,
41
+ layers: () => layers,
42
+ registerGaugeType: () => registerGaugeType,
43
+ types: () => types,
44
+ unmountGauge: () => unmountGauge
45
+ });
46
+
47
+ // src/core/utils.js
48
+ function isPlainObject(value) {
49
+ if (value === null || typeof value !== "object") return false;
50
+ if (Object.prototype.toString.call(value) !== "[object Object]") return false;
51
+ const prototype = Object.getPrototypeOf(value);
52
+ if (prototype === null || prototype === Object.prototype) return true;
53
+ const constructor = Object.prototype.hasOwnProperty.call(prototype, "constructor") ? prototype.constructor : null;
54
+ return typeof constructor === "function" && constructor.name === "Object";
55
+ }
56
+ function deepMerge(base, override) {
57
+ if (!isPlainObject(base)) return cloneValue(override);
58
+ const result = { ...base };
59
+ if (!isPlainObject(override)) return result;
60
+ for (const [key, value] of Object.entries(override)) {
61
+ if (isUnsafeObjectKey(key)) continue;
62
+ if (isPlainObject(value) && isPlainObject(result[key])) {
63
+ result[key] = deepMerge(result[key], value);
64
+ } else {
65
+ result[key] = cloneValue(value);
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+ function cloneValue(value) {
71
+ if (Array.isArray(value)) return value.map(cloneValue);
72
+ if (isPlainObject(value)) {
73
+ const copy = {};
74
+ for (const [key, child] of Object.entries(value)) {
75
+ if (!isUnsafeObjectKey(key)) copy[key] = cloneValue(child);
76
+ }
77
+ return copy;
78
+ }
79
+ return value;
80
+ }
81
+ function isUnsafeObjectKey(key) {
82
+ return key === "__proto__" || key === "prototype" || key === "constructor";
83
+ }
84
+ function clamp(value, min, max) {
85
+ return Math.min(max, Math.max(min, value));
86
+ }
87
+ function finiteNumber(value, fallback) {
88
+ const number = Number(value);
89
+ return Number.isFinite(number) ? number : fallback;
90
+ }
91
+ var idCounter = 0;
92
+ function uniqueId(prefix = "gaugeit") {
93
+ idCounter += 1;
94
+ return `${prefix}-${idCounter}`;
95
+ }
96
+ function resolveElement(target) {
97
+ if (typeof target === "string") {
98
+ const element = document.querySelector(target);
99
+ if (!element) throw new Error(`Gaugeit target not found: ${target}`);
100
+ return element;
101
+ }
102
+ if (target && typeof target === "object" && target.nodeType === 1) return target;
103
+ throw new TypeError("Gaugeit target must be an Element or a valid CSS selector.");
104
+ }
105
+ function isTransparentColor(color) {
106
+ if (color === null || color === void 0) return true;
107
+ const normalized = String(color).trim().toLowerCase();
108
+ return normalized === "" || normalized === "transparent" || normalized === "none";
109
+ }
110
+ function requestFrame(callback) {
111
+ if (typeof requestAnimationFrame === "function") return requestAnimationFrame(callback);
112
+ return setTimeout(() => callback(Date.now()), 16);
113
+ }
114
+ function cancelFrame(handle) {
115
+ if (handle === null || handle === void 0) return;
116
+ if (typeof cancelAnimationFrame === "function") {
117
+ cancelAnimationFrame(handle);
118
+ return;
119
+ }
120
+ clearTimeout(handle);
121
+ }
122
+ function dispatchGaugeEvent(target, name, detail) {
123
+ if (!target || typeof target.dispatchEvent !== "function" || typeof CustomEvent !== "function") return;
124
+ target.dispatchEvent(new CustomEvent(`gaugeit:${name}`, { detail }));
125
+ }
126
+ function parseJsonObject(value, fallback = {}) {
127
+ if (typeof value !== "string" || value.trim() === "") return fallback;
128
+ try {
129
+ const parsed = JSON.parse(value);
130
+ return isPlainObject(parsed) ? parsed : fallback;
131
+ } catch (e) {
132
+ return fallback;
133
+ }
134
+ }
135
+
136
+ // src/geometry/scale.js
137
+ function degreesToRadians(degrees) {
138
+ return degrees * Math.PI / 180;
139
+ }
140
+ function polarPoint(centerX, centerY, radius, angleDegrees) {
141
+ const radians = degreesToRadians(angleDegrees);
142
+ return {
143
+ x: centerX + radius * Math.cos(radians),
144
+ y: centerY + radius * Math.sin(radians)
145
+ };
146
+ }
147
+ function gaugeSweep(options) {
148
+ return options.geometry.endAngle - options.geometry.startAngle;
149
+ }
150
+ function centeredArcAngles(sweep, centerAngle = 0) {
151
+ const safeSweep = Math.min(360, Math.max(1e-3, Math.abs(finiteNumber(sweep, 180))));
152
+ const svgCenter = 270 + finiteNumber(centerAngle, 0);
153
+ const halfSweep = safeSweep / 2;
154
+ return {
155
+ startAngle: svgCenter - halfSweep,
156
+ endAngle: svgCenter + halfSweep
157
+ };
158
+ }
159
+ function valueToAngle(value, options, clampValue = true) {
160
+ const min = options.min;
161
+ const max = options.max;
162
+ const normalizedValue = clampValue ? clamp(value, min, max) : value;
163
+ const valueRatio = (normalizedValue - min) / (max - min);
164
+ const visualRatio = options.invert ? 1 - valueRatio : valueRatio;
165
+ return options.geometry.startAngle + gaugeSweep(options) * visualRatio;
166
+ }
167
+ function angleToValue(angle, options, clampResult = true) {
168
+ const sweep = gaugeSweep(options);
169
+ const visualRatio = (angle - options.geometry.startAngle) / sweep;
170
+ const valueRatio = options.invert ? 1 - visualRatio : visualRatio;
171
+ const value = options.min + valueRatio * (options.max - options.min);
172
+ return clampResult ? clamp(value, options.min, options.max) : value;
173
+ }
174
+ function arcPath(centerX, centerY, radius, startAngle, endAngle) {
175
+ const start = polarPoint(centerX, centerY, radius, startAngle);
176
+ const end = polarPoint(centerX, centerY, radius, endAngle);
177
+ const delta = endAngle - startAngle;
178
+ const sweepFlag = delta >= 0 ? 1 : 0;
179
+ if (Math.abs(delta) >= 360 - 1e-3) {
180
+ const midpoint = polarPoint(centerX, centerY, radius, startAngle + delta / 2);
181
+ return [
182
+ `M ${roundSvg(start.x)} ${roundSvg(start.y)}`,
183
+ `A ${roundSvg(radius)} ${roundSvg(radius)} 0 1 ${sweepFlag} ${roundSvg(midpoint.x)} ${roundSvg(midpoint.y)}`,
184
+ `A ${roundSvg(radius)} ${roundSvg(radius)} 0 1 ${sweepFlag} ${roundSvg(end.x)} ${roundSvg(end.y)}`
185
+ ].join(" ");
186
+ }
187
+ const largeArcFlag = Math.abs(delta) > 180 ? 1 : 0;
188
+ return [
189
+ `M ${roundSvg(start.x)} ${roundSvg(start.y)}`,
190
+ `A ${roundSvg(radius)} ${roundSvg(radius)} 0 ${largeArcFlag} ${sweepFlag} ${roundSvg(end.x)} ${roundSvg(end.y)}`
191
+ ].join(" ");
192
+ }
193
+ function taperedArcPath(centerX, centerY, radius, startAngle, endAngle, startWidth, endWidth, segments = 48) {
194
+ const delta = endAngle - startAngle;
195
+ const sampleCount = Math.max(2, Math.min(256, Math.ceil(finiteNumber(segments, 48))));
196
+ const safeRadius = Math.max(0, finiteNumber(radius, 0));
197
+ const firstWidth = Math.max(0, finiteNumber(startWidth, 0));
198
+ const lastWidth = Math.max(0, finiteNumber(endWidth, firstWidth));
199
+ const outer = [];
200
+ const inner = [];
201
+ for (let index = 0; index <= sampleCount; index += 1) {
202
+ const ratio = index / sampleCount;
203
+ const angle = startAngle + delta * ratio;
204
+ const width = firstWidth + (lastWidth - firstWidth) * ratio;
205
+ outer.push(polarPoint(centerX, centerY, safeRadius + width / 2, angle));
206
+ inner.push(polarPoint(centerX, centerY, Math.max(0, safeRadius - width / 2), angle));
207
+ }
208
+ const commands = [`M ${roundSvg(outer[0].x)} ${roundSvg(outer[0].y)}`];
209
+ for (let index = 1; index < outer.length; index += 1) {
210
+ commands.push(`L ${roundSvg(outer[index].x)} ${roundSvg(outer[index].y)}`);
211
+ }
212
+ for (let index = inner.length - 1; index >= 0; index -= 1) {
213
+ commands.push(`L ${roundSvg(inner[index].x)} ${roundSvg(inner[index].y)}`);
214
+ }
215
+ commands.push("Z");
216
+ return commands.join(" ");
217
+ }
218
+ function niceInterval(range, desiredDivisions = 5) {
219
+ const safeRange = Math.abs(finiteNumber(range, 1)) || 1;
220
+ const raw = safeRange / Math.max(1, desiredDivisions);
221
+ const exponent = Math.floor(Math.log10(raw));
222
+ const fraction = raw / Math.pow(10, exponent);
223
+ let niceFraction;
224
+ if (fraction <= 1) niceFraction = 1;
225
+ else if (fraction <= 2) niceFraction = 2;
226
+ else if (fraction <= 2.5) niceFraction = 2.5;
227
+ else if (fraction <= 5) niceFraction = 5;
228
+ else niceFraction = 10;
229
+ return niceFraction * Math.pow(10, exponent);
230
+ }
231
+ function buildScaleValues(min, max, interval) {
232
+ const safeInterval = Math.abs(finiteNumber(interval, 0));
233
+ if (safeInterval <= 0 || max <= min) return [min, max];
234
+ const precision = Math.min(12, Math.max(0, decimalPlaces(safeInterval) + 2));
235
+ const epsilon = safeInterval / 1e6;
236
+ const values = [];
237
+ let current = min;
238
+ let guard = 0;
239
+ while (current <= max + epsilon && guard < 1e4) {
240
+ values.push(roundTo(current, precision));
241
+ current += safeInterval;
242
+ guard += 1;
243
+ }
244
+ if (values.length === 0 || Math.abs(values[values.length - 1] - max) > epsilon) {
245
+ values.push(max);
246
+ } else {
247
+ values[values.length - 1] = max;
248
+ }
249
+ return values;
250
+ }
251
+ function buildMinorValues(majorValues, subdivisions) {
252
+ const count = Math.max(0, Math.floor(finiteNumber(subdivisions, 0)));
253
+ if (count === 0 || majorValues.length < 2) return [];
254
+ const values = [];
255
+ for (let index = 0; index < majorValues.length - 1; index += 1) {
256
+ const start = majorValues[index];
257
+ const end = majorValues[index + 1];
258
+ const step = (end - start) / (count + 1);
259
+ for (let subdivision = 1; subdivision <= count; subdivision += 1) {
260
+ values.push(start + step * subdivision);
261
+ }
262
+ }
263
+ return values;
264
+ }
265
+ function resolveZoneSegments(min, max, zones) {
266
+ const normalized = Array.isArray(zones) ? zones.filter((zone) => zone && typeof zone === "object").map((zone, index) => ({
267
+ ...zone,
268
+ min: clamp(finiteNumber(zone.min, min), min, max),
269
+ max: clamp(finiteNumber(zone.max, max), min, max),
270
+ _index: index
271
+ })).filter((zone) => zone.max > zone.min) : [];
272
+ const breakpoints = /* @__PURE__ */ new Set([min, max]);
273
+ for (const zone of normalized) {
274
+ breakpoints.add(zone.min);
275
+ breakpoints.add(zone.max);
276
+ }
277
+ const points = [...breakpoints].sort((left, right) => left - right);
278
+ const segments = [];
279
+ for (let index = 0; index < points.length - 1; index += 1) {
280
+ const segmentMin = points[index];
281
+ const segmentMax = points[index + 1];
282
+ const midpoint = (segmentMin + segmentMax) / 2;
283
+ const candidates = normalized.filter((zone2) => midpoint >= zone2.min && midpoint <= zone2.max);
284
+ const zone = candidates.length ? candidates[candidates.length - 1] : null;
285
+ segments.push({ min: segmentMin, max: segmentMax, zone });
286
+ }
287
+ return segments;
288
+ }
289
+ function findZoneForValue(value, zones) {
290
+ if (!Array.isArray(zones)) return null;
291
+ let match = null;
292
+ for (const zone of zones) {
293
+ if (!zone || typeof zone !== "object") continue;
294
+ if (value >= Number(zone.min) && value <= Number(zone.max)) match = zone;
295
+ }
296
+ return match;
297
+ }
298
+ function roundSvg(value) {
299
+ return Math.round(value * 1e3) / 1e3;
300
+ }
301
+ function roundTo(value, precision) {
302
+ const factor = Math.pow(10, precision);
303
+ return Math.round(value * factor) / factor;
304
+ }
305
+ function decimalPlaces(value) {
306
+ const text = String(value).toLowerCase();
307
+ if (text.includes("e-")) return Number(text.split("e-")[1]);
308
+ const decimal = text.split(".")[1];
309
+ return decimal ? decimal.length : 0;
310
+ }
311
+
312
+ // src/geometry/linear.js
313
+ function linearYToSvg(y, options) {
314
+ const linear = options.linear || {};
315
+ const originY = finiteNumber(linear.originY, 140);
316
+ return originY - finiteNumber(y, 0);
317
+ }
318
+ function linearAxis(options) {
319
+ const linear = options.linear || {};
320
+ const startX = finiteNumber(linear.startX, 36);
321
+ const startY = linearYToSvg(linear.startY, options);
322
+ let endX = finiteNumber(linear.endX, 284);
323
+ let endY = linearYToSvg(linear.endY, options);
324
+ let dx = endX - startX;
325
+ let dy = endY - startY;
326
+ let length = Math.hypot(dx, dy);
327
+ if (length < 1e-3) {
328
+ endX = startX + 1;
329
+ endY = startY;
330
+ dx = 1;
331
+ dy = 0;
332
+ length = 1;
333
+ }
334
+ const tangentX = dx / length;
335
+ const tangentY = dy / length;
336
+ return {
337
+ startX,
338
+ startY,
339
+ endX,
340
+ endY,
341
+ dx,
342
+ dy,
343
+ length,
344
+ tangentX,
345
+ tangentY,
346
+ normalX: -tangentY,
347
+ normalY: tangentX,
348
+ angle: Math.atan2(dy, dx) * 180 / Math.PI
349
+ };
350
+ }
351
+ function valueToLinearRatio(value, options, clampValue = true) {
352
+ const normalizedValue = clampValue ? clamp(value, options.min, options.max) : value;
353
+ const valueRatio = (normalizedValue - options.min) / (options.max - options.min);
354
+ return options.invert ? 1 - valueRatio : valueRatio;
355
+ }
356
+ function valueToLinearPoint(value, options, clampValue = true) {
357
+ const axis = linearAxis(options);
358
+ const ratio = valueToLinearRatio(value, options, clampValue);
359
+ return {
360
+ x: axis.startX + axis.dx * ratio,
361
+ y: axis.startY + axis.dy * ratio
362
+ };
363
+ }
364
+ function linearPointToValue(x, y, options, clampResult = true) {
365
+ const axis = linearAxis(options);
366
+ const relativeX = finiteNumber(x, axis.startX) - axis.startX;
367
+ const relativeY = finiteNumber(y, axis.startY) - axis.startY;
368
+ const visualRatio = (relativeX * axis.tangentX + relativeY * axis.tangentY) / axis.length;
369
+ const boundedVisualRatio = clampResult ? clamp(visualRatio, 0, 1) : visualRatio;
370
+ const valueRatio = options.invert ? 1 - boundedVisualRatio : boundedVisualRatio;
371
+ const value = options.min + valueRatio * (options.max - options.min);
372
+ return clampResult ? clamp(value, options.min, options.max) : value;
373
+ }
374
+ function offsetLinearPoint(point, axis, distance) {
375
+ const amount = finiteNumber(distance, 0);
376
+ return {
377
+ x: point.x + axis.normalX * amount,
378
+ y: point.y + axis.normalY * amount
379
+ };
380
+ }
381
+ function linearNormalOffset(value) {
382
+ return -finiteNumber(value, 0);
383
+ }
384
+ function linearIndicatorNormalOffset(indicator) {
385
+ return linearNormalOffset(indicator == null ? void 0 : indicator.centerOffset);
386
+ }
387
+ function linearNormalSpan(length, position = "cross", offset = 0) {
388
+ const size = Math.max(0, finiteNumber(length, 0));
389
+ const shift = finiteNumber(offset, 0);
390
+ if (position === "negative") return { start: shift - size, end: shift };
391
+ if (position === "positive") return { start: shift, end: shift + size };
392
+ return { start: shift - size / 2, end: shift + size / 2 };
393
+ }
394
+
395
+ // src/geometry/tape.js
396
+ function tapeWindowBox(options) {
397
+ const tape = options.tape || {};
398
+ return {
399
+ x: finiteNumber(tape.x, 64),
400
+ y: finiteNumber(tape.y, 58),
401
+ width: Math.max(20, finiteNumber(tape.width, 192)),
402
+ height: Math.max(20, finiteNumber(tape.height, 210))
403
+ };
404
+ }
405
+ function resolveTapeInterval(options) {
406
+ var _a, _b, _c;
407
+ const explicit = finiteNumber((_a = options.tape) == null ? void 0 : _a.interval, 0);
408
+ if (explicit > 0) return explicit;
409
+ return niceInterval(options.max - options.min, Math.max(2, ((_c = (_b = options.scale) == null ? void 0 : _b.major) == null ? void 0 : _c.divisions) || 5));
410
+ }
411
+ function tapeMinorStep(options) {
412
+ var _a;
413
+ const subdivisions = Math.max(0, Math.floor(finiteNumber((_a = options.tape) == null ? void 0 : _a.minorSubdivisions, 4)));
414
+ return resolveTapeInterval(options) / (subdivisions + 1);
415
+ }
416
+ function tapeMinorSpacing(options) {
417
+ var _a, _b;
418
+ const subdivisions = Math.max(0, Math.floor(finiteNumber((_a = options.tape) == null ? void 0 : _a.minorSubdivisions, 4)));
419
+ return Math.max(2, finiteNumber((_b = options.tape) == null ? void 0 : _b.majorSpacing, 54)) / (subdivisions + 1);
420
+ }
421
+ function tapeValueOffset(value, currentValue, options) {
422
+ const step = tapeMinorStep(options);
423
+ const spacing = tapeMinorSpacing(options);
424
+ const direction = options.invert ? -1 : 1;
425
+ return (Number(value) - Number(currentValue)) / step * spacing * direction;
426
+ }
427
+
428
+ // src/geometry/light.js
429
+ function resolveLightPosition(light, options) {
430
+ return {
431
+ x: finiteNumber(light == null ? void 0 : light.x, options.geometry.width / 2),
432
+ y: options.geometry.mode === "linear" ? linearYToSvg(light == null ? void 0 : light.y, options) : finiteNumber(light == null ? void 0 : light.y, options.geometry.height / 2)
433
+ };
434
+ }
435
+ function lightIsActive(light, value) {
436
+ if (!(light == null ? void 0 : light.visible)) return false;
437
+ const trigger = light.trigger || {};
438
+ const sample = finiteNumber(value, 0);
439
+ switch (trigger.mode) {
440
+ case "below":
441
+ return sample <= finiteNumber(trigger.value, 0);
442
+ case "above":
443
+ return sample >= finiteNumber(trigger.value, 0);
444
+ case "between": {
445
+ const first = finiteNumber(trigger.min, 0);
446
+ const second = finiteNumber(trigger.max, first);
447
+ return sample >= Math.min(first, second) && sample <= Math.max(first, second);
448
+ }
449
+ case "manual":
450
+ default:
451
+ return light.on === true;
452
+ }
453
+ }
454
+
455
+ // src/geometry/bounds.js
456
+ var EPSILON = 1e-3;
457
+ function createBounds(x = Infinity, y = Infinity, width = -Infinity, height = -Infinity) {
458
+ if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(width) && Number.isFinite(height)) {
459
+ return { x, y, width, height };
460
+ }
461
+ return { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
462
+ }
463
+ function includePoint(bounds, x, y) {
464
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return bounds;
465
+ bounds.minX = Math.min(bounds.minX, x);
466
+ bounds.minY = Math.min(bounds.minY, y);
467
+ bounds.maxX = Math.max(bounds.maxX, x);
468
+ bounds.maxY = Math.max(bounds.maxY, y);
469
+ return bounds;
470
+ }
471
+ function includeRect(bounds, x, y, width, height) {
472
+ includePoint(bounds, x, y);
473
+ includePoint(bounds, x + Math.max(0, width), y + Math.max(0, height));
474
+ return bounds;
475
+ }
476
+ function finalizeBounds(bounds, fallback = { x: 0, y: 0, width: 1, height: 1 }) {
477
+ if (!bounds || !Number.isFinite(bounds.minX) || !Number.isFinite(bounds.minY) || !Number.isFinite(bounds.maxX) || !Number.isFinite(bounds.maxY)) {
478
+ return { ...fallback };
479
+ }
480
+ return {
481
+ x: bounds.minX,
482
+ y: bounds.minY,
483
+ width: Math.max(1, bounds.maxX - bounds.minX),
484
+ height: Math.max(1, bounds.maxY - bounds.minY)
485
+ };
486
+ }
487
+ function padBounds(bounds, padding = 0) {
488
+ const amount = Math.max(0, finiteNumber(padding, 0));
489
+ return {
490
+ x: bounds.x - amount,
491
+ y: bounds.y - amount,
492
+ width: Math.max(1, bounds.width + amount * 2),
493
+ height: Math.max(1, bounds.height + amount * 2)
494
+ };
495
+ }
496
+ function unionBounds(...rectangles) {
497
+ const accumulator = createBounds();
498
+ for (const rectangle of rectangles) {
499
+ if (!rectangle) continue;
500
+ includeRect(accumulator, rectangle.x, rectangle.y, rectangle.width, rectangle.height);
501
+ }
502
+ return finalizeBounds(accumulator);
503
+ }
504
+ function isClosedGauge(options) {
505
+ return Math.abs(gaugeSweep(options)) >= 360 - EPSILON;
506
+ }
507
+ function resolveScaleEndpointValues(values, options) {
508
+ var _a;
509
+ const mode = ((_a = options.scale) == null ? void 0 : _a.endpointMode) || "auto";
510
+ if (values.length < 2 || mode === "both") return values;
511
+ if (mode === "dedupe" || mode === "auto" && isClosedGauge(options)) return values.slice(0, -1);
512
+ return values;
513
+ }
514
+ function resolveReadoutPosition(part, options) {
515
+ const reference = readoutReferenceBox(options);
516
+ const fontSize = Math.max(1, finiteNumber(part.fontSize, 12));
517
+ const margin = Math.max(0, finiteNumber(part.margin, 12));
518
+ const position = part.position || "top";
519
+ const automaticX = part.x === "auto";
520
+ const automaticY = part.y === "auto";
521
+ const x = automaticX ? reference.x + reference.width / 2 : finiteNumber(part.x, reference.x + reference.width / 2);
522
+ let y;
523
+ if (automaticY) {
524
+ if (position === "bottom") y = reference.y + reference.height - margin - fontSize / 2;
525
+ else if (position === "center") y = reference.y + reference.height / 2;
526
+ else y = reference.y + margin + fontSize / 2;
527
+ } else if (options.geometry.mode === "linear") {
528
+ y = linearYToSvg(part.y, options);
529
+ } else {
530
+ y = finiteNumber(part.y, reference.y + margin + fontSize / 2);
531
+ }
532
+ return {
533
+ // Offsets refine automatic placement. Numeric coordinates are final, absolute
534
+ // SVG positions and therefore do not inherit a type preset's auto offset.
535
+ x: x + (automaticX ? finiteNumber(part.offsetX, 0) : 0),
536
+ y: y + (automaticY ? finiteNumber(part.offsetY, 0) : 0)
537
+ };
538
+ }
539
+ function resolveFaceBox(options, contentBounds = null) {
540
+ var _a, _b;
541
+ const face = options.face;
542
+ const geometry2 = options.geometry;
543
+ if (face.fit !== "content" || !contentBounds) {
544
+ return {
545
+ x: face.x,
546
+ y: face.y,
547
+ width: face.width,
548
+ height: face.height
549
+ };
550
+ }
551
+ const padding = Math.max(0, finiteNumber(face.contentPadding, 8));
552
+ const configured = { x: face.x, y: face.y, width: face.width, height: face.height };
553
+ const minimumWidth = (_a = face.minWidth) != null ? _a : configured.width;
554
+ const minimumHeight = (_b = face.minHeight) != null ? _b : configured.height;
555
+ if (face.shape === "circle") {
556
+ const cx = geometry2.centerX;
557
+ const cy = geometry2.centerY;
558
+ const configuredRadius = Math.min(minimumWidth, minimumHeight) / 2;
559
+ const contentRadius = Math.max(
560
+ cx - contentBounds.x,
561
+ contentBounds.x + contentBounds.width - cx,
562
+ cy - contentBounds.y,
563
+ contentBounds.y + contentBounds.height - cy
564
+ ) + padding;
565
+ const radius = Math.max(1, configuredRadius, contentRadius);
566
+ return { x: cx - radius, y: cy - radius, width: radius * 2, height: radius * 2 };
567
+ }
568
+ const padded = {
569
+ x: contentBounds.x - padding,
570
+ y: contentBounds.y - padding,
571
+ width: contentBounds.width + padding * 2,
572
+ height: contentBounds.height + padding * 2
573
+ };
574
+ const horizontalExtent = Math.max(
575
+ geometry2.centerX - padded.x,
576
+ padded.x + padded.width - geometry2.centerX
577
+ );
578
+ const width = Math.max(1, minimumWidth, horizontalExtent * 2);
579
+ const height = Math.max(1, minimumHeight, padded.height);
580
+ const contentCenterY = padded.y + padded.height / 2;
581
+ return {
582
+ x: geometry2.centerX - width / 2,
583
+ y: contentCenterY - height / 2,
584
+ width,
585
+ height
586
+ };
587
+ }
588
+ function calculateGaugeBounds(options, configuration = {}) {
589
+ var _a;
590
+ const includeFace = configuration.includeFace !== false;
591
+ const includeBase = configuration.includeBase === void 0 ? ((_a = options.layout) == null ? void 0 : _a.includeGeometry) !== false : configuration.includeBase === true;
592
+ const contentBounds = configuration.contentBounds || null;
593
+ const geometry2 = options.geometry;
594
+ const bounds = createBounds();
595
+ if (includeBase) includeRect(bounds, 0, 0, geometry2.width, geometry2.height);
596
+ if (includeFace && options.face.visible) {
597
+ const box = resolveFaceBox(options, contentBounds);
598
+ const expansion = options.face.shadow ? 10 : Math.max(0, options.face.strokeWidth / 2);
599
+ includeRect(bounds, box.x - expansion, box.y - expansion, box.width + expansion * 2, box.height + expansion * 2);
600
+ if (options.face.clipContent) {
601
+ return finalizeBounds(bounds, { x: 0, y: 0, width: geometry2.width, height: geometry2.height });
602
+ }
603
+ }
604
+ if (geometry2.mode === "linear") {
605
+ includeLinearTrackAndZones(bounds, options);
606
+ includeLinearScale(bounds, options);
607
+ includeLinearLabels(bounds, options);
608
+ includeLinearIndicator(bounds, options);
609
+ includeZeroMarker(bounds, options);
610
+ } else if (geometry2.mode === "tape") {
611
+ includeTape(bounds, options);
612
+ } else {
613
+ includeTrackAndZones(bounds, options);
614
+ includeScale(bounds, options);
615
+ includeLabels(bounds, options);
616
+ includeZeroMarker(bounds, options);
617
+ includePointer(bounds, options);
618
+ }
619
+ includeReadout(bounds, options);
620
+ includeLight(bounds, options);
621
+ return finalizeBounds(bounds, { x: 0, y: 0, width: geometry2.width, height: geometry2.height });
622
+ }
623
+ function includeTrackAndZones(bounds, options) {
624
+ var _a, _b;
625
+ const { geometry: geometry2, track } = options;
626
+ if (track.visible) {
627
+ includeArc(bounds, geometry2.centerX, geometry2.centerY, track.radius, track.width, geometry2.startAngle, geometry2.endAngle);
628
+ }
629
+ for (const zone of options.zones || []) {
630
+ if (isTransparentColor(zone.color)) continue;
631
+ includeArc(
632
+ bounds,
633
+ geometry2.centerX,
634
+ geometry2.centerY,
635
+ (_b = (_a = zone.radius) != null ? _a : track.radius) != null ? _b : geometry2.radius,
636
+ zoneMaximumWidth(zone, track.width),
637
+ valueToAngle(zone.min, options),
638
+ valueToAngle(zone.max, options)
639
+ );
640
+ }
641
+ }
642
+ function zoneMaximumWidth(zone, fallbackWidth) {
643
+ var _a, _b, _c;
644
+ if (!(zone == null ? void 0 : zone.taper)) return (_a = zone == null ? void 0 : zone.width) != null ? _a : fallbackWidth;
645
+ return Math.max(
646
+ 0,
647
+ finiteNumber(zone.taper.startWidth, (_b = zone.width) != null ? _b : fallbackWidth),
648
+ finiteNumber(zone.taper.endWidth, (_c = zone.width) != null ? _c : fallbackWidth)
649
+ );
650
+ }
651
+ function includeScale(bounds, options) {
652
+ const { geometry: geometry2, scale } = options;
653
+ const lengths = [];
654
+ if (scale.major.visible) lengths.push(scale.major.length);
655
+ if (scale.minor.visible) lengths.push(scale.minor.length);
656
+ if (!lengths.length) return;
657
+ const length = Math.max(...lengths, 0);
658
+ let inner = scale.radius;
659
+ let outer = scale.radius;
660
+ if (scale.position === "outside") outer += length;
661
+ else if (scale.position === "cross") {
662
+ inner -= length / 2;
663
+ outer += length / 2;
664
+ } else inner -= length;
665
+ includeArcSector(bounds, geometry2.centerX, geometry2.centerY, Math.max(0, inner), outer, geometry2.startAngle, geometry2.endAngle);
666
+ }
667
+ function includeLabels(bounds, options) {
668
+ const labels = options.labels;
669
+ if (!labels.visible) return;
670
+ const interval = Number(labels.interval) > 0 ? Number(labels.interval) : Math.abs(options.max - options.min) / Math.max(1, options.scale.major.divisions || 5);
671
+ const values = resolveScaleEndpointValues(buildScaleValues(options.min, options.max, interval), options);
672
+ for (const value of values) {
673
+ const point = polarPoint(options.geometry.centerX, options.geometry.centerY, labels.radius, valueToAngle(value, options));
674
+ let rendered = Number(value).toFixed(labels.fractionDigits);
675
+ if (typeof labels.formatter === "function") {
676
+ try {
677
+ rendered = String(labels.formatter(value, { min: options.min, max: options.max, options }));
678
+ } catch (e) {
679
+ }
680
+ }
681
+ const halfWidth = Math.max(labels.fontSize * 0.55, rendered.length * labels.fontSize * 0.34);
682
+ const halfHeight = labels.fontSize * 0.68;
683
+ includeRect(bounds, point.x - halfWidth, point.y - halfHeight, halfWidth * 2, halfHeight * 2);
684
+ }
685
+ }
686
+ function includePointer(bounds, options) {
687
+ const { geometry: geometry2, pointer } = options;
688
+ if (!pointer.visible) return;
689
+ const expansion = Math.max(pointer.width / 2, pointer.strokeWidth / 2, 1);
690
+ includeArcSector(
691
+ bounds,
692
+ geometry2.centerX,
693
+ geometry2.centerY,
694
+ 0,
695
+ pointer.length + expansion,
696
+ geometry2.startAngle,
697
+ geometry2.endAngle
698
+ );
699
+ if (pointer.tailLength > 0) {
700
+ includeArcSector(
701
+ bounds,
702
+ geometry2.centerX,
703
+ geometry2.centerY,
704
+ 0,
705
+ pointer.tailLength + expansion,
706
+ geometry2.startAngle + 180,
707
+ geometry2.endAngle + 180
708
+ );
709
+ }
710
+ if (pointer.cap.visible) {
711
+ const radius = pointer.cap.radius + pointer.cap.strokeWidth / 2;
712
+ includeRect(bounds, geometry2.centerX - radius, geometry2.centerY - radius, radius * 2, radius * 2);
713
+ }
714
+ }
715
+ function includeLinearTrackAndZones(bounds, options) {
716
+ var _a, _b;
717
+ const axis = linearAxis(options);
718
+ if (options.track.visible) {
719
+ includeLinearStroke(
720
+ bounds,
721
+ axis,
722
+ valueToLinearPoint(options.min, options),
723
+ valueToLinearPoint(options.max, options),
724
+ linearNormalOffset(options.linear.zoneOffset),
725
+ options.track.width
726
+ );
727
+ }
728
+ for (const zone of options.zones || []) {
729
+ if (isTransparentColor(zone.color)) continue;
730
+ includeLinearStroke(
731
+ bounds,
732
+ axis,
733
+ valueToLinearPoint(zone.min, options),
734
+ valueToLinearPoint(zone.max, options),
735
+ linearNormalOffset((_a = zone.offset) != null ? _a : options.linear.zoneOffset),
736
+ (_b = zone.width) != null ? _b : options.track.width
737
+ );
738
+ }
739
+ }
740
+ function includeLinearScale(bounds, options) {
741
+ const axis = linearAxis(options);
742
+ const lengths = [];
743
+ if (options.scale.major.visible) lengths.push(options.scale.major.length);
744
+ if (options.scale.minor.visible) lengths.push(options.scale.minor.length);
745
+ if (!lengths.length) return;
746
+ const span = linearNormalSpan(Math.max(...lengths, 0), options.linear.tickSide);
747
+ includeLinearExtent(bounds, axis, span.start, span.end);
748
+ }
749
+ function includeLinearLabels(bounds, options) {
750
+ const labels = options.labels;
751
+ if (!labels.visible) return;
752
+ const interval = Number(labels.interval) > 0 ? Number(labels.interval) : Math.abs(options.max - options.min) / Math.max(1, options.scale.major.divisions || 5);
753
+ const values = buildScaleValues(options.min, options.max, interval);
754
+ const axis = linearAxis(options);
755
+ for (const value of values) {
756
+ const point = offsetLinearPoint(
757
+ valueToLinearPoint(value, options),
758
+ axis,
759
+ linearNormalOffset(options.linear.labelOffset)
760
+ );
761
+ let rendered = Number(value).toFixed(labels.fractionDigits);
762
+ if (typeof labels.formatter === "function") {
763
+ try {
764
+ rendered = String(labels.formatter(value, { min: options.min, max: options.max, options }));
765
+ } catch (e) {
766
+ }
767
+ }
768
+ const halfWidth = Math.max(labels.fontSize * 0.55, rendered.length * labels.fontSize * 0.34);
769
+ const halfHeight = labels.fontSize * 0.68;
770
+ includeRect(bounds, point.x - halfWidth, point.y - halfHeight, halfWidth * 2, halfHeight * 2);
771
+ }
772
+ }
773
+ function includeLinearIndicator(bounds, options) {
774
+ const indicator = options.indicator;
775
+ if (!indicator.visible) return;
776
+ const axis = linearAxis(options);
777
+ const span = linearNormalSpan(
778
+ indicator.length,
779
+ indicator.position,
780
+ linearIndicatorNormalOffset(indicator)
781
+ );
782
+ const strokeExpansion = Math.max(indicator.width / 2, indicator.strokeWidth / 2, 1);
783
+ const normalStart = Math.min(span.start, span.end) - strokeExpansion;
784
+ const normalEnd = Math.max(span.start, span.end) + strokeExpansion;
785
+ let tangentExpansion = strokeExpansion;
786
+ if (indicator.type === "marker") tangentExpansion = Math.max(tangentExpansion, indicator.markerSize);
787
+ if (indicator.type === "carriage") tangentExpansion = Math.max(tangentExpansion, indicator.carriageWidth / 2);
788
+ if (indicator.cap.visible) tangentExpansion = Math.max(tangentExpansion, indicator.cap.radius + indicator.cap.strokeWidth / 2);
789
+ includeLinearExtent(bounds, axis, normalStart, normalEnd, tangentExpansion);
790
+ }
791
+ function includeZeroMarker(bounds, options) {
792
+ var _a, _b;
793
+ const marker = (_a = options.centerZero) == null ? void 0 : _a.marker;
794
+ if (!(marker == null ? void 0 : marker.visible)) return;
795
+ const expansion = Math.max(marker.width / 2, 0.5);
796
+ if (options.geometry.mode === "linear") {
797
+ const axis = linearAxis(options);
798
+ const point = valueToLinearPoint(options.centerZero.zeroValue, options);
799
+ const span = linearNormalSpan(
800
+ marker.length,
801
+ marker.position,
802
+ linearNormalOffset(marker.centerOffset)
803
+ );
804
+ const start2 = offsetLinearPoint(point, axis, span.start);
805
+ const end2 = offsetLinearPoint(point, axis, span.end);
806
+ includeRect(
807
+ bounds,
808
+ Math.min(start2.x, end2.x) - expansion,
809
+ Math.min(start2.y, end2.y) - expansion,
810
+ Math.abs(end2.x - start2.x) + expansion * 2,
811
+ Math.abs(end2.y - start2.y) + expansion * 2
812
+ );
813
+ return;
814
+ }
815
+ const angle = valueToAngle(options.centerZero.zeroValue, options);
816
+ const radius = (_b = marker.radius) != null ? _b : options.scale.radius;
817
+ const start = polarPoint(
818
+ options.geometry.centerX,
819
+ options.geometry.centerY,
820
+ Math.max(0, radius - marker.length),
821
+ angle
822
+ );
823
+ const end = polarPoint(options.geometry.centerX, options.geometry.centerY, radius, angle);
824
+ includeRect(
825
+ bounds,
826
+ Math.min(start.x, end.x) - expansion,
827
+ Math.min(start.y, end.y) - expansion,
828
+ Math.abs(end.x - start.x) + expansion * 2,
829
+ Math.abs(end.y - start.y) + expansion * 2
830
+ );
831
+ }
832
+ function includeLinearStroke(bounds, axis, startPoint, endPoint, offset, width) {
833
+ const halfWidth = Math.max(0, finiteNumber(width, 0)) / 2;
834
+ const start = offsetLinearPoint(startPoint, axis, offset);
835
+ const end = offsetLinearPoint(endPoint, axis, offset);
836
+ const minX = Math.min(start.x, end.x) - halfWidth;
837
+ const minY = Math.min(start.y, end.y) - halfWidth;
838
+ includeRect(
839
+ bounds,
840
+ minX,
841
+ minY,
842
+ Math.abs(end.x - start.x) + halfWidth * 2,
843
+ Math.abs(end.y - start.y) + halfWidth * 2
844
+ );
845
+ }
846
+ function includeLinearExtent(bounds, axis, normalStart, normalEnd, tangentExpansion = 0) {
847
+ for (const base of [
848
+ { x: axis.startX, y: axis.startY },
849
+ { x: axis.endX, y: axis.endY }
850
+ ]) {
851
+ for (const normal of [normalStart, normalEnd]) {
852
+ const point = offsetLinearPoint(base, axis, normal);
853
+ includePoint(
854
+ bounds,
855
+ point.x - axis.tangentX * tangentExpansion,
856
+ point.y - axis.tangentY * tangentExpansion
857
+ );
858
+ includePoint(
859
+ bounds,
860
+ point.x + axis.tangentX * tangentExpansion,
861
+ point.y + axis.tangentY * tangentExpansion
862
+ );
863
+ }
864
+ }
865
+ }
866
+ function includeTape(bounds, options) {
867
+ const box = tapeWindowBox(options);
868
+ const expansion = Math.max(
869
+ options.tape.windowStrokeWidth / 2,
870
+ options.tape.windowInnerStrokeWidth / 2
871
+ );
872
+ includeRect(
873
+ bounds,
874
+ box.x - expansion,
875
+ box.y - expansion,
876
+ box.width + expansion * 2,
877
+ box.height + expansion * 2
878
+ );
879
+ }
880
+ function includeLight(bounds, options) {
881
+ const light = options.light;
882
+ if (!(light == null ? void 0 : light.visible)) return;
883
+ const point = resolveLightPosition(light, options);
884
+ const radius = Math.max(1, finiteNumber(light.radius, 8));
885
+ const halo = light.glow ? radius * 2.96 : radius;
886
+ const bezel = radius + Math.max(1.5, radius * 0.18);
887
+ const extent = Math.max(halo, bezel);
888
+ includeRect(bounds, point.x - extent, point.y - extent, extent * 2, extent * 2);
889
+ }
890
+ function includeReadout(bounds, options) {
891
+ if (!options.readout.visible) return;
892
+ for (const [kind, part] of Object.entries(options.readout)) {
893
+ if (kind === "visible" || !(part == null ? void 0 : part.visible)) continue;
894
+ const text = kind === "value" ? `${part.prefix || ""}${options.max}${part.suffix || ""}` : String(part.text || "");
895
+ if (!text && kind !== "value") continue;
896
+ const point = resolveReadoutPosition(part, options);
897
+ const halfWidth = Math.max(part.fontSize * 0.55, text.length * part.fontSize * 0.34);
898
+ const halfHeight = part.fontSize * 0.68;
899
+ includeRect(bounds, point.x - halfWidth, point.y - halfHeight, halfWidth * 2, halfHeight * 2);
900
+ }
901
+ }
902
+ function readoutReferenceBox(options) {
903
+ const face = options.face;
904
+ if (face.visible && face.fit !== "content") return { x: face.x, y: face.y, width: face.width, height: face.height };
905
+ return { x: 0, y: 0, width: options.geometry.width, height: options.geometry.height };
906
+ }
907
+ function includeArc(bounds, cx, cy, radius, width, startAngle, endAngle) {
908
+ const halfWidth = Math.max(0, finiteNumber(width, 0)) / 2;
909
+ includeArcSector(bounds, cx, cy, Math.max(0, radius - halfWidth), radius + halfWidth, startAngle, endAngle);
910
+ }
911
+ function includeArcSector(bounds, cx, cy, innerRadius, outerRadius, startAngle, endAngle) {
912
+ const angles = [startAngle, endAngle];
913
+ for (const cardinal of [0, 90, 180, 270]) {
914
+ if (angleIsWithinSweep(cardinal, startAngle, endAngle)) angles.push(cardinal);
915
+ }
916
+ if (innerRadius <= EPSILON) includePoint(bounds, cx, cy);
917
+ for (const angle of angles) {
918
+ const outer = polarPoint(cx, cy, Math.max(0, outerRadius), angle);
919
+ includePoint(bounds, outer.x, outer.y);
920
+ if (innerRadius > EPSILON) {
921
+ const inner = polarPoint(cx, cy, innerRadius, angle);
922
+ includePoint(bounds, inner.x, inner.y);
923
+ }
924
+ }
925
+ }
926
+ function angleIsWithinSweep(angle, startAngle, endAngle) {
927
+ const delta = endAngle - startAngle;
928
+ if (Math.abs(delta) >= 360 - EPSILON) return true;
929
+ if (delta >= 0) return positiveModulo(angle - startAngle, 360) <= delta + EPSILON;
930
+ return positiveModulo(startAngle - angle, 360) <= -delta + EPSILON;
931
+ }
932
+ function positiveModulo(value, divisor) {
933
+ return (value % divisor + divisor) % divisor;
934
+ }
935
+
936
+ // src/renderer/svg.js
937
+ var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
938
+ function svgElement(tagName, attributes = {}, text = null) {
939
+ const element = document.createElementNS(SVG_NAMESPACE, tagName);
940
+ setSvgAttributes(element, attributes);
941
+ if (text !== null && text !== void 0) element.textContent = String(text);
942
+ return element;
943
+ }
944
+ function setSvgAttributes(element, attributes) {
945
+ for (const [name, value] of Object.entries(attributes || {})) {
946
+ if (value === null || value === void 0 || value === false) continue;
947
+ if (name === "className") element.setAttribute("class", String(value));
948
+ else element.setAttribute(name, String(value));
949
+ }
950
+ return element;
951
+ }
952
+ var SvgRenderer = class {
953
+ constructor(host, options) {
954
+ this.host = host;
955
+ this.options = options;
956
+ this.id = uniqueId("gaugeit");
957
+ this.controllers = [];
958
+ this.references = /* @__PURE__ */ new Map();
959
+ this.root = null;
960
+ this.svg = null;
961
+ this.content = null;
962
+ this.contentBounds = null;
963
+ this.layoutBounds = null;
964
+ this.contentClipPathId = null;
965
+ }
966
+ /** Creates a clean SVG root for a full render. */
967
+ begin() {
968
+ this.destroyControllers();
969
+ this.references.clear();
970
+ this.contentClipPathId = null;
971
+ const root = document.createElement("div");
972
+ root.className = [
973
+ "gaugeit",
974
+ `gaugeit--${this.options.type}`,
975
+ `gaugeit--layout-${this.options.layout.mode}`,
976
+ this.options.className
977
+ ].filter(Boolean).join(" ");
978
+ root.dataset.gaugeitRoot = "true";
979
+ root.dataset.gaugeitLayout = this.options.layout.mode;
980
+ this.contentBounds = calculateGaugeBounds(this.options, { includeFace: false, includeBase: false });
981
+ this.layoutBounds = calculateGaugeBounds(this.options, { contentBounds: this.contentBounds });
982
+ const initialViewport = padBounds(this.layoutBounds, this.options.layout.padding);
983
+ const geometry2 = this.options.geometry;
984
+ const svg = svgElement("svg", {
985
+ class: "gaugeit__svg",
986
+ viewBox: viewBoxValue(initialViewport),
987
+ preserveAspectRatio: geometry2.preserveAspectRatio,
988
+ focusable: "false"
989
+ });
990
+ const title = svgElement("title", { id: `${this.id}-title` }, this.options.accessibility.label);
991
+ svg.append(title);
992
+ if (this.options.accessibility.description) {
993
+ const description = svgElement(
994
+ "desc",
995
+ { id: `${this.id}-description` },
996
+ this.options.accessibility.description
997
+ );
998
+ svg.append(description);
999
+ svg.setAttribute("aria-describedby", description.id);
1000
+ }
1001
+ svg.setAttribute("aria-labelledby", title.id);
1002
+ const content = svgElement("g", {
1003
+ class: "gaugeit__viewport",
1004
+ "data-gaugeit-viewport": "true"
1005
+ });
1006
+ svg.append(content);
1007
+ root.append(svg);
1008
+ this.host.replaceChildren(root);
1009
+ this.host.classList.add("gaugeit-host");
1010
+ this.root = root;
1011
+ this.svg = svg;
1012
+ this.content = content;
1013
+ this.applySizing(initialViewport);
1014
+ return svg;
1015
+ }
1016
+ /** Finalizes the responsive viewBox after all layers have been rendered. */
1017
+ finish() {
1018
+ if (!this.svg || !this.content) return null;
1019
+ const measured = measureContent(this.content);
1020
+ const contentIsClipped = this.options.face.visible && this.options.face.clipContent;
1021
+ const combined = measured && !contentIsClipped ? unionBounds(this.layoutBounds, measured) : this.layoutBounds;
1022
+ const viewport = padBounds(combined, this.options.layout.padding);
1023
+ this.layoutBounds = combined;
1024
+ this.svg.setAttribute("viewBox", viewBoxValue(viewport));
1025
+ this.applySizing(viewport);
1026
+ this.root.dataset.gaugeitViewBox = viewBoxValue(viewport);
1027
+ return viewport;
1028
+ }
1029
+ /** Creates an SVG group for one logical layer. */
1030
+ group(name, attributes = {}) {
1031
+ const clipPath = name !== "face" && this.contentClipPathId ? `url(#${this.contentClipPathId})` : void 0;
1032
+ const group = svgElement("g", {
1033
+ class: `gaugeit__layer gaugeit__layer--${name}`,
1034
+ "data-gaugeit-layer": name,
1035
+ "clip-path": clipPath,
1036
+ ...attributes
1037
+ });
1038
+ this.content.append(group);
1039
+ return group;
1040
+ }
1041
+ /** Clips subsequently created non-face layers to a reusable SVG clip path. */
1042
+ setContentClipPath(clipPathId) {
1043
+ this.contentClipPathId = clipPathId ? String(clipPathId) : null;
1044
+ }
1045
+ /** Returns conservative content bounds available before layer rendering. */
1046
+ getContentBounds() {
1047
+ return this.contentBounds;
1048
+ }
1049
+ /** Stores a generated node or value for later dynamic updates. */
1050
+ setReference(name, value) {
1051
+ this.references.set(name, value);
1052
+ return value;
1053
+ }
1054
+ getReference(name) {
1055
+ return this.references.get(name);
1056
+ }
1057
+ addController(controller) {
1058
+ if (controller && typeof controller.update === "function") this.controllers.push(controller);
1059
+ }
1060
+ update(value, state) {
1061
+ for (const controller of this.controllers) controller.update(value, state);
1062
+ }
1063
+ destroyControllers() {
1064
+ for (const controller of this.controllers) {
1065
+ if (typeof controller.destroy === "function") controller.destroy();
1066
+ }
1067
+ this.controllers = [];
1068
+ }
1069
+ destroy() {
1070
+ this.destroyControllers();
1071
+ this.references.clear();
1072
+ if (this.root && this.root.parentNode === this.host) this.root.remove();
1073
+ this.host.classList.remove("gaugeit-host");
1074
+ this.root = null;
1075
+ this.svg = null;
1076
+ this.content = null;
1077
+ this.contentBounds = null;
1078
+ this.layoutBounds = null;
1079
+ this.contentClipPathId = null;
1080
+ }
1081
+ applySizing(viewport) {
1082
+ const responsive = this.options.responsive;
1083
+ const contain = this.options.layout.mode === "contain";
1084
+ this.root.style.width = responsive ? "100%" : `${viewport.width}px`;
1085
+ this.root.style.maxWidth = "100%";
1086
+ this.root.style.height = contain ? "100%" : "auto";
1087
+ this.svg.style.display = "block";
1088
+ this.svg.style.width = responsive ? "100%" : `${viewport.width}px`;
1089
+ this.svg.style.height = contain ? "100%" : "auto";
1090
+ this.svg.style.maxWidth = "100%";
1091
+ this.svg.style.maxHeight = contain ? "100%" : "none";
1092
+ this.svg.style.overflow = "hidden";
1093
+ }
1094
+ };
1095
+ function viewBoxValue(bounds) {
1096
+ return `${round(bounds.x)} ${round(bounds.y)} ${round(bounds.width)} ${round(bounds.height)}`;
1097
+ }
1098
+ function round(value) {
1099
+ return Math.round(value * 1e3) / 1e3;
1100
+ }
1101
+ function measureContent(content) {
1102
+ if (typeof content.getBBox !== "function") return null;
1103
+ try {
1104
+ const box = content.getBBox();
1105
+ if (![box.x, box.y, box.width, box.height].every(Number.isFinite)) return null;
1106
+ if (box.width <= 0 && box.height <= 0) return null;
1107
+ return { x: box.x, y: box.y, width: Math.max(1, box.width), height: Math.max(1, box.height) };
1108
+ } catch (e) {
1109
+ return null;
1110
+ }
1111
+ }
1112
+
1113
+ // src/core/easing.js
1114
+ var EASINGS = {
1115
+ linear: (t) => t,
1116
+ easeInQuad: (t) => t * t,
1117
+ easeOutQuad: (t) => t * (2 - t),
1118
+ easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
1119
+ easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
1120
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
1121
+ easeOutBack: (t) => {
1122
+ const c1 = 1.70158;
1123
+ const c3 = c1 + 1;
1124
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1125
+ }
1126
+ };
1127
+ function resolveEasing(easing) {
1128
+ if (typeof easing === "function") return easing;
1129
+ return EASINGS[easing] || EASINGS.easeOutCubic;
1130
+ }
1131
+ var easingNames = Object.freeze(Object.keys(EASINGS));
1132
+
1133
+ // src/core/defaults.js
1134
+ var BASE_DEFAULTS = Object.freeze({
1135
+ type: "arc",
1136
+ min: 0,
1137
+ max: 100,
1138
+ value: 0,
1139
+ invert: false,
1140
+ overflow: "clamp",
1141
+ precision: 3,
1142
+ className: "",
1143
+ layout: {
1144
+ mode: "intrinsic",
1145
+ padding: 8,
1146
+ includeGeometry: true
1147
+ },
1148
+ geometry: {
1149
+ mode: "radial",
1150
+ width: 320,
1151
+ height: 220,
1152
+ centerX: 160,
1153
+ centerY: 174,
1154
+ radius: 120,
1155
+ startAngle: 180,
1156
+ endAngle: 360,
1157
+ preserveAspectRatio: "xMidYMid meet",
1158
+ aspectRatio: null
1159
+ },
1160
+ linear: {
1161
+ originY: 140,
1162
+ startX: 36,
1163
+ startY: 0,
1164
+ endX: 284,
1165
+ endY: 0,
1166
+ tickSide: "negative",
1167
+ labelOffset: 34,
1168
+ zoneOffset: -12
1169
+ },
1170
+ face: {
1171
+ visible: false,
1172
+ shape: "panel",
1173
+ fit: "geometry",
1174
+ contentPadding: 8,
1175
+ minWidth: null,
1176
+ minHeight: null,
1177
+ clipContent: false,
1178
+ x: 6,
1179
+ y: 6,
1180
+ width: 308,
1181
+ height: 208,
1182
+ cornerRadius: 18,
1183
+ color: "#ffffff",
1184
+ strokeColor: "#d1d5db",
1185
+ strokeWidth: 2,
1186
+ innerStrokeColor: "#f3f4f6",
1187
+ innerStrokeWidth: 1,
1188
+ shadow: false,
1189
+ glass: false
1190
+ },
1191
+ track: {
1192
+ visible: true,
1193
+ color: "#e5e7eb",
1194
+ width: 28,
1195
+ radius: 112,
1196
+ opacity: 1,
1197
+ lineCap: "butt",
1198
+ showUnderZones: false
1199
+ },
1200
+ zones: [],
1201
+ scale: {
1202
+ endpointMode: "auto",
1203
+ radius: 112,
1204
+ position: "inside",
1205
+ major: {
1206
+ visible: true,
1207
+ interval: null,
1208
+ divisions: 5,
1209
+ length: 13,
1210
+ width: 2,
1211
+ color: "#111827",
1212
+ opacity: 1
1213
+ },
1214
+ minor: {
1215
+ visible: true,
1216
+ subdivisions: 4,
1217
+ length: 7,
1218
+ width: 1,
1219
+ color: "#6b7280",
1220
+ opacity: 0.9
1221
+ }
1222
+ },
1223
+ labels: {
1224
+ visible: true,
1225
+ interval: null,
1226
+ radius: 84,
1227
+ color: "#111827",
1228
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1229
+ fontSize: 11,
1230
+ fontWeight: 500,
1231
+ fractionDigits: 0,
1232
+ rotation: "horizontal",
1233
+ formatter: null
1234
+ },
1235
+ pointer: {
1236
+ visible: true,
1237
+ type: "needle",
1238
+ length: 94,
1239
+ shaftLength: null,
1240
+ tailLength: 13,
1241
+ width: 8,
1242
+ color: "#111827",
1243
+ strokeColor: "none",
1244
+ strokeWidth: 0,
1245
+ opacity: 1,
1246
+ colorByZone: false,
1247
+ cap: {
1248
+ visible: true,
1249
+ stackOrder: "above",
1250
+ radius: 8,
1251
+ color: "#111827",
1252
+ strokeColor: "#ffffff",
1253
+ strokeWidth: 2
1254
+ }
1255
+ },
1256
+ indicator: {
1257
+ visible: false,
1258
+ type: "hairline",
1259
+ position: "cross",
1260
+ length: 76,
1261
+ centerOffset: null,
1262
+ offset: 0,
1263
+ width: 2,
1264
+ color: "#b91c1c",
1265
+ strokeColor: "none",
1266
+ strokeWidth: 0,
1267
+ opacity: 1,
1268
+ lineCap: "round",
1269
+ colorByZone: false,
1270
+ markerSize: 8,
1271
+ carriageWidth: 14,
1272
+ carriageRadius: 4,
1273
+ cap: {
1274
+ visible: false,
1275
+ stackOrder: "above",
1276
+ radius: 4,
1277
+ color: "#b91c1c",
1278
+ strokeColor: "#ffffff",
1279
+ strokeWidth: 1
1280
+ }
1281
+ },
1282
+ centerZero: {
1283
+ zeroValue: 0,
1284
+ symmetric: false,
1285
+ marker: {
1286
+ visible: false,
1287
+ radius: null,
1288
+ position: "cross",
1289
+ centerOffset: 0,
1290
+ length: 20,
1291
+ width: 3,
1292
+ color: "#111827",
1293
+ opacity: 1,
1294
+ lineCap: "round"
1295
+ }
1296
+ },
1297
+ tape: {
1298
+ orientation: "vertical",
1299
+ x: 64,
1300
+ y: 58,
1301
+ width: 192,
1302
+ height: 210,
1303
+ cornerRadius: 12,
1304
+ windowColor: "#e5e7eb",
1305
+ windowStrokeColor: "#475569",
1306
+ windowStrokeWidth: 2,
1307
+ windowInnerStrokeColor: "#d1d5db",
1308
+ windowInnerStrokeWidth: 1,
1309
+ stripColor: "#ffffff",
1310
+ textColor: "#111827",
1311
+ tickColor: "#334155",
1312
+ minorTickColor: "#64748b",
1313
+ minorTickOpacity: 0.85,
1314
+ majorTickWidth: 2,
1315
+ minorTickWidth: 1,
1316
+ majorTickLength: 24,
1317
+ minorTickLength: 12,
1318
+ tickInset: 64,
1319
+ labelOffset: 10,
1320
+ interval: null,
1321
+ minorSubdivisions: 4,
1322
+ majorSpacing: 52,
1323
+ indicatorColor: "#b91c1c",
1324
+ indicatorWidth: 2.5,
1325
+ fadeSize: 32,
1326
+ fadeColor: "#ffffff",
1327
+ colorByZone: false
1328
+ },
1329
+ light: {
1330
+ visible: false,
1331
+ x: 160,
1332
+ y: 110,
1333
+ radius: 8,
1334
+ color: "amber",
1335
+ customColor: "",
1336
+ offColor: "#f3ead6",
1337
+ offEdgeColor: "#817768",
1338
+ bezelColor: "#4b5563",
1339
+ bezelHighlightColor: "#f8fafc",
1340
+ opacity: 1,
1341
+ on: false,
1342
+ glow: true,
1343
+ pulse: true,
1344
+ pulseInterval: 2600,
1345
+ blink: false,
1346
+ blinkInterval: 1e3,
1347
+ trigger: {
1348
+ mode: "manual",
1349
+ source: "target",
1350
+ value: 0,
1351
+ min: 0,
1352
+ max: 0
1353
+ }
1354
+ },
1355
+ readout: {
1356
+ visible: true,
1357
+ title: {
1358
+ visible: false,
1359
+ text: "",
1360
+ x: "auto",
1361
+ y: "auto",
1362
+ position: "top",
1363
+ margin: 12,
1364
+ offsetX: 0,
1365
+ offsetY: 0,
1366
+ color: "#374151",
1367
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1368
+ fontSize: 14,
1369
+ fontWeight: 700,
1370
+ letterSpacing: 0.8
1371
+ },
1372
+ value: {
1373
+ visible: false,
1374
+ x: "auto",
1375
+ y: 54,
1376
+ position: "center",
1377
+ margin: 12,
1378
+ offsetX: 0,
1379
+ offsetY: 0,
1380
+ color: "#111827",
1381
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1382
+ fontSize: 38,
1383
+ fontWeight: 750,
1384
+ fractionDigits: 0,
1385
+ prefix: "",
1386
+ suffix: "",
1387
+ locale: void 0,
1388
+ formatter: null
1389
+ },
1390
+ unit: {
1391
+ visible: false,
1392
+ text: "",
1393
+ x: "auto",
1394
+ y: 76,
1395
+ position: "bottom",
1396
+ margin: 12,
1397
+ offsetX: 0,
1398
+ offsetY: 0,
1399
+ color: "#6b7280",
1400
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1401
+ fontSize: 12,
1402
+ fontWeight: 600
1403
+ }
1404
+ },
1405
+ animation: {
1406
+ enabled: true,
1407
+ duration: 850,
1408
+ easing: "easeOutCubic",
1409
+ animateOnInit: true,
1410
+ startFrom: "min",
1411
+ respectReducedMotion: true
1412
+ },
1413
+ effects: {
1414
+ jitter: {
1415
+ enabled: false,
1416
+ amplitude: 0.45,
1417
+ amplitudeUnit: "value",
1418
+ frequency: 4,
1419
+ smoothing: 0.86,
1420
+ whenIdleOnly: true
1421
+ }
1422
+ },
1423
+ accessibility: {
1424
+ label: "Gauge",
1425
+ description: "",
1426
+ valueText: null
1427
+ },
1428
+ performance: {
1429
+ pauseWhenHidden: true,
1430
+ pauseWhenOffscreen: true
1431
+ },
1432
+ responsive: true
1433
+ });
1434
+
1435
+ // src/core/options.js
1436
+ var VALID_OVERFLOW = /* @__PURE__ */ new Set(["clamp", "expand", "allow"]);
1437
+ var VALID_POINTERS = /* @__PURE__ */ new Set(["needle", "line", "arrow", "spear"]);
1438
+ var VALID_POSITIONS = /* @__PURE__ */ new Set(["inside", "outside", "cross"]);
1439
+ var VALID_ROTATIONS = /* @__PURE__ */ new Set(["horizontal", "radial", "tangent"]);
1440
+ var VALID_LAYOUT_MODES = /* @__PURE__ */ new Set(["intrinsic", "contain"]);
1441
+ var VALID_FACE_FITS = /* @__PURE__ */ new Set(["geometry", "content"]);
1442
+ var VALID_FACE_SHAPES = /* @__PURE__ */ new Set(["panel", "circle"]);
1443
+ var VALID_ENDPOINT_MODES = /* @__PURE__ */ new Set(["auto", "both", "dedupe"]);
1444
+ var VALID_READOUT_POSITIONS = /* @__PURE__ */ new Set(["top", "center", "bottom"]);
1445
+ var VALID_GEOMETRY_MODES = /* @__PURE__ */ new Set(["radial", "linear", "tape"]);
1446
+ var VALID_LINEAR_TICK_SIDES = /* @__PURE__ */ new Set(["negative", "positive", "cross"]);
1447
+ var VALID_INDICATOR_TYPES = /* @__PURE__ */ new Set(["hairline", "marker", "carriage"]);
1448
+ var VALID_INDICATOR_POSITIONS = /* @__PURE__ */ new Set(["negative", "positive", "cross"]);
1449
+ var VALID_CAP_STACK_ORDERS = /* @__PURE__ */ new Set(["above", "below"]);
1450
+ var VALID_ORIENTATIONS = /* @__PURE__ */ new Set(["vertical", "horizontal"]);
1451
+ var VALID_LIGHT_COLORS = /* @__PURE__ */ new Set([
1452
+ "red",
1453
+ "green",
1454
+ "blue",
1455
+ "amber",
1456
+ "yellow",
1457
+ "orange",
1458
+ "white",
1459
+ "halogen",
1460
+ "cyan",
1461
+ "brown"
1462
+ ]);
1463
+ var VALID_LIGHT_TRIGGER_MODES = /* @__PURE__ */ new Set(["manual", "below", "above", "between"]);
1464
+ var VALID_LIGHT_TRIGGER_SOURCES = /* @__PURE__ */ new Set(["target", "displayed", "pointer"]);
1465
+ function resolveGaugeOptions(userOptions, registry) {
1466
+ const requested = isPlainObject(userOptions) ? userOptions : {};
1467
+ const requestedType = String(requested.type || BASE_DEFAULTS.type);
1468
+ const typeDefinition = registry.get(requestedType);
1469
+ const fallbacks = deepMerge(BASE_DEFAULTS, typeDefinition.defaults);
1470
+ let options = deepMerge(fallbacks, requested);
1471
+ options.type = requestedType;
1472
+ options = restoreOptionShape(options, fallbacks);
1473
+ if (typeof typeDefinition.configure === "function") {
1474
+ const configured = typeDefinition.configure(options, {
1475
+ requested,
1476
+ typeDefaults: typeDefinition.defaults
1477
+ });
1478
+ if (isPlainObject(configured)) options = configured;
1479
+ }
1480
+ options = restoreOptionShape(options, fallbacks);
1481
+ options.min = finiteNumber(options.min, 0);
1482
+ options.max = finiteNumber(options.max, 100);
1483
+ if (options.max <= options.min) options.max = options.min + 1;
1484
+ options.value = finiteNumber(options.value, options.min);
1485
+ options.invert = options.invert === true;
1486
+ options.precision = Math.max(0, Math.min(12, Math.floor(finiteNumber(options.precision, 3))));
1487
+ options.overflow = VALID_OVERFLOW.has(options.overflow) ? options.overflow : "clamp";
1488
+ validateLayout(options.layout);
1489
+ validateGeometry(options.geometry);
1490
+ validateLinear(options.linear);
1491
+ validateFace(options.face);
1492
+ validateScale(options);
1493
+ validatePointer(options.pointer);
1494
+ validateIndicator(options.indicator, requested.indicator);
1495
+ validateCenterZero(options.centerZero, options);
1496
+ validateTape(options.tape);
1497
+ validateLight(options.light);
1498
+ validateLabels(options.labels);
1499
+ validateReadout(options.readout);
1500
+ validateAnimation(options.animation);
1501
+ validateJitter(options.effects.jitter);
1502
+ options.zones = normalizeZones(options.zones, options.min, options.max, options.track.width);
1503
+ return options;
1504
+ }
1505
+ function resolveMajorInterval(options) {
1506
+ const explicit = finiteNumber(options.scale.major.interval, 0);
1507
+ if (explicit > 0) return explicit;
1508
+ const divisions = Math.max(1, Math.floor(finiteNumber(options.scale.major.divisions, 5)));
1509
+ return niceInterval(options.max - options.min, divisions);
1510
+ }
1511
+ function mergeUserOptions(currentUserOptions, patch) {
1512
+ return deepMerge(currentUserOptions || {}, patch || {});
1513
+ }
1514
+ function validateLayout(layout) {
1515
+ layout.mode = VALID_LAYOUT_MODES.has(layout.mode) ? layout.mode : "intrinsic";
1516
+ layout.padding = Math.max(0, finiteNumber(layout.padding, 8));
1517
+ layout.includeGeometry = layout.includeGeometry !== false;
1518
+ }
1519
+ function validateGeometry(geometry2) {
1520
+ geometry2.mode = VALID_GEOMETRY_MODES.has(geometry2.mode) ? geometry2.mode : "radial";
1521
+ geometry2.width = Math.max(40, finiteNumber(geometry2.width, 320));
1522
+ geometry2.height = Math.max(40, finiteNumber(geometry2.height, 220));
1523
+ geometry2.centerX = finiteNumber(geometry2.centerX, geometry2.width / 2);
1524
+ geometry2.centerY = finiteNumber(geometry2.centerY, geometry2.height * 0.78);
1525
+ geometry2.radius = Math.max(1, finiteNumber(geometry2.radius, Math.min(geometry2.width, geometry2.height) * 0.4));
1526
+ geometry2.startAngle = finiteNumber(geometry2.startAngle, 180);
1527
+ geometry2.endAngle = finiteNumber(geometry2.endAngle, 360);
1528
+ if (Math.abs(geometry2.endAngle - geometry2.startAngle) < 1e-3) geometry2.endAngle = geometry2.startAngle + 180;
1529
+ if (Math.abs(geometry2.endAngle - geometry2.startAngle) > 360) {
1530
+ geometry2.endAngle = geometry2.startAngle + Math.sign(geometry2.endAngle - geometry2.startAngle) * 360;
1531
+ }
1532
+ geometry2.preserveAspectRatio = String(geometry2.preserveAspectRatio || "xMidYMid meet");
1533
+ geometry2.aspectRatio = geometry2.aspectRatio === null || geometry2.aspectRatio === void 0 ? null : Math.max(1e-6, finiteNumber(geometry2.aspectRatio, geometry2.width / geometry2.height));
1534
+ }
1535
+ function validateLinear(linear) {
1536
+ linear.originY = finiteNumber(linear.originY, 140);
1537
+ linear.startX = finiteNumber(linear.startX, 36);
1538
+ linear.startY = finiteNumber(linear.startY, 0);
1539
+ linear.endX = finiteNumber(linear.endX, 284);
1540
+ linear.endY = finiteNumber(linear.endY, linear.startY);
1541
+ if (Math.hypot(linear.endX - linear.startX, linear.endY - linear.startY) < 1e-3) {
1542
+ linear.endX = linear.startX + 1;
1543
+ linear.endY = linear.startY;
1544
+ }
1545
+ linear.tickSide = VALID_LINEAR_TICK_SIDES.has(linear.tickSide) ? linear.tickSide : "negative";
1546
+ linear.labelOffset = finiteNumber(linear.labelOffset, 34);
1547
+ linear.zoneOffset = finiteNumber(linear.zoneOffset, -12);
1548
+ }
1549
+ function validateFace(face) {
1550
+ face.shape = VALID_FACE_SHAPES.has(face.shape) ? face.shape : "panel";
1551
+ face.fit = VALID_FACE_FITS.has(face.fit) ? face.fit : "geometry";
1552
+ face.contentPadding = Math.max(0, finiteNumber(face.contentPadding, 8));
1553
+ face.minWidth = face.minWidth === null || face.minWidth === void 0 ? null : Math.max(1, finiteNumber(face.minWidth, 1));
1554
+ face.minHeight = face.minHeight === null || face.minHeight === void 0 ? null : Math.max(1, finiteNumber(face.minHeight, 1));
1555
+ face.clipContent = face.clipContent === true;
1556
+ face.x = finiteNumber(face.x, 6);
1557
+ face.y = finiteNumber(face.y, 6);
1558
+ face.width = Math.max(1, finiteNumber(face.width, 308));
1559
+ face.height = Math.max(1, finiteNumber(face.height, 208));
1560
+ face.cornerRadius = Math.max(0, finiteNumber(face.cornerRadius, 18));
1561
+ face.strokeWidth = Math.max(0, finiteNumber(face.strokeWidth, 2));
1562
+ face.innerStrokeWidth = Math.max(0, finiteNumber(face.innerStrokeWidth, 1));
1563
+ }
1564
+ function validateScale(options) {
1565
+ options.scale.endpointMode = VALID_ENDPOINT_MODES.has(options.scale.endpointMode) ? options.scale.endpointMode : "auto";
1566
+ options.scale.radius = Math.max(1, finiteNumber(options.scale.radius, options.geometry.radius));
1567
+ options.scale.position = VALID_POSITIONS.has(options.scale.position) ? options.scale.position : "inside";
1568
+ options.scale.major.divisions = Math.max(1, Math.floor(finiteNumber(options.scale.major.divisions, 5)));
1569
+ options.scale.major.length = Math.max(0, finiteNumber(options.scale.major.length, 13));
1570
+ options.scale.major.width = Math.max(0, finiteNumber(options.scale.major.width, 2));
1571
+ options.scale.minor.subdivisions = Math.max(0, Math.floor(finiteNumber(options.scale.minor.subdivisions, 4)));
1572
+ options.scale.minor.length = Math.max(0, finiteNumber(options.scale.minor.length, 7));
1573
+ options.scale.minor.width = Math.max(0, finiteNumber(options.scale.minor.width, 1));
1574
+ }
1575
+ function validatePointer(pointer) {
1576
+ pointer.type = VALID_POINTERS.has(pointer.type) ? pointer.type : "needle";
1577
+ pointer.length = Math.max(0, finiteNumber(pointer.length, 94));
1578
+ pointer.shaftLength = pointer.shaftLength === null || pointer.shaftLength === void 0 ? null : Math.min(pointer.length, Math.max(0, finiteNumber(pointer.shaftLength, pointer.length)));
1579
+ pointer.tailLength = Math.max(0, finiteNumber(pointer.tailLength, 13));
1580
+ pointer.width = Math.max(0.5, finiteNumber(pointer.width, 8));
1581
+ pointer.strokeWidth = Math.max(0, finiteNumber(pointer.strokeWidth, 0));
1582
+ pointer.cap.radius = Math.max(0, finiteNumber(pointer.cap.radius, 8));
1583
+ pointer.cap.strokeWidth = Math.max(0, finiteNumber(pointer.cap.strokeWidth, 2));
1584
+ pointer.cap.stackOrder = VALID_CAP_STACK_ORDERS.has(pointer.cap.stackOrder) ? pointer.cap.stackOrder : "above";
1585
+ }
1586
+ function validateIndicator(indicator, requestedIndicator = {}) {
1587
+ indicator.visible = indicator.visible === true;
1588
+ indicator.type = VALID_INDICATOR_TYPES.has(indicator.type) ? indicator.type : "hairline";
1589
+ indicator.position = VALID_INDICATOR_POSITIONS.has(indicator.position) ? indicator.position : "cross";
1590
+ indicator.length = Math.max(0, finiteNumber(indicator.length, 76));
1591
+ const explicitCenterOffset = isPlainObject(requestedIndicator) && Object.prototype.hasOwnProperty.call(requestedIndicator, "centerOffset");
1592
+ const explicitLegacyOffset = isPlainObject(requestedIndicator) && Object.prototype.hasOwnProperty.call(requestedIndicator, "offset");
1593
+ if (explicitCenterOffset) indicator.centerOffset = finiteNumber(requestedIndicator.centerOffset, 0);
1594
+ else if (explicitLegacyOffset) indicator.centerOffset = -finiteNumber(requestedIndicator.offset, 0);
1595
+ else if (indicator.centerOffset === null || indicator.centerOffset === void 0) {
1596
+ indicator.centerOffset = -finiteNumber(indicator.offset, 0);
1597
+ } else indicator.centerOffset = finiteNumber(indicator.centerOffset, 0);
1598
+ indicator.offset = -indicator.centerOffset;
1599
+ indicator.width = Math.max(0.5, finiteNumber(indicator.width, 2));
1600
+ indicator.strokeWidth = Math.max(0, finiteNumber(indicator.strokeWidth, 0));
1601
+ indicator.opacity = Math.min(1, Math.max(0, finiteNumber(indicator.opacity, 1)));
1602
+ indicator.markerSize = Math.max(1, finiteNumber(indicator.markerSize, 8));
1603
+ indicator.carriageWidth = Math.max(1, finiteNumber(indicator.carriageWidth, 14));
1604
+ indicator.carriageRadius = Math.max(0, finiteNumber(indicator.carriageRadius, 4));
1605
+ indicator.cap.radius = Math.max(0, finiteNumber(indicator.cap.radius, 4));
1606
+ indicator.cap.strokeWidth = Math.max(0, finiteNumber(indicator.cap.strokeWidth, 1));
1607
+ indicator.cap.stackOrder = VALID_CAP_STACK_ORDERS.has(indicator.cap.stackOrder) ? indicator.cap.stackOrder : "above";
1608
+ }
1609
+ function validateCenterZero(centerZero, options) {
1610
+ centerZero.zeroValue = finiteNumber(centerZero.zeroValue, 0);
1611
+ centerZero.symmetric = centerZero.symmetric === true;
1612
+ if (centerZero.symmetric) {
1613
+ const magnitude = Math.max(
1614
+ Math.abs(options.min - centerZero.zeroValue),
1615
+ Math.abs(options.max - centerZero.zeroValue),
1616
+ 1
1617
+ );
1618
+ options.min = centerZero.zeroValue - magnitude;
1619
+ options.max = centerZero.zeroValue + magnitude;
1620
+ }
1621
+ centerZero.marker.visible = centerZero.marker.visible === true;
1622
+ centerZero.marker.radius = centerZero.marker.radius === null || centerZero.marker.radius === void 0 ? null : Math.max(0, finiteNumber(centerZero.marker.radius, options.scale.radius));
1623
+ centerZero.marker.position = VALID_INDICATOR_POSITIONS.has(centerZero.marker.position) ? centerZero.marker.position : "cross";
1624
+ centerZero.marker.centerOffset = finiteNumber(centerZero.marker.centerOffset, 0);
1625
+ centerZero.marker.length = Math.max(0, finiteNumber(centerZero.marker.length, 20));
1626
+ centerZero.marker.width = Math.max(0.5, finiteNumber(centerZero.marker.width, 3));
1627
+ centerZero.marker.opacity = Math.min(1, Math.max(0, finiteNumber(centerZero.marker.opacity, 1)));
1628
+ centerZero.marker.lineCap = String(centerZero.marker.lineCap || "round");
1629
+ }
1630
+ function validateTape(tape) {
1631
+ tape.orientation = VALID_ORIENTATIONS.has(tape.orientation) ? tape.orientation : "vertical";
1632
+ tape.x = finiteNumber(tape.x, 64);
1633
+ tape.y = finiteNumber(tape.y, 58);
1634
+ tape.width = Math.max(20, finiteNumber(tape.width, 192));
1635
+ tape.height = Math.max(20, finiteNumber(tape.height, 210));
1636
+ tape.cornerRadius = Math.max(0, finiteNumber(tape.cornerRadius, 12));
1637
+ tape.windowStrokeWidth = Math.max(0, finiteNumber(tape.windowStrokeWidth, 2));
1638
+ tape.windowInnerStrokeWidth = Math.max(0, finiteNumber(tape.windowInnerStrokeWidth, 1));
1639
+ tape.minorTickOpacity = Math.min(1, Math.max(0, finiteNumber(tape.minorTickOpacity, 0.85)));
1640
+ tape.majorTickWidth = Math.max(0.5, finiteNumber(tape.majorTickWidth, 2));
1641
+ tape.minorTickWidth = Math.max(0.5, finiteNumber(tape.minorTickWidth, 1));
1642
+ tape.majorTickLength = Math.max(0, finiteNumber(tape.majorTickLength, 24));
1643
+ tape.minorTickLength = Math.max(0, finiteNumber(tape.minorTickLength, 12));
1644
+ tape.tickInset = finiteNumber(tape.tickInset, 64);
1645
+ tape.labelOffset = finiteNumber(tape.labelOffset, 10);
1646
+ tape.interval = tape.interval === null || tape.interval === void 0 ? null : Math.max(1e-6, finiteNumber(tape.interval, 1));
1647
+ tape.minorSubdivisions = Math.max(0, Math.floor(finiteNumber(tape.minorSubdivisions, 4)));
1648
+ tape.majorSpacing = Math.max(2, finiteNumber(tape.majorSpacing, 52));
1649
+ tape.indicatorWidth = Math.max(0.5, finiteNumber(tape.indicatorWidth, 2.5));
1650
+ const fadeAxisSize = tape.orientation === "horizontal" ? tape.width : tape.height;
1651
+ tape.fadeSize = Math.min(fadeAxisSize / 2, Math.max(0, finiteNumber(tape.fadeSize, 32)));
1652
+ tape.colorByZone = tape.colorByZone === true;
1653
+ }
1654
+ function validateLight(light) {
1655
+ light.visible = light.visible === true;
1656
+ light.x = finiteNumber(light.x, 160);
1657
+ light.y = finiteNumber(light.y, 110);
1658
+ light.radius = Math.max(1, finiteNumber(light.radius, 8));
1659
+ light.color = VALID_LIGHT_COLORS.has(light.color) ? light.color : "amber";
1660
+ light.customColor = light.customColor === null || light.customColor === void 0 ? "" : String(light.customColor).trim();
1661
+ light.offColor = normalizeColor(light.offColor, "#f3ead6");
1662
+ light.offEdgeColor = normalizeColor(light.offEdgeColor, "#817768");
1663
+ light.bezelColor = normalizeColor(light.bezelColor, "#4b5563");
1664
+ light.bezelHighlightColor = normalizeColor(light.bezelHighlightColor, "#f8fafc");
1665
+ light.opacity = Math.min(1, Math.max(0, finiteNumber(light.opacity, 1)));
1666
+ light.on = light.on === true;
1667
+ light.glow = light.glow !== false;
1668
+ light.pulse = light.pulse !== false;
1669
+ light.pulseInterval = Math.max(400, finiteNumber(light.pulseInterval, 2600));
1670
+ light.blink = light.blink === true;
1671
+ light.blinkInterval = Math.max(100, finiteNumber(light.blinkInterval, 1e3));
1672
+ light.trigger = isPlainObject(light.trigger) ? light.trigger : {};
1673
+ light.trigger.mode = VALID_LIGHT_TRIGGER_MODES.has(light.trigger.mode) ? light.trigger.mode : "manual";
1674
+ light.trigger.source = VALID_LIGHT_TRIGGER_SOURCES.has(light.trigger.source) ? light.trigger.source : "target";
1675
+ light.trigger.value = finiteNumber(light.trigger.value, 0);
1676
+ light.trigger.min = finiteNumber(light.trigger.min, light.trigger.value);
1677
+ light.trigger.max = finiteNumber(light.trigger.max, light.trigger.value);
1678
+ if (light.trigger.max < light.trigger.min) {
1679
+ [light.trigger.min, light.trigger.max] = [light.trigger.max, light.trigger.min];
1680
+ }
1681
+ }
1682
+ function normalizeColor(value, fallback) {
1683
+ if (value === null || value === void 0) return fallback;
1684
+ const normalized = String(value).trim();
1685
+ return normalized || fallback;
1686
+ }
1687
+ function validateLabels(labels) {
1688
+ labels.radius = Math.max(0, finiteNumber(labels.radius, 84));
1689
+ labels.fontSize = Math.max(1, finiteNumber(labels.fontSize, 11));
1690
+ labels.fractionDigits = Math.max(0, Math.min(12, Math.floor(finiteNumber(labels.fractionDigits, 0))));
1691
+ labels.rotation = VALID_ROTATIONS.has(labels.rotation) ? labels.rotation : "horizontal";
1692
+ }
1693
+ function validateReadout(readout) {
1694
+ readout.visible = readout.visible !== false;
1695
+ validateReadoutPart(readout.title, 14);
1696
+ validateReadoutPart(readout.value, 38);
1697
+ validateReadoutPart(readout.unit, 12);
1698
+ readout.value.fractionDigits = Math.max(
1699
+ 0,
1700
+ Math.min(12, Math.floor(finiteNumber(readout.value.fractionDigits, 0)))
1701
+ );
1702
+ }
1703
+ function validateReadoutPart(part, defaultFontSize) {
1704
+ part.visible = part.visible === true;
1705
+ part.x = part.x === "auto" ? "auto" : finiteNumber(part.x, 160);
1706
+ part.y = part.y === "auto" ? "auto" : finiteNumber(part.y, 0);
1707
+ part.position = VALID_READOUT_POSITIONS.has(part.position) ? part.position : "top";
1708
+ part.margin = Math.max(0, finiteNumber(part.margin, 12));
1709
+ part.offsetX = finiteNumber(part.offsetX, 0);
1710
+ part.offsetY = finiteNumber(part.offsetY, 0);
1711
+ part.fontSize = Math.max(1, finiteNumber(part.fontSize, defaultFontSize));
1712
+ part.letterSpacing = finiteNumber(part.letterSpacing, 0);
1713
+ }
1714
+ function validateAnimation(animation) {
1715
+ animation.duration = Math.max(0, finiteNumber(animation.duration, 850));
1716
+ if (typeof animation.startFrom !== "number" && !["min", "max", "value"].includes(animation.startFrom)) {
1717
+ animation.startFrom = "min";
1718
+ }
1719
+ }
1720
+ function validateJitter(jitter) {
1721
+ jitter.amplitude = Math.max(0, finiteNumber(jitter.amplitude, 0.45));
1722
+ jitter.amplitudeUnit = jitter.amplitudeUnit === "percent" ? "percent" : "value";
1723
+ jitter.frequency = Math.max(0.1, finiteNumber(jitter.frequency, 4));
1724
+ jitter.smoothing = Math.min(0.999, Math.max(0, finiteNumber(jitter.smoothing, 0.86)));
1725
+ }
1726
+ function normalizeZones(zones, min, max, defaultWidth) {
1727
+ if (!Array.isArray(zones)) return [];
1728
+ return zones.filter(isPlainObject).map((zone) => {
1729
+ var _a, _b;
1730
+ const width = zone.width === void 0 ? void 0 : Math.max(0, finiteNumber(zone.width, 0));
1731
+ const taper = normalizeZoneTaper(zone.taper, width != null ? width : defaultWidth);
1732
+ return {
1733
+ ...zone,
1734
+ min: Math.max(min, finiteNumber(zone.min, min)),
1735
+ max: Math.min(max, finiteNumber(zone.max, max)),
1736
+ color: (_b = (_a = zone.color) != null ? _a : zone.strokeStyle) != null ? _b : "transparent",
1737
+ width,
1738
+ taper,
1739
+ radius: zone.radius === void 0 ? void 0 : Math.max(0, finiteNumber(zone.radius, 0)),
1740
+ offset: zone.offset === void 0 ? void 0 : finiteNumber(zone.offset, 0),
1741
+ opacity: zone.opacity === void 0 ? 1 : Math.min(1, Math.max(0, finiteNumber(zone.opacity, 1)))
1742
+ };
1743
+ }).filter((zone) => zone.max > zone.min);
1744
+ }
1745
+ function restoreOptionShape(options, fallbacks) {
1746
+ if (!isPlainObject(options)) return cloneValue(fallbacks);
1747
+ for (const [key, fallback] of Object.entries(fallbacks)) {
1748
+ if (!isPlainObject(fallback)) continue;
1749
+ options[key] = restoreOptionShape(options[key], fallback);
1750
+ }
1751
+ return options;
1752
+ }
1753
+ function normalizeZoneTaper(taper, fallbackWidth) {
1754
+ var _a;
1755
+ if (!isPlainObject(taper)) return void 0;
1756
+ const startWidth = taper.startWidth === void 0 ? fallbackWidth : Math.max(0, finiteNumber(taper.startWidth, fallbackWidth != null ? fallbackWidth : 0));
1757
+ const endWidth = taper.endWidth === void 0 ? fallbackWidth : Math.max(0, finiteNumber(taper.endWidth, (_a = fallbackWidth != null ? fallbackWidth : startWidth) != null ? _a : 0));
1758
+ return {
1759
+ startWidth,
1760
+ endWidth,
1761
+ segments: Math.max(2, Math.min(256, Math.floor(finiteNumber(taper.segments, 48))))
1762
+ };
1763
+ }
1764
+
1765
+ // src/core/gauge.js
1766
+ var Gauge = class {
1767
+ constructor(target, userOptions, registry, lifecycle = {}) {
1768
+ this.host = resolveElement(target);
1769
+ this.registry = registry;
1770
+ this._onDestroy = typeof lifecycle.onDestroy === "function" ? lifecycle.onDestroy : null;
1771
+ this.userOptions = mergeUserOptions({}, userOptions || {});
1772
+ this.options = resolveGaugeOptions(this.userOptions, registry);
1773
+ this.renderer = new SvgRenderer(this.host, this.options);
1774
+ this._destroyed = false;
1775
+ this._paused = false;
1776
+ this._pausedAt = 0;
1777
+ this._lifecycleSuspendedAt = 0;
1778
+ this._onscreen = true;
1779
+ this._frameHandle = null;
1780
+ this._animation = null;
1781
+ this._targetValue = this._normalizeIncomingValue(this.options.value);
1782
+ this._baseValue = this._targetValue;
1783
+ this._displayValue = this._targetValue;
1784
+ this._pointerValue = this._targetValue;
1785
+ this._jitterOffset = 0;
1786
+ this._jitterTarget = 0;
1787
+ this._lastJitterTargetAt = 0;
1788
+ this._lastFrameAt = 0;
1789
+ this._visibilityHandler = null;
1790
+ this._intersectionObserver = null;
1791
+ this._syncLifecycleObservers();
1792
+ this.render();
1793
+ this._initializeValue();
1794
+ }
1795
+ /** Fully rebuilds static layers and dynamic controllers. */
1796
+ render() {
1797
+ this._assertAlive();
1798
+ this.renderer.options = this.options;
1799
+ this.renderer.begin();
1800
+ const definition = this.registry.get(this.options.type);
1801
+ const context = {
1802
+ gauge: this,
1803
+ options: this.options,
1804
+ renderer: this.renderer,
1805
+ registry: this.registry
1806
+ };
1807
+ for (const layer of definition.layers) {
1808
+ const controller = layer.render(context);
1809
+ this.renderer.addController(controller);
1810
+ }
1811
+ this._updateDynamic(this._displayValue, true, this._pointerValue);
1812
+ this.renderer.finish();
1813
+ dispatchGaugeEvent(this.host, "render", {
1814
+ gauge: this,
1815
+ type: this.options.type,
1816
+ value: this._targetValue
1817
+ });
1818
+ return this;
1819
+ }
1820
+ /** Sets a new gauge value. */
1821
+ setValue(value, updateOptions = {}) {
1822
+ this._assertAlive();
1823
+ const normalized = this._normalizeIncomingValue(value);
1824
+ const duration = Math.max(0, finiteNumber(updateOptions.duration, this.options.animation.duration));
1825
+ const animate = updateOptions.animate !== false && this.options.animation.enabled && duration > 0 && !this._prefersReducedMotion();
1826
+ this._targetValue = normalized;
1827
+ this.options.value = normalized;
1828
+ if (!animate) {
1829
+ this._animation = null;
1830
+ this._baseValue = normalized;
1831
+ this._displayValue = normalized;
1832
+ this._pointerValue = normalized;
1833
+ this._jitterOffset = 0;
1834
+ this._updateDynamic(normalized, true, normalized);
1835
+ this._ensureLoop();
1836
+ this._emitChange();
1837
+ return this;
1838
+ }
1839
+ const easing = resolveEasing(updateOptions.easing || this.options.animation.easing);
1840
+ const now = performanceNow();
1841
+ this._animation = {
1842
+ from: this._baseValue,
1843
+ to: normalized,
1844
+ startedAt: now,
1845
+ duration,
1846
+ easing
1847
+ };
1848
+ dispatchGaugeEvent(this.host, "animationstart", {
1849
+ gauge: this,
1850
+ from: this._animation.from,
1851
+ to: normalized,
1852
+ duration
1853
+ });
1854
+ this._ensureLoop();
1855
+ this._emitChange();
1856
+ return this;
1857
+ }
1858
+ /** Gauge.js-compatible convenience alias. */
1859
+ set(value, updateOptions = {}) {
1860
+ return this.setValue(value, updateOptions);
1861
+ }
1862
+ /** Updates options by recursively merging a partial patch into the current user options. */
1863
+ updateOptions(patch, behavior = {}) {
1864
+ this._assertAlive();
1865
+ const requested = patch || {};
1866
+ return this._applyUserOptions(
1867
+ mergeUserOptions(this.userOptions, requested),
1868
+ Object.prototype.hasOwnProperty.call(requested, "value"),
1869
+ behavior
1870
+ );
1871
+ }
1872
+ /** Replaces the complete user-option object while preserving the current value unless supplied. */
1873
+ replaceOptions(options = {}, behavior = {}) {
1874
+ this._assertAlive();
1875
+ const requested = options || {};
1876
+ return this._applyUserOptions(
1877
+ mergeUserOptions({}, requested),
1878
+ Object.prototype.hasOwnProperty.call(requested, "value"),
1879
+ behavior
1880
+ );
1881
+ }
1882
+ /** Gauge.js-style alias for updateOptions. */
1883
+ setOptions(patch, behavior = {}) {
1884
+ return this.updateOptions(patch, behavior);
1885
+ }
1886
+ _applyUserOptions(nextUserOptions, hadExplicitValue, behavior) {
1887
+ const previousType = this.options.type;
1888
+ this.userOptions = nextUserOptions;
1889
+ this.options = resolveGaugeOptions(this.userOptions, this.registry);
1890
+ this.renderer.options = this.options;
1891
+ this._syncLifecycleObservers();
1892
+ const value = hadExplicitValue ? this.options.value : this._targetValue;
1893
+ this._targetValue = this._normalizeIncomingValue(value);
1894
+ if (hadExplicitValue && !behavior.animateToValue) {
1895
+ this._animation = null;
1896
+ this._baseValue = this._targetValue;
1897
+ this._displayValue = this._targetValue;
1898
+ this._pointerValue = this._targetValue;
1899
+ this._jitterOffset = 0;
1900
+ } else {
1901
+ this._baseValue = this._normalizeIncomingValue(this._baseValue);
1902
+ this._displayValue = this._normalizeIncomingValue(this._displayValue);
1903
+ this._pointerValue = this._displayClamp(this._baseValue + this._jitterOffset);
1904
+ }
1905
+ this.options.value = this._targetValue;
1906
+ this.render();
1907
+ if (behavior.animateToValue) this.setValue(this._targetValue, { animate: true });
1908
+ else this._updateDynamic(this._displayValue, true, this._pointerValue);
1909
+ if (previousType !== this.options.type) {
1910
+ dispatchGaugeEvent(this.host, "typechange", {
1911
+ gauge: this,
1912
+ previousType,
1913
+ type: this.options.type
1914
+ });
1915
+ }
1916
+ this._ensureLoop();
1917
+ return this;
1918
+ }
1919
+ /** Returns the last requested stable value. */
1920
+ getValue() {
1921
+ return this._targetValue;
1922
+ }
1923
+ /** Returns the currently rendered base value; visual pointer jitter is excluded. */
1924
+ getDisplayedValue() {
1925
+ return this._displayValue;
1926
+ }
1927
+ /** Temporarily stops animation and jitter work. */
1928
+ pause() {
1929
+ if (this._destroyed || this._paused) return this;
1930
+ this._paused = true;
1931
+ this._pausedAt = performanceNow();
1932
+ cancelFrame(this._frameHandle);
1933
+ this._frameHandle = null;
1934
+ dispatchGaugeEvent(this.host, "pause", { gauge: this });
1935
+ return this;
1936
+ }
1937
+ /** Resumes pending animation and jitter work. */
1938
+ resume() {
1939
+ if (this._destroyed || !this._paused) return this;
1940
+ const now = performanceNow();
1941
+ const suspensionStartedAt = this._lifecycleSuspendedAt > 0 ? Math.min(this._pausedAt, this._lifecycleSuspendedAt) : this._pausedAt;
1942
+ this._paused = false;
1943
+ if (this._animation && suspensionStartedAt > 0) {
1944
+ this._animation.startedAt += Math.max(0, now - suspensionStartedAt);
1945
+ }
1946
+ this._pausedAt = 0;
1947
+ this._lifecycleSuspendedAt = this._isLifecycleBlocked() ? now : 0;
1948
+ this._lastFrameAt = 0;
1949
+ this._ensureLoop();
1950
+ dispatchGaugeEvent(this.host, "resume", { gauge: this });
1951
+ return this;
1952
+ }
1953
+ /** Re-renders after custom CSS variables, fonts, or plugin state changed. */
1954
+ refresh() {
1955
+ return this.render();
1956
+ }
1957
+ /** Releases frames, observers, listeners, and generated DOM. */
1958
+ destroy() {
1959
+ if (this._destroyed) return;
1960
+ this._destroyed = true;
1961
+ cancelFrame(this._frameHandle);
1962
+ this._frameHandle = null;
1963
+ this._animation = null;
1964
+ this._removeLifecycleObservers();
1965
+ this.renderer.destroy();
1966
+ const onDestroy = this._onDestroy;
1967
+ this._onDestroy = null;
1968
+ if (onDestroy) onDestroy(this);
1969
+ dispatchGaugeEvent(this.host, "destroy", { gauge: this });
1970
+ }
1971
+ _initializeValue() {
1972
+ const animation = this.options.animation;
1973
+ if (!animation.enabled || !animation.animateOnInit || animation.duration <= 0 || this._prefersReducedMotion()) {
1974
+ this._baseValue = this._targetValue;
1975
+ this._displayValue = this._targetValue;
1976
+ this._pointerValue = this._targetValue;
1977
+ this._updateDynamic(this._targetValue, true, this._targetValue);
1978
+ this._ensureLoop();
1979
+ return;
1980
+ }
1981
+ const start = this._resolveInitialValue(animation.startFrom);
1982
+ if (Object.is(start, this._targetValue)) {
1983
+ this._updateDynamic(this._targetValue, true);
1984
+ this._ensureLoop();
1985
+ return;
1986
+ }
1987
+ const target = this._targetValue;
1988
+ this._baseValue = start;
1989
+ this._displayValue = start;
1990
+ this._pointerValue = start;
1991
+ this._updateDynamic(start, true, start);
1992
+ this.setValue(target, { animate: true });
1993
+ }
1994
+ _resolveInitialValue(startFrom) {
1995
+ if (typeof startFrom === "number") return this._normalizeIncomingValue(startFrom);
1996
+ if (startFrom === "max") return this.options.max;
1997
+ if (startFrom === "value") return this._targetValue;
1998
+ return this.options.min;
1999
+ }
2000
+ _normalizeIncomingValue(value) {
2001
+ const number = finiteNumber(value, this.options.min);
2002
+ if (this.options.overflow === "allow") return number;
2003
+ if (this.options.overflow === "expand") {
2004
+ let changed = false;
2005
+ if (number < this.options.min) {
2006
+ this.userOptions.min = number;
2007
+ changed = true;
2008
+ }
2009
+ if (number > this.options.max) {
2010
+ this.userOptions.max = number;
2011
+ changed = true;
2012
+ }
2013
+ if (changed) {
2014
+ this.options = resolveGaugeOptions(this.userOptions, this.registry);
2015
+ if (this.renderer) {
2016
+ this.renderer.options = this.options;
2017
+ if (this.renderer.svg) this.render();
2018
+ }
2019
+ }
2020
+ return number;
2021
+ }
2022
+ return clamp(number, this.options.min, this.options.max);
2023
+ }
2024
+ _ensureLoop() {
2025
+ if (this._destroyed || this._paused || this._frameHandle !== null) return;
2026
+ if (!this._shouldRun()) return;
2027
+ this._frameHandle = requestFrame((timestamp) => this._frame(timestamp));
2028
+ }
2029
+ _frame(timestamp) {
2030
+ this._frameHandle = null;
2031
+ if (this._destroyed || this._paused || !this._shouldRun()) return;
2032
+ const now = Number.isFinite(timestamp) ? timestamp : performanceNow();
2033
+ const delta = this._lastFrameAt > 0 ? Math.min(100, Math.max(0, now - this._lastFrameAt)) : 16.667;
2034
+ this._lastFrameAt = now;
2035
+ let animationCompleted = false;
2036
+ if (this._animation) {
2037
+ const elapsed = now - this._animation.startedAt;
2038
+ const progress = this._animation.duration <= 0 ? 1 : clamp(elapsed / this._animation.duration, 0, 1);
2039
+ const eased = this._animation.easing(progress);
2040
+ this._baseValue = this._animation.from + (this._animation.to - this._animation.from) * eased;
2041
+ if (progress >= 1) {
2042
+ const completed = this._animation;
2043
+ this._baseValue = completed.to;
2044
+ this._animation = null;
2045
+ animationCompleted = true;
2046
+ dispatchGaugeEvent(this.host, "animationend", {
2047
+ gauge: this,
2048
+ value: completed.to
2049
+ });
2050
+ }
2051
+ }
2052
+ const jitter = this.options.effects.jitter;
2053
+ const jitterActive = jitter.enabled && (!jitter.whenIdleOnly || !this._animation);
2054
+ if (jitterActive) {
2055
+ const interval = 1e3 / jitter.frequency;
2056
+ if (this._lastJitterTargetAt === 0 || now - this._lastJitterTargetAt >= interval) {
2057
+ this._jitterTarget = (Math.random() * 2 - 1) * this._jitterAmplitude();
2058
+ this._lastJitterTargetAt = now;
2059
+ }
2060
+ const smoothingFactor = 1 - Math.pow(jitter.smoothing, delta / 16.667);
2061
+ this._jitterOffset += (this._jitterTarget - this._jitterOffset) * smoothingFactor;
2062
+ } else {
2063
+ this._jitterTarget = 0;
2064
+ const smoothingFactor = 1 - Math.pow(jitter.smoothing, delta / 16.667);
2065
+ this._jitterOffset += (0 - this._jitterOffset) * smoothingFactor;
2066
+ if (Math.abs(this._jitterOffset) < 1e-6) this._jitterOffset = 0;
2067
+ }
2068
+ const nextDisplay = this._displayClamp(this._baseValue);
2069
+ const nextPointer = this._displayClamp(this._baseValue + this._jitterOffset);
2070
+ this._updateDynamic(nextDisplay, false, nextPointer);
2071
+ if (animationCompleted) this._emitSettled();
2072
+ if (this._needsAnotherFrame()) this._ensureLoop();
2073
+ }
2074
+ _needsAnotherFrame() {
2075
+ if (this._animation) return true;
2076
+ const jitter = this.options.effects.jitter;
2077
+ if (jitter.enabled) return true;
2078
+ return Math.abs(this._jitterOffset) > 1e-6;
2079
+ }
2080
+ _jitterAmplitude() {
2081
+ const jitter = this.options.effects.jitter;
2082
+ if (jitter.amplitudeUnit === "percent") {
2083
+ return (this.options.max - this.options.min) * jitter.amplitude / 100;
2084
+ }
2085
+ return jitter.amplitude;
2086
+ }
2087
+ _displayClamp(value) {
2088
+ return this.options.overflow === "allow" ? value : clamp(value, this.options.min, this.options.max);
2089
+ }
2090
+ _updateDynamic(value, force, pointerValue = value) {
2091
+ const rounded = roundValue(value, this.options.precision);
2092
+ const displayChanged = !Object.is(rounded, this._displayValue);
2093
+ const pointerChanged = !Object.is(pointerValue, this._pointerValue);
2094
+ if (!force && !displayChanged && !pointerChanged) return;
2095
+ this._displayValue = rounded;
2096
+ this._pointerValue = pointerValue;
2097
+ this.renderer.update(rounded, {
2098
+ gauge: this,
2099
+ targetValue: this._targetValue,
2100
+ baseValue: this._baseValue,
2101
+ pointerValue,
2102
+ animated: Boolean(this._animation),
2103
+ jitterOffset: this._jitterOffset
2104
+ });
2105
+ if (force || displayChanged) {
2106
+ this._updateAccessibility(rounded);
2107
+ dispatchGaugeEvent(this.host, "displaychange", {
2108
+ gauge: this,
2109
+ value: this._targetValue,
2110
+ displayedValue: rounded
2111
+ });
2112
+ }
2113
+ }
2114
+ _updateAccessibility(value) {
2115
+ const root = this.renderer.root;
2116
+ if (!root) return;
2117
+ root.setAttribute("role", "meter");
2118
+ root.setAttribute("aria-label", this.options.accessibility.label);
2119
+ root.setAttribute("aria-valuemin", String(this.options.min));
2120
+ root.setAttribute("aria-valuemax", String(this.options.max));
2121
+ root.setAttribute("aria-valuenow", String(value));
2122
+ const valueText = this.options.accessibility.valueText;
2123
+ if (typeof valueText === "function") {
2124
+ root.setAttribute("aria-valuetext", String(valueText(value, this.options)));
2125
+ } else if (typeof valueText === "string" && valueText) {
2126
+ root.setAttribute("aria-valuetext", valueText);
2127
+ } else {
2128
+ root.removeAttribute("aria-valuetext");
2129
+ }
2130
+ }
2131
+ _emitChange() {
2132
+ dispatchGaugeEvent(this.host, "change", {
2133
+ gauge: this,
2134
+ value: this._targetValue,
2135
+ displayedValue: this._displayValue
2136
+ });
2137
+ }
2138
+ _emitSettled() {
2139
+ dispatchGaugeEvent(this.host, "settled", {
2140
+ gauge: this,
2141
+ value: this._targetValue,
2142
+ displayedValue: this._displayValue
2143
+ });
2144
+ }
2145
+ _syncLifecycleObservers() {
2146
+ this._removeLifecycleObservers();
2147
+ this._onscreen = true;
2148
+ this._lifecycleSuspendedAt = 0;
2149
+ this._installLifecycleObservers();
2150
+ }
2151
+ _removeLifecycleObservers() {
2152
+ if (this._visibilityHandler && typeof document !== "undefined") {
2153
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
2154
+ }
2155
+ this._visibilityHandler = null;
2156
+ if (this._intersectionObserver) this._intersectionObserver.disconnect();
2157
+ this._intersectionObserver = null;
2158
+ }
2159
+ _installLifecycleObservers() {
2160
+ if (typeof document !== "undefined" && this.options.performance.pauseWhenHidden) {
2161
+ this._visibilityHandler = () => {
2162
+ if (document.hidden) {
2163
+ this._suspendForLifecycle();
2164
+ return;
2165
+ }
2166
+ this._resumeFromLifecycle();
2167
+ };
2168
+ document.addEventListener("visibilitychange", this._visibilityHandler, { passive: true });
2169
+ if (document.hidden) this._suspendForLifecycle();
2170
+ }
2171
+ if (typeof IntersectionObserver === "function" && this.options.performance.pauseWhenOffscreen) {
2172
+ this._intersectionObserver = new IntersectionObserver((entries) => {
2173
+ const entry = entries[0];
2174
+ this._onscreen = Boolean(entry && entry.isIntersecting);
2175
+ if (!this._onscreen) {
2176
+ this._suspendForLifecycle();
2177
+ } else {
2178
+ this._resumeFromLifecycle();
2179
+ }
2180
+ }, { rootMargin: "100px" });
2181
+ this._intersectionObserver.observe(this.host);
2182
+ }
2183
+ }
2184
+ _shouldRun() {
2185
+ if (this._isLifecycleBlocked()) return false;
2186
+ return this._needsAnotherFrame();
2187
+ }
2188
+ _isLifecycleBlocked() {
2189
+ if (typeof document !== "undefined" && this.options.performance.pauseWhenHidden && document.hidden) return true;
2190
+ return this.options.performance.pauseWhenOffscreen && !this._onscreen;
2191
+ }
2192
+ _suspendForLifecycle() {
2193
+ if (this._lifecycleSuspendedAt === 0) this._lifecycleSuspendedAt = performanceNow();
2194
+ cancelFrame(this._frameHandle);
2195
+ this._frameHandle = null;
2196
+ }
2197
+ _resumeFromLifecycle() {
2198
+ if (typeof document !== "undefined" && this.options.performance.pauseWhenHidden && document.hidden) return;
2199
+ if (this.options.performance.pauseWhenOffscreen && !this._onscreen) return;
2200
+ if (this._paused) return;
2201
+ if (this._lifecycleSuspendedAt > 0 && this._animation) {
2202
+ this._animation.startedAt += Math.max(0, performanceNow() - this._lifecycleSuspendedAt);
2203
+ }
2204
+ this._lifecycleSuspendedAt = 0;
2205
+ this._lastFrameAt = 0;
2206
+ this._ensureLoop();
2207
+ }
2208
+ _prefersReducedMotion() {
2209
+ if (!this.options.animation.respectReducedMotion || typeof matchMedia !== "function") return false;
2210
+ return matchMedia("(prefers-reduced-motion: reduce)").matches;
2211
+ }
2212
+ _assertAlive() {
2213
+ if (this._destroyed) throw new Error("This Gaugeit instance has been destroyed.");
2214
+ }
2215
+ };
2216
+ function performanceNow() {
2217
+ if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
2218
+ return Date.now();
2219
+ }
2220
+ function roundValue(value, precision) {
2221
+ const factor = Math.pow(10, precision);
2222
+ return Math.round(value * factor) / factor;
2223
+ }
2224
+
2225
+ // src/core/registry.js
2226
+ var GaugeTypeRegistry = class {
2227
+ constructor() {
2228
+ this._types = /* @__PURE__ */ new Map();
2229
+ }
2230
+ /** Registers or replaces a gauge type. */
2231
+ register(name, definition) {
2232
+ const normalizedName = String(name || "").trim();
2233
+ if (!normalizedName) throw new TypeError("Gauge type name must be a non-empty string.");
2234
+ if (!isPlainObject(definition)) throw new TypeError(`Gauge type "${normalizedName}" must be an object.`);
2235
+ if (!Array.isArray(definition.layers) || definition.layers.length === 0) {
2236
+ throw new TypeError(`Gauge type "${normalizedName}" must define at least one layer.`);
2237
+ }
2238
+ const normalizedLayers = definition.layers.map((layer, index) => {
2239
+ if (!layer || typeof layer.render !== "function") {
2240
+ throw new TypeError(`Layer ${index} in gauge type "${normalizedName}" must expose render(context).`);
2241
+ }
2242
+ return layer;
2243
+ });
2244
+ this._types.set(normalizedName, {
2245
+ name: normalizedName,
2246
+ defaults: cloneValue(definition.defaults || {}),
2247
+ layers: normalizedLayers,
2248
+ description: String(definition.description || ""),
2249
+ configure: typeof definition.configure === "function" ? definition.configure : null
2250
+ });
2251
+ return this;
2252
+ }
2253
+ /** Returns a registered gauge type definition. */
2254
+ get(name) {
2255
+ const definition = this._types.get(String(name));
2256
+ if (!definition) throw new Error(`Unknown Gaugeit gauge type: ${name}`);
2257
+ return definition;
2258
+ }
2259
+ /** Returns whether a type exists. */
2260
+ has(name) {
2261
+ return this._types.has(String(name));
2262
+ }
2263
+ /** Returns registered type names. */
2264
+ names() {
2265
+ return [...this._types.keys()];
2266
+ }
2267
+ /** Returns a serializable description of the registry. */
2268
+ describe() {
2269
+ return [...this._types.values()].map((definition) => ({
2270
+ name: definition.name,
2271
+ description: definition.description
2272
+ }));
2273
+ }
2274
+ };
2275
+ var gaugeTypeRegistry = new GaugeTypeRegistry();
2276
+
2277
+ // src/layers/face.js
2278
+ var faceLayer = {
2279
+ id: "face",
2280
+ render(context) {
2281
+ const { options, renderer } = context;
2282
+ const face = options.face;
2283
+ if (!face.visible) return null;
2284
+ const group = renderer.group("face", { "aria-hidden": "true" });
2285
+ const box = resolveFaceBox(options, renderer.getContentBounds());
2286
+ const shadowId = `${renderer.id}-face-shadow`;
2287
+ const glassId = `${renderer.id}-glass`;
2288
+ const clipId = `${renderer.id}-face-content-clip`;
2289
+ const inset = Math.max(3, face.strokeWidth + 2);
2290
+ const innerBox = insetFaceBox(box, inset);
2291
+ const defs = svgElement("defs");
2292
+ if (face.shadow) {
2293
+ const filter = svgElement("filter", {
2294
+ id: shadowId,
2295
+ x: "-20%",
2296
+ y: "-20%",
2297
+ width: "140%",
2298
+ height: "140%"
2299
+ });
2300
+ filter.append(svgElement("feDropShadow", {
2301
+ dx: 0,
2302
+ dy: 3,
2303
+ stdDeviation: 4,
2304
+ "flood-color": "#000000",
2305
+ "flood-opacity": 0.18
2306
+ }));
2307
+ defs.append(filter);
2308
+ }
2309
+ if (face.glass) {
2310
+ const gradient = svgElement("linearGradient", {
2311
+ id: glassId,
2312
+ x1: "0%",
2313
+ y1: "0%",
2314
+ x2: "0%",
2315
+ y2: "100%"
2316
+ });
2317
+ gradient.append(
2318
+ svgElement("stop", { offset: "0%", "stop-color": "#ffffff", "stop-opacity": 0.38 }),
2319
+ svgElement("stop", { offset: "48%", "stop-color": "#ffffff", "stop-opacity": 0.06 }),
2320
+ svgElement("stop", { offset: "100%", "stop-color": "#ffffff", "stop-opacity": 0 })
2321
+ );
2322
+ defs.append(gradient);
2323
+ }
2324
+ if (face.clipContent) {
2325
+ const clipPath = svgElement("clipPath", {
2326
+ id: clipId,
2327
+ clipPathUnits: "userSpaceOnUse"
2328
+ });
2329
+ clipPath.append(createFaceShape(face, innerBox, {
2330
+ fill: "#ffffff",
2331
+ stroke: "none"
2332
+ }, Math.max(0, face.cornerRadius - inset)));
2333
+ defs.append(clipPath);
2334
+ renderer.setContentClipPath(clipId);
2335
+ }
2336
+ if (defs.children.length > 0) group.append(defs);
2337
+ const base = createFaceShape(face, box, {
2338
+ fill: face.color,
2339
+ stroke: face.strokeColor,
2340
+ "stroke-width": face.strokeWidth,
2341
+ filter: face.shadow ? `url(#${shadowId})` : void 0
2342
+ });
2343
+ base.classList.add("gaugeit__face");
2344
+ group.append(base);
2345
+ group.append(createFaceShape(face, innerBox, {
2346
+ class: "gaugeit__face-inner",
2347
+ fill: "none",
2348
+ stroke: face.innerStrokeColor,
2349
+ "stroke-width": face.innerStrokeWidth
2350
+ }, Math.max(0, face.cornerRadius - inset)));
2351
+ if (face.glass) {
2352
+ if (face.shape === "circle") {
2353
+ const radius = Math.min(box.width, box.height) / 2;
2354
+ group.append(svgElement("ellipse", {
2355
+ class: "gaugeit__glass",
2356
+ cx: box.x + box.width / 2,
2357
+ cy: box.y + box.height / 2 - radius * 0.27,
2358
+ rx: radius * 0.68,
2359
+ ry: radius * 0.32,
2360
+ fill: `url(#${glassId})`,
2361
+ "pointer-events": "none"
2362
+ }));
2363
+ } else {
2364
+ group.append(svgElement("path", {
2365
+ class: "gaugeit__glass",
2366
+ d: `M ${box.x + 14} ${box.y + 14} H ${box.x + box.width - 14} V ${box.y + box.height * 0.52} Q ${box.x + box.width / 2} ${box.y + box.height * 0.38} ${box.x + 14} ${box.y + box.height * 0.52} Z`,
2367
+ fill: `url(#${glassId})`,
2368
+ "pointer-events": "none"
2369
+ }));
2370
+ }
2371
+ }
2372
+ return null;
2373
+ }
2374
+ };
2375
+ function insetFaceBox(box, inset) {
2376
+ return {
2377
+ x: box.x + inset,
2378
+ y: box.y + inset,
2379
+ width: Math.max(1, box.width - inset * 2),
2380
+ height: Math.max(1, box.height - inset * 2)
2381
+ };
2382
+ }
2383
+ function createFaceShape(face, box, attributes = {}, cornerRadius = face.cornerRadius) {
2384
+ if (face.shape === "circle") {
2385
+ return svgElement("circle", {
2386
+ cx: box.x + box.width / 2,
2387
+ cy: box.y + box.height / 2,
2388
+ r: Math.max(1, Math.min(box.width, box.height) / 2),
2389
+ ...attributes
2390
+ });
2391
+ }
2392
+ const radius = Math.max(0, Math.min(cornerRadius, box.width / 2, box.height / 2));
2393
+ return svgElement("rect", {
2394
+ x: box.x,
2395
+ y: box.y,
2396
+ width: box.width,
2397
+ height: box.height,
2398
+ rx: radius,
2399
+ ry: radius,
2400
+ ...attributes
2401
+ });
2402
+ }
2403
+
2404
+ // src/layers/zones.js
2405
+ var zonesLayer = {
2406
+ id: "zones",
2407
+ render(context) {
2408
+ const { options, renderer } = context;
2409
+ const group = renderer.group("zones", { "aria-hidden": "true" });
2410
+ const segments = resolveZoneSegments(options.min, options.max, options.zones);
2411
+ if (options.track.showUnderZones && options.track.visible) {
2412
+ appendArc(group, options.min, options.max, options.track, "gaugeit__track");
2413
+ }
2414
+ for (const segment of segments) {
2415
+ const style = segment.zone || options.track;
2416
+ const shouldDraw = segment.zone ? !isTransparentColor(segment.zone.color) : options.track.visible && !options.track.showUnderZones;
2417
+ if (!shouldDraw) continue;
2418
+ appendArc(
2419
+ group,
2420
+ segment.min,
2421
+ segment.max,
2422
+ style,
2423
+ segment.zone ? "gaugeit__zone" : "gaugeit__track"
2424
+ );
2425
+ }
2426
+ return null;
2427
+ function appendArc(parent, min, max, style, className) {
2428
+ const nodes = createArcNodes(min, max, style, className, options);
2429
+ for (const node of nodes) {
2430
+ if (style.className) node.classList.add(String(style.className));
2431
+ parent.append(node);
2432
+ }
2433
+ }
2434
+ }
2435
+ };
2436
+ function createArcNodes(min, max, style, className, options) {
2437
+ var _a, _b, _c, _d, _e;
2438
+ const geometry2 = options.geometry;
2439
+ const radius = (_b = (_a = style.radius) != null ? _a : options.track.radius) != null ? _b : geometry2.radius;
2440
+ const fallbackWidth = (_c = style.width) != null ? _c : options.track.width;
2441
+ const color = (_d = style.color) != null ? _d : options.track.color;
2442
+ const opacity = (_e = style.opacity) != null ? _e : 1;
2443
+ const lineCap = style.lineCap || options.track.lineCap || "butt";
2444
+ const startAngle = valueToAngle(min, options);
2445
+ const endAngle = valueToAngle(max, options);
2446
+ if (!style.taper) {
2447
+ return [svgElement("path", {
2448
+ class: className,
2449
+ d: arcPath(geometry2.centerX, geometry2.centerY, radius, startAngle, endAngle),
2450
+ fill: "none",
2451
+ stroke: color,
2452
+ "stroke-width": fallbackWidth,
2453
+ "stroke-linecap": lineCap,
2454
+ opacity
2455
+ })];
2456
+ }
2457
+ const startWidth = widthAtValue(style, min, fallbackWidth);
2458
+ const endWidth = widthAtValue(style, max, fallbackWidth);
2459
+ const nodes = [svgElement("path", {
2460
+ class: `${className} ${className}--tapered`,
2461
+ d: taperedArcPath(
2462
+ geometry2.centerX,
2463
+ geometry2.centerY,
2464
+ radius,
2465
+ startAngle,
2466
+ endAngle,
2467
+ startWidth,
2468
+ endWidth,
2469
+ style.taper.segments
2470
+ ),
2471
+ fill: color,
2472
+ stroke: "none",
2473
+ opacity
2474
+ })];
2475
+ if (lineCap === "round") {
2476
+ const start = polarPoint(geometry2.centerX, geometry2.centerY, radius, startAngle);
2477
+ const end = polarPoint(geometry2.centerX, geometry2.centerY, radius, endAngle);
2478
+ const atZoneStart = Math.abs(min - style.min) < 1e-9;
2479
+ const atZoneEnd = Math.abs(max - style.max) < 1e-9;
2480
+ if (atZoneStart && startWidth > 0) nodes.push(capNode(className, start, startWidth, color, opacity, "start"));
2481
+ if (atZoneEnd && endWidth > 0) nodes.push(capNode(className, end, endWidth, color, opacity, "end"));
2482
+ }
2483
+ return nodes;
2484
+ }
2485
+ function widthAtValue(zone, value, fallbackWidth) {
2486
+ const taper = zone.taper || {};
2487
+ const startWidth = Math.max(0, finiteNumber(taper.startWidth, fallbackWidth));
2488
+ const endWidth = Math.max(0, finiteNumber(taper.endWidth, fallbackWidth));
2489
+ const range = Math.max(1e-12, zone.max - zone.min);
2490
+ const ratio = Math.min(1, Math.max(0, (value - zone.min) / range));
2491
+ return startWidth + (endWidth - startWidth) * ratio;
2492
+ }
2493
+ function capNode(className, point, width, color, opacity, edge) {
2494
+ return svgElement("circle", {
2495
+ class: `${className} ${className}--taper-cap`,
2496
+ "data-gaugeit-zone-cap": edge,
2497
+ cx: point.x,
2498
+ cy: point.y,
2499
+ r: width / 2,
2500
+ fill: color,
2501
+ opacity
2502
+ });
2503
+ }
2504
+
2505
+ // src/layers/ticks.js
2506
+ var ticksLayer = {
2507
+ id: "ticks",
2508
+ render(context) {
2509
+ const { options, renderer } = context;
2510
+ const scale = options.scale;
2511
+ if (!scale.major.visible && !scale.minor.visible) return null;
2512
+ const group = renderer.group("ticks", { "aria-hidden": "true" });
2513
+ const interval = resolveMajorInterval(options);
2514
+ const completeMajorValues = buildScaleValues(options.min, options.max, interval);
2515
+ const majorValues = resolveScaleEndpointValues(completeMajorValues, options);
2516
+ const minorValues = buildMinorValues(completeMajorValues, scale.minor.subdivisions);
2517
+ if (scale.minor.visible && scale.minor.length > 0 && scale.minor.width > 0 && minorValues.length) {
2518
+ group.append(svgElement("path", {
2519
+ class: "gaugeit__ticks gaugeit__ticks--minor",
2520
+ d: tickPath(minorValues, scale.minor.length),
2521
+ fill: "none",
2522
+ stroke: scale.minor.color,
2523
+ "stroke-width": scale.minor.width,
2524
+ "stroke-linecap": "butt",
2525
+ opacity: scale.minor.opacity
2526
+ }));
2527
+ }
2528
+ if (scale.major.visible && scale.major.length > 0 && scale.major.width > 0) {
2529
+ group.append(svgElement("path", {
2530
+ class: "gaugeit__ticks gaugeit__ticks--major",
2531
+ d: tickPath(majorValues, scale.major.length),
2532
+ fill: "none",
2533
+ stroke: scale.major.color,
2534
+ "stroke-width": scale.major.width,
2535
+ "stroke-linecap": "butt",
2536
+ opacity: scale.major.opacity
2537
+ }));
2538
+ }
2539
+ renderer.setReference("majorValues", majorValues);
2540
+ return null;
2541
+ function tickPath(values, length) {
2542
+ const commands = [];
2543
+ for (const value of values) {
2544
+ const angle = valueToAngle(value, options);
2545
+ const radii = tickRadii(scale.radius, length, scale.position);
2546
+ const start = polarPoint(options.geometry.centerX, options.geometry.centerY, radii.start, angle);
2547
+ const end = polarPoint(options.geometry.centerX, options.geometry.centerY, radii.end, angle);
2548
+ commands.push(`M ${roundSvg(start.x)} ${roundSvg(start.y)} L ${roundSvg(end.x)} ${roundSvg(end.y)}`);
2549
+ }
2550
+ return commands.join(" ");
2551
+ }
2552
+ }
2553
+ };
2554
+ function tickRadii(radius, length, position) {
2555
+ if (position === "outside") return { start: radius, end: radius + length };
2556
+ if (position === "cross") return { start: radius - length / 2, end: radius + length / 2 };
2557
+ return { start: radius, end: radius - length };
2558
+ }
2559
+
2560
+ // src/layers/labels.js
2561
+ var labelsLayer = {
2562
+ id: "labels",
2563
+ render(context) {
2564
+ const { options, renderer } = context;
2565
+ const labels = options.labels;
2566
+ if (!labels.visible) return null;
2567
+ const group = renderer.group("labels", { "aria-hidden": "true" });
2568
+ const interval = Number(labels.interval) > 0 ? Number(labels.interval) : resolveMajorInterval(options);
2569
+ const values = resolveScaleEndpointValues(
2570
+ buildScaleValues(options.min, options.max, interval),
2571
+ options
2572
+ );
2573
+ for (const value of values) {
2574
+ const angle = valueToAngle(value, options);
2575
+ const point = polarPoint(options.geometry.centerX, options.geometry.centerY, labels.radius, angle);
2576
+ const text = svgElement("text", {
2577
+ class: "gaugeit__label",
2578
+ x: point.x,
2579
+ y: point.y,
2580
+ fill: labels.color,
2581
+ "font-family": labels.fontFamily,
2582
+ "font-size": labels.fontSize,
2583
+ "font-weight": labels.fontWeight,
2584
+ "text-anchor": "middle",
2585
+ "dominant-baseline": "middle",
2586
+ transform: labelTransform(labels.rotation, angle, point)
2587
+ }, formatLabel(value, labels, options));
2588
+ group.append(text);
2589
+ }
2590
+ return null;
2591
+ }
2592
+ };
2593
+ function labelTransform(rotation, angle, point) {
2594
+ if (rotation === "radial") return `rotate(${angle} ${point.x} ${point.y})`;
2595
+ if (rotation === "tangent") return `rotate(${angle + 90} ${point.x} ${point.y})`;
2596
+ return void 0;
2597
+ }
2598
+ function formatLabel(value, labels, options) {
2599
+ if (typeof labels.formatter === "function") {
2600
+ return labels.formatter(value, { min: options.min, max: options.max, options });
2601
+ }
2602
+ return Number(value).toFixed(labels.fractionDigits);
2603
+ }
2604
+
2605
+ // src/layers/pointer.js
2606
+ var pointerLayer = {
2607
+ id: "pointer",
2608
+ render(context) {
2609
+ const { options, renderer } = context;
2610
+ const pointer = options.pointer;
2611
+ if (!pointer.visible) return null;
2612
+ const geometry2 = options.geometry;
2613
+ const group = renderer.group("pointer", { "aria-hidden": "true" });
2614
+ const rotating = svgElement("g", { class: "gaugeit__pointer-rotating" });
2615
+ const shape = createPointerShape(pointer, geometry2);
2616
+ rotating.append(shape);
2617
+ let cap = null;
2618
+ if (pointer.cap.visible) {
2619
+ cap = svgElement("circle", {
2620
+ class: "gaugeit__pointer-cap",
2621
+ cx: geometry2.centerX,
2622
+ cy: geometry2.centerY,
2623
+ r: pointer.cap.radius,
2624
+ fill: pointer.cap.color,
2625
+ stroke: pointer.cap.strokeColor,
2626
+ "stroke-width": pointer.cap.strokeWidth
2627
+ });
2628
+ }
2629
+ appendVisualStack(group, rotating, cap, pointer.cap.stackOrder);
2630
+ return {
2631
+ update(value, state = {}) {
2632
+ const pointerValue = Number.isFinite(state.pointerValue) ? state.pointerValue : value;
2633
+ const angle = valueToAngle(pointerValue, options, options.overflow !== "allow");
2634
+ rotating.setAttribute("transform", `rotate(${angle} ${geometry2.centerX} ${geometry2.centerY})`);
2635
+ if (pointer.colorByZone) {
2636
+ const zone = findZoneForValue(pointerValue, options.zones);
2637
+ const color = zone && !isTransparentColor(zone.color) ? zone.color : pointer.color;
2638
+ if (shape.tagName.toLowerCase() === "line") shape.setAttribute("stroke", color);
2639
+ else shape.setAttribute("fill", color);
2640
+ }
2641
+ }
2642
+ };
2643
+ }
2644
+ };
2645
+ function appendVisualStack(group, pointer, cap, capStackOrder) {
2646
+ if (cap && capStackOrder === "below") group.append(cap, pointer);
2647
+ else if (cap) group.append(pointer, cap);
2648
+ else group.append(pointer);
2649
+ }
2650
+ function createPointerShape(pointer, geometry2) {
2651
+ const cx = geometry2.centerX;
2652
+ const cy = geometry2.centerY;
2653
+ const front = pointer.length;
2654
+ const tail = pointer.tailLength;
2655
+ const half = pointer.width / 2;
2656
+ const shaft = resolveShaftLength(pointer);
2657
+ let shape;
2658
+ if (pointer.type === "line") {
2659
+ shape = svgElement("line", {
2660
+ class: "gaugeit__pointer gaugeit__pointer--line",
2661
+ x1: cx - tail,
2662
+ y1: cy,
2663
+ x2: cx + front,
2664
+ y2: cy,
2665
+ stroke: pointer.color,
2666
+ "stroke-width": pointer.width,
2667
+ "stroke-linecap": "round"
2668
+ });
2669
+ } else {
2670
+ let points;
2671
+ if (pointer.type === "arrow") {
2672
+ points = [
2673
+ [cx - tail, cy - half * 0.55],
2674
+ [cx + shaft, cy - half * 0.55],
2675
+ [cx + shaft, cy - half * 1.5],
2676
+ [cx + front, cy],
2677
+ [cx + shaft, cy + half * 1.5],
2678
+ [cx + shaft, cy + half * 0.55],
2679
+ [cx - tail, cy + half * 0.55]
2680
+ ];
2681
+ } else if (pointer.type === "spear") {
2682
+ const shoulder = shaft + (front - shaft) * 0.56;
2683
+ points = [
2684
+ [cx - tail, cy - half * 0.45],
2685
+ [cx + shaft, cy - half * 0.38],
2686
+ [cx + shoulder, cy - half * 0.85],
2687
+ [cx + front, cy],
2688
+ [cx + shoulder, cy + half * 0.85],
2689
+ [cx + shaft, cy + half * 0.38],
2690
+ [cx - tail, cy + half * 0.45]
2691
+ ];
2692
+ } else {
2693
+ points = [
2694
+ [cx - tail, cy - half * 0.65],
2695
+ [cx + shaft, cy - half * 0.22],
2696
+ [cx + front, cy],
2697
+ [cx + shaft, cy + half * 0.22],
2698
+ [cx - tail, cy + half * 0.65]
2699
+ ];
2700
+ }
2701
+ const path = points.map((point, index) => `${index === 0 ? "M" : "L"} ${point[0]} ${point[1]}`).join(" ");
2702
+ shape = svgElement("path", {
2703
+ class: `gaugeit__pointer gaugeit__pointer--${pointer.type}`,
2704
+ d: `${path} Z`,
2705
+ fill: pointer.color
2706
+ });
2707
+ }
2708
+ setSvgAttributes(shape, {
2709
+ opacity: pointer.opacity,
2710
+ stroke: pointer.type === "line" ? pointer.color : pointer.strokeColor,
2711
+ "stroke-width": pointer.type === "line" ? pointer.width : pointer.strokeWidth
2712
+ });
2713
+ return shape;
2714
+ }
2715
+ function resolveShaftLength(pointer) {
2716
+ if (Number.isFinite(pointer.shaftLength)) return Math.min(pointer.length, Math.max(0, pointer.shaftLength));
2717
+ if (pointer.type === "arrow") return pointer.length * 0.7;
2718
+ if (pointer.type === "spear") return pointer.length * 0.68;
2719
+ return pointer.length * 0.86;
2720
+ }
2721
+
2722
+ // src/layers/readout.js
2723
+ var readoutLayer = {
2724
+ id: "readout",
2725
+ render(context) {
2726
+ const { options, renderer } = context;
2727
+ const readout = options.readout;
2728
+ if (!readout.visible) return null;
2729
+ const group = renderer.group("readout");
2730
+ if (readout.title.visible && readout.title.text) {
2731
+ group.append(textNode("title", readout.title, readout.title.text, options));
2732
+ }
2733
+ let valueNode = null;
2734
+ const valueFormatter = createValueFormatter(readout.value, options);
2735
+ if (readout.value.visible) {
2736
+ valueNode = textNode("value", readout.value, "", options);
2737
+ valueNode.setAttribute("aria-hidden", "true");
2738
+ group.append(valueNode);
2739
+ }
2740
+ if (readout.unit.visible && readout.unit.text) {
2741
+ group.append(textNode("unit", readout.unit, readout.unit.text, options));
2742
+ }
2743
+ return {
2744
+ update(value) {
2745
+ if (valueNode) valueNode.textContent = valueFormatter(value);
2746
+ }
2747
+ };
2748
+ }
2749
+ };
2750
+ function textNode(kind, config, value, options) {
2751
+ const position = resolveReadoutPosition(config, options);
2752
+ return svgElement("text", {
2753
+ class: `gaugeit__readout gaugeit__readout--${kind}`,
2754
+ x: position.x,
2755
+ y: position.y,
2756
+ fill: config.color,
2757
+ "font-family": config.fontFamily,
2758
+ "font-size": config.fontSize,
2759
+ "font-weight": config.fontWeight,
2760
+ "letter-spacing": config.letterSpacing,
2761
+ "text-anchor": "middle",
2762
+ "dominant-baseline": "middle"
2763
+ }, value);
2764
+ }
2765
+ function createValueFormatter(config, options) {
2766
+ if (typeof config.formatter === "function") {
2767
+ return (value) => String(config.formatter(value, { min: options.min, max: options.max, options }));
2768
+ }
2769
+ const formatter = new Intl.NumberFormat(config.locale, {
2770
+ minimumFractionDigits: config.fractionDigits,
2771
+ maximumFractionDigits: config.fractionDigits
2772
+ });
2773
+ return (value) => `${config.prefix || ""}${formatter.format(value)}${config.suffix || ""}`;
2774
+ }
2775
+
2776
+ // src/layers/light.js
2777
+ var LIGHT_PALETTES = Object.freeze({
2778
+ red: { core: "#ef4444", highlight: "#fecaca", glow: "#f87171", rim: "#7f1d1d" },
2779
+ green: { core: "#22c55e", highlight: "#bbf7d0", glow: "#4ade80", rim: "#14532d" },
2780
+ blue: { core: "#3b82f6", highlight: "#bfdbfe", glow: "#60a5fa", rim: "#1e3a8a" },
2781
+ amber: { core: "#f59e0b", highlight: "#fde68a", glow: "#fb923c", rim: "#92400e" },
2782
+ yellow: { core: "#fde047", highlight: "#fef9c3", glow: "#facc15", rim: "#a16207" },
2783
+ orange: { core: "#f97316", highlight: "#fed7aa", glow: "#fb923c", rim: "#9a3412" },
2784
+ white: { core: "#f8fafc", highlight: "#ffffff", glow: "#e2e8f0", rim: "#64748b" },
2785
+ halogen: { core: "#fff2bf", highlight: "#fffdf0", glow: "#ffe79a", rim: "#9a7b24" },
2786
+ cyan: { core: "#22d3ee", highlight: "#cffafe", glow: "#67e8f9", rim: "#155e75" },
2787
+ brown: { core: "#a16207", highlight: "#f5d0a1", glow: "#d6a56b", rim: "#5b3b10" }
2788
+ });
2789
+ var lightLayer = {
2790
+ id: "light",
2791
+ render({ options, renderer }) {
2792
+ const light = options.light;
2793
+ if (!(light == null ? void 0 : light.visible)) return null;
2794
+ const { x, y } = resolveLightPosition(light, options);
2795
+ const palette = resolvePalette(light);
2796
+ const colorName = Object.prototype.hasOwnProperty.call(LIGHT_PALETTES, light.color) ? light.color : "amber";
2797
+ const glowFilterId = `${renderer.id}-light-glow`;
2798
+ const onGradientId = `${renderer.id}-light-on`;
2799
+ const offGradientId = `${renderer.id}-light-off`;
2800
+ const bezelGradientId = `${renderer.id}-light-bezel`;
2801
+ const defs = svgElement("defs");
2802
+ defs.append(
2803
+ createGlowFilter(glowFilterId, light.radius),
2804
+ createBulbGradient(onGradientId, {
2805
+ center: "#ffffff",
2806
+ highlight: palette.highlight,
2807
+ middle: palette.core,
2808
+ edge: palette.rim
2809
+ }),
2810
+ createBulbGradient(offGradientId, {
2811
+ center: "#fffdf7",
2812
+ highlight: light.offColor,
2813
+ middle: light.offColor,
2814
+ edge: light.offEdgeColor
2815
+ }),
2816
+ createBezelGradient(bezelGradientId, light)
2817
+ );
2818
+ const group = renderer.group("light", { "aria-hidden": "true" });
2819
+ group.append(defs);
2820
+ const lamp = svgElement("g", {
2821
+ class: `gaugeit__light gaugeit__light--${colorName}`,
2822
+ transform: `translate(${x} ${y})`,
2823
+ "data-gaugeit-light-color": colorName,
2824
+ "data-gaugeit-light-state": "off"
2825
+ });
2826
+ lamp.style.setProperty("--gaugeit-light-blink-duration", `${light.blinkInterval}ms`);
2827
+ lamp.style.setProperty("--gaugeit-light-pulse-duration", `${light.pulseInterval}ms`);
2828
+ lamp.style.setProperty("--gaugeit-light-opacity", String(light.opacity));
2829
+ const bezel = svgElement("circle", {
2830
+ class: "gaugeit__light-bezel",
2831
+ cx: 0,
2832
+ cy: 0,
2833
+ r: light.radius + Math.max(1.8, light.radius * 0.2),
2834
+ fill: `url(#${bezelGradientId})`
2835
+ });
2836
+ const bezelHighlight = svgElement("circle", {
2837
+ class: "gaugeit__light-bezel-highlight",
2838
+ cx: 0,
2839
+ cy: 0,
2840
+ r: light.radius + Math.max(0.75, light.radius * 0.08),
2841
+ fill: "none",
2842
+ stroke: light.bezelHighlightColor,
2843
+ "stroke-width": Math.max(0.55, light.radius * 0.075),
2844
+ "stroke-opacity": 0.65
2845
+ });
2846
+ const offVisual = createLampVisual({
2847
+ state: "off",
2848
+ radius: light.radius,
2849
+ gradientId: offGradientId,
2850
+ edgeColor: light.offEdgeColor,
2851
+ glowColor: null,
2852
+ glowFilterId: null,
2853
+ sparkleOpacity: 0.58,
2854
+ shadeOpacity: 0.14
2855
+ });
2856
+ const onVisual = createLampVisual({
2857
+ state: "on",
2858
+ radius: light.radius,
2859
+ gradientId: onGradientId,
2860
+ edgeColor: palette.rim,
2861
+ glowColor: palette.glow,
2862
+ glowFilterId,
2863
+ sparkleOpacity: 0.72,
2864
+ shadeOpacity: 0.1
2865
+ });
2866
+ lamp.append(bezel, bezelHighlight, offVisual, onVisual);
2867
+ group.append(lamp);
2868
+ return {
2869
+ update(value, state = {}) {
2870
+ var _a;
2871
+ const sample = resolveTriggerSample((_a = light.trigger) == null ? void 0 : _a.source, value, state);
2872
+ const active = lightIsActive(light, sample);
2873
+ const blinking = active && light.blink;
2874
+ const pulsing = active && light.pulse && !blinking;
2875
+ lamp.classList.toggle("is-on", active);
2876
+ lamp.classList.toggle("is-blinking", blinking);
2877
+ lamp.classList.toggle("is-pulsing", pulsing);
2878
+ lamp.classList.toggle("has-glow", light.glow);
2879
+ lamp.setAttribute("data-gaugeit-light-state", active ? "on" : "off");
2880
+ }
2881
+ };
2882
+ }
2883
+ };
2884
+ function createLampVisual({
2885
+ state,
2886
+ radius,
2887
+ gradientId,
2888
+ edgeColor,
2889
+ glowColor,
2890
+ glowFilterId,
2891
+ sparkleOpacity,
2892
+ shadeOpacity
2893
+ }) {
2894
+ const visual = svgElement("g", {
2895
+ class: `gaugeit__light-emission gaugeit__light-emission--${state}`,
2896
+ "data-gaugeit-light-visual": state
2897
+ });
2898
+ if (glowColor && glowFilterId) {
2899
+ visual.append(svgElement("circle", {
2900
+ class: "gaugeit__light-glow",
2901
+ cx: 0,
2902
+ cy: 0,
2903
+ r: radius * 1.82,
2904
+ fill: glowColor,
2905
+ filter: `url(#${glowFilterId})`
2906
+ }));
2907
+ }
2908
+ visual.append(
2909
+ svgElement("circle", {
2910
+ class: `gaugeit__light-bulb gaugeit__light-bulb--${state}`,
2911
+ cx: 0,
2912
+ cy: 0,
2913
+ r: radius,
2914
+ fill: `url(#${gradientId})`,
2915
+ stroke: edgeColor,
2916
+ "stroke-width": Math.max(0.8, radius * 0.13)
2917
+ }),
2918
+ svgElement("path", {
2919
+ class: `gaugeit__light-lens-shade gaugeit__light-lens-shade--${state}`,
2920
+ d: lowerLensShadePath(radius),
2921
+ fill: edgeColor,
2922
+ opacity: shadeOpacity
2923
+ }),
2924
+ svgElement("ellipse", {
2925
+ class: `gaugeit__light-sparkle gaugeit__light-sparkle--${state}`,
2926
+ cx: -radius * 0.29,
2927
+ cy: -radius * 0.35,
2928
+ rx: Math.max(0.8, radius * 0.36),
2929
+ ry: Math.max(0.6, radius * 0.2),
2930
+ fill: "#ffffff",
2931
+ opacity: sparkleOpacity
2932
+ })
2933
+ );
2934
+ return visual;
2935
+ }
2936
+ function resolvePalette(light) {
2937
+ const named = LIGHT_PALETTES[light.color] || LIGHT_PALETTES.amber;
2938
+ const custom = String(light.customColor || "").trim();
2939
+ if (!custom) return named;
2940
+ return {
2941
+ core: custom,
2942
+ highlight: "#ffffff",
2943
+ glow: custom,
2944
+ rim: custom
2945
+ };
2946
+ }
2947
+ function createGlowFilter(id, radius) {
2948
+ const filter = svgElement("filter", {
2949
+ id,
2950
+ x: "-120%",
2951
+ y: "-120%",
2952
+ width: "340%",
2953
+ height: "340%"
2954
+ });
2955
+ filter.append(svgElement("feGaussianBlur", {
2956
+ stdDeviation: Math.max(1.4, radius * 0.38)
2957
+ }));
2958
+ return filter;
2959
+ }
2960
+ function createBulbGradient(id, colors) {
2961
+ const gradient = svgElement("radialGradient", {
2962
+ id,
2963
+ cx: "31%",
2964
+ cy: "27%",
2965
+ r: "74%"
2966
+ });
2967
+ gradient.append(
2968
+ svgElement("stop", { offset: "0%", "stop-color": colors.center, "stop-opacity": 0.98 }),
2969
+ svgElement("stop", { offset: "23%", "stop-color": colors.highlight, "stop-opacity": 0.96 }),
2970
+ svgElement("stop", { offset: "62%", "stop-color": colors.middle, "stop-opacity": 1 }),
2971
+ svgElement("stop", { offset: "100%", "stop-color": colors.edge, "stop-opacity": 1 })
2972
+ );
2973
+ return gradient;
2974
+ }
2975
+ function createBezelGradient(id, light) {
2976
+ const gradient = svgElement("linearGradient", {
2977
+ id,
2978
+ x1: "0%",
2979
+ y1: "0%",
2980
+ x2: "100%",
2981
+ y2: "100%"
2982
+ });
2983
+ gradient.append(
2984
+ svgElement("stop", { offset: "0%", "stop-color": light.bezelHighlightColor, "stop-opacity": 0.92 }),
2985
+ svgElement("stop", { offset: "34%", "stop-color": light.bezelColor, "stop-opacity": 0.9 }),
2986
+ svgElement("stop", { offset: "72%", "stop-color": light.bezelColor, "stop-opacity": 1 }),
2987
+ svgElement("stop", { offset: "100%", "stop-color": "#1f2937", "stop-opacity": 0.92 })
2988
+ );
2989
+ return gradient;
2990
+ }
2991
+ function lowerLensShadePath(radius) {
2992
+ const r = Math.max(1, radius);
2993
+ return [
2994
+ `M ${-r * 0.86} ${r * 0.42}`,
2995
+ `A ${r} ${r} 0 0 0 ${r * 0.86} ${r * 0.42}`,
2996
+ `A ${r} ${r} 0 0 1 ${-r * 0.86} ${r * 0.42}`,
2997
+ "Z"
2998
+ ].join(" ");
2999
+ }
3000
+ function resolveTriggerSample(source, displayedValue, state) {
3001
+ if (source === "pointer" && Number.isFinite(state.pointerValue)) return state.pointerValue;
3002
+ if (source === "displayed") return displayedValue;
3003
+ if (Number.isFinite(state.targetValue)) return state.targetValue;
3004
+ return displayedValue;
3005
+ }
3006
+
3007
+ // src/types/arc.js
3008
+ var arcGaugeType = {
3009
+ description: "A lightweight semicircular arc with zones, labels, and an animated pointer.",
3010
+ defaults: {
3011
+ geometry: {
3012
+ width: 320,
3013
+ height: 220,
3014
+ centerX: 160,
3015
+ centerY: 176,
3016
+ radius: 120,
3017
+ startAngle: 180,
3018
+ endAngle: 360
3019
+ },
3020
+ face: { visible: false },
3021
+ track: {
3022
+ visible: true,
3023
+ color: "#e5e7eb",
3024
+ width: 28,
3025
+ radius: 112,
3026
+ lineCap: "butt"
3027
+ },
3028
+ scale: {
3029
+ radius: 112,
3030
+ position: "inside",
3031
+ major: { visible: false },
3032
+ minor: { visible: false }
3033
+ },
3034
+ labels: {
3035
+ visible: true,
3036
+ radius: 82,
3037
+ fontSize: 11,
3038
+ fontWeight: 600
3039
+ },
3040
+ pointer: {
3041
+ type: "needle",
3042
+ length: 91,
3043
+ tailLength: 12,
3044
+ width: 8,
3045
+ cap: { radius: 7, stackOrder: "above" }
3046
+ },
3047
+ light: {
3048
+ visible: false,
3049
+ x: 160,
3050
+ y: 142,
3051
+ radius: 7,
3052
+ color: "amber"
3053
+ },
3054
+ readout: {
3055
+ title: { visible: false, x: "auto", y: 35 },
3056
+ value: { visible: false, x: 160, y: 130, fontSize: 30 },
3057
+ unit: { visible: false, x: 160, y: 150 }
3058
+ }
3059
+ },
3060
+ // SVG paint order: readout first, pointer above it, cap above the pointer.
3061
+ layers: [faceLayer, zonesLayer, ticksLayer, labelsLayer, lightLayer, readoutLayer, pointerLayer]
3062
+ };
3063
+
3064
+ // src/types/classic.js
3065
+ var classicGaugeType = {
3066
+ description: "A framed white analog instrument inspired by vintage electrical and pressure meters.",
3067
+ defaults: {
3068
+ layout: {
3069
+ includeGeometry: false
3070
+ },
3071
+ geometry: {
3072
+ width: 320,
3073
+ height: 260,
3074
+ centerX: 160,
3075
+ centerY: 183,
3076
+ radius: 124,
3077
+ startAngle: 195,
3078
+ endAngle: 345
3079
+ },
3080
+ face: {
3081
+ visible: true,
3082
+ shape: "panel",
3083
+ fit: "content",
3084
+ clipContent: true,
3085
+ x: 7,
3086
+ y: 7,
3087
+ width: 306,
3088
+ height: 246,
3089
+ cornerRadius: 20,
3090
+ color: "#fffef8",
3091
+ strokeColor: "#4b5563",
3092
+ strokeWidth: 3,
3093
+ innerStrokeColor: "#d1d5db",
3094
+ innerStrokeWidth: 1.2,
3095
+ shadow: true,
3096
+ glass: true
3097
+ },
3098
+ track: {
3099
+ visible: false,
3100
+ width: 8,
3101
+ radius: 119
3102
+ },
3103
+ scale: {
3104
+ radius: 119,
3105
+ position: "inside",
3106
+ major: {
3107
+ visible: true,
3108
+ divisions: 5,
3109
+ length: 17,
3110
+ width: 2.4,
3111
+ color: "#111827"
3112
+ },
3113
+ minor: {
3114
+ visible: true,
3115
+ subdivisions: 4,
3116
+ length: 9,
3117
+ width: 1,
3118
+ color: "#4b5563"
3119
+ }
3120
+ },
3121
+ labels: {
3122
+ visible: true,
3123
+ radius: 89,
3124
+ fontSize: 12,
3125
+ fontWeight: 650
3126
+ },
3127
+ pointer: {
3128
+ type: "spear",
3129
+ length: 99,
3130
+ tailLength: 15,
3131
+ width: 9,
3132
+ color: "#111827",
3133
+ cap: {
3134
+ visible: true,
3135
+ stackOrder: "above",
3136
+ radius: 10,
3137
+ color: "#20242a",
3138
+ strokeColor: "#d1d5db",
3139
+ strokeWidth: 2
3140
+ }
3141
+ },
3142
+ light: {
3143
+ visible: false,
3144
+ x: 112,
3145
+ y: 112,
3146
+ radius: 8,
3147
+ color: "amber"
3148
+ },
3149
+ readout: {
3150
+ title: {
3151
+ visible: true,
3152
+ text: "ANALOG",
3153
+ x: "auto",
3154
+ y: "auto",
3155
+ position: "top",
3156
+ margin: 12,
3157
+ offsetY: 18,
3158
+ fontSize: 13,
3159
+ letterSpacing: 1.6
3160
+ },
3161
+ value: {
3162
+ visible: false,
3163
+ x: 160,
3164
+ y: 224,
3165
+ fontSize: 24,
3166
+ fontWeight: 750
3167
+ },
3168
+ unit: {
3169
+ visible: false,
3170
+ x: 160,
3171
+ y: 242,
3172
+ fontSize: 10
3173
+ }
3174
+ }
3175
+ },
3176
+ // SVG paint order: readout first, pointer above it, cap above the pointer.
3177
+ layers: [faceLayer, zonesLayer, ticksLayer, labelsLayer, lightLayer, readoutLayer, pointerLayer]
3178
+ };
3179
+
3180
+ // src/types/line-scale.js
3181
+ var lineScaleGaugeType = {
3182
+ description: "A dense line-scale gauge with independently configurable major and minor ticks.",
3183
+ defaults: {
3184
+ geometry: {
3185
+ width: 320,
3186
+ height: 225,
3187
+ centerX: 160,
3188
+ centerY: 184,
3189
+ radius: 127,
3190
+ startAngle: 180,
3191
+ endAngle: 360
3192
+ },
3193
+ face: { visible: false },
3194
+ track: {
3195
+ visible: true,
3196
+ color: "#cbd5e1",
3197
+ width: 2,
3198
+ radius: 126,
3199
+ lineCap: "round"
3200
+ },
3201
+ scale: {
3202
+ radius: 124,
3203
+ position: "inside",
3204
+ major: {
3205
+ visible: true,
3206
+ divisions: 8,
3207
+ length: 23,
3208
+ width: 2.4,
3209
+ color: "#0f172a"
3210
+ },
3211
+ minor: {
3212
+ visible: true,
3213
+ subdivisions: 4,
3214
+ length: 11,
3215
+ width: 1,
3216
+ color: "#475569"
3217
+ }
3218
+ },
3219
+ labels: {
3220
+ visible: true,
3221
+ radius: 88,
3222
+ fontSize: 10,
3223
+ fontWeight: 650
3224
+ },
3225
+ pointer: {
3226
+ type: "line",
3227
+ length: 101,
3228
+ tailLength: 10,
3229
+ width: 4,
3230
+ color: "#b91c1c",
3231
+ cap: {
3232
+ visible: true,
3233
+ stackOrder: "above",
3234
+ radius: 7,
3235
+ color: "#b91c1c",
3236
+ strokeColor: "#ffffff",
3237
+ strokeWidth: 2
3238
+ }
3239
+ },
3240
+ light: {
3241
+ visible: false,
3242
+ x: 160,
3243
+ y: 151,
3244
+ radius: 7,
3245
+ color: "amber"
3246
+ },
3247
+ readout: {
3248
+ title: { visible: false, x: "auto", y: 140 },
3249
+ value: { visible: false, x: 160, y: 220, fontSize: 33 },
3250
+ unit: { visible: false, x: 160, y: 240 }
3251
+ }
3252
+ },
3253
+ // SVG paint order: readout first, pointer above it, cap above the pointer.
3254
+ layers: [faceLayer, zonesLayer, ticksLayer, labelsLayer, lightLayer, readoutLayer, pointerLayer]
3255
+ };
3256
+
3257
+ // src/types/heritage-round.js
3258
+ var heritageRoundGaugeType = {
3259
+ description: "A circular, glass-fronted vintage instrument with a 270-degree scale.",
3260
+ defaults: {
3261
+ geometry: {
3262
+ width: 320,
3263
+ height: 320,
3264
+ centerX: 160,
3265
+ centerY: 160,
3266
+ radius: 132,
3267
+ startAngle: 225,
3268
+ endAngle: 495
3269
+ },
3270
+ face: {
3271
+ visible: true,
3272
+ shape: "circle",
3273
+ fit: "geometry",
3274
+ clipContent: true,
3275
+ x: 8,
3276
+ y: 8,
3277
+ width: 304,
3278
+ height: 304,
3279
+ cornerRadius: 0,
3280
+ color: "#fffdf5",
3281
+ strokeColor: "#374151",
3282
+ strokeWidth: 4,
3283
+ innerStrokeColor: "#c7bfae",
3284
+ innerStrokeWidth: 1.4,
3285
+ shadow: true,
3286
+ glass: true
3287
+ },
3288
+ track: {
3289
+ visible: false,
3290
+ width: 7,
3291
+ radius: 124
3292
+ },
3293
+ scale: {
3294
+ endpointMode: "auto",
3295
+ radius: 124,
3296
+ position: "inside",
3297
+ major: {
3298
+ visible: true,
3299
+ divisions: 9,
3300
+ length: 18,
3301
+ width: 2.6,
3302
+ color: "#171717"
3303
+ },
3304
+ minor: {
3305
+ visible: true,
3306
+ subdivisions: 4,
3307
+ length: 9,
3308
+ width: 1,
3309
+ color: "#57534e"
3310
+ }
3311
+ },
3312
+ labels: {
3313
+ visible: true,
3314
+ radius: 92,
3315
+ fontSize: 11,
3316
+ fontWeight: 700
3317
+ },
3318
+ pointer: {
3319
+ type: "spear",
3320
+ length: 104,
3321
+ tailLength: 18,
3322
+ width: 9,
3323
+ color: "#7f1d1d",
3324
+ cap: {
3325
+ visible: true,
3326
+ stackOrder: "above",
3327
+ radius: 11,
3328
+ color: "#292524",
3329
+ strokeColor: "#d6d3d1",
3330
+ strokeWidth: 2
3331
+ }
3332
+ },
3333
+ light: {
3334
+ visible: false,
3335
+ x: 108,
3336
+ y: 110,
3337
+ radius: 10,
3338
+ color: "amber",
3339
+ blink: false,
3340
+ blinkInterval: 1e3,
3341
+ trigger: { mode: "below", source: "target", value: 15, min: 0, max: 15 }
3342
+ },
3343
+ readout: {
3344
+ title: {
3345
+ visible: true,
3346
+ text: "HERITAGE",
3347
+ x: "auto",
3348
+ y: "auto",
3349
+ position: "top",
3350
+ margin: 14,
3351
+ offsetY: 85,
3352
+ fontSize: 13,
3353
+ letterSpacing: 1.8
3354
+ },
3355
+ value: {
3356
+ visible: false,
3357
+ x: "auto",
3358
+ y: 224,
3359
+ fontSize: 25,
3360
+ fontWeight: 750
3361
+ },
3362
+ unit: {
3363
+ visible: false,
3364
+ x: "auto",
3365
+ y: 244,
3366
+ fontSize: 10
3367
+ }
3368
+ }
3369
+ },
3370
+ // SVG paint order: readout first, pointer above it, cap above the pointer.
3371
+ layers: [faceLayer, zonesLayer, ticksLayer, labelsLayer, lightLayer, readoutLayer, pointerLayer]
3372
+ };
3373
+
3374
+ // src/layers/linear-zones.js
3375
+ var linearZonesLayer = {
3376
+ id: "linear-zones",
3377
+ render(context) {
3378
+ const { options, renderer } = context;
3379
+ const group = renderer.group("linear-zones", { "aria-hidden": "true" });
3380
+ const segments = resolveZoneSegments(options.min, options.max, options.zones);
3381
+ if (options.track.showUnderZones && options.track.visible) {
3382
+ group.append(createSegment(options.min, options.max, options.track, "gaugeit__track"));
3383
+ }
3384
+ for (const segment of segments) {
3385
+ const style = segment.zone || options.track;
3386
+ const shouldDraw = segment.zone ? !isTransparentColor(segment.zone.color) : options.track.visible && !options.track.showUnderZones;
3387
+ if (!shouldDraw) continue;
3388
+ const className = segment.zone ? "gaugeit__zone" : "gaugeit__track";
3389
+ const line = createSegment(segment.min, segment.max, style, className);
3390
+ if (segment.zone && segment.zone.className) line.classList.add(String(segment.zone.className));
3391
+ group.append(line);
3392
+ }
3393
+ return null;
3394
+ function createSegment(min, max, style, className) {
3395
+ var _a, _b, _c, _d;
3396
+ const axis = linearAxis(options);
3397
+ const offset = linearNormalOffset((_a = style.offset) != null ? _a : options.linear.zoneOffset);
3398
+ const start = offsetLinearPoint(valueToLinearPoint(min, options), axis, offset);
3399
+ const end = offsetLinearPoint(valueToLinearPoint(max, options), axis, offset);
3400
+ return svgElement("line", {
3401
+ class: className,
3402
+ x1: start.x,
3403
+ y1: start.y,
3404
+ x2: end.x,
3405
+ y2: end.y,
3406
+ stroke: (_b = style.color) != null ? _b : options.track.color,
3407
+ "stroke-width": (_c = style.width) != null ? _c : options.track.width,
3408
+ "stroke-linecap": style.lineCap || options.track.lineCap || "butt",
3409
+ opacity: (_d = style.opacity) != null ? _d : 1
3410
+ });
3411
+ }
3412
+ }
3413
+ };
3414
+
3415
+ // src/layers/linear-ticks.js
3416
+ var linearTicksLayer = {
3417
+ id: "linear-ticks",
3418
+ render(context) {
3419
+ const { options, renderer } = context;
3420
+ const scale = options.scale;
3421
+ if (!scale.major.visible && !scale.minor.visible) return null;
3422
+ const group = renderer.group("linear-ticks", { "aria-hidden": "true" });
3423
+ const interval = resolveMajorInterval(options);
3424
+ const majorValues = buildScaleValues(options.min, options.max, interval);
3425
+ const minorValues = buildMinorValues(majorValues, scale.minor.subdivisions);
3426
+ if (scale.minor.visible && scale.minor.length > 0 && scale.minor.width > 0 && minorValues.length) {
3427
+ group.append(svgElement("path", {
3428
+ class: "gaugeit__ticks gaugeit__ticks--minor gaugeit__linear-ticks",
3429
+ d: tickPath(minorValues, scale.minor.length),
3430
+ fill: "none",
3431
+ stroke: scale.minor.color,
3432
+ "stroke-width": scale.minor.width,
3433
+ "stroke-linecap": "butt",
3434
+ opacity: scale.minor.opacity
3435
+ }));
3436
+ }
3437
+ if (scale.major.visible && scale.major.length > 0 && scale.major.width > 0) {
3438
+ group.append(svgElement("path", {
3439
+ class: "gaugeit__ticks gaugeit__ticks--major gaugeit__linear-ticks",
3440
+ d: tickPath(majorValues, scale.major.length),
3441
+ fill: "none",
3442
+ stroke: scale.major.color,
3443
+ "stroke-width": scale.major.width,
3444
+ "stroke-linecap": "butt",
3445
+ opacity: scale.major.opacity
3446
+ }));
3447
+ }
3448
+ renderer.setReference("majorValues", majorValues);
3449
+ return null;
3450
+ function tickPath(values, length) {
3451
+ const axis = linearAxis(options);
3452
+ const span = linearNormalSpan(length, options.linear.tickSide);
3453
+ const commands = [];
3454
+ for (const value of values) {
3455
+ const point = valueToLinearPoint(value, options);
3456
+ const start = offsetLinearPoint(point, axis, span.start);
3457
+ const end = offsetLinearPoint(point, axis, span.end);
3458
+ commands.push(`M ${roundSvg(start.x)} ${roundSvg(start.y)} L ${roundSvg(end.x)} ${roundSvg(end.y)}`);
3459
+ }
3460
+ return commands.join(" ");
3461
+ }
3462
+ }
3463
+ };
3464
+
3465
+ // src/layers/linear-labels.js
3466
+ var linearLabelsLayer = {
3467
+ id: "linear-labels",
3468
+ render(context) {
3469
+ const { options, renderer } = context;
3470
+ const labels = options.labels;
3471
+ if (!labels.visible) return null;
3472
+ const group = renderer.group("linear-labels", { "aria-hidden": "true" });
3473
+ const interval = Number(labels.interval) > 0 ? Number(labels.interval) : resolveMajorInterval(options);
3474
+ const values = buildScaleValues(options.min, options.max, interval);
3475
+ const axis = linearAxis(options);
3476
+ for (const value of values) {
3477
+ const point = offsetLinearPoint(
3478
+ valueToLinearPoint(value, options),
3479
+ axis,
3480
+ linearNormalOffset(options.linear.labelOffset)
3481
+ );
3482
+ const text = svgElement("text", {
3483
+ class: "gaugeit__label gaugeit__linear-label",
3484
+ x: point.x,
3485
+ y: point.y,
3486
+ fill: labels.color,
3487
+ "font-family": labels.fontFamily,
3488
+ "font-size": labels.fontSize,
3489
+ "font-weight": labels.fontWeight,
3490
+ "text-anchor": "middle",
3491
+ "dominant-baseline": "middle",
3492
+ transform: labelTransform2(labels.rotation, axis.angle, point)
3493
+ }, formatLabel2(value, labels, options));
3494
+ group.append(text);
3495
+ }
3496
+ return null;
3497
+ }
3498
+ };
3499
+ function labelTransform2(rotation, axisAngle, point) {
3500
+ if (rotation === "radial") return `rotate(${axisAngle + 90} ${point.x} ${point.y})`;
3501
+ if (rotation === "tangent") return `rotate(${axisAngle} ${point.x} ${point.y})`;
3502
+ return void 0;
3503
+ }
3504
+ function formatLabel2(value, labels, options) {
3505
+ if (typeof labels.formatter === "function") {
3506
+ return labels.formatter(value, { min: options.min, max: options.max, options });
3507
+ }
3508
+ return Number(value).toFixed(labels.fractionDigits);
3509
+ }
3510
+
3511
+ // src/layers/linear-indicator.js
3512
+ var linearIndicatorLayer = {
3513
+ id: "linear-indicator",
3514
+ render(context) {
3515
+ const { options, renderer } = context;
3516
+ const indicator = options.indicator;
3517
+ if (!indicator.visible) return null;
3518
+ const axis = linearAxis(options);
3519
+ const normalOffset = linearIndicatorNormalOffset(indicator);
3520
+ const group = renderer.group("linear-indicator", { "aria-hidden": "true" });
3521
+ const moving = svgElement("g", {
3522
+ class: "gaugeit__linear-indicator-moving"
3523
+ });
3524
+ const shape = createIndicatorShape(indicator, normalOffset);
3525
+ moving.append(shape);
3526
+ let cap = null;
3527
+ if (indicator.cap.visible) {
3528
+ cap = svgElement("circle", {
3529
+ class: "gaugeit__linear-indicator-cap",
3530
+ cx: 0,
3531
+ cy: normalOffset,
3532
+ r: indicator.cap.radius,
3533
+ fill: indicator.cap.color,
3534
+ stroke: indicator.cap.strokeColor,
3535
+ "stroke-width": indicator.cap.strokeWidth
3536
+ });
3537
+ if (indicator.cap.stackOrder === "below") moving.prepend(cap);
3538
+ else moving.append(cap);
3539
+ }
3540
+ group.append(moving);
3541
+ return {
3542
+ update(value, state = {}) {
3543
+ const visualValue = Number.isFinite(state.pointerValue) ? state.pointerValue : value;
3544
+ const point = valueToLinearPoint(visualValue, options, options.overflow !== "allow");
3545
+ moving.setAttribute("transform", `translate(${point.x} ${point.y}) rotate(${axis.angle})`);
3546
+ if (indicator.colorByZone) {
3547
+ const zone = findZoneForValue(visualValue, options.zones);
3548
+ const color = zone && !isTransparentColor(zone.color) ? zone.color : indicator.color;
3549
+ setIndicatorColor(shape, indicator, color);
3550
+ }
3551
+ }
3552
+ };
3553
+ }
3554
+ };
3555
+ function createIndicatorShape(indicator, normalOffset) {
3556
+ const span = linearNormalSpan(indicator.length, indicator.position, normalOffset);
3557
+ let shape;
3558
+ if (indicator.type === "marker") {
3559
+ const direction = markerDirection(indicator.position);
3560
+ if (direction === 0) {
3561
+ const size = indicator.markerSize;
3562
+ const centerY = normalOffset;
3563
+ shape = svgElement("path", {
3564
+ class: "gaugeit__linear-indicator gaugeit__linear-indicator--marker",
3565
+ d: `M 0 ${centerY - size} L ${size * 0.72} ${centerY} L 0 ${centerY + size} L ${-size * 0.72} ${centerY} Z`,
3566
+ fill: indicator.color
3567
+ });
3568
+ } else {
3569
+ const tipY = normalOffset;
3570
+ const baseY = tipY + direction * indicator.markerSize;
3571
+ const halfWidth = indicator.markerSize * 0.72;
3572
+ shape = svgElement("path", {
3573
+ class: "gaugeit__linear-indicator gaugeit__linear-indicator--marker",
3574
+ d: `M 0 ${tipY} L ${halfWidth} ${baseY} L ${-halfWidth} ${baseY} Z`,
3575
+ fill: indicator.color
3576
+ });
3577
+ }
3578
+ } else if (indicator.type === "carriage") {
3579
+ const y = Math.min(span.start, span.end);
3580
+ shape = svgElement("rect", {
3581
+ class: "gaugeit__linear-indicator gaugeit__linear-indicator--carriage",
3582
+ x: -indicator.carriageWidth / 2,
3583
+ y,
3584
+ width: indicator.carriageWidth,
3585
+ height: Math.max(1, Math.abs(span.end - span.start)),
3586
+ rx: indicator.carriageRadius,
3587
+ ry: indicator.carriageRadius,
3588
+ fill: indicator.color
3589
+ });
3590
+ } else {
3591
+ shape = svgElement("line", {
3592
+ class: "gaugeit__linear-indicator gaugeit__linear-indicator--hairline",
3593
+ x1: 0,
3594
+ y1: span.start,
3595
+ x2: 0,
3596
+ y2: span.end,
3597
+ stroke: indicator.color,
3598
+ "stroke-width": indicator.width,
3599
+ "stroke-linecap": indicator.lineCap
3600
+ });
3601
+ }
3602
+ setSvgAttributes(shape, {
3603
+ opacity: indicator.opacity,
3604
+ stroke: indicator.type === "hairline" ? indicator.color : indicator.strokeColor,
3605
+ "stroke-width": indicator.type === "hairline" ? indicator.width : indicator.strokeWidth
3606
+ });
3607
+ return shape;
3608
+ }
3609
+ function markerDirection(position) {
3610
+ if (position === "negative") return -1;
3611
+ if (position === "positive") return 1;
3612
+ return 0;
3613
+ }
3614
+ function setIndicatorColor(shape, indicator, color) {
3615
+ if (indicator.type === "hairline") shape.setAttribute("stroke", color);
3616
+ else shape.setAttribute("fill", color);
3617
+ }
3618
+
3619
+ // src/types/instrument-sizing.js
3620
+ function resolveIntrinsicGeometrySize(options, context) {
3621
+ var _a, _b;
3622
+ const requestedGeometry = isPlainObject((_a = context.requested) == null ? void 0 : _a.geometry) ? context.requested.geometry : {};
3623
+ const typeGeometry = ((_b = context.typeDefaults) == null ? void 0 : _b.geometry) || {};
3624
+ const widthExplicit = hasOwn(requestedGeometry, "width");
3625
+ const heightExplicit = hasOwn(requestedGeometry, "height");
3626
+ const ratioExplicit = hasOwn(requestedGeometry, "aspectRatio");
3627
+ const ratio = finiteNumber(requestedGeometry.aspectRatio, 0);
3628
+ if (ratioExplicit && ratio > 0) {
3629
+ if (widthExplicit && !heightExplicit) options.geometry.height = options.geometry.width / ratio;
3630
+ else if (heightExplicit && !widthExplicit) options.geometry.width = options.geometry.height * ratio;
3631
+ else if (!widthExplicit && !heightExplicit) {
3632
+ const baseWidth = finiteNumber(typeGeometry.width, options.geometry.width);
3633
+ options.geometry.width = baseWidth;
3634
+ options.geometry.height = baseWidth / ratio;
3635
+ }
3636
+ }
3637
+ options.geometry.aspectRatio = ratioExplicit && ratio > 0 ? ratio : null;
3638
+ return {
3639
+ requestedGeometry,
3640
+ widthExplicit: widthExplicit || ratioExplicit && ratio > 0 && !heightExplicit,
3641
+ heightExplicit: heightExplicit || ratioExplicit && ratio > 0,
3642
+ baseWidth: Math.max(1, finiteNumber(typeGeometry.width, options.geometry.width)),
3643
+ baseHeight: Math.max(1, finiteNumber(typeGeometry.height, options.geometry.height))
3644
+ };
3645
+ }
3646
+ function configureClassicLinearSize(options, context) {
3647
+ const sizing = resolveIntrinsicGeometrySize(options, context);
3648
+ if (!sizing.widthExplicit && !sizing.heightExplicit) return options;
3649
+ const scaleX = options.geometry.width / sizing.baseWidth;
3650
+ const scaleY = options.geometry.height / sizing.baseHeight;
3651
+ const uniform = Math.min(scaleX, scaleY);
3652
+ scalePaths(options, context, scaleX, [
3653
+ "geometry.centerX",
3654
+ "face.x",
3655
+ "face.width",
3656
+ "face.minWidth",
3657
+ "linear.startX",
3658
+ "linear.endX",
3659
+ "readout.title.x",
3660
+ "readout.value.x",
3661
+ "readout.unit.x"
3662
+ ]);
3663
+ scalePaths(options, context, scaleY, [
3664
+ "geometry.centerY",
3665
+ "face.y",
3666
+ "face.height",
3667
+ "face.minHeight",
3668
+ "linear.originY",
3669
+ "linear.startY",
3670
+ "linear.endY",
3671
+ "linear.labelOffset",
3672
+ "linear.zoneOffset",
3673
+ "scale.major.length",
3674
+ "scale.minor.length",
3675
+ "indicator.length",
3676
+ "indicator.centerOffset",
3677
+ "indicator.markerSize",
3678
+ "indicator.carriageWidth",
3679
+ "indicator.carriageRadius",
3680
+ "indicator.cap.radius",
3681
+ "centerZero.marker.length",
3682
+ "centerZero.marker.centerOffset",
3683
+ "readout.title.y",
3684
+ "readout.value.y",
3685
+ "readout.unit.y"
3686
+ ]);
3687
+ scalePaths(options, context, uniform, [
3688
+ "face.contentPadding",
3689
+ "face.cornerRadius"
3690
+ ]);
3691
+ return options;
3692
+ }
3693
+ function configureRollingTapeSize(options, context) {
3694
+ const sizing = resolveIntrinsicGeometrySize(options, context);
3695
+ if (!sizing.widthExplicit && !sizing.heightExplicit) return options;
3696
+ const scaleX = options.geometry.width / sizing.baseWidth;
3697
+ const scaleY = options.geometry.height / sizing.baseHeight;
3698
+ const uniform = Math.min(scaleX, scaleY);
3699
+ const vertical = options.tape.orientation !== "horizontal";
3700
+ scalePaths(options, context, scaleX, [
3701
+ "geometry.centerX",
3702
+ "face.x",
3703
+ "face.width",
3704
+ "face.minWidth",
3705
+ "tape.x",
3706
+ "tape.width",
3707
+ "readout.title.x",
3708
+ "readout.value.x",
3709
+ "readout.unit.x"
3710
+ ]);
3711
+ scalePaths(options, context, scaleY, [
3712
+ "geometry.centerY",
3713
+ "face.y",
3714
+ "face.height",
3715
+ "face.minHeight",
3716
+ "tape.y",
3717
+ "tape.height",
3718
+ "readout.title.y",
3719
+ "readout.value.y",
3720
+ "readout.unit.y"
3721
+ ]);
3722
+ scalePaths(options, context, vertical ? scaleX : scaleY, [
3723
+ "tape.tickInset",
3724
+ "tape.labelOffset",
3725
+ "tape.majorTickLength",
3726
+ "tape.minorTickLength"
3727
+ ]);
3728
+ scalePaths(options, context, vertical ? scaleY : scaleX, [
3729
+ "tape.majorSpacing",
3730
+ "tape.fadeSize"
3731
+ ]);
3732
+ scalePaths(options, context, uniform, [
3733
+ "face.cornerRadius",
3734
+ "tape.cornerRadius"
3735
+ ]);
3736
+ return options;
3737
+ }
3738
+ function scalePaths(options, context, factor, paths) {
3739
+ if (!Number.isFinite(factor) || factor <= 0 || Math.abs(factor - 1) < 1e-9) return;
3740
+ for (const path of paths) {
3741
+ if (hasPath(context.requested, path)) continue;
3742
+ const baseValue = getPath(context.typeDefaults, path);
3743
+ if (!Number.isFinite(baseValue)) continue;
3744
+ setPath(options, path, baseValue * factor);
3745
+ }
3746
+ }
3747
+ function hasOwn(value, key) {
3748
+ return isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, key);
3749
+ }
3750
+ function hasPath(value, path) {
3751
+ const keys = path.split(".");
3752
+ let cursor = value;
3753
+ for (const key of keys) {
3754
+ if (!isPlainObject(cursor) || !Object.prototype.hasOwnProperty.call(cursor, key)) return false;
3755
+ cursor = cursor[key];
3756
+ }
3757
+ return true;
3758
+ }
3759
+ function getPath(value, path) {
3760
+ let cursor = value;
3761
+ for (const key of path.split(".")) {
3762
+ if (cursor === null || cursor === void 0) return void 0;
3763
+ cursor = cursor[key];
3764
+ }
3765
+ return cursor;
3766
+ }
3767
+ function setPath(value, path, nextValue) {
3768
+ const keys = path.split(".");
3769
+ let cursor = value;
3770
+ for (let index = 0; index < keys.length - 1; index += 1) {
3771
+ const key = keys[index];
3772
+ if (!isPlainObject(cursor[key])) cursor[key] = {};
3773
+ cursor = cursor[key];
3774
+ }
3775
+ cursor[keys[keys.length - 1]] = nextValue;
3776
+ }
3777
+
3778
+ // src/types/classic-linear.js
3779
+ var classicLinearDefaults = {
3780
+ layout: {
3781
+ includeGeometry: false
3782
+ },
3783
+ geometry: {
3784
+ mode: "linear",
3785
+ width: 320,
3786
+ height: 220,
3787
+ centerX: 160,
3788
+ centerY: 110
3789
+ },
3790
+ face: {
3791
+ visible: true,
3792
+ shape: "panel",
3793
+ fit: "content",
3794
+ contentPadding: 14,
3795
+ minWidth: 1,
3796
+ minHeight: 1,
3797
+ clipContent: true,
3798
+ x: 7,
3799
+ y: 7,
3800
+ width: 306,
3801
+ height: 206,
3802
+ cornerRadius: 20,
3803
+ color: "#fffef8",
3804
+ strokeColor: "#4b5563",
3805
+ strokeWidth: 3,
3806
+ innerStrokeColor: "#d1d5db",
3807
+ innerStrokeWidth: 1.2,
3808
+ shadow: true,
3809
+ glass: true
3810
+ },
3811
+ linear: {
3812
+ originY: 142,
3813
+ startX: 38,
3814
+ startY: 0,
3815
+ endX: 282,
3816
+ endY: 0,
3817
+ tickSide: "negative",
3818
+ labelOffset: 38,
3819
+ zoneOffset: -16
3820
+ },
3821
+ track: {
3822
+ visible: true,
3823
+ color: "#a8a29e",
3824
+ width: 5,
3825
+ opacity: 0.85,
3826
+ lineCap: "round",
3827
+ showUnderZones: false
3828
+ },
3829
+ scale: {
3830
+ major: {
3831
+ visible: true,
3832
+ divisions: 5,
3833
+ length: 20,
3834
+ width: 2.4,
3835
+ color: "#111827"
3836
+ },
3837
+ minor: {
3838
+ visible: true,
3839
+ subdivisions: 4,
3840
+ length: 10,
3841
+ width: 1,
3842
+ color: "#57534e"
3843
+ }
3844
+ },
3845
+ labels: {
3846
+ visible: true,
3847
+ fontSize: 11,
3848
+ fontWeight: 700,
3849
+ rotation: "horizontal"
3850
+ },
3851
+ pointer: {
3852
+ visible: false
3853
+ },
3854
+ indicator: {
3855
+ visible: true,
3856
+ type: "hairline",
3857
+ position: "cross",
3858
+ length: 84,
3859
+ centerOffset: 0,
3860
+ width: 2.4,
3861
+ color: "#991b1b",
3862
+ strokeColor: "#fffef8",
3863
+ strokeWidth: 0.8,
3864
+ cap: {
3865
+ visible: false,
3866
+ stackOrder: "above",
3867
+ radius: 4.5,
3868
+ color: "#991b1b",
3869
+ strokeColor: "#fffef8",
3870
+ strokeWidth: 1.2
3871
+ }
3872
+ },
3873
+ light: {
3874
+ visible: false,
3875
+ x: 72,
3876
+ y: 60,
3877
+ radius: 8,
3878
+ color: "amber"
3879
+ },
3880
+ readout: {
3881
+ title: {
3882
+ visible: true,
3883
+ text: "LINEAR",
3884
+ x: "auto",
3885
+ y: 60,
3886
+ fontSize: 13,
3887
+ letterSpacing: 1.6
3888
+ },
3889
+ value: {
3890
+ visible: false,
3891
+ x: "auto",
3892
+ y: -42,
3893
+ fontSize: 24,
3894
+ fontWeight: 750
3895
+ },
3896
+ unit: {
3897
+ visible: false,
3898
+ x: "auto",
3899
+ y: -60,
3900
+ fontSize: 10
3901
+ }
3902
+ }
3903
+ };
3904
+ var classicLinearLayers = [
3905
+ faceLayer,
3906
+ linearZonesLayer,
3907
+ linearTicksLayer,
3908
+ linearLabelsLayer,
3909
+ lightLayer,
3910
+ readoutLayer,
3911
+ linearIndicatorLayer
3912
+ ];
3913
+ var classicLinearGaugeType = {
3914
+ description: "A framed vintage linear instrument with zones, ticks, labels, and a moving hairline.",
3915
+ defaults: classicLinearDefaults,
3916
+ configure: configureClassicLinearSize,
3917
+ layers: classicLinearLayers
3918
+ };
3919
+
3920
+ // src/layers/zero-marker.js
3921
+ var zeroMarkerLayer = {
3922
+ id: "zero-marker",
3923
+ render(context) {
3924
+ var _a;
3925
+ const { options, renderer } = context;
3926
+ const marker = (_a = options.centerZero) == null ? void 0 : _a.marker;
3927
+ if (!(marker == null ? void 0 : marker.visible)) return null;
3928
+ const group = renderer.group("zero-marker", { "aria-hidden": "true" });
3929
+ const line = options.geometry.mode === "linear" ? linearZeroLine(options, marker) : radialZeroLine(options, marker);
3930
+ group.append(svgElement("line", {
3931
+ class: "gaugeit__zero-marker",
3932
+ ...line,
3933
+ stroke: marker.color,
3934
+ "stroke-width": marker.width,
3935
+ "stroke-linecap": marker.lineCap,
3936
+ opacity: marker.opacity
3937
+ }));
3938
+ return null;
3939
+ }
3940
+ };
3941
+ function radialZeroLine(options, marker) {
3942
+ var _a;
3943
+ const angle = valueToAngle(options.centerZero.zeroValue, options);
3944
+ const radius = (_a = marker.radius) != null ? _a : options.scale.radius;
3945
+ const start = polarPoint(
3946
+ options.geometry.centerX,
3947
+ options.geometry.centerY,
3948
+ Math.max(0, radius - marker.length),
3949
+ angle
3950
+ );
3951
+ const end = polarPoint(options.geometry.centerX, options.geometry.centerY, radius, angle);
3952
+ return { x1: start.x, y1: start.y, x2: end.x, y2: end.y };
3953
+ }
3954
+ function linearZeroLine(options, marker) {
3955
+ const axis = linearAxis(options);
3956
+ const point = valueToLinearPoint(options.centerZero.zeroValue, options);
3957
+ const span = linearNormalSpan(
3958
+ marker.length,
3959
+ marker.position,
3960
+ linearNormalOffset(marker.centerOffset)
3961
+ );
3962
+ const start = offsetLinearPoint(point, axis, span.start);
3963
+ const end = offsetLinearPoint(point, axis, span.end);
3964
+ return { x1: start.x, y1: start.y, x2: end.x, y2: end.y };
3965
+ }
3966
+
3967
+ // src/types/classic-linear-zero.js
3968
+ var defaults = deepMerge(classicLinearDefaults, {
3969
+ min: -100,
3970
+ max: 100,
3971
+ value: 0,
3972
+ centerZero: {
3973
+ zeroValue: 0,
3974
+ symmetric: true,
3975
+ marker: {
3976
+ visible: true,
3977
+ position: "cross",
3978
+ centerOffset: 0,
3979
+ length: 34,
3980
+ width: 3.2,
3981
+ color: "#111827",
3982
+ opacity: 1,
3983
+ lineCap: "butt"
3984
+ }
3985
+ },
3986
+ zones: [
3987
+ { min: -100, max: -35, color: "#b91c1c", width: 7 },
3988
+ { min: -35, max: 35, color: "transparent" },
3989
+ { min: 35, max: 100, color: "#15803d", width: 7 }
3990
+ ],
3991
+ readout: {
3992
+ title: {
3993
+ visible: true,
3994
+ text: "DEVIATION"
3995
+ }
3996
+ }
3997
+ });
3998
+ var classicLinearZeroGaugeType = {
3999
+ description: "A classic linear instrument with a fixed center-zero reference and signed travel.",
4000
+ defaults,
4001
+ configure: configureClassicLinearSize,
4002
+ layers: [
4003
+ ...classicLinearLayers.slice(0, 3),
4004
+ zeroMarkerLayer,
4005
+ ...classicLinearLayers.slice(3)
4006
+ ]
4007
+ };
4008
+
4009
+ // src/types/center-zero.js
4010
+ var centerZeroGaugeType = {
4011
+ description: "A heritage-styled bidirectional center-zero dial for deviation, balance, and signed values.",
4012
+ defaults: {
4013
+ min: -100,
4014
+ max: 100,
4015
+ value: 0,
4016
+ geometry: {
4017
+ width: 320,
4018
+ height: 320,
4019
+ centerX: 160,
4020
+ centerY: 160,
4021
+ radius: 132,
4022
+ startAngle: 150,
4023
+ endAngle: 390
4024
+ },
4025
+ centerZero: {
4026
+ zeroValue: 0,
4027
+ symmetric: true,
4028
+ marker: {
4029
+ visible: true,
4030
+ radius: 124,
4031
+ length: 25,
4032
+ width: 3.2,
4033
+ color: "#292524",
4034
+ opacity: 1,
4035
+ lineCap: "round"
4036
+ }
4037
+ },
4038
+ face: {
4039
+ visible: true,
4040
+ shape: "circle",
4041
+ fit: "geometry",
4042
+ clipContent: true,
4043
+ x: 8,
4044
+ y: 8,
4045
+ width: 304,
4046
+ height: 304,
4047
+ color: "#fffdf5",
4048
+ strokeColor: "#374151",
4049
+ strokeWidth: 4,
4050
+ innerStrokeColor: "#c7bfae",
4051
+ innerStrokeWidth: 1.4,
4052
+ shadow: true,
4053
+ glass: true
4054
+ },
4055
+ track: {
4056
+ visible: true,
4057
+ color: "#d6d0c2",
4058
+ width: 10,
4059
+ radius: 123,
4060
+ opacity: 1,
4061
+ lineCap: "round",
4062
+ showUnderZones: true
4063
+ },
4064
+ zones: [
4065
+ { min: -100, max: -35, color: "#b91c1c", width: 8, radius: 123, opacity: 0.92, lineCap: "round" },
4066
+ { min: -35, max: 35, color: "transparent" },
4067
+ { min: 35, max: 100, color: "#b45309", width: 8, radius: 123, opacity: 0.92, lineCap: "round" }
4068
+ ],
4069
+ scale: {
4070
+ radius: 119,
4071
+ position: "inside",
4072
+ major: {
4073
+ visible: true,
4074
+ interval: 25,
4075
+ divisions: 8,
4076
+ length: 18,
4077
+ width: 2.4,
4078
+ color: "#292524"
4079
+ },
4080
+ minor: {
4081
+ visible: true,
4082
+ subdivisions: 4,
4083
+ length: 9,
4084
+ width: 1,
4085
+ color: "#78716c"
4086
+ }
4087
+ },
4088
+ labels: {
4089
+ visible: true,
4090
+ interval: 25,
4091
+ radius: 88,
4092
+ color: "#292524",
4093
+ fontSize: 11,
4094
+ fontWeight: 700
4095
+ },
4096
+ pointer: {
4097
+ type: "spear",
4098
+ length: 101,
4099
+ shaftLength: 78,
4100
+ tailLength: 18,
4101
+ width: 8,
4102
+ color: "#7f1d1d",
4103
+ strokeColor: "#fffdf5",
4104
+ strokeWidth: 0.8,
4105
+ cap: {
4106
+ visible: true,
4107
+ stackOrder: "above",
4108
+ radius: 10,
4109
+ color: "#292524",
4110
+ strokeColor: "#d6d3d1",
4111
+ strokeWidth: 2
4112
+ }
4113
+ },
4114
+ light: {
4115
+ visible: false,
4116
+ x: 112,
4117
+ y: 205,
4118
+ radius: 8,
4119
+ color: "amber"
4120
+ },
4121
+ readout: {
4122
+ title: {
4123
+ visible: true,
4124
+ text: "BALANCE",
4125
+ x: "auto",
4126
+ y: 205,
4127
+ color: "#44403c",
4128
+ fontSize: 13,
4129
+ letterSpacing: 1.8
4130
+ },
4131
+ value: {
4132
+ visible: true,
4133
+ x: "auto",
4134
+ y: 238,
4135
+ color: "#292524",
4136
+ fontSize: 28,
4137
+ fontWeight: 760,
4138
+ prefix: ""
4139
+ },
4140
+ unit: {
4141
+ visible: false,
4142
+ text: "",
4143
+ x: "auto",
4144
+ y: 258,
4145
+ color: "#78716c",
4146
+ fontSize: 10
4147
+ }
4148
+ }
4149
+ },
4150
+ layers: [faceLayer, zonesLayer, ticksLayer, labelsLayer, zeroMarkerLayer, lightLayer, readoutLayer, pointerLayer]
4151
+ };
4152
+
4153
+ // src/layers/rolling-tape.js
4154
+ var rollingTapeLayer = {
4155
+ id: "rolling-tape",
4156
+ render(context) {
4157
+ const { options, renderer } = context;
4158
+ const tape = options.tape;
4159
+ const windowBox = tapeWindowBox(options);
4160
+ const vertical = tape.orientation !== "horizontal";
4161
+ const group = renderer.group("rolling-tape", { "aria-hidden": "true" });
4162
+ const clipId = `${renderer.id}-rolling-tape-clip`;
4163
+ const fadeStartId = `${renderer.id}-rolling-tape-fade-start`;
4164
+ const fadeEndId = `${renderer.id}-rolling-tape-fade-end`;
4165
+ const defs = svgElement("defs");
4166
+ const clip = svgElement("clipPath", { id: clipId, clipPathUnits: "userSpaceOnUse" });
4167
+ clip.append(svgElement("rect", {
4168
+ x: windowBox.x,
4169
+ y: windowBox.y,
4170
+ width: windowBox.width,
4171
+ height: windowBox.height,
4172
+ rx: tape.cornerRadius,
4173
+ ry: tape.cornerRadius
4174
+ }));
4175
+ defs.append(clip);
4176
+ defs.append(fadeGradient(fadeStartId, vertical, true, tape.fadeColor));
4177
+ defs.append(fadeGradient(fadeEndId, vertical, false, tape.fadeColor));
4178
+ group.append(defs);
4179
+ group.append(svgElement("rect", {
4180
+ class: "gaugeit__tape-window-background",
4181
+ x: windowBox.x,
4182
+ y: windowBox.y,
4183
+ width: windowBox.width,
4184
+ height: windowBox.height,
4185
+ rx: tape.cornerRadius,
4186
+ ry: tape.cornerRadius,
4187
+ fill: tape.windowColor,
4188
+ stroke: "none"
4189
+ }));
4190
+ const strip = svgElement("g", {
4191
+ class: "gaugeit__tape-strip",
4192
+ "clip-path": `url(#${clipId})`
4193
+ });
4194
+ strip.append(svgElement("rect", {
4195
+ x: windowBox.x,
4196
+ y: windowBox.y,
4197
+ width: windowBox.width,
4198
+ height: windowBox.height,
4199
+ fill: tape.stripColor
4200
+ }));
4201
+ group.append(strip);
4202
+ const minorStep = tapeMinorStep(options);
4203
+ const minorSpacing = tapeMinorSpacing(options);
4204
+ const majorEvery = Math.max(1, Math.floor(resolveTapeInterval(options) / minorStep + 0.5));
4205
+ const visibleLength = vertical ? windowBox.height : windowBox.width;
4206
+ const slotCount = Math.max(9, Math.ceil(visibleLength / minorSpacing) + 8);
4207
+ const halfSlots = Math.ceil(slotCount / 2);
4208
+ const slots = [];
4209
+ for (let index = 0; index < slotCount; index += 1) {
4210
+ const slot = createSlot(vertical, options);
4211
+ strip.append(slot.group);
4212
+ slots.push(slot);
4213
+ }
4214
+ drawFades(group, windowBox, tape, fadeStartId, fadeEndId, vertical);
4215
+ drawFixedIndicator(group, windowBox, tape, vertical);
4216
+ drawWindowBorder(group, windowBox, tape);
4217
+ return {
4218
+ update(value, state = {}) {
4219
+ const current = Number.isFinite(state.pointerValue) ? state.pointerValue : value;
4220
+ const centerIndex = Math.floor((current - options.min) / minorStep);
4221
+ for (let slotIndex = 0; slotIndex < slots.length; slotIndex += 1) {
4222
+ const tapeIndex = centerIndex + slotIndex - halfSlots;
4223
+ const slotValue = options.min + tapeIndex * minorStep;
4224
+ const slot = slots[slotIndex];
4225
+ if (slotValue < options.min - minorStep * 0.25 || slotValue > options.max + minorStep * 0.25) {
4226
+ slot.group.setAttribute("display", "none");
4227
+ continue;
4228
+ }
4229
+ const offset = tapeValueOffset(slotValue, current, options);
4230
+ const coordinate = vertical ? { x: windowBox.x + windowBox.width / 2, y: windowBox.y + windowBox.height / 2 - offset } : { x: windowBox.x + windowBox.width / 2 + offset, y: windowBox.y + windowBox.height / 2 };
4231
+ slot.group.removeAttribute("display");
4232
+ slot.group.setAttribute("transform", `translate(${coordinate.x} ${coordinate.y})`);
4233
+ const isMajor = positiveModulo2(tapeIndex, majorEvery) === 0;
4234
+ updateSlot(slot, slotValue, isMajor, options);
4235
+ }
4236
+ }
4237
+ };
4238
+ }
4239
+ };
4240
+ function createSlot(vertical, options) {
4241
+ const group = svgElement("g", { class: "gaugeit__tape-slot" });
4242
+ const tick = svgElement("line", { class: "gaugeit__tape-tick" });
4243
+ const text = svgElement("text", {
4244
+ class: "gaugeit__label gaugeit__tape-label",
4245
+ fill: options.tape.textColor,
4246
+ "font-family": options.labels.fontFamily,
4247
+ "font-size": options.labels.fontSize,
4248
+ "font-weight": options.labels.fontWeight,
4249
+ "text-anchor": "middle",
4250
+ "dominant-baseline": "middle"
4251
+ });
4252
+ group.append(tick, text);
4253
+ group.dataset.vertical = vertical ? "true" : "false";
4254
+ return { group, tick, text, vertical };
4255
+ }
4256
+ function updateSlot(slot, value, major, options) {
4257
+ const tape = options.tape;
4258
+ const tickLength = major ? tape.majorTickLength : tape.minorTickLength;
4259
+ const zone = tape.colorByZone ? findZoneForValue(value, options.zones) : null;
4260
+ const zoneColor = zone && !isTransparentColor(zone.color) ? zone.color : null;
4261
+ const tickColor = zoneColor || (major ? tape.tickColor : tape.minorTickColor);
4262
+ const textColor = zoneColor || tape.textColor;
4263
+ const lineWidth = major ? tape.majorTickWidth : tape.minorTickWidth;
4264
+ if (slot.vertical) {
4265
+ setSvgAttributes(slot.tick, {
4266
+ x1: -tape.tickInset,
4267
+ y1: 0,
4268
+ x2: -tape.tickInset - tickLength,
4269
+ y2: 0
4270
+ });
4271
+ setSvgAttributes(slot.text, { x: tape.labelOffset, y: 0 });
4272
+ } else {
4273
+ setSvgAttributes(slot.tick, {
4274
+ x1: 0,
4275
+ y1: tape.tickInset,
4276
+ x2: 0,
4277
+ y2: tape.tickInset + tickLength
4278
+ });
4279
+ setSvgAttributes(slot.text, { x: 0, y: -tape.labelOffset });
4280
+ }
4281
+ setSvgAttributes(slot.tick, {
4282
+ stroke: tickColor,
4283
+ "stroke-width": lineWidth,
4284
+ "stroke-linecap": "butt",
4285
+ opacity: major ? 1 : tape.minorTickOpacity
4286
+ });
4287
+ slot.text.setAttribute("fill", textColor);
4288
+ slot.text.style.display = major && options.labels.visible ? "" : "none";
4289
+ slot.text.textContent = formatLabel3(value, options);
4290
+ }
4291
+ function drawFixedIndicator(group, box, tape, vertical) {
4292
+ const centerX = box.x + box.width / 2;
4293
+ const centerY = box.y + box.height / 2;
4294
+ const edgeInset = Math.max(0, tape.windowStrokeWidth / 2);
4295
+ if (vertical) {
4296
+ group.append(svgElement("line", {
4297
+ class: "gaugeit__tape-indicator",
4298
+ x1: box.x + edgeInset,
4299
+ y1: centerY,
4300
+ x2: box.x + box.width - edgeInset,
4301
+ y2: centerY,
4302
+ stroke: tape.indicatorColor,
4303
+ "stroke-width": tape.indicatorWidth,
4304
+ "stroke-linecap": "butt"
4305
+ }));
4306
+ } else {
4307
+ group.append(svgElement("line", {
4308
+ class: "gaugeit__tape-indicator",
4309
+ x1: centerX,
4310
+ y1: box.y + edgeInset,
4311
+ x2: centerX,
4312
+ y2: box.y + box.height - edgeInset,
4313
+ stroke: tape.indicatorColor,
4314
+ "stroke-width": tape.indicatorWidth,
4315
+ "stroke-linecap": "butt"
4316
+ }));
4317
+ }
4318
+ }
4319
+ function drawWindowBorder(group, box, tape) {
4320
+ const outerRadius = Math.max(0, Math.min(tape.cornerRadius, box.width / 2, box.height / 2));
4321
+ const innerInset = Math.max(tape.windowStrokeWidth, tape.windowInnerStrokeWidth, 0);
4322
+ group.append(svgElement("rect", {
4323
+ class: "gaugeit__tape-window",
4324
+ x: box.x,
4325
+ y: box.y,
4326
+ width: box.width,
4327
+ height: box.height,
4328
+ rx: outerRadius,
4329
+ ry: outerRadius,
4330
+ fill: "none",
4331
+ stroke: tape.windowStrokeColor,
4332
+ "stroke-width": tape.windowStrokeWidth
4333
+ }));
4334
+ if (tape.windowInnerStrokeWidth > 0 && box.width > innerInset * 2 && box.height > innerInset * 2) {
4335
+ group.append(svgElement("rect", {
4336
+ class: "gaugeit__tape-window-inner",
4337
+ x: box.x + innerInset,
4338
+ y: box.y + innerInset,
4339
+ width: box.width - innerInset * 2,
4340
+ height: box.height - innerInset * 2,
4341
+ rx: Math.max(0, outerRadius - innerInset),
4342
+ ry: Math.max(0, outerRadius - innerInset),
4343
+ fill: "none",
4344
+ stroke: tape.windowInnerStrokeColor,
4345
+ "stroke-width": tape.windowInnerStrokeWidth,
4346
+ "pointer-events": "none"
4347
+ }));
4348
+ }
4349
+ }
4350
+ function drawFades(group, box, tape, startId, endId, vertical) {
4351
+ if (tape.fadeSize <= 0) return;
4352
+ if (vertical) {
4353
+ group.append(
4354
+ svgElement("rect", { x: box.x, y: box.y, width: box.width, height: tape.fadeSize, fill: `url(#${startId})`, "pointer-events": "none" }),
4355
+ svgElement("rect", { x: box.x, y: box.y + box.height - tape.fadeSize, width: box.width, height: tape.fadeSize, fill: `url(#${endId})`, "pointer-events": "none" })
4356
+ );
4357
+ } else {
4358
+ group.append(
4359
+ svgElement("rect", { x: box.x, y: box.y, width: tape.fadeSize, height: box.height, fill: `url(#${startId})`, "pointer-events": "none" }),
4360
+ svgElement("rect", { x: box.x + box.width - tape.fadeSize, y: box.y, width: tape.fadeSize, height: box.height, fill: `url(#${endId})`, "pointer-events": "none" })
4361
+ );
4362
+ }
4363
+ }
4364
+ function fadeGradient(id, vertical, start, color) {
4365
+ const gradient = svgElement("linearGradient", {
4366
+ id,
4367
+ x1: "0%",
4368
+ y1: "0%",
4369
+ x2: vertical ? "0%" : "100%",
4370
+ y2: vertical ? "100%" : "0%"
4371
+ });
4372
+ const firstOpacity = start ? 0.96 : 0;
4373
+ const secondOpacity = start ? 0 : 0.96;
4374
+ gradient.append(
4375
+ svgElement("stop", { offset: "0%", "stop-color": color, "stop-opacity": firstOpacity }),
4376
+ svgElement("stop", { offset: "100%", "stop-color": color, "stop-opacity": secondOpacity })
4377
+ );
4378
+ return gradient;
4379
+ }
4380
+ function formatLabel3(value, options) {
4381
+ if (typeof options.labels.formatter === "function") {
4382
+ return String(options.labels.formatter(value, { min: options.min, max: options.max, options }));
4383
+ }
4384
+ return Number(value).toFixed(options.labels.fractionDigits);
4385
+ }
4386
+ function positiveModulo2(value, divisor) {
4387
+ return (value % divisor + divisor) % divisor;
4388
+ }
4389
+
4390
+ // src/types/rolling-tape.js
4391
+ var rollingTapeGaugeType = {
4392
+ description: "A cream-faced rolling numeric tape with a fixed edge-to-edge aircraft reference line.",
4393
+ defaults: {
4394
+ geometry: {
4395
+ mode: "tape",
4396
+ width: 320,
4397
+ height: 340,
4398
+ centerX: 160,
4399
+ centerY: 170
4400
+ },
4401
+ face: {
4402
+ visible: true,
4403
+ shape: "panel",
4404
+ fit: "geometry",
4405
+ clipContent: true,
4406
+ x: 8,
4407
+ y: 8,
4408
+ width: 304,
4409
+ height: 324,
4410
+ cornerRadius: 23,
4411
+ color: "#fffdf4",
4412
+ strokeColor: "#4b5563",
4413
+ strokeWidth: 4,
4414
+ innerStrokeColor: "#c8bfaa",
4415
+ innerStrokeWidth: 1.4,
4416
+ shadow: true,
4417
+ glass: true
4418
+ },
4419
+ pointer: { visible: false },
4420
+ indicator: { visible: false },
4421
+ track: { visible: false },
4422
+ tape: {
4423
+ orientation: "vertical",
4424
+ x: 62,
4425
+ y: 69,
4426
+ width: 196,
4427
+ height: 212,
4428
+ cornerRadius: 12,
4429
+ windowColor: "#fff9ea",
4430
+ windowStrokeColor: "#544f45",
4431
+ windowStrokeWidth: 2,
4432
+ windowInnerStrokeColor: "#d8cfbd",
4433
+ windowInnerStrokeWidth: 1,
4434
+ stripColor: "#fff9ea",
4435
+ textColor: "#211f1a",
4436
+ tickColor: "#3f3a32",
4437
+ minorTickColor: "#817867",
4438
+ minorTickOpacity: 0.86,
4439
+ majorTickWidth: 2.2,
4440
+ minorTickWidth: 1,
4441
+ majorTickLength: 26,
4442
+ minorTickLength: 13,
4443
+ tickInset: 66,
4444
+ labelOffset: 10,
4445
+ interval: 20,
4446
+ minorSubdivisions: 4,
4447
+ majorSpacing: 54,
4448
+ indicatorColor: "#991b1b",
4449
+ indicatorWidth: 2.8,
4450
+ fadeSize: 38,
4451
+ fadeColor: "#fffdf4",
4452
+ colorByZone: true
4453
+ },
4454
+ scale: {
4455
+ major: { visible: true, interval: 20, divisions: 5 },
4456
+ minor: { visible: true, subdivisions: 4 }
4457
+ },
4458
+ labels: {
4459
+ visible: true,
4460
+ color: "#211f1a",
4461
+ fontSize: 15,
4462
+ fontWeight: 760,
4463
+ fractionDigits: 0
4464
+ },
4465
+ zones: [
4466
+ { min: 0, max: 20, color: "#b91c1c" },
4467
+ { min: 20, max: 80, color: "transparent" },
4468
+ { min: 80, max: 100, color: "#15803d" }
4469
+ ],
4470
+ light: {
4471
+ visible: false,
4472
+ x: 72,
4473
+ y: 46,
4474
+ radius: 8,
4475
+ color: "amber"
4476
+ },
4477
+ readout: {
4478
+ title: {
4479
+ visible: true,
4480
+ text: "ALTITUDE",
4481
+ x: "auto",
4482
+ y: 42,
4483
+ color: "#3f3a32",
4484
+ fontSize: 13,
4485
+ letterSpacing: 1.8
4486
+ },
4487
+ value: {
4488
+ visible: false,
4489
+ x: "auto",
4490
+ y: 303,
4491
+ color: "#211f1a",
4492
+ fontSize: 24,
4493
+ fontWeight: 760
4494
+ },
4495
+ unit: {
4496
+ visible: true,
4497
+ text: "FT",
4498
+ x: "auto",
4499
+ y: 307,
4500
+ color: "#6b6253",
4501
+ fontSize: 10,
4502
+ letterSpacing: 1.2
4503
+ }
4504
+ }
4505
+ },
4506
+ configure: configureRollingTapeSize,
4507
+ layers: [faceLayer, rollingTapeLayer, lightLayer, readoutLayer]
4508
+ };
4509
+
4510
+ // src/types/register-builtins.js
4511
+ function registerBuiltInTypes(registry) {
4512
+ registry.register("arc", arcGaugeType).register("classic", classicGaugeType).register("classic-linear", classicLinearGaugeType).register("classic-linear-zero", classicLinearZeroGaugeType).register("center-zero", centerZeroGaugeType).register("heritage-round", heritageRoundGaugeType).register("rolling-tape", rollingTapeGaugeType).register("line-scale", lineScaleGaugeType);
4513
+ return registry;
4514
+ }
4515
+ var builtInTypes = Object.freeze({
4516
+ arc: arcGaugeType,
4517
+ classic: classicGaugeType,
4518
+ "classic-linear": classicLinearGaugeType,
4519
+ "classic-linear-zero": classicLinearZeroGaugeType,
4520
+ "center-zero": centerZeroGaugeType,
4521
+ "heritage-round": heritageRoundGaugeType,
4522
+ "rolling-tape": rollingTapeGaugeType,
4523
+ "line-scale": lineScaleGaugeType
4524
+ });
4525
+
4526
+ // src/core/auto-mount.js
4527
+ var mountedInstances = /* @__PURE__ */ new WeakMap();
4528
+ function createManagedGauge(target, options, GaugeClass, registry) {
4529
+ const existing = mountedInstances.get(target);
4530
+ if (existing) existing.destroy();
4531
+ const gauge = new GaugeClass(target, options, registry, managedLifecycle(target));
4532
+ mountedInstances.set(target, gauge);
4533
+ return gauge;
4534
+ }
4535
+ function autoMountElements(selector, GaugeClass, registry, root = document) {
4536
+ const elements = [...root.querySelectorAll(selector || "[data-gaugeit]")];
4537
+ return elements.map((element) => {
4538
+ const existing = mountedInstances.get(element);
4539
+ if (existing) return existing;
4540
+ const options = optionsFromDataset(element);
4541
+ const gauge = new GaugeClass(element, options, registry, managedLifecycle(element));
4542
+ mountedInstances.set(element, gauge);
4543
+ return gauge;
4544
+ });
4545
+ }
4546
+ function getMountedGauge(element) {
4547
+ return mountedInstances.get(element) || null;
4548
+ }
4549
+ function unmountGauge(element) {
4550
+ const gauge = mountedInstances.get(element);
4551
+ if (!gauge) return false;
4552
+ gauge.destroy();
4553
+ mountedInstances.delete(element);
4554
+ return true;
4555
+ }
4556
+ function optionsFromDataset(element) {
4557
+ const options = parseJsonObject(element.dataset.gaugeitOptions, {});
4558
+ if (element.dataset.gaugeitType) options.type = element.dataset.gaugeitType;
4559
+ if (element.dataset.gaugeitValue !== void 0) options.value = Number(element.dataset.gaugeitValue);
4560
+ if (element.dataset.gaugeitMin !== void 0) options.min = Number(element.dataset.gaugeitMin);
4561
+ if (element.dataset.gaugeitMax !== void 0) options.max = Number(element.dataset.gaugeitMax);
4562
+ return options;
4563
+ }
4564
+ function managedLifecycle(element) {
4565
+ return {
4566
+ onDestroy(gauge) {
4567
+ if (mountedInstances.get(element) === gauge) mountedInstances.delete(element);
4568
+ }
4569
+ };
4570
+ }
4571
+
4572
+ // src/web-component.js
4573
+ function defineCustomElement(createGauge2, tagName = "gauge-it") {
4574
+ if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") return null;
4575
+ if (customElements.get(tagName)) return customElements.get(tagName);
4576
+ class GaugeitElement extends HTMLElement {
4577
+ static get observedAttributes() {
4578
+ return ["value", "min", "max", "type", "options"];
4579
+ }
4580
+ constructor() {
4581
+ super();
4582
+ this._gauge = null;
4583
+ this._mount = document.createElement("div");
4584
+ this._mount.className = "gaugeit-element__mount";
4585
+ }
4586
+ connectedCallback() {
4587
+ if (!this._mount.isConnected) this.append(this._mount);
4588
+ if (!this._gauge) this._gauge = createGauge2(this._mount, this._readOptions());
4589
+ }
4590
+ disconnectedCallback() {
4591
+ if (this._gauge) {
4592
+ this._gauge.destroy();
4593
+ this._gauge = null;
4594
+ }
4595
+ }
4596
+ attributeChangedCallback() {
4597
+ if (this._gauge) this._gauge.replaceOptions(this._readOptions());
4598
+ }
4599
+ get value() {
4600
+ return this._gauge ? this._gauge.getValue() : Number(this.getAttribute("value") || 0);
4601
+ }
4602
+ set value(nextValue) {
4603
+ this.setAttribute("value", String(nextValue));
4604
+ }
4605
+ get gauge() {
4606
+ return this._gauge;
4607
+ }
4608
+ _readOptions() {
4609
+ const options = parseJsonObject(this.getAttribute("options"), {});
4610
+ if (this.hasAttribute("type")) options.type = this.getAttribute("type");
4611
+ if (this.hasAttribute("value")) options.value = Number(this.getAttribute("value"));
4612
+ if (this.hasAttribute("min")) options.min = Number(this.getAttribute("min"));
4613
+ if (this.hasAttribute("max")) options.max = Number(this.getAttribute("max"));
4614
+ return options;
4615
+ }
4616
+ }
4617
+ customElements.define(tagName, GaugeitElement);
4618
+ return GaugeitElement;
4619
+ }
4620
+
4621
+ // src/index.js
4622
+ registerBuiltInTypes(gaugeTypeRegistry);
4623
+ function createGauge(target, options = {}) {
4624
+ const element = typeof target === "string" ? document.querySelector(target) : target;
4625
+ if (!element) throw new Error(`Gaugeit target not found: ${target}`);
4626
+ return createManagedGauge(element, options, Gauge, gaugeTypeRegistry);
4627
+ }
4628
+ function registerGaugeType(name, definition) {
4629
+ gaugeTypeRegistry.register(name, definition);
4630
+ return Gaugeit;
4631
+ }
4632
+ function autoMount(selector = "[data-gaugeit]", root = document) {
4633
+ return autoMountElements(selector, Gauge, gaugeTypeRegistry, root);
4634
+ }
4635
+ function defineGaugeitElement(tagName = "gauge-it") {
4636
+ return defineCustomElement(createGauge, tagName);
4637
+ }
4638
+ var layers = Object.freeze({
4639
+ face: faceLayer,
4640
+ zones: zonesLayer,
4641
+ ticks: ticksLayer,
4642
+ labels: labelsLayer,
4643
+ pointer: pointerLayer,
4644
+ readout: readoutLayer,
4645
+ linearZones: linearZonesLayer,
4646
+ linearTicks: linearTicksLayer,
4647
+ linearLabels: linearLabelsLayer,
4648
+ linearIndicator: linearIndicatorLayer,
4649
+ zeroMarker: zeroMarkerLayer,
4650
+ rollingTape: rollingTapeLayer,
4651
+ light: lightLayer
4652
+ });
4653
+ var geometry = Object.freeze({
4654
+ angleToValue,
4655
+ arcPath,
4656
+ calculateGaugeBounds,
4657
+ buildMinorValues,
4658
+ buildScaleValues,
4659
+ centeredArcAngles,
4660
+ findZoneForValue,
4661
+ gaugeSweep,
4662
+ isClosedGauge,
4663
+ linearAxis,
4664
+ linearIndicatorNormalOffset,
4665
+ linearNormalOffset,
4666
+ linearNormalSpan,
4667
+ linearYToSvg,
4668
+ linearPointToValue,
4669
+ niceInterval,
4670
+ offsetLinearPoint,
4671
+ padBounds,
4672
+ polarPoint,
4673
+ resolveFaceBox,
4674
+ resolveReadoutPosition,
4675
+ resolveScaleEndpointValues,
4676
+ resolveZoneSegments,
4677
+ taperedArcPath,
4678
+ unionBounds,
4679
+ valueToAngle,
4680
+ valueToLinearPoint,
4681
+ valueToLinearRatio,
4682
+ resolveTapeInterval,
4683
+ tapeMinorSpacing,
4684
+ tapeMinorStep,
4685
+ tapeValueOffset,
4686
+ tapeWindowBox,
4687
+ lightIsActive,
4688
+ resolveLightPosition
4689
+ });
4690
+ var types = builtInTypes;
4691
+ var VERSION = "0.1.0";
4692
+ var Gaugeit = Object.freeze({
4693
+ VERSION,
4694
+ Gauge,
4695
+ GaugeTypeRegistry,
4696
+ createGauge,
4697
+ registerGaugeType,
4698
+ autoMount,
4699
+ defineGaugeitElement,
4700
+ getMountedGauge,
4701
+ unmountGauge,
4702
+ registry: gaugeTypeRegistry,
4703
+ types,
4704
+ layers,
4705
+ geometry,
4706
+ deepMerge,
4707
+ easingNames
4708
+ });
4709
+ var index_default = Gaugeit;
4710
+ return __toCommonJS(index_exports);
4711
+ })();
4712
+ return Gaugeit;
4713
+ });