gaugeit.js 0.1.0 → 0.1.1

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.
@@ -0,0 +1,5097 @@
1
+ # Gaugeit.js 0.1.1 complete AI context
2
+
3
+ This file is the single-file context bundle for an AI that cannot navigate several
4
+ repository documents. It combines the detailed Gaugeit.js AI usage, option,
5
+ integration, contributor, and troubleshooting guides.
6
+
7
+ Do not auto-load this file when root `AGENTS.md` and one focused guide are sufficient.
8
+ It is intentionally large. The canonical implementation remains in `src/`, canonical
9
+ core declarations in `src/index.d.ts`, React adapter declarations in
10
+ `adapters/react/Gaugeit.d.ts`, and executable behavior in `tests/`.
11
+
12
+ Package identity:
13
+
14
+ ```text
15
+ npm: gaugeit.js
16
+ Composer: kasperi/gaugeit-js
17
+ browser global: Gaugeit
18
+ React component: Gaugeit
19
+ version described: 0.1.1
20
+ repository: https://github.com/kasperikoski/gaugeit.js
21
+ ```
22
+
23
+ Contents:
24
+
25
+ 1. Library usage
26
+ 2. Complete option reference
27
+ 3. Integration recipes
28
+ 4. Contributor architecture and workflow
29
+ 5. Troubleshooting
30
+
31
+ ---
32
+
33
+ # Part I — Library usage
34
+
35
+ This guide is written for an AI that must generate working Gaugeit.js 0.1.1
36
+ integration code without guessing. It focuses on selecting the correct distribution,
37
+ creating and updating gauges, integrating lifecycle ownership, and avoiding the most
38
+ common package and browser mistakes.
39
+
40
+ For individual option fields, read `options-reference.md`. For complete copy-ready
41
+ files, read `integration-recipes.md`.
42
+
43
+ ## Fast decision tree
44
+
45
+ Use this decision order:
46
+
47
+ 1. **React application?**
48
+ - Import the component from `gaugeit.js/react`.
49
+ - Import `gaugeit.js/css` once.
50
+ - Keep frequently recreated option objects stable with `useMemo`.
51
+ - Use a ref when imperative instance methods are needed.
52
+
53
+ 2. **Bundler or native ESM application?**
54
+ - Import from `gaugeit.js`.
55
+ - Import `gaugeit.js/css`.
56
+ - In a browser without a package resolver, import `dist/gaugeit.esm.js` by URL and
57
+ include `dist/gaugeit.css`.
58
+
59
+ 3. **CommonJS application?**
60
+ - `require('gaugeit.js')`.
61
+ - Ensure the application also loads or bundles the CSS.
62
+
63
+ 4. **Plain HTML with two cacheable files?**
64
+ - Include `gaugeit.min.css`.
65
+ - Include `gaugeit.umd.min.js`.
66
+ - Use the global `Gaugeit`.
67
+
68
+ 5. **Plain HTML that must use one JavaScript file?**
69
+ - Include `gaugeit.standalone.min.js`.
70
+ - Do not add separate Gaugeit CSS unless intentionally overriding it.
71
+ - Use the global `Gaugeit`.
72
+
73
+ 6. **PHP or Composer-managed server-rendered application?**
74
+ - Install `kasperi/gaugeit-js`.
75
+ - Run `vendor/bin/gaugeit-install-assets`.
76
+ - Include the copied CSS and UMD bundle from a public path.
77
+ - Render normal host elements and initialize in browser JavaScript.
78
+
79
+ 7. **Declarative custom element?**
80
+ - Load a normal Gaugeit distribution.
81
+ - Call `Gaugeit.defineGaugeitElement()` or import
82
+ `defineGaugeitElement` from `gaugeit.js/web-component`.
83
+ - Use `<gauge-it>` attributes.
84
+
85
+ 8. **Declarative data attributes?**
86
+ - Add `data-gaugeit`.
87
+ - Load a normal Gaugeit distribution.
88
+ - Call `Gaugeit.autoMount()` after the elements exist.
89
+
90
+ ## Package and distribution matrix
91
+
92
+ | Environment | JavaScript | CSS | API name |
93
+ | --- | --- | --- | --- |
94
+ | npm ESM | `gaugeit.js` | `gaugeit.js/css` | named exports or default `Gaugeit` |
95
+ | npm CommonJS | `require('gaugeit.js')` | load separately | returned exports |
96
+ | npm React | `gaugeit.js/react` | `gaugeit.js/css` | component `Gaugeit` |
97
+ | npm custom element | `gaugeit.js/web-component` | `gaugeit.js/css` | `defineGaugeitElement` |
98
+ | Browser UMD | `dist/gaugeit.umd.min.js` | `dist/gaugeit.min.css` | `window.Gaugeit` |
99
+ | Browser standalone | `dist/gaugeit.standalone.min.js` | embedded | `window.Gaugeit` |
100
+ | Browser ESM | `dist/gaugeit.esm.js` | `dist/gaugeit.css` | module exports |
101
+ | Composer | copied UMD and CSS | copied CSS | `window.Gaugeit` |
102
+
103
+ The UMD build does **not** inject CSS. The standalone build injects canonical CSS once
104
+ using the stable `#gaugeit-styles` marker.
105
+
106
+ ## Installation
107
+
108
+ ### npm
109
+
110
+ ```bash
111
+ npm install gaugeit.js@0.1.1
112
+ ```
113
+
114
+ Recommended ESM usage:
115
+
116
+ ```js
117
+ import { createGauge } from 'gaugeit.js';
118
+ import 'gaugeit.js/css';
119
+
120
+ const gauge = createGauge('#speed', {
121
+ type: 'arc',
122
+ min: 0,
123
+ max: 240,
124
+ value: 88
125
+ });
126
+ ```
127
+
128
+ Default-object usage:
129
+
130
+ ```js
131
+ import Gaugeit from 'gaugeit.js';
132
+ import 'gaugeit.js/css';
133
+
134
+ const gauge = Gaugeit.createGauge('#speed', {
135
+ type: 'classic',
136
+ value: 42
137
+ });
138
+ ```
139
+
140
+ ### CommonJS
141
+
142
+ ```js
143
+ const { createGauge } = require('gaugeit.js');
144
+
145
+ const gauge = createGauge(document.querySelector('#speed'), {
146
+ type: 'line-scale',
147
+ value: 42
148
+ });
149
+ ```
150
+
151
+ CommonJS resolves the JavaScript entry. CSS still belongs to the consuming application's
152
+ asset pipeline or page.
153
+
154
+ ### Native browser ESM
155
+
156
+ ```html
157
+ <link rel="stylesheet" href="./dist/gaugeit.css">
158
+ <div id="temperature"></div>
159
+
160
+ <script type="module">
161
+ import { createGauge } from './dist/gaugeit.esm.js';
162
+
163
+ createGauge('#temperature', {
164
+ type: 'classic',
165
+ min: -20,
166
+ max: 120,
167
+ value: 68
168
+ });
169
+ </script>
170
+ ```
171
+
172
+ A native browser cannot resolve the bare package specifier `gaugeit.js` unless an
173
+ import map, bundler, or server-side package resolver is present.
174
+
175
+ ### UMD with separate CSS
176
+
177
+ ```html
178
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
179
+
180
+ <div id="score"></div>
181
+
182
+ <script src="./dist/gaugeit.umd.min.js"></script>
183
+ <script>
184
+ const gauge = Gaugeit.createGauge('#score', {
185
+ type: 'arc',
186
+ min: 0,
187
+ max: 100,
188
+ value: 64
189
+ });
190
+ </script>
191
+ ```
192
+
193
+ The global name is exactly `Gaugeit`. It is not `GaugeIt`, `GaugeIT`, or `gaugeit`.
194
+
195
+ ### Standalone one-file browser build
196
+
197
+ ```html
198
+ <div id="score"></div>
199
+
200
+ <script src="./dist/gaugeit.standalone.min.js"></script>
201
+ <script>
202
+ Gaugeit.createGauge('#score', {
203
+ type: 'classic',
204
+ value: 64
205
+ });
206
+ </script>
207
+ ```
208
+
209
+ Standalone is appropriate when one JavaScript file is more important than separately
210
+ cacheable CSS. It contains the same public API as UMD.
211
+
212
+ ### jsDelivr
213
+
214
+ Use exact versions in production:
215
+
216
+ ```html
217
+ <link
218
+ rel="stylesheet"
219
+ href="https://cdn.jsdelivr.net/npm/gaugeit.js@0.1.1/dist/gaugeit.min.css"
220
+ >
221
+ <script
222
+ src="https://cdn.jsdelivr.net/npm/gaugeit.js@0.1.1/dist/gaugeit.umd.min.js"
223
+ ></script>
224
+ ```
225
+
226
+ One-file form:
227
+
228
+ ```html
229
+ <script
230
+ src="https://cdn.jsdelivr.net/npm/gaugeit.js@0.1.1/dist/gaugeit.standalone.min.js"
231
+ ></script>
232
+ ```
233
+
234
+ ### UNPKG
235
+
236
+ ```html
237
+ <link
238
+ rel="stylesheet"
239
+ href="https://unpkg.com/gaugeit.js@0.1.1/dist/gaugeit.min.css"
240
+ >
241
+ <script
242
+ src="https://unpkg.com/gaugeit.js@0.1.1/dist/gaugeit.umd.min.js"
243
+ ></script>
244
+ ```
245
+
246
+ One-file form:
247
+
248
+ ```html
249
+ <script
250
+ src="https://unpkg.com/gaugeit.js@0.1.1/dist/gaugeit.standalone.min.js"
251
+ ></script>
252
+ ```
253
+
254
+ Do not omit the version in production examples. An unpinned CDN URL may change after a
255
+ new release.
256
+
257
+ ### Composer
258
+
259
+ ```bash
260
+ composer require kasperi/gaugeit-js:^0.1
261
+ vendor/bin/gaugeit-install-assets public/assets/vendor/gaugeit
262
+ ```
263
+
264
+ Default installed assets:
265
+
266
+ ```text
267
+ gaugeit.umd.min.js
268
+ gaugeit.min.css
269
+ ```
270
+
271
+ Other modes:
272
+
273
+ ```bash
274
+ vendor/bin/gaugeit-install-assets --standalone public/assets/vendor/gaugeit
275
+ vendor/bin/gaugeit-install-assets --all public/assets/vendor/gaugeit
276
+ vendor/bin/gaugeit-install-assets --clean public/assets/vendor/gaugeit
277
+ ```
278
+
279
+ `--clean` removes only known Gaugeit.js assets from the destination. It preserves
280
+ unrelated files. Composer consumers use prebuilt assets and do not need Node.js.
281
+
282
+ Gaugeit.js does not write into an application's public directory automatically during
283
+ dependency installation. The consuming project owns the explicit asset-copy command.
284
+
285
+ ## Built-in types
286
+
287
+ Use one of these exact names:
288
+
289
+ | Type | Geometry family | Intended use |
290
+ | --- | --- | --- |
291
+ | `arc` | radial | lightweight semicircular zones and pointer |
292
+ | `classic` | radial | framed vintage analog instrument |
293
+ | `heritage-round` | radial | circular glass-fronted 270-degree instrument |
294
+ | `line-scale` | radial | dense configurable tick-focused scale |
295
+ | `classic-linear` | linear | framed straight scale with moving indicator |
296
+ | `classic-linear-zero` | linear | signed straight scale with fixed zero marker |
297
+ | `center-zero` | radial | signed heritage dial around a center reference |
298
+ | `rolling-tape` | tape | moving numeric strip behind a fixed reference line |
299
+
300
+ Do not infer geometry solely from the visual name. The selected type supplies the
301
+ correct geometry mode and layer family.
302
+
303
+ ## Minimal creation
304
+
305
+ The target may be an element or a CSS selector:
306
+
307
+ ```js
308
+ const gauge = Gaugeit.createGauge('#gauge', {
309
+ type: 'arc',
310
+ value: 72
311
+ });
312
+ ```
313
+
314
+ Equivalent element form:
315
+
316
+ ```js
317
+ const host = document.querySelector('#gauge');
318
+ const gauge = Gaugeit.createGauge(host, {
319
+ type: 'arc',
320
+ value: 72
321
+ });
322
+ ```
323
+
324
+ The target must exist before creation. Creating another managed gauge on the same host
325
+ destroys the previous managed instance.
326
+
327
+ ## A practical configuration pattern
328
+
329
+ ```js
330
+ const gauge = Gaugeit.createGauge('#engine-load', {
331
+ type: 'arc',
332
+ min: 0,
333
+ max: 100,
334
+ value: 38,
335
+ zones: [
336
+ { min: 0, max: 60, color: '#22c55e' },
337
+ { min: 60, max: 85, color: '#facc15' },
338
+ { min: 85, max: 100, color: '#ef4444' }
339
+ ],
340
+ readout: {
341
+ title: {
342
+ visible: true,
343
+ text: 'ENGINE LOAD'
344
+ },
345
+ value: {
346
+ visible: true,
347
+ suffix: '%'
348
+ }
349
+ },
350
+ accessibility: {
351
+ label: 'Engine load',
352
+ valueText: (value) => `${Math.round(value)} percent`
353
+ }
354
+ });
355
+ ```
356
+
357
+ Prefer normal option objects over DOM manipulation of generated SVG.
358
+
359
+ ## Updating values
360
+
361
+ Use `setValue()` for ordinary numeric updates:
362
+
363
+ ```js
364
+ gauge.setValue(76);
365
+ gauge.setValue(18, {
366
+ duration: 1400,
367
+ easing: 'easeInOutCubic'
368
+ });
369
+ gauge.setValue(50, {
370
+ animate: false
371
+ });
372
+ ```
373
+
374
+ `set()` is an alias:
375
+
376
+ ```js
377
+ gauge.set(64);
378
+ ```
379
+
380
+ Do not call `updateOptions({ value })` for a high-frequency value stream unless the
381
+ configuration must also be rebuilt. `setValue()` updates dynamic controllers without
382
+ re-rendering static layers.
383
+
384
+ ## Updating options
385
+
386
+ Deep-merge a partial patch:
387
+
388
+ ```js
389
+ gauge.updateOptions({
390
+ pointer: {
391
+ type: 'line',
392
+ color: '#dc2626'
393
+ },
394
+ labels: {
395
+ fractionDigits: 1
396
+ }
397
+ });
398
+ ```
399
+
400
+ `setOptions()` is an alias for `updateOptions()`.
401
+
402
+ Replace all earlier instance overrides:
403
+
404
+ ```js
405
+ gauge.replaceOptions({
406
+ type: 'classic',
407
+ min: 0,
408
+ max: 100,
409
+ value: 42
410
+ });
411
+ ```
412
+
413
+ Arrays are replaced, not concatenated. Therefore:
414
+
415
+ ```js
416
+ gauge.updateOptions({
417
+ zones: [
418
+ { min: 0, max: 100, color: '#22c55e' }
419
+ ]
420
+ });
421
+ ```
422
+
423
+ replaces the previous complete zone list.
424
+
425
+ When `updateOptions()` or `replaceOptions()` includes `value`, the value is applied
426
+ immediately by default. Pass `{ animateToValue: true }` as the second argument to
427
+ animate after the rebuild:
428
+
429
+ ```js
430
+ gauge.updateOptions(
431
+ {
432
+ value: 90,
433
+ pointer: { color: '#dc2626' }
434
+ },
435
+ {
436
+ animateToValue: true
437
+ }
438
+ );
439
+ ```
440
+
441
+ ## Reading values
442
+
443
+ ```js
444
+ const requested = gauge.getValue();
445
+ const displayed = gauge.getDisplayedValue();
446
+ ```
447
+
448
+ - `getValue()` returns the stable requested target.
449
+ - `getDisplayedValue()` returns the current animated base value.
450
+ - Neither method returns the jittered pointer-only visual value.
451
+
452
+ ## Lifecycle
453
+
454
+ ```js
455
+ gauge.pause();
456
+ gauge.resume();
457
+ gauge.refresh();
458
+ gauge.destroy();
459
+ ```
460
+
461
+ - `pause()` stops active animation and jitter work.
462
+ - `resume()` continues pending visual work without counting paused time.
463
+ - `refresh()` rebuilds the gauge from current options. Use it after relevant fonts,
464
+ CSS variables, or external plugin state changes.
465
+ - `destroy()` releases animation frames, observers, listeners, controller resources,
466
+ and generated content.
467
+
468
+ Do not call instance methods after `destroy()`.
469
+
470
+ ## Overflow behavior
471
+
472
+ ```js
473
+ overflow: 'clamp'
474
+ ```
475
+
476
+ - `clamp`: incoming values are constrained to `min ... max`.
477
+ - `expand`: an incoming out-of-range value expands the configured range and re-renders.
478
+ - `allow`: stores and renders out-of-range values without changing the configured
479
+ range; display clamping is disabled.
480
+
481
+ Use `clamp` unless the application explicitly needs one of the other contracts.
482
+
483
+ ## Inversion
484
+
485
+ ```js
486
+ invert: true
487
+ ```
488
+
489
+ Inversion reverses visual direction without changing the numeric definitions. Zones
490
+ remain attached to their numeric values. Do not reverse zone arrays or swap their
491
+ numeric endpoints to implement inversion.
492
+
493
+ ## Responsive layout
494
+
495
+ Default layout is intrinsic and responsive:
496
+
497
+ ```js
498
+ layout: {
499
+ mode: 'intrinsic',
500
+ padding: 8,
501
+ includeGeometry: true
502
+ },
503
+ responsive: true
504
+ ```
505
+
506
+ The host should have an available width. Gaugeit.js applies responsive SVG sizing and
507
+ calculates a content-aware viewBox.
508
+
509
+ For a fixed-size card area, constrain the host with CSS and use contain mode:
510
+
511
+ ```css
512
+ .dashboard-gauge {
513
+ width: 320px;
514
+ height: 220px;
515
+ }
516
+ ```
517
+
518
+ ```js
519
+ Gaugeit.createGauge('.dashboard-gauge', {
520
+ type: 'heritage-round',
521
+ layout: {
522
+ mode: 'contain'
523
+ }
524
+ });
525
+ ```
526
+
527
+ Do not set only a zero or collapsed parent size and expect SVG to invent page layout.
528
+
529
+ ## Readouts
530
+
531
+ The readout group is enabled by default, but individual parts are not all visible:
532
+
533
+ ```js
534
+ readout: {
535
+ visible: true,
536
+ title: {
537
+ visible: true,
538
+ text: 'PRESSURE'
539
+ },
540
+ value: {
541
+ visible: true,
542
+ fractionDigits: 1
543
+ },
544
+ unit: {
545
+ visible: true,
546
+ text: 'bar'
547
+ }
548
+ }
549
+ ```
550
+
551
+ `readout.visible: true` alone does not enable `title`, `value`, or `unit`.
552
+
553
+ ## Lights
554
+
555
+ Every built-in type includes the shared light layer. A partial object inherits shared
556
+ defaults:
557
+
558
+ ```js
559
+ light: {
560
+ visible: true
561
+ }
562
+ ```
563
+
564
+ A threshold light:
565
+
566
+ ```js
567
+ light: {
568
+ visible: true,
569
+ color: 'red',
570
+ trigger: {
571
+ mode: 'above',
572
+ source: 'target',
573
+ value: 85
574
+ }
575
+ }
576
+ ```
577
+
578
+ Trigger sources:
579
+
580
+ - `target`: stable requested value; default and usually best for warnings;
581
+ - `displayed`: current animated base value;
582
+ - `pointer`: animated value plus visual jitter.
583
+
584
+ Effect interaction:
585
+
586
+ - blink takes precedence over pulse;
587
+ - flicker may overlay blink or pulse;
588
+ - pulse and flicker run only while the light is active;
589
+ - reduced-motion preference suppresses the motion effects;
590
+ - `glowRadius` is independent from lamp `radius` in 0.1.1;
591
+ - `glowOpacity` is independent from illuminated-lens `opacity`.
592
+
593
+ For a deterministic steady light, disable all motion:
594
+
595
+ ```js
596
+ light: {
597
+ visible: true,
598
+ glow: true,
599
+ pulse: false,
600
+ blink: false,
601
+ flicker: false,
602
+ trigger: {
603
+ mode: 'manual'
604
+ },
605
+ on: true
606
+ }
607
+ ```
608
+
609
+ ## Data-attribute mounting
610
+
611
+ ```html
612
+ <div
613
+ data-gaugeit
614
+ data-gaugeit-type="arc"
615
+ data-gaugeit-min="0"
616
+ data-gaugeit-max="100"
617
+ data-gaugeit-value="63"
618
+ data-gaugeit-options='{"animation":{"startFrom":"value"}}'
619
+ ></div>
620
+
621
+ <script src="./dist/gaugeit.umd.min.js"></script>
622
+ <script>
623
+ Gaugeit.autoMount();
624
+ </script>
625
+ ```
626
+
627
+ Supported dataset fields are translated into normal options. The JSON in
628
+ `data-gaugeit-options` must be valid JSON, not JavaScript object syntax. Auto-mounting
629
+ is duplicate-safe.
630
+
631
+ A custom selector and root are supported:
632
+
633
+ ```js
634
+ Gaugeit.autoMount('[data-dashboard-gauge]', document.querySelector('#dashboard'));
635
+ ```
636
+
637
+ Managed-instance helpers:
638
+
639
+ ```js
640
+ const host = document.querySelector('#gauge');
641
+ const instance = Gaugeit.getMountedGauge(host);
642
+ const existed = Gaugeit.unmountGauge(host);
643
+ ```
644
+
645
+ ## Custom element
646
+
647
+ ```html
648
+ <script src="./dist/gaugeit.umd.min.js"></script>
649
+ <script>
650
+ Gaugeit.defineGaugeitElement();
651
+ </script>
652
+
653
+ <gauge-it
654
+ type="classic"
655
+ min="0"
656
+ max="240"
657
+ value="88"
658
+ options='{"readout":{"title":{"visible":true,"text":"VOLTAGE"}}}'
659
+ ></gauge-it>
660
+ ```
661
+
662
+ Observed attributes are `value`, `min`, `max`, `type`, and `options`. Attribute changes
663
+ replace the complete declarative option object. The element destroys its instance when
664
+ disconnected.
665
+
666
+ With npm:
667
+
668
+ ```js
669
+ import { defineGaugeitElement } from 'gaugeit.js/web-component';
670
+ import 'gaugeit.js/css';
671
+
672
+ defineGaugeitElement();
673
+ ```
674
+
675
+ ## React
676
+
677
+ ```jsx
678
+ import { useMemo, useState } from 'react';
679
+ import Gaugeit from 'gaugeit.js/react';
680
+ import 'gaugeit.js/css';
681
+
682
+ export default function LoadGauge() {
683
+ const [value, setValue] = useState(62);
684
+
685
+ const options = useMemo(() => ({
686
+ type: 'arc',
687
+ min: 0,
688
+ max: 100,
689
+ zones: [
690
+ { min: 0, max: 60, color: '#22c55e' },
691
+ { min: 60, max: 85, color: '#facc15' },
692
+ { min: 85, max: 100, color: '#ef4444' }
693
+ ]
694
+ }), []);
695
+
696
+ return (
697
+ <Gaugeit
698
+ value={value}
699
+ options={options}
700
+ className="load-gauge"
701
+ />
702
+ );
703
+ }
704
+ ```
705
+
706
+ React owns the host element. Gaugeit.js owns only the generated content inside it. The
707
+ adapter:
708
+
709
+ - creates an instance on mount;
710
+ - replaces the complete options object when `options` identity changes;
711
+ - keeps explicit `value` authoritative;
712
+ - forwards a ref to the live `Gauge` instance;
713
+ - destroys the instance on unmount.
714
+
715
+ Do not import the source adapter by repository-relative path from an installed package.
716
+ Use `gaugeit.js/react`.
717
+
718
+ ## Laravel Blade
719
+
720
+ After Composer asset installation, a Blade page may include:
721
+
722
+ ```blade
723
+ @push('styles')
724
+ <link
725
+ rel="stylesheet"
726
+ href="{{ asset('assets/vendor/gaugeit/gaugeit.min.css') }}"
727
+ >
728
+ @endpush
729
+
730
+ <div
731
+ id="pressure-gauge"
732
+ data-gauge-options='@json([
733
+ "type" => "classic",
734
+ "min" => 0,
735
+ "max" => 10,
736
+ "value" => $pressure,
737
+ ])'
738
+ ></div>
739
+
740
+ @push('scripts')
741
+ <script src="{{ asset('assets/vendor/gaugeit/gaugeit.umd.min.js') }}"></script>
742
+ <script>
743
+ document.addEventListener('DOMContentLoaded', () => {
744
+ const host = document.querySelector('#pressure-gauge');
745
+ const options = JSON.parse(host.dataset.gaugeOptions);
746
+ Gaugeit.createGauge(host, options);
747
+ });
748
+ </script>
749
+ @endpush
750
+ ```
751
+
752
+ The PHP layer emits data. Browser JavaScript creates the SVG gauge.
753
+
754
+ ## Twig
755
+
756
+ ```twig
757
+ <link
758
+ rel="stylesheet"
759
+ href="{{ asset('assets/vendor/gaugeit/gaugeit.min.css') }}"
760
+ >
761
+
762
+ <div
763
+ id="temperature-gauge"
764
+ data-options="{{ {
765
+ type: 'classic',
766
+ min: -20,
767
+ max: 120,
768
+ value: temperature
769
+ }|json_encode|e('html_attr') }}"
770
+ ></div>
771
+
772
+ <script src="{{ asset('assets/vendor/gaugeit/gaugeit.umd.min.js') }}"></script>
773
+ <script>
774
+ const host = document.querySelector('#temperature-gauge');
775
+ Gaugeit.createGauge(host, JSON.parse(host.dataset.options));
776
+ </script>
777
+ ```
778
+
779
+ Escape JSON for an HTML attribute. Do not place unescaped application data directly
780
+ inside executable JavaScript.
781
+
782
+ ## Plain PHP
783
+
784
+ ```php
785
+ <?php
786
+ $options = [
787
+ 'type' => 'arc',
788
+ 'min' => 0,
789
+ 'max' => 100,
790
+ 'value' => $value,
791
+ ];
792
+ ?>
793
+
794
+ <link rel="stylesheet" href="/assets/vendor/gaugeit/gaugeit.min.css">
795
+
796
+ <div
797
+ id="status-gauge"
798
+ data-options="<?= htmlspecialchars(
799
+ json_encode($options, JSON_THROW_ON_ERROR),
800
+ ENT_QUOTES,
801
+ 'UTF-8'
802
+ ) ?>"
803
+ ></div>
804
+
805
+ <script src="/assets/vendor/gaugeit/gaugeit.umd.min.js"></script>
806
+ <script>
807
+ const host = document.querySelector('#status-gauge');
808
+ Gaugeit.createGauge(host, JSON.parse(host.dataset.options));
809
+ </script>
810
+ ```
811
+
812
+ ## DOM events
813
+
814
+ Events are dispatched from the host element:
815
+
816
+ ```js
817
+ const host = document.querySelector('#gauge');
818
+
819
+ host.addEventListener('gaugeit:change', (event) => {
820
+ console.log(event.detail.value);
821
+ });
822
+
823
+ host.addEventListener('gaugeit:settled', (event) => {
824
+ console.log(event.detail.displayedValue);
825
+ });
826
+ ```
827
+
828
+ Available event names:
829
+
830
+ ```text
831
+ gaugeit:render
832
+ gaugeit:change
833
+ gaugeit:displaychange
834
+ gaugeit:animationstart
835
+ gaugeit:animationend
836
+ gaugeit:settled
837
+ gaugeit:typechange
838
+ gaugeit:pause
839
+ gaugeit:resume
840
+ gaugeit:destroy
841
+ ```
842
+
843
+ Use namespaced events instead of global callbacks.
844
+
845
+ ## Custom types
846
+
847
+ Register a type before creating instances that use it:
848
+
849
+ ```js
850
+ Gaugeit.registerGaugeType('compact-status', {
851
+ description: 'Compact dashboard status gauge.',
852
+ defaults: {
853
+ geometry: {
854
+ width: 260,
855
+ height: 150,
856
+ centerX: 130,
857
+ centerY: 132,
858
+ startAngle: 200,
859
+ endAngle: 340
860
+ },
861
+ pointer: {
862
+ type: 'arrow',
863
+ length: 75,
864
+ width: 6
865
+ }
866
+ },
867
+ layers: [
868
+ Gaugeit.layers.face,
869
+ Gaugeit.layers.zones,
870
+ Gaugeit.layers.ticks,
871
+ Gaugeit.layers.labels,
872
+ Gaugeit.layers.light,
873
+ Gaugeit.layers.readout,
874
+ Gaugeit.layers.pointer
875
+ ]
876
+ });
877
+ ```
878
+
879
+ Layer order is paint order. In the example, the pointer is last so it paints above the
880
+ readout.
881
+
882
+ ## Accessibility
883
+
884
+ Always provide a meaningful label for production gauges:
885
+
886
+ ```js
887
+ accessibility: {
888
+ label: 'Battery charge',
889
+ description: 'Remaining charge in the main battery.',
890
+ valueText: (value) => `${Math.round(value)} percent`
891
+ }
892
+ ```
893
+
894
+ The generated root uses `role="meter"` and updates `aria-valuemin`,
895
+ `aria-valuemax`, `aria-valuenow`, and optional `aria-valuetext`.
896
+
897
+ A visually decorative gauge still needs an intentional accessibility decision. Do not
898
+ silently remove meter semantics from the library.
899
+
900
+ ## Browser support assumptions
901
+
902
+ Gaugeit.js uses modern browser APIs and targets ES2018-compatible runtime behavior.
903
+ When generating code for an older environment, do not mutate Gaugeit.js source
904
+ arbitrarily. Add an application-level transpilation or compatibility plan.
905
+
906
+ The optional `test:chromium` development command expects a globally available
907
+ `chromium` executable. That requirement does not apply to normal library consumers.
908
+
909
+ ---
910
+
911
+ # Part II — Complete option reference
912
+
913
+ This document describes the public option model in Gaugeit.js 0.1.1. Use exact key
914
+ names and nesting. Do not invent aliases unless this document explicitly lists one.
915
+
916
+ Runtime option resolution order is:
917
+
918
+ 1. shared base defaults;
919
+ 2. selected type defaults;
920
+ 3. instance options;
921
+ 4. optional type `configure()` hook;
922
+ 5. validation and normalization.
923
+
924
+ Plain objects merge recursively. Arrays are replaced. A selected type may override many
925
+ shared defaults, so the values below are the **base defaults**, not necessarily the
926
+ final resolved values for every built-in type.
927
+
928
+ ## Complete base-default shape
929
+
930
+ ```js
931
+ {
932
+ type: 'arc',
933
+ min: 0,
934
+ max: 100,
935
+ value: 0,
936
+ invert: false,
937
+ overflow: 'clamp',
938
+ precision: 3,
939
+ className: '',
940
+
941
+ layout: {
942
+ mode: 'intrinsic',
943
+ padding: 8,
944
+ includeGeometry: true
945
+ },
946
+
947
+ geometry: {
948
+ mode: 'radial',
949
+ width: 320,
950
+ height: 220,
951
+ centerX: 160,
952
+ centerY: 174,
953
+ radius: 120,
954
+ startAngle: 180,
955
+ endAngle: 360,
956
+ preserveAspectRatio: 'xMidYMid meet',
957
+ aspectRatio: null
958
+ },
959
+
960
+ linear: {
961
+ originY: 140,
962
+ startX: 36,
963
+ startY: 0,
964
+ endX: 284,
965
+ endY: 0,
966
+ tickSide: 'negative',
967
+ labelOffset: 34,
968
+ zoneOffset: -12
969
+ },
970
+
971
+ face: {
972
+ visible: false,
973
+ shape: 'panel',
974
+ fit: 'geometry',
975
+ contentPadding: 8,
976
+ minWidth: null,
977
+ minHeight: null,
978
+ clipContent: false,
979
+ x: 6,
980
+ y: 6,
981
+ width: 308,
982
+ height: 208,
983
+ cornerRadius: 18,
984
+ color: '#ffffff',
985
+ strokeColor: '#d1d5db',
986
+ strokeWidth: 2,
987
+ innerStrokeColor: '#f3f4f6',
988
+ innerStrokeWidth: 1,
989
+ shadow: false,
990
+ glass: false
991
+ },
992
+
993
+ track: {
994
+ visible: true,
995
+ color: '#e5e7eb',
996
+ width: 28,
997
+ radius: 112,
998
+ opacity: 1,
999
+ lineCap: 'butt',
1000
+ showUnderZones: false
1001
+ },
1002
+
1003
+ zones: [],
1004
+
1005
+ scale: {
1006
+ endpointMode: 'auto',
1007
+ radius: 112,
1008
+ position: 'inside',
1009
+ major: {
1010
+ visible: true,
1011
+ interval: null,
1012
+ divisions: 5,
1013
+ length: 13,
1014
+ width: 2,
1015
+ color: '#111827',
1016
+ opacity: 1
1017
+ },
1018
+ minor: {
1019
+ visible: true,
1020
+ subdivisions: 4,
1021
+ length: 7,
1022
+ width: 1,
1023
+ color: '#6b7280',
1024
+ opacity: 0.9
1025
+ }
1026
+ },
1027
+
1028
+ labels: {
1029
+ visible: true,
1030
+ interval: null,
1031
+ radius: 84,
1032
+ color: '#111827',
1033
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1034
+ fontSize: 11,
1035
+ fontWeight: 500,
1036
+ fractionDigits: 0,
1037
+ rotation: 'horizontal',
1038
+ formatter: null
1039
+ },
1040
+
1041
+ pointer: {
1042
+ visible: true,
1043
+ type: 'needle',
1044
+ length: 94,
1045
+ shaftLength: null,
1046
+ tailLength: 13,
1047
+ width: 8,
1048
+ color: '#111827',
1049
+ strokeColor: 'none',
1050
+ strokeWidth: 0,
1051
+ opacity: 1,
1052
+ colorByZone: false,
1053
+ cap: {
1054
+ visible: true,
1055
+ stackOrder: 'above',
1056
+ radius: 8,
1057
+ color: '#111827',
1058
+ strokeColor: '#ffffff',
1059
+ strokeWidth: 2
1060
+ }
1061
+ },
1062
+
1063
+ indicator: {
1064
+ visible: false,
1065
+ type: 'hairline',
1066
+ position: 'cross',
1067
+ length: 76,
1068
+ centerOffset: null,
1069
+ offset: 0,
1070
+ width: 2,
1071
+ color: '#b91c1c',
1072
+ strokeColor: 'none',
1073
+ strokeWidth: 0,
1074
+ opacity: 1,
1075
+ lineCap: 'round',
1076
+ colorByZone: false,
1077
+ markerSize: 8,
1078
+ carriageWidth: 14,
1079
+ carriageRadius: 4,
1080
+ cap: {
1081
+ visible: false,
1082
+ stackOrder: 'above',
1083
+ radius: 4,
1084
+ color: '#b91c1c',
1085
+ strokeColor: '#ffffff',
1086
+ strokeWidth: 1
1087
+ }
1088
+ },
1089
+
1090
+ centerZero: {
1091
+ zeroValue: 0,
1092
+ symmetric: false,
1093
+ marker: {
1094
+ visible: false,
1095
+ radius: null,
1096
+ position: 'cross',
1097
+ centerOffset: 0,
1098
+ length: 20,
1099
+ width: 3,
1100
+ color: '#111827',
1101
+ opacity: 1,
1102
+ lineCap: 'round'
1103
+ }
1104
+ },
1105
+
1106
+ tape: {
1107
+ orientation: 'vertical',
1108
+ x: 64,
1109
+ y: 58,
1110
+ width: 192,
1111
+ height: 210,
1112
+ cornerRadius: 12,
1113
+ windowColor: '#e5e7eb',
1114
+ windowStrokeColor: '#475569',
1115
+ windowStrokeWidth: 2,
1116
+ windowInnerStrokeColor: '#d1d5db',
1117
+ windowInnerStrokeWidth: 1,
1118
+ stripColor: '#ffffff',
1119
+ textColor: '#111827',
1120
+ tickColor: '#334155',
1121
+ minorTickColor: '#64748b',
1122
+ minorTickOpacity: 0.85,
1123
+ majorTickWidth: 2,
1124
+ minorTickWidth: 1,
1125
+ majorTickLength: 24,
1126
+ minorTickLength: 12,
1127
+ tickInset: 64,
1128
+ labelOffset: 10,
1129
+ interval: null,
1130
+ minorSubdivisions: 4,
1131
+ majorSpacing: 52,
1132
+ indicatorColor: '#b91c1c',
1133
+ indicatorWidth: 2.5,
1134
+ fadeSize: 32,
1135
+ fadeColor: '#ffffff',
1136
+ colorByZone: false
1137
+ },
1138
+
1139
+ light: {
1140
+ visible: false,
1141
+ x: 93,
1142
+ y: 110,
1143
+ radius: 8,
1144
+ glowRadius: 20,
1145
+ glowSharpness: 0,
1146
+ color: 'red',
1147
+ customColor: '',
1148
+ offColor: '#f3ead6',
1149
+ offEdgeColor: '#817768',
1150
+ bezelColor: '#4b5563',
1151
+ bezelHighlightColor: '#f8fafc',
1152
+ opacity: 1,
1153
+ glowOpacity: 0.15,
1154
+ on: false,
1155
+ glow: true,
1156
+ pulse: false,
1157
+ pulseInterval: 2600,
1158
+ blink: false,
1159
+ blinkInterval: 1000,
1160
+ flicker: true,
1161
+ flickerIntensity: 0.1,
1162
+ flickerGlowRadius: 0.1,
1163
+ flickerMinInterval: 45,
1164
+ flickerMaxInterval: 140,
1165
+ trigger: {
1166
+ mode: 'below',
1167
+ source: 'target',
1168
+ value: 15,
1169
+ min: 0,
1170
+ max: 15
1171
+ }
1172
+ },
1173
+
1174
+ readout: {
1175
+ visible: true,
1176
+ title: {
1177
+ visible: false,
1178
+ text: '',
1179
+ x: 'auto',
1180
+ y: 'auto',
1181
+ position: 'top',
1182
+ margin: 12,
1183
+ offsetX: 0,
1184
+ offsetY: 0,
1185
+ color: '#374151',
1186
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1187
+ fontSize: 14,
1188
+ fontWeight: 700,
1189
+ letterSpacing: 0.8
1190
+ },
1191
+ value: {
1192
+ visible: false,
1193
+ x: 'auto',
1194
+ y: 54,
1195
+ position: 'center',
1196
+ margin: 12,
1197
+ offsetX: 0,
1198
+ offsetY: 0,
1199
+ color: '#111827',
1200
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1201
+ fontSize: 38,
1202
+ fontWeight: 750,
1203
+ fractionDigits: 0,
1204
+ prefix: '',
1205
+ suffix: '',
1206
+ locale: undefined,
1207
+ formatter: null
1208
+ },
1209
+ unit: {
1210
+ visible: false,
1211
+ text: '',
1212
+ x: 'auto',
1213
+ y: 76,
1214
+ position: 'bottom',
1215
+ margin: 12,
1216
+ offsetX: 0,
1217
+ offsetY: 0,
1218
+ color: '#6b7280',
1219
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1220
+ fontSize: 12,
1221
+ fontWeight: 600
1222
+ }
1223
+ },
1224
+
1225
+ animation: {
1226
+ enabled: true,
1227
+ duration: 850,
1228
+ easing: 'easeOutCubic',
1229
+ animateOnInit: true,
1230
+ startFrom: 'min',
1231
+ respectReducedMotion: true
1232
+ },
1233
+
1234
+ effects: {
1235
+ jitter: {
1236
+ enabled: false,
1237
+ amplitude: 0.45,
1238
+ amplitudeUnit: 'value',
1239
+ frequency: 4,
1240
+ smoothing: 0.86,
1241
+ whenIdleOnly: true
1242
+ }
1243
+ },
1244
+
1245
+ accessibility: {
1246
+ label: 'Gauge',
1247
+ description: '',
1248
+ valueText: null
1249
+ },
1250
+
1251
+ performance: {
1252
+ pauseWhenHidden: true,
1253
+ pauseWhenOffscreen: true
1254
+ },
1255
+
1256
+ responsive: true
1257
+ }
1258
+ ```
1259
+
1260
+ ## Top-level options
1261
+
1262
+ | Key | Type | Base default | Allowed or normalized behavior | Layout impact |
1263
+ | --- | --- | --- | --- | --- |
1264
+ | `type` | string | `arc` | must be a registered type name | selects all type defaults and layers |
1265
+ | `min` | number | `0` | non-finite becomes `0` | scale, zones, labels, movement |
1266
+ | `max` | number | `100` | forced to greater than `min` | scale, zones, labels, movement |
1267
+ | `value` | number | `0` | normalized according to `overflow` | dynamic only unless range expands |
1268
+ | `invert` | boolean | `false` | only literal `true` enables | reverses visual mapping |
1269
+ | `overflow` | string | `clamp` | `clamp`, `expand`, `allow` | `expand` may rebuild range |
1270
+ | `precision` | integer | `3` | clamped to `0 ... 12` | display/event rounding |
1271
+ | `className` | string | empty | extra generated-root class | CSS only |
1272
+ | `responsive` | boolean | `true` | false disables width-100% sizing | host sizing |
1273
+ | `layout` | object | see below | restored when malformed | viewBox and host size |
1274
+ | `geometry` | object | see below | restored and normalized | primary composition |
1275
+ | `linear` | object | see below | used by linear family | primary composition |
1276
+ | `face` | object | see below | shared static layer | bounds and clipping |
1277
+ | `track` | object | see below | radial and linear tracks | bounds |
1278
+ | `zones` | array | `[]` | array replaced; entries normalized | bounds and colors |
1279
+ | `scale` | object | see below | major/minor scale | bounds |
1280
+ | `labels` | object | see below | static text labels | bounds |
1281
+ | `pointer` | object | see below | radial moving visual | bounds and dynamic update |
1282
+ | `indicator` | object | see below | linear moving visual | bounds and dynamic update |
1283
+ | `centerZero` | object | see below | fixed zero reference | range and bounds |
1284
+ | `tape` | object | see below | rolling tape family | bounds and dynamic update |
1285
+ | `light` | object | see below | shared annunciator | bounds, timers, dynamic state |
1286
+ | `readout` | object | see below | title/value/unit | bounds and dynamic text |
1287
+ | `animation` | object | see below | base-value transition | runtime state |
1288
+ | `effects.jitter` | object | see below | visual-only movement | moving artwork only |
1289
+ | `accessibility` | object | see below | meter semantics | no visual layout |
1290
+ | `performance` | object | see below | lifecycle observers | no visual layout |
1291
+
1292
+ ### `type`
1293
+
1294
+ Example:
1295
+
1296
+ ```js
1297
+ { type: 'heritage-round' }
1298
+ ```
1299
+
1300
+ Use one exact built-in name or a previously registered custom type. An unknown type
1301
+ causes registry lookup to fail. Do not silently substitute a made-up name.
1302
+
1303
+ ### `min`, `max`, and `value`
1304
+
1305
+ Example:
1306
+
1307
+ ```js
1308
+ {
1309
+ min: -50,
1310
+ max: 50,
1311
+ value: 12
1312
+ }
1313
+ ```
1314
+
1315
+ If `max <= min`, runtime normalization sets `max = min + 1`.
1316
+
1317
+ ### `overflow`
1318
+
1319
+ ```js
1320
+ { overflow: 'clamp' }
1321
+ ```
1322
+
1323
+ - `clamp`: constrain incoming values to the range.
1324
+ - `expand`: extend `min` or `max` when an incoming value exceeds the range, then
1325
+ re-resolve options and render.
1326
+ - `allow`: preserve the configured range but allow the dynamic value outside it.
1327
+
1328
+ Common misuse: using `allow` when the visual should stay within the scale. Prefer
1329
+ `clamp` for meters that represent bounded values.
1330
+
1331
+ ### `precision`
1332
+
1333
+ This is the internal displayed-value rounding precision, not the same as label or
1334
+ readout fraction digits.
1335
+
1336
+ ## `layout`
1337
+
1338
+ | Key | Type | Base default | Normalization | Layout impact |
1339
+ | --- | --- | --- | --- | --- |
1340
+ | `mode` | `'intrinsic' \| 'contain'` | `intrinsic` | invalid becomes `intrinsic` | determines host sizing mode |
1341
+ | `padding` | number | `8` | minimum `0` | expands final viewBox |
1342
+ | `includeGeometry` | boolean | `true` | false only when explicitly false | keeps logical dimensions as minimum bounds |
1343
+
1344
+ Example:
1345
+
1346
+ ```js
1347
+ layout: {
1348
+ mode: 'contain',
1349
+ padding: 12,
1350
+ includeGeometry: true
1351
+ }
1352
+ ```
1353
+
1354
+ `intrinsic` calculates content-aware bounds and gives the SVG a natural aspect ratio.
1355
+ `contain` uses the same viewBox inside a host whose width and height are controlled by
1356
+ CSS.
1357
+
1358
+ Common misuse: selecting `contain` without giving the host a usable CSS height.
1359
+
1360
+ ## `geometry`
1361
+
1362
+ | Key | Type | Base default | Normalization | Used by |
1363
+ | --- | --- | --- | --- | --- |
1364
+ | `mode` | `'radial' \| 'linear' \| 'tape'` | `radial` | invalid becomes `radial` | type family |
1365
+ | `width` | number | `320` | minimum `40` | all families and intrinsic sizing |
1366
+ | `height` | number | `220` | minimum `40` | all families and intrinsic sizing |
1367
+ | `centerX` | number | `160` | fallback `width / 2` | radial |
1368
+ | `centerY` | number | `174` | fallback `height * 0.78` | radial |
1369
+ | `radius` | number | `120` | minimum `1` | radial and custom layers |
1370
+ | `startAngle` | number | `180` | finite; zero sweep repaired | radial |
1371
+ | `endAngle` | number | `360` | sweep limited to 360 degrees | radial |
1372
+ | `preserveAspectRatio` | string | `xMidYMid meet` | converted to string | SVG |
1373
+ | `aspectRatio` | number or `null` | `null` | positive or null | resizable presets |
1374
+
1375
+ Example semicircle:
1376
+
1377
+ ```js
1378
+ geometry: {
1379
+ startAngle: 180,
1380
+ endAngle: 360
1381
+ }
1382
+ ```
1383
+
1384
+ Example using the public helper:
1385
+
1386
+ ```js
1387
+ const angles = Gaugeit.geometry.centeredArcAngles(220, 15);
1388
+
1389
+ const options = {
1390
+ geometry: angles
1391
+ };
1392
+ ```
1393
+
1394
+ `centerAngle: 0` means the normal upright orientation, corresponding to SVG angle 270.
1395
+ Positive center angles rotate clockwise.
1396
+
1397
+ Common misuse: manually setting `geometry.mode` to a family inconsistent with the
1398
+ selected built-in type. Prefer selecting the correct type and overriding only necessary
1399
+ coordinates.
1400
+
1401
+ ## `linear`
1402
+
1403
+ Public linear Y coordinates are Cartesian around `originY`: positive values move
1404
+ upward even though internal SVG Y increases downward.
1405
+
1406
+ | Key | Type | Base default | Meaning |
1407
+ | --- | --- | --- | --- |
1408
+ | `originY` | number | `140` | SVG baseline representing public Y zero |
1409
+ | `startX` | number | `36` | minimum-side X before inversion |
1410
+ | `startY` | number | `0` | minimum-side positive-up Y |
1411
+ | `endX` | number | `284` | maximum-side X before inversion |
1412
+ | `endY` | number | `0` | maximum-side positive-up Y |
1413
+ | `tickSide` | `'negative' \| 'positive' \| 'cross'` | `negative` | side of axis normal |
1414
+ | `labelOffset` | number | `34` | positive moves visually upward on left-to-right axis |
1415
+ | `zoneOffset` | number | `-12` | positive moves visually upward on left-to-right axis |
1416
+
1417
+ A zero-length axis is repaired to one logical unit.
1418
+
1419
+ Example vertical axis:
1420
+
1421
+ ```js
1422
+ linear: {
1423
+ originY: 270,
1424
+ startX: 90,
1425
+ startY: 0,
1426
+ endX: 90,
1427
+ endY: 220,
1428
+ tickSide: 'positive'
1429
+ }
1430
+ ```
1431
+
1432
+ Common misuse: applying raw SVG downward-positive Y assumptions to these public fields.
1433
+
1434
+ ## `face`
1435
+
1436
+ | Key | Type | Base default | Notes |
1437
+ | --- | --- | --- | --- |
1438
+ | `visible` | boolean | `false` | type presets may enable |
1439
+ | `shape` | `'panel' \| 'circle'` | `panel` | invalid becomes panel |
1440
+ | `fit` | `'geometry' \| 'content'` | `geometry` | content fit follows rendered content |
1441
+ | `contentPadding` | number | `8` | minimum zero |
1442
+ | `minWidth` | number or `null` | `null` | minimum one when present |
1443
+ | `minHeight` | number or `null` | `null` | minimum one when present |
1444
+ | `clipContent` | boolean | `false` | clips later non-face layers to inner bezel |
1445
+ | `x`, `y` | number | `6`, `6` | face origin |
1446
+ | `width`, `height` | number | `308`, `208` | minimum one |
1447
+ | `cornerRadius` | number | `18` | minimum zero |
1448
+ | `color` | CSS color | `#ffffff` | face fill |
1449
+ | `strokeColor` | CSS color | `#d1d5db` | outer stroke |
1450
+ | `strokeWidth` | number | `2` | minimum zero |
1451
+ | `innerStrokeColor` | CSS color | `#f3f4f6` | inner bezel |
1452
+ | `innerStrokeWidth` | number | `1` | minimum zero |
1453
+ | `shadow` | boolean | `false` | reserves extra bounds |
1454
+ | `glass` | boolean | `false` | visual highlight |
1455
+
1456
+ Example:
1457
+
1458
+ ```js
1459
+ face: {
1460
+ visible: true,
1461
+ fit: 'content',
1462
+ contentPadding: 12,
1463
+ color: '#fff8e7',
1464
+ glass: true
1465
+ }
1466
+ ```
1467
+
1468
+ Interaction: when `clipContent` is true and the face layer is first, the renderer
1469
+ applies the clip path to later layers. New custom layers should use `renderer.group()`
1470
+ to inherit this behavior.
1471
+
1472
+ ## `track`
1473
+
1474
+ | Key | Type | Base default | Notes |
1475
+ | --- | --- | --- | --- |
1476
+ | `visible` | boolean | `true` | base scale track |
1477
+ | `color` | CSS color | `#e5e7eb` | track color |
1478
+ | `width` | number | `28` | zone default-width fallback |
1479
+ | `radius` | number | `112` | radial track radius |
1480
+ | `opacity` | number | `1` | visual opacity |
1481
+ | `lineCap` | string | `butt` | SVG stroke-linecap |
1482
+ | `showUnderZones` | boolean | `false` | controls transparent-zone gaps |
1483
+
1484
+ When `showUnderZones` is false, a transparent zone is a true gap. When true, the track
1485
+ remains visible under every zone.
1486
+
1487
+ ## `zones`
1488
+
1489
+ Type: array of zone objects. The entire array is replaced by option updates.
1490
+
1491
+ ```js
1492
+ zones: [
1493
+ {
1494
+ min: 0,
1495
+ max: 25,
1496
+ color: '#ef4444',
1497
+ width: 20,
1498
+ taper: {
1499
+ startWidth: 0,
1500
+ endWidth: 32,
1501
+ segments: 48
1502
+ },
1503
+ radius: 112,
1504
+ opacity: 1,
1505
+ lineCap: 'butt',
1506
+ className: 'warning-zone',
1507
+ offset: 0
1508
+ }
1509
+ ]
1510
+ ```
1511
+
1512
+ | Key | Type | Default | Applies to |
1513
+ | --- | --- | --- | --- |
1514
+ | `min` | number | required | all |
1515
+ | `max` | number | required | all |
1516
+ | `color` | CSS color | transparent when missing | all |
1517
+ | `strokeStyle` | CSS color | alias for `color` | compatibility |
1518
+ | `width` | number | track width fallback | radial and linear |
1519
+ | `taper.startWidth` | number | zone width fallback | radial |
1520
+ | `taper.endWidth` | number | zone width fallback | radial |
1521
+ | `taper.segments` | integer | `48` | radial, clamped `2 ... 256` |
1522
+ | `radius` | number | track/type fallback | radial |
1523
+ | `opacity` | number | `1` | all |
1524
+ | `lineCap` | string | type/track fallback | uniform radial and linear |
1525
+ | `className` | string | empty | generated zone element |
1526
+ | `offset` | number | linear zone offset fallback | linear |
1527
+
1528
+ Zones are clamped to `min ... max`; invalid or empty intervals are removed. Later
1529
+ overlapping zones win. `pointer.colorByZone`, `indicator.colorByZone`, and
1530
+ `tape.colorByZone` use the last matching zone.
1531
+
1532
+ A missing color, `transparent`, or `none` produces a gap when the track is not shown
1533
+ under zones.
1534
+
1535
+ ## `scale`
1536
+
1537
+ Shared fields:
1538
+
1539
+ | Key | Type | Base default | Notes |
1540
+ | --- | --- | --- | --- |
1541
+ | `endpointMode` | `'auto' \| 'both' \| 'dedupe'` | `auto` | auto deduplicates closed-scale maximum |
1542
+ | `radius` | number | `112` | minimum one |
1543
+ | `position` | `'inside' \| 'outside' \| 'cross'` | `inside` | tick placement |
1544
+
1545
+ ### `scale.major`
1546
+
1547
+ | Key | Type | Base default | Notes |
1548
+ | --- | --- | --- | --- |
1549
+ | `visible` | boolean | `true` | type preset may change |
1550
+ | `interval` | number or `null` | `null` | positive explicit spacing |
1551
+ | `divisions` | integer | `5` | desired divisions if no interval |
1552
+ | `length` | number | `13` | minimum zero |
1553
+ | `width` | number | `2` | minimum zero |
1554
+ | `color` | CSS color | `#111827` | stroke |
1555
+ | `opacity` | number | `1` | stroke opacity |
1556
+
1557
+ ### `scale.minor`
1558
+
1559
+ | Key | Type | Base default | Notes |
1560
+ | --- | --- | --- | --- |
1561
+ | `visible` | boolean | `true` | type preset may change |
1562
+ | `subdivisions` | integer | `4` | count between adjacent major ticks |
1563
+ | `length` | number | `7` | minimum zero |
1564
+ | `width` | number | `1` | minimum zero |
1565
+ | `color` | CSS color | `#6b7280` | stroke |
1566
+ | `opacity` | number | `0.9` | stroke opacity |
1567
+
1568
+ `major.interval` takes precedence over `major.divisions`.
1569
+
1570
+ ## `labels`
1571
+
1572
+ | Key | Type | Base default | Notes |
1573
+ | --- | --- | --- | --- |
1574
+ | `visible` | boolean | `true` | static scale labels |
1575
+ | `interval` | number or `null` | `null` | falls back to major interval |
1576
+ | `radius` | number | `84` | radial placement |
1577
+ | `color` | CSS color | `#111827` | text color |
1578
+ | `fontFamily` | string | system stack | SVG text |
1579
+ | `fontSize` | number | `11` | minimum one |
1580
+ | `fontWeight` | string or number | `500` | SVG text |
1581
+ | `fractionDigits` | integer | `0` | clamped `0 ... 12` |
1582
+ | `rotation` | `'horizontal' \| 'radial' \| 'tangent'` | `horizontal` | radial labels |
1583
+ | `formatter` | function or `null` | `null` | `(value, context) => string` |
1584
+
1585
+ Formatter context:
1586
+
1587
+ ```js
1588
+ {
1589
+ min,
1590
+ max,
1591
+ options
1592
+ }
1593
+ ```
1594
+
1595
+ Common misuse: returning non-string application objects. Coerce to readable text.
1596
+
1597
+ ## `pointer`
1598
+
1599
+ Radial moving visual:
1600
+
1601
+ | Key | Type | Base default | Notes |
1602
+ | --- | --- | --- | --- |
1603
+ | `visible` | boolean | `true` | radial layer |
1604
+ | `type` | `'needle' \| 'line' \| 'arrow' \| 'spear'` | `needle` | invalid becomes needle |
1605
+ | `length` | number | `94` | minimum zero |
1606
+ | `shaftLength` | number or `null` | `null` | clamped to pointer length |
1607
+ | `tailLength` | number | `13` | minimum zero |
1608
+ | `width` | number | `8` | minimum `0.5` |
1609
+ | `color` | CSS color | `#111827` | fill/stroke by type |
1610
+ | `strokeColor` | CSS color | `none` | outline |
1611
+ | `strokeWidth` | number | `0` | minimum zero |
1612
+ | `opacity` | number | `1` | visual opacity |
1613
+ | `colorByZone` | boolean | `false` | use active zone color |
1614
+ | `cap.visible` | boolean | `true` | center cap |
1615
+ | `cap.stackOrder` | `'above' \| 'below'` | `above` | SVG paint relation |
1616
+ | `cap.radius` | number | `8` | minimum zero |
1617
+ | `cap.color` | CSS color | `#111827` | fill |
1618
+ | `cap.strokeColor` | CSS color | `#ffffff` | outline |
1619
+ | `cap.strokeWidth` | number | `2` | minimum zero |
1620
+
1621
+ Layer order, not CSS z-index, controls whether the entire pointer paints over readout
1622
+ text. `cap.stackOrder` only controls cap-versus-pointer order inside the pointer layer.
1623
+
1624
+ ## `indicator`
1625
+
1626
+ Linear moving visual:
1627
+
1628
+ | Key | Type | Base default | Notes |
1629
+ | --- | --- | --- | --- |
1630
+ | `visible` | boolean | `false` | linear presets enable |
1631
+ | `type` | `'hairline' \| 'marker' \| 'carriage'` | `hairline` | indicator shape |
1632
+ | `position` | `'negative' \| 'positive' \| 'cross'` | `cross` | normal span |
1633
+ | `length` | number | `76` | minimum zero |
1634
+ | `centerOffset` | number | `null` in base | positive-up public offset |
1635
+ | `offset` | number | `0` | deprecated compatibility alias |
1636
+ | `width` | number | `2` | minimum `0.5` |
1637
+ | `color` | CSS color | `#b91c1c` | fill/stroke |
1638
+ | `strokeColor` | CSS color | `none` | outline |
1639
+ | `strokeWidth` | number | `0` | minimum zero |
1640
+ | `opacity` | number | `1` | visual opacity |
1641
+ | `lineCap` | string | `round` | SVG linecap |
1642
+ | `colorByZone` | boolean | `false` | use active zone color |
1643
+ | `markerSize` | number | `8` | marker type |
1644
+ | `carriageWidth` | number | `14` | carriage type |
1645
+ | `carriageRadius` | number | `4` | carriage corners |
1646
+ | `cap.visible` | boolean | `false` | optional cap |
1647
+ | `cap.stackOrder` | `'above' \| 'below'` | `above` | cap relation |
1648
+ | `cap.radius` | number | `4` | minimum zero |
1649
+ | `cap.color` | CSS color | `#b91c1c` | fill |
1650
+ | `cap.strokeColor` | CSS color | `#ffffff` | outline |
1651
+ | `cap.strokeWidth` | number | `1` | minimum zero |
1652
+
1653
+ Use `centerOffset` in new code. Positive values move upward for a left-to-right axis.
1654
+ The older `offset` retains its original raw axis-normal sign.
1655
+
1656
+ ## `centerZero`
1657
+
1658
+ | Key | Type | Base default | Notes |
1659
+ | --- | --- | --- | --- |
1660
+ | `zeroValue` | number | `0` | numeric center reference |
1661
+ | `symmetric` | boolean | `false` | expands range equally around zeroValue |
1662
+ | `marker.visible` | boolean | `false` | preset may enable |
1663
+ | `marker.radius` | number or `null` | `null` | radial marker radius |
1664
+ | `marker.position` | `'negative' \| 'positive' \| 'cross'` | `cross` | linear marker span |
1665
+ | `marker.centerOffset` | number | `0` | positive-up on horizontal axis |
1666
+ | `marker.length` | number | `20` | minimum zero |
1667
+ | `marker.width` | number | `3` | minimum `0.5` |
1668
+ | `marker.color` | CSS color | `#111827` | stroke |
1669
+ | `marker.opacity` | number | `1` | clamped `0 ... 1` |
1670
+ | `marker.lineCap` | string | `round` | SVG linecap |
1671
+
1672
+ For center-zero types, use a range that contains `zeroValue`. With
1673
+ `symmetric: true`, the runtime expands the larger side across the reference:
1674
+
1675
+ ```js
1676
+ {
1677
+ min: -30,
1678
+ max: 80,
1679
+ centerZero: {
1680
+ zeroValue: 0,
1681
+ symmetric: true
1682
+ }
1683
+ }
1684
+ ```
1685
+
1686
+ resolves to a symmetric `-80 ... 80` range.
1687
+
1688
+ ## `tape`
1689
+
1690
+ Used by `rolling-tape` and custom tape types:
1691
+
1692
+ | Key | Type | Base default | Notes |
1693
+ | --- | --- | --- | --- |
1694
+ | `orientation` | `'vertical' \| 'horizontal'` | `vertical` | travel axis |
1695
+ | `x`, `y` | number | `64`, `58` | window origin |
1696
+ | `width`, `height` | number | `192`, `210` | minimum 20 |
1697
+ | `cornerRadius` | number | `12` | minimum zero |
1698
+ | `windowColor` | CSS color | `#e5e7eb` | outer window |
1699
+ | `windowStrokeColor` | CSS color | `#475569` | border |
1700
+ | `windowStrokeWidth` | number | `2` | minimum zero |
1701
+ | `windowInnerStrokeColor` | CSS color | `#d1d5db` | inner border |
1702
+ | `windowInnerStrokeWidth` | number | `1` | minimum zero |
1703
+ | `stripColor` | CSS color | `#ffffff` | moving strip |
1704
+ | `textColor` | CSS color | `#111827` | numeric labels |
1705
+ | `tickColor` | CSS color | `#334155` | major ticks |
1706
+ | `minorTickColor` | CSS color | `#64748b` | minor ticks |
1707
+ | `minorTickOpacity` | number | `0.85` | clamped `0 ... 1` |
1708
+ | `majorTickWidth` | number | `2` | minimum `0.5` |
1709
+ | `minorTickWidth` | number | `1` | minimum `0.5` |
1710
+ | `majorTickLength` | number | `24` | minimum zero |
1711
+ | `minorTickLength` | number | `12` | minimum zero |
1712
+ | `tickInset` | number | `64` | placement from edge |
1713
+ | `labelOffset` | number | `10` | text placement |
1714
+ | `interval` | number or `null` | `null` | positive explicit major interval |
1715
+ | `minorSubdivisions` | integer | `4` | count between majors |
1716
+ | `majorSpacing` | number | `52` | minimum two |
1717
+ | `indicatorColor` | CSS color | `#b91c1c` | fixed reference line |
1718
+ | `indicatorWidth` | number | `2.5` | minimum `0.5` |
1719
+ | `fadeSize` | number | `32` | clamped to half travel-axis size |
1720
+ | `fadeColor` | CSS color | `#ffffff` | edge fade |
1721
+ | `colorByZone` | boolean | `false` | slot/reference color behavior |
1722
+
1723
+ The scale moves behind a fixed edge-to-edge indicator. Do not animate a separate
1724
+ pointer for this type. Slot reuse keeps DOM size independent of the full numeric range.
1725
+
1726
+ ## `light`
1727
+
1728
+ The shared light is available to every built-in type. A custom type must include
1729
+ `Gaugeit.layers.light`.
1730
+
1731
+ | Key | Type | Base default | Normalization and interaction |
1732
+ | --- | --- | --- | --- |
1733
+ | `visible` | boolean | `false` | only true renders |
1734
+ | `x`, `y` | number | `93`, `110` | linear Y uses positive-up convention |
1735
+ | `radius` | number | `8` | minimum one; physical bulb |
1736
+ | `glowRadius` | number or `null` | `20` | minimum zero; null derives `radius * 1.82` |
1737
+ | `glowSharpness` | number | `0` | clamped `0 ... 1`; 1 removes blur |
1738
+ | `color` | named palette | `red` | ten allowed names |
1739
+ | `customColor` | CSS color string | empty | overrides active palette when non-empty |
1740
+ | `offColor` | CSS color | `#f3ead6` | unlit lens center |
1741
+ | `offEdgeColor` | CSS color | `#817768` | unlit lens edge |
1742
+ | `bezelColor` | CSS color | `#4b5563` | hardware |
1743
+ | `bezelHighlightColor` | CSS color | `#f8fafc` | hardware highlight |
1744
+ | `opacity` | number | `1` | illuminated-lens opacity only |
1745
+ | `glowOpacity` | number | `0.15` | halo opacity only |
1746
+ | `on` | boolean | `false` | manual trigger state |
1747
+ | `glow` | boolean | `true` | false hides halo |
1748
+ | `pulse` | boolean | `false` | active steady-light breathing |
1749
+ | `pulseInterval` | number | `2600` | minimum 400 ms |
1750
+ | `blink` | boolean | `false` | active blink; takes precedence over pulse |
1751
+ | `blinkInterval` | number | `1000` | minimum 100 ms |
1752
+ | `flicker` | boolean | `true` | randomized overlay while active |
1753
+ | `flickerIntensity` | number | `0.1` | clamped `0 ... 1` |
1754
+ | `flickerGlowRadius` | number | `0.1` | relative ± variation, clamped `0 ... 1` |
1755
+ | `flickerMinInterval` | number | `45` | minimum 16 ms |
1756
+ | `flickerMaxInterval` | number | `140` | minimum 16 ms; order repaired |
1757
+ | `trigger.mode` | enum | `below` | `manual`, `below`, `above`, `between` |
1758
+ | `trigger.source` | enum | `target` | `target`, `displayed`, `pointer` |
1759
+ | `trigger.value` | number | `15` | inclusive below/above threshold |
1760
+ | `trigger.min` | number | `0` | inclusive between lower bound |
1761
+ | `trigger.max` | number | `15` | inclusive between upper bound |
1762
+
1763
+ Allowed palettes:
1764
+
1765
+ ```text
1766
+ red
1767
+ green
1768
+ blue
1769
+ amber
1770
+ yellow
1771
+ orange
1772
+ white
1773
+ halogen
1774
+ cyan
1775
+ brown
1776
+ ```
1777
+
1778
+ Examples:
1779
+
1780
+ ```js
1781
+ light: {
1782
+ visible: true,
1783
+ color: 'amber',
1784
+ glowRadius: 28,
1785
+ glowSharpness: 0.25,
1786
+ glowOpacity: 0.22,
1787
+ trigger: {
1788
+ mode: 'above',
1789
+ source: 'target',
1790
+ value: 80
1791
+ }
1792
+ }
1793
+ ```
1794
+
1795
+ ```js
1796
+ light: {
1797
+ visible: true,
1798
+ on: true,
1799
+ pulse: true,
1800
+ blink: false,
1801
+ flicker: false,
1802
+ trigger: {
1803
+ mode: 'manual'
1804
+ }
1805
+ }
1806
+ ```
1807
+
1808
+ Important interactions:
1809
+
1810
+ - blink disables pulse while active;
1811
+ - flicker may run with blink or pulse;
1812
+ - flicker and pulse run only while the trigger is active;
1813
+ - reduced motion suppresses blink, pulse, and flicker motion;
1814
+ - `opacity` does not fade the unlit physical lens or bezel;
1815
+ - `glowOpacity` is independent from lens `opacity`;
1816
+ - `glowRadius` controls halo size independently from bulb `radius`;
1817
+ - glow bounds are calculated automatically and must stay synchronized with rendering.
1818
+
1819
+ Common misuse: enabling `on: true` while leaving trigger mode at the default `below`.
1820
+ `on` is read only by `trigger.mode: 'manual'`.
1821
+
1822
+ ## `readout`
1823
+
1824
+ `readout.visible` controls the complete group. Each child also needs `visible: true`.
1825
+
1826
+ Shared text-part fields:
1827
+
1828
+ | Key | Type | Base title default | Meaning |
1829
+ | --- | --- | --- | --- |
1830
+ | `visible` | boolean | `false` | only true renders |
1831
+ | `text` | string | empty | title/unit content |
1832
+ | `x` | number or `'auto'` | `auto` | auto centers |
1833
+ | `y` | number or `'auto'` | `auto` for title | auto uses position/margin |
1834
+ | `position` | `'top' \| 'center' \| 'bottom'` | `top` | automatic Y anchor |
1835
+ | `margin` | number | `12` | minimum zero |
1836
+ | `offsetX`, `offsetY` | number | `0` | refine automatic placement only |
1837
+ | `color` | CSS color | type-specific | text fill |
1838
+ | `fontFamily` | string | system stack | SVG text |
1839
+ | `fontSize` | number | `14` title | minimum one |
1840
+ | `fontWeight` | string or number | `700` title | SVG text |
1841
+ | `letterSpacing` | number | `0.8` title | SVG text |
1842
+
1843
+ `readout.value` additionally supports:
1844
+
1845
+ | Key | Type | Base default |
1846
+ | --- | --- | --- |
1847
+ | `fractionDigits` | integer | `0`, clamped `0 ... 12` |
1848
+ | `prefix` | string | empty |
1849
+ | `suffix` | string | empty |
1850
+ | `locale` | string or string array | undefined |
1851
+ | `formatter` | function or null | null |
1852
+
1853
+ A value formatter takes precedence over locale and fraction settings.
1854
+
1855
+ Numeric coordinates are final SVG coordinates. Automatic offsets apply only when the
1856
+ corresponding coordinate is `'auto'`.
1857
+
1858
+ Example:
1859
+
1860
+ ```js
1861
+ readout: {
1862
+ visible: true,
1863
+ title: {
1864
+ visible: true,
1865
+ text: 'TEMPERATURE'
1866
+ },
1867
+ value: {
1868
+ visible: true,
1869
+ fractionDigits: 1
1870
+ },
1871
+ unit: {
1872
+ visible: true,
1873
+ text: '°C'
1874
+ }
1875
+ }
1876
+ ```
1877
+
1878
+ Common misuse: setting only `readout.visible: true` and expecting the numeric value to
1879
+ appear.
1880
+
1881
+ ## `animation`
1882
+
1883
+ | Key | Type | Base default | Notes |
1884
+ | --- | --- | --- | --- |
1885
+ | `enabled` | boolean | `true` | global value animation |
1886
+ | `duration` | number | `850` | milliseconds, minimum zero |
1887
+ | `easing` | string or function | `easeOutCubic` | resolved by easing system |
1888
+ | `animateOnInit` | boolean | `true` | startup animation |
1889
+ | `startFrom` | `'min' \| 'max' \| 'value' \| number` | `min` | invalid becomes min |
1890
+ | `respectReducedMotion` | boolean | `true` | suppresses animation when requested |
1891
+
1892
+ Per-update overrides:
1893
+
1894
+ ```js
1895
+ gauge.setValue(80, {
1896
+ animate: true,
1897
+ duration: 1200,
1898
+ easing: 'easeInOutCubic'
1899
+ });
1900
+ ```
1901
+
1902
+ ## `effects.jitter`
1903
+
1904
+ | Key | Type | Base default | Notes |
1905
+ | --- | --- | --- | --- |
1906
+ | `enabled` | boolean | `false` | visual-only |
1907
+ | `amplitude` | number | `0.45` | minimum zero |
1908
+ | `amplitudeUnit` | `'value' \| 'percent'` | `value` | percent of range |
1909
+ | `frequency` | number | `4` | targets per second, minimum 0.1 |
1910
+ | `smoothing` | number | `0.86` | clamped `0 ... 0.999` |
1911
+ | `whenIdleOnly` | boolean | `true` | suppress during base animation |
1912
+
1913
+ Jitter affects only radial pointer, linear indicator, or rolling tape visual position.
1914
+ It does not affect:
1915
+
1916
+ - `getValue()`;
1917
+ - `getDisplayedValue()`;
1918
+ - readout text;
1919
+ - DOM event values;
1920
+ - ARIA value;
1921
+ - default light trigger source.
1922
+
1923
+ ## `accessibility`
1924
+
1925
+ | Key | Type | Base default | Notes |
1926
+ | --- | --- | --- | --- |
1927
+ | `label` | string | `Gauge` | becomes `aria-label` |
1928
+ | `description` | string | empty | rendered in SVG description |
1929
+ | `valueText` | string, function, or null | null | becomes `aria-valuetext` |
1930
+
1931
+ Example:
1932
+
1933
+ ```js
1934
+ accessibility: {
1935
+ label: 'Battery charge',
1936
+ description: 'Remaining charge in the main battery.',
1937
+ valueText: (value) => `${Math.round(value)} percent`
1938
+ }
1939
+ ```
1940
+
1941
+ The generated root receives `role="meter"`, `aria-valuemin`, `aria-valuemax`, and
1942
+ `aria-valuenow`.
1943
+
1944
+ ## `performance`
1945
+
1946
+ | Key | Type | Base default | Notes |
1947
+ | --- | --- | --- | --- |
1948
+ | `pauseWhenHidden` | boolean | `true` | uses page visibility |
1949
+ | `pauseWhenOffscreen` | boolean | `true` | uses IntersectionObserver when available |
1950
+
1951
+ Runtime option replacement reconfigures the observers.
1952
+
1953
+ ## Type-specific defaults and AI strategy
1954
+
1955
+ Built-in types intentionally override base defaults. An AI should:
1956
+
1957
+ 1. select the closest built-in type;
1958
+ 2. provide only application-specific overrides;
1959
+ 3. avoid copying all base defaults into every example;
1960
+ 4. read the selected type file when exact preset coordinates or colors matter;
1961
+ 5. use `replaceOptions()` when switching complete declarative configurations.
1962
+
1963
+ Minimal type-specific usage:
1964
+
1965
+ ```js
1966
+ Gaugeit.createGauge('#gauge', {
1967
+ type: 'center-zero',
1968
+ value: -18
1969
+ });
1970
+ ```
1971
+
1972
+ is usually safer than reconstructing a center-zero instrument from base defaults.
1973
+
1974
+ ## Update semantics reference
1975
+
1976
+ ```js
1977
+ gauge.setValue(value, {
1978
+ animate,
1979
+ duration,
1980
+ easing
1981
+ });
1982
+ ```
1983
+
1984
+ Dynamic value update; no static re-render unless `overflow: 'expand'` changes range.
1985
+
1986
+ ```js
1987
+ gauge.updateOptions(patch, {
1988
+ animateToValue
1989
+ });
1990
+ ```
1991
+
1992
+ Recursive patch merge; arrays replaced.
1993
+
1994
+ ```js
1995
+ gauge.replaceOptions(options, {
1996
+ animateToValue
1997
+ });
1998
+ ```
1999
+
2000
+ Complete declarative replacement; omitted keys return to selected type defaults.
2001
+
2002
+ ## Option-generation checklist
2003
+
2004
+ Before generating a Gaugeit.js option object, verify:
2005
+
2006
+ - the type name exists;
2007
+ - `min < max`;
2008
+ - center-zero ranges contain the zero reference;
2009
+ - radial, linear, and tape options match the selected family;
2010
+ - readout child visibility is explicit;
2011
+ - light manual mode is used when `on` controls state;
2012
+ - blink/pulse precedence is intentional;
2013
+ - CDN or package version is pinned where appropriate;
2014
+ - accessibility label is meaningful;
2015
+ - CSS is included unless standalone is used;
2016
+ - high-frequency value changes use `setValue()`;
2017
+ - arrays are intentionally replaced during option updates.
2018
+
2019
+ ---
2020
+
2021
+ # Part III — Integration recipes
2022
+
2023
+ These recipes target Gaugeit.js 0.1.1. Each recipe is intended to be copied as a
2024
+ complete starting point. Replace application values and labels, but preserve package
2025
+ names, import paths, global names, lifecycle cleanup, and option nesting.
2026
+
2027
+ ## 1. Simple semicircular gauge in plain HTML
2028
+
2029
+ Save as `index.html` beside `dist/`:
2030
+
2031
+ ```html
2032
+ <!doctype html>
2033
+ <html lang="en">
2034
+ <head>
2035
+ <meta charset="utf-8">
2036
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2037
+ <title>Gaugeit.js arc gauge</title>
2038
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2039
+ <style>
2040
+ body {
2041
+ max-width: 42rem;
2042
+ margin: 2rem auto;
2043
+ padding: 0 1rem;
2044
+ font-family: system-ui, sans-serif;
2045
+ }
2046
+
2047
+ #load-gauge {
2048
+ width: min(100%, 32rem);
2049
+ margin-inline: auto;
2050
+ }
2051
+ </style>
2052
+ </head>
2053
+ <body>
2054
+ <h1>Engine load</h1>
2055
+ <div id="load-gauge"></div>
2056
+
2057
+ <script src="./dist/gaugeit.umd.min.js"></script>
2058
+ <script>
2059
+ const gauge = Gaugeit.createGauge('#load-gauge', {
2060
+ type: 'arc',
2061
+ min: 0,
2062
+ max: 100,
2063
+ value: 42,
2064
+ zones: [
2065
+ { min: 0, max: 60, color: '#22c55e' },
2066
+ { min: 60, max: 85, color: '#facc15' },
2067
+ { min: 85, max: 100, color: '#ef4444' }
2068
+ ],
2069
+ readout: {
2070
+ title: {
2071
+ visible: true,
2072
+ text: 'ENGINE LOAD'
2073
+ },
2074
+ value: {
2075
+ visible: true,
2076
+ suffix: '%'
2077
+ }
2078
+ },
2079
+ accessibility: {
2080
+ label: 'Engine load',
2081
+ valueText: (value) => `${Math.round(value)} percent`
2082
+ }
2083
+ });
2084
+
2085
+ window.setTimeout(() => {
2086
+ gauge.setValue(76);
2087
+ }, 1000);
2088
+ </script>
2089
+ </body>
2090
+ </html>
2091
+ ```
2092
+
2093
+ ## 2. Classic framed instrument
2094
+
2095
+ ```html
2096
+ <!doctype html>
2097
+ <html lang="en">
2098
+ <head>
2099
+ <meta charset="utf-8">
2100
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2101
+ <title>Classic Gaugeit.js instrument</title>
2102
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2103
+ <style>
2104
+ #voltage-gauge {
2105
+ width: min(100%, 34rem);
2106
+ margin: 2rem auto;
2107
+ }
2108
+ </style>
2109
+ </head>
2110
+ <body>
2111
+ <div id="voltage-gauge"></div>
2112
+
2113
+ <script src="./dist/gaugeit.umd.min.js"></script>
2114
+ <script>
2115
+ const voltageGauge = Gaugeit.createGauge('#voltage-gauge', {
2116
+ type: 'classic',
2117
+ min: 0,
2118
+ max: 240,
2119
+ value: 118,
2120
+ zones: [
2121
+ { min: 0, max: 90, color: '#ef4444', width: 12 },
2122
+ { min: 90, max: 150, color: '#22c55e', width: 12 },
2123
+ { min: 150, max: 240, color: '#ef4444', width: 12 }
2124
+ ],
2125
+ pointer: {
2126
+ type: 'spear',
2127
+ colorByZone: true
2128
+ },
2129
+ readout: {
2130
+ title: {
2131
+ visible: true,
2132
+ text: 'VOLTAGE'
2133
+ },
2134
+ value: {
2135
+ visible: true,
2136
+ fractionDigits: 0
2137
+ },
2138
+ unit: {
2139
+ visible: true,
2140
+ text: 'V'
2141
+ }
2142
+ },
2143
+ accessibility: {
2144
+ label: 'Voltage',
2145
+ valueText: (value) => `${Math.round(value)} volts`
2146
+ }
2147
+ });
2148
+ </script>
2149
+ </body>
2150
+ </html>
2151
+ ```
2152
+
2153
+ ## 3. Radial center-zero balance gauge
2154
+
2155
+ ```html
2156
+ <!doctype html>
2157
+ <html lang="en">
2158
+ <head>
2159
+ <meta charset="utf-8">
2160
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2161
+ <title>Center-zero Gaugeit.js gauge</title>
2162
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2163
+ </head>
2164
+ <body>
2165
+ <div id="balance-gauge"></div>
2166
+
2167
+ <script src="./dist/gaugeit.umd.min.js"></script>
2168
+ <script>
2169
+ const balanceGauge = Gaugeit.createGauge('#balance-gauge', {
2170
+ type: 'center-zero',
2171
+ min: -100,
2172
+ max: 100,
2173
+ value: -18,
2174
+ centerZero: {
2175
+ zeroValue: 0,
2176
+ symmetric: true,
2177
+ marker: {
2178
+ visible: true
2179
+ }
2180
+ },
2181
+ zones: [
2182
+ { min: -100, max: -60, color: '#ef4444' },
2183
+ { min: -60, max: 60, color: '#22c55e' },
2184
+ { min: 60, max: 100, color: '#ef4444' }
2185
+ ],
2186
+ readout: {
2187
+ title: {
2188
+ visible: true,
2189
+ text: 'BALANCE'
2190
+ },
2191
+ value: {
2192
+ visible: true,
2193
+ prefix: ''
2194
+ }
2195
+ },
2196
+ accessibility: {
2197
+ label: 'Balance deviation',
2198
+ valueText: (value) => `${Math.round(value)} balance units`
2199
+ }
2200
+ });
2201
+ </script>
2202
+ </body>
2203
+ </html>
2204
+ ```
2205
+
2206
+ The numeric range contains `zeroValue`. `symmetric: true` protects the centered
2207
+ reference if the supplied endpoints are uneven.
2208
+
2209
+ ## 4. Classic linear center-zero instrument
2210
+
2211
+ ```html
2212
+ <!doctype html>
2213
+ <html lang="en">
2214
+ <head>
2215
+ <meta charset="utf-8">
2216
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2217
+ <title>Classic linear center-zero instrument</title>
2218
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2219
+ <style>
2220
+ #trim-gauge {
2221
+ width: min(100%, 42rem);
2222
+ margin: 2rem auto;
2223
+ }
2224
+ </style>
2225
+ </head>
2226
+ <body>
2227
+ <div id="trim-gauge"></div>
2228
+
2229
+ <script src="./dist/gaugeit.umd.min.js"></script>
2230
+ <script>
2231
+ const trimGauge = Gaugeit.createGauge('#trim-gauge', {
2232
+ type: 'classic-linear-zero',
2233
+ min: -50,
2234
+ max: 50,
2235
+ value: 12,
2236
+ centerZero: {
2237
+ zeroValue: 0,
2238
+ symmetric: true,
2239
+ marker: {
2240
+ visible: true,
2241
+ color: '#111827'
2242
+ }
2243
+ },
2244
+ indicator: {
2245
+ type: 'hairline',
2246
+ color: '#b91c1c',
2247
+ centerOffset: 0
2248
+ },
2249
+ readout: {
2250
+ title: {
2251
+ visible: true,
2252
+ text: 'TRIM'
2253
+ },
2254
+ value: {
2255
+ visible: true,
2256
+ fractionDigits: 0
2257
+ }
2258
+ },
2259
+ accessibility: {
2260
+ label: 'Trim setting'
2261
+ }
2262
+ });
2263
+ </script>
2264
+ </body>
2265
+ </html>
2266
+ ```
2267
+
2268
+ Public linear Y and center offsets are positive-up, not raw SVG coordinates.
2269
+
2270
+ ## 5. Rolling tape gauge
2271
+
2272
+ ```html
2273
+ <!doctype html>
2274
+ <html lang="en">
2275
+ <head>
2276
+ <meta charset="utf-8">
2277
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2278
+ <title>Gaugeit.js rolling tape</title>
2279
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2280
+ <style>
2281
+ #altitude-tape {
2282
+ width: min(100%, 20rem);
2283
+ margin: 2rem auto;
2284
+ }
2285
+ </style>
2286
+ </head>
2287
+ <body>
2288
+ <div id="altitude-tape"></div>
2289
+
2290
+ <script src="./dist/gaugeit.umd.min.js"></script>
2291
+ <script>
2292
+ const altitudeTape = Gaugeit.createGauge('#altitude-tape', {
2293
+ type: 'rolling-tape',
2294
+ min: 0,
2295
+ max: 40000,
2296
+ value: 12500,
2297
+ tape: {
2298
+ orientation: 'vertical',
2299
+ interval: 1000,
2300
+ minorSubdivisions: 4,
2301
+ majorSpacing: 54,
2302
+ indicatorColor: '#b91c1c',
2303
+ colorByZone: true
2304
+ },
2305
+ zones: [
2306
+ { min: 0, max: 10000, color: '#22c55e' },
2307
+ { min: 10000, max: 25000, color: '#facc15' },
2308
+ { min: 25000, max: 40000, color: '#ef4444' }
2309
+ ],
2310
+ readout: {
2311
+ title: {
2312
+ visible: true,
2313
+ text: 'ALTITUDE'
2314
+ },
2315
+ unit: {
2316
+ visible: true,
2317
+ text: 'ft'
2318
+ }
2319
+ },
2320
+ accessibility: {
2321
+ label: 'Altitude',
2322
+ valueText: (value) => `${Math.round(value)} feet`
2323
+ }
2324
+ });
2325
+
2326
+ altitudeTape.setValue(13800, {
2327
+ duration: 1400
2328
+ });
2329
+ </script>
2330
+ </body>
2331
+ </html>
2332
+ ```
2333
+
2334
+ The tape moves behind its fixed reference line. Do not add a radial pointer to this
2335
+ type.
2336
+
2337
+ ## 6. Dynamic value stream
2338
+
2339
+ Use `setValue()` rather than rebuilding options for each sample:
2340
+
2341
+ ```html
2342
+ <!doctype html>
2343
+ <html lang="en">
2344
+ <head>
2345
+ <meta charset="utf-8">
2346
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2347
+ <title>Dynamic Gaugeit.js value</title>
2348
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2349
+ </head>
2350
+ <body>
2351
+ <div id="live-gauge"></div>
2352
+ <button id="stop" type="button">Stop updates</button>
2353
+
2354
+ <script src="./dist/gaugeit.umd.min.js"></script>
2355
+ <script>
2356
+ const gauge = Gaugeit.createGauge('#live-gauge', {
2357
+ type: 'line-scale',
2358
+ min: 0,
2359
+ max: 100,
2360
+ value: 50,
2361
+ animation: {
2362
+ duration: 400
2363
+ },
2364
+ readout: {
2365
+ title: {
2366
+ visible: true,
2367
+ text: 'LIVE VALUE'
2368
+ },
2369
+ value: {
2370
+ visible: true
2371
+ }
2372
+ }
2373
+ });
2374
+
2375
+ const timer = window.setInterval(() => {
2376
+ const nextValue = Math.round(Math.random() * 100);
2377
+ gauge.setValue(nextValue);
2378
+ }, 1000);
2379
+
2380
+ document.querySelector('#stop').addEventListener('click', () => {
2381
+ window.clearInterval(timer);
2382
+ gauge.destroy();
2383
+ }, { once: true });
2384
+ </script>
2385
+ </body>
2386
+ </html>
2387
+ ```
2388
+
2389
+ Application-owned timers remain application-owned and must be cleaned up separately
2390
+ from the gauge.
2391
+
2392
+ ## 7. Multiple gauges on one page
2393
+
2394
+ ```html
2395
+ <!doctype html>
2396
+ <html lang="en">
2397
+ <head>
2398
+ <meta charset="utf-8">
2399
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2400
+ <title>Multiple Gaugeit.js instruments</title>
2401
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2402
+ <style>
2403
+ .gauge-grid {
2404
+ display: grid;
2405
+ grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
2406
+ gap: 1rem;
2407
+ }
2408
+ </style>
2409
+ </head>
2410
+ <body>
2411
+ <div class="gauge-grid">
2412
+ <div id="speed"></div>
2413
+ <div id="temperature"></div>
2414
+ <div id="balance"></div>
2415
+ </div>
2416
+
2417
+ <script src="./dist/gaugeit.umd.min.js"></script>
2418
+ <script>
2419
+ const gauges = [
2420
+ Gaugeit.createGauge('#speed', {
2421
+ type: 'arc',
2422
+ min: 0,
2423
+ max: 240,
2424
+ value: 88,
2425
+ accessibility: { label: 'Speed' }
2426
+ }),
2427
+
2428
+ Gaugeit.createGauge('#temperature', {
2429
+ type: 'classic',
2430
+ min: -20,
2431
+ max: 120,
2432
+ value: 68,
2433
+ accessibility: { label: 'Temperature' }
2434
+ }),
2435
+
2436
+ Gaugeit.createGauge('#balance', {
2437
+ type: 'center-zero',
2438
+ min: -100,
2439
+ max: 100,
2440
+ value: 6,
2441
+ accessibility: { label: 'Balance' }
2442
+ })
2443
+ ];
2444
+
2445
+ window.addEventListener('pagehide', () => {
2446
+ for (const gauge of gauges) {
2447
+ gauge.destroy();
2448
+ }
2449
+ }, { once: true });
2450
+ </script>
2451
+ </body>
2452
+ </html>
2453
+ ```
2454
+
2455
+ Each gauge owns one host. Do not reuse one instance across several hosts.
2456
+
2457
+ ## 8. Threshold warning light
2458
+
2459
+ ```js
2460
+ const gauge = Gaugeit.createGauge('#temperature', {
2461
+ type: 'heritage-round',
2462
+ min: 0,
2463
+ max: 120,
2464
+ value: 72,
2465
+ light: {
2466
+ visible: true,
2467
+ x: 160,
2468
+ y: 88,
2469
+ radius: 8,
2470
+ glowRadius: 24,
2471
+ color: 'red',
2472
+ pulse: false,
2473
+ blink: false,
2474
+ flicker: false,
2475
+ trigger: {
2476
+ mode: 'above',
2477
+ source: 'target',
2478
+ value: 95
2479
+ }
2480
+ },
2481
+ accessibility: {
2482
+ label: 'Coolant temperature'
2483
+ }
2484
+ });
2485
+ ```
2486
+
2487
+ `source: 'target'` makes the warning react to application intent immediately, without
2488
+ waiting for pointer animation.
2489
+
2490
+ ## 9. Pulsing active light
2491
+
2492
+ ```js
2493
+ const gauge = Gaugeit.createGauge('#status', {
2494
+ type: 'classic',
2495
+ value: 50,
2496
+ light: {
2497
+ visible: true,
2498
+ x: 250,
2499
+ y: 55,
2500
+ radius: 8,
2501
+ glowRadius: 26,
2502
+ glowSharpness: 0.15,
2503
+ glowOpacity: 0.2,
2504
+ color: 'green',
2505
+ on: true,
2506
+ glow: true,
2507
+ pulse: true,
2508
+ pulseInterval: 2600,
2509
+ blink: false,
2510
+ flicker: false,
2511
+ trigger: {
2512
+ mode: 'manual'
2513
+ }
2514
+ }
2515
+ });
2516
+ ```
2517
+
2518
+ The light must be active before pulse is visible. `on` controls the state only in
2519
+ manual trigger mode.
2520
+
2521
+ ## 10. Blinking warning light with flicker
2522
+
2523
+ ```js
2524
+ const gauge = Gaugeit.createGauge('#pressure', {
2525
+ type: 'classic',
2526
+ min: 0,
2527
+ max: 10,
2528
+ value: 4.2,
2529
+ light: {
2530
+ visible: true,
2531
+ color: 'amber',
2532
+ radius: 8,
2533
+ glowRadius: 22,
2534
+ glowOpacity: 0.18,
2535
+ pulse: true,
2536
+ blink: true,
2537
+ blinkInterval: 900,
2538
+ flicker: true,
2539
+ flickerIntensity: 0.08,
2540
+ flickerGlowRadius: 0.06,
2541
+ flickerMinInterval: 55,
2542
+ flickerMaxInterval: 150,
2543
+ trigger: {
2544
+ mode: 'below',
2545
+ source: 'displayed',
2546
+ value: 2
2547
+ }
2548
+ }
2549
+ });
2550
+ ```
2551
+
2552
+ Blink takes precedence over pulse. Flicker remains an independent overlay. Browsers
2553
+ requesting reduced motion receive a steady accessible state instead of motion.
2554
+
2555
+ ## 11. React controlled component
2556
+
2557
+ ```jsx
2558
+ import { useMemo, useState } from 'react';
2559
+ import Gaugeit from 'gaugeit.js/react';
2560
+ import 'gaugeit.js/css';
2561
+
2562
+ export default function EngineLoadGauge() {
2563
+ const [value, setValue] = useState(42);
2564
+
2565
+ const options = useMemo(() => ({
2566
+ type: 'arc',
2567
+ min: 0,
2568
+ max: 100,
2569
+ zones: [
2570
+ { min: 0, max: 60, color: '#22c55e' },
2571
+ { min: 60, max: 85, color: '#facc15' },
2572
+ { min: 85, max: 100, color: '#ef4444' }
2573
+ ],
2574
+ readout: {
2575
+ title: {
2576
+ visible: true,
2577
+ text: 'ENGINE LOAD'
2578
+ },
2579
+ value: {
2580
+ visible: true,
2581
+ suffix: '%'
2582
+ }
2583
+ },
2584
+ accessibility: {
2585
+ label: 'Engine load',
2586
+ valueText: (current) => `${Math.round(current)} percent`
2587
+ }
2588
+ }), []);
2589
+
2590
+ return (
2591
+ <section>
2592
+ <Gaugeit
2593
+ value={value}
2594
+ options={options}
2595
+ className="engine-load-gauge"
2596
+ />
2597
+
2598
+ <label>
2599
+ Load
2600
+ <input
2601
+ type="range"
2602
+ min="0"
2603
+ max="100"
2604
+ value={value}
2605
+ onChange={(event) => setValue(Number(event.target.value))}
2606
+ />
2607
+ </label>
2608
+ </section>
2609
+ );
2610
+ }
2611
+ ```
2612
+
2613
+ The explicit `value` prop remains authoritative even if `options` also contains a
2614
+ value. Prefer not to duplicate it.
2615
+
2616
+ ## 12. React ref and imperative API
2617
+
2618
+ ```jsx
2619
+ import { useMemo, useRef } from 'react';
2620
+ import Gaugeit from 'gaugeit.js/react';
2621
+ import 'gaugeit.js/css';
2622
+
2623
+ export default function ImperativeGauge() {
2624
+ const gaugeRef = useRef(null);
2625
+
2626
+ const options = useMemo(() => ({
2627
+ type: 'classic',
2628
+ min: 0,
2629
+ max: 100,
2630
+ value: 25,
2631
+ accessibility: {
2632
+ label: 'Imperative demonstration gauge'
2633
+ }
2634
+ }), []);
2635
+
2636
+ function setWarningValue() {
2637
+ gaugeRef.current?.setValue(90, {
2638
+ duration: 1200,
2639
+ easing: 'easeInOutCubic'
2640
+ });
2641
+ }
2642
+
2643
+ function changeTheme() {
2644
+ gaugeRef.current?.updateOptions({
2645
+ pointer: {
2646
+ color: '#dc2626'
2647
+ },
2648
+ face: {
2649
+ color: '#fff7ed'
2650
+ }
2651
+ });
2652
+ }
2653
+
2654
+ return (
2655
+ <div>
2656
+ <Gaugeit
2657
+ ref={gaugeRef}
2658
+ options={options}
2659
+ />
2660
+
2661
+ <button type="button" onClick={setWarningValue}>
2662
+ Set warning value
2663
+ </button>
2664
+
2665
+ <button type="button" onClick={changeTheme}>
2666
+ Change theme
2667
+ </button>
2668
+ </div>
2669
+ );
2670
+ }
2671
+ ```
2672
+
2673
+ The adapter destroys the live instance on unmount. Do not call `destroy()` manually
2674
+ from ordinary component cleanup unless the component owns a separately created
2675
+ instance outside the adapter.
2676
+
2677
+ ## 13. Laravel Composer asset installation
2678
+
2679
+ Install:
2680
+
2681
+ ```bash
2682
+ composer require kasperi/gaugeit-js:^0.1
2683
+ vendor/bin/gaugeit-install-assets public/assets/vendor/gaugeit
2684
+ ```
2685
+
2686
+ Optional consuming-project automation:
2687
+
2688
+ ```json
2689
+ {
2690
+ "scripts": {
2691
+ "assets:gaugeit": "vendor/bin/gaugeit-install-assets public/assets/vendor/gaugeit",
2692
+ "post-install-cmd": [
2693
+ "@assets:gaugeit"
2694
+ ],
2695
+ "post-update-cmd": [
2696
+ "@assets:gaugeit"
2697
+ ]
2698
+ }
2699
+ }
2700
+ ```
2701
+
2702
+ Blade view:
2703
+
2704
+ ```blade
2705
+ @push('styles')
2706
+ <link
2707
+ rel="stylesheet"
2708
+ href="{{ asset('assets/vendor/gaugeit/gaugeit.min.css') }}"
2709
+ >
2710
+ @endpush
2711
+
2712
+ <section>
2713
+ <h2>Prediction accuracy</h2>
2714
+
2715
+ <div
2716
+ id="accuracy-gauge"
2717
+ data-options='@json([
2718
+ "type" => "arc",
2719
+ "min" => 0,
2720
+ "max" => 100,
2721
+ "value" => $accuracy,
2722
+ "readout" => [
2723
+ "value" => [
2724
+ "visible" => true,
2725
+ "suffix" => "%"
2726
+ ]
2727
+ ],
2728
+ "accessibility" => [
2729
+ "label" => "Prediction accuracy"
2730
+ ]
2731
+ ])'
2732
+ ></div>
2733
+ </section>
2734
+
2735
+ @push('scripts')
2736
+ <script src="{{ asset('assets/vendor/gaugeit/gaugeit.umd.min.js') }}"></script>
2737
+ <script>
2738
+ document.addEventListener('DOMContentLoaded', () => {
2739
+ const host = document.querySelector('#accuracy-gauge');
2740
+ if (!host) return;
2741
+
2742
+ const options = JSON.parse(host.dataset.options);
2743
+ Gaugeit.createGauge(host, options);
2744
+ });
2745
+ </script>
2746
+ @endpush
2747
+ ```
2748
+
2749
+ The package is a browser-asset distribution. PHP does not render the SVG server-side.
2750
+
2751
+ ## 14. Custom element
2752
+
2753
+ ```html
2754
+ <!doctype html>
2755
+ <html lang="en">
2756
+ <head>
2757
+ <meta charset="utf-8">
2758
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2759
+ <title>Gaugeit.js custom element</title>
2760
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2761
+ </head>
2762
+ <body>
2763
+ <gauge-it
2764
+ id="voltage"
2765
+ type="classic"
2766
+ min="0"
2767
+ max="240"
2768
+ value="88"
2769
+ options='{"readout":{"title":{"visible":true,"text":"VOLTAGE"},"unit":{"visible":true,"text":"V"}},"accessibility":{"label":"Voltage"}}'
2770
+ ></gauge-it>
2771
+
2772
+ <button id="increase" type="button">Increase</button>
2773
+
2774
+ <script src="./dist/gaugeit.umd.min.js"></script>
2775
+ <script>
2776
+ Gaugeit.defineGaugeitElement();
2777
+
2778
+ const element = document.querySelector('#voltage');
2779
+
2780
+ document.querySelector('#increase').addEventListener('click', () => {
2781
+ element.value = Number(element.value) + 10;
2782
+ });
2783
+ </script>
2784
+ </body>
2785
+ </html>
2786
+ ```
2787
+
2788
+ Define the element before relying on its upgraded behavior. The `options` attribute
2789
+ must contain valid JSON.
2790
+
2791
+ ## 15. Data-attribute auto mounting
2792
+
2793
+ ```html
2794
+ <!doctype html>
2795
+ <html lang="en">
2796
+ <head>
2797
+ <meta charset="utf-8">
2798
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2799
+ <title>Gaugeit.js auto mounting</title>
2800
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2801
+ </head>
2802
+ <body>
2803
+ <div
2804
+ data-gaugeit
2805
+ data-gaugeit-type="arc"
2806
+ data-gaugeit-min="0"
2807
+ data-gaugeit-max="100"
2808
+ data-gaugeit-value="63"
2809
+ data-gaugeit-options='{"readout":{"value":{"visible":true,"suffix":"%"}},"accessibility":{"label":"Completion"}}'
2810
+ ></div>
2811
+
2812
+ <script src="./dist/gaugeit.umd.min.js"></script>
2813
+ <script>
2814
+ const instances = Gaugeit.autoMount();
2815
+
2816
+ const first = instances[0];
2817
+ first?.setValue(78);
2818
+ </script>
2819
+ </body>
2820
+ </html>
2821
+ ```
2822
+
2823
+ Repeated `autoMount()` calls return existing managed instances instead of duplicating
2824
+ them.
2825
+
2826
+ ## 16. Custom gauge type from built-in layers
2827
+
2828
+ ```html
2829
+ <!doctype html>
2830
+ <html lang="en">
2831
+ <head>
2832
+ <meta charset="utf-8">
2833
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2834
+ <title>Custom Gaugeit.js type</title>
2835
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2836
+ </head>
2837
+ <body>
2838
+ <div id="form-gauge"></div>
2839
+
2840
+ <script src="./dist/gaugeit.umd.min.js"></script>
2841
+ <script>
2842
+ Gaugeit.registerGaugeType('football-form', {
2843
+ description: 'Compact recent-form performance instrument.',
2844
+
2845
+ defaults: {
2846
+ min: 0,
2847
+ max: 100,
2848
+
2849
+ geometry: {
2850
+ width: 280,
2851
+ height: 170,
2852
+ centerX: 140,
2853
+ centerY: 148,
2854
+ radius: 104,
2855
+ startAngle: 200,
2856
+ endAngle: 340
2857
+ },
2858
+
2859
+ track: {
2860
+ radius: 96,
2861
+ width: 22
2862
+ },
2863
+
2864
+ scale: {
2865
+ radius: 96
2866
+ },
2867
+
2868
+ labels: {
2869
+ radius: 72
2870
+ },
2871
+
2872
+ pointer: {
2873
+ type: 'arrow',
2874
+ length: 82,
2875
+ width: 6,
2876
+ color: '#0f172a'
2877
+ },
2878
+
2879
+ readout: {
2880
+ title: {
2881
+ visible: true,
2882
+ text: 'FORM'
2883
+ },
2884
+ value: {
2885
+ visible: true,
2886
+ suffix: '%'
2887
+ }
2888
+ }
2889
+ },
2890
+
2891
+ layers: [
2892
+ Gaugeit.layers.face,
2893
+ Gaugeit.layers.zones,
2894
+ Gaugeit.layers.ticks,
2895
+ Gaugeit.layers.labels,
2896
+ Gaugeit.layers.light,
2897
+ Gaugeit.layers.readout,
2898
+ Gaugeit.layers.pointer
2899
+ ]
2900
+ });
2901
+
2902
+ Gaugeit.createGauge('#form-gauge', {
2903
+ type: 'football-form',
2904
+ value: 74,
2905
+ zones: [
2906
+ { min: 0, max: 40, color: '#ef4444' },
2907
+ { min: 40, max: 70, color: '#facc15' },
2908
+ { min: 70, max: 100, color: '#22c55e' }
2909
+ ],
2910
+ accessibility: {
2911
+ label: 'Recent form'
2912
+ }
2913
+ });
2914
+ </script>
2915
+ </body>
2916
+ </html>
2917
+ ```
2918
+
2919
+ The pointer is placed after the readout in the layer array, so it paints above text.
2920
+
2921
+ ## 17. Custom dynamic layer
2922
+
2923
+ ```js
2924
+ const targetMarkerLayer = {
2925
+ id: 'target-marker',
2926
+
2927
+ render({ options, renderer }) {
2928
+ const group = renderer.group('target-marker', {
2929
+ 'aria-hidden': 'true'
2930
+ });
2931
+
2932
+ const marker = document.createElementNS(
2933
+ 'http://www.w3.org/2000/svg',
2934
+ 'circle'
2935
+ );
2936
+
2937
+ marker.setAttribute('r', String(options.targetMarker.radius));
2938
+ marker.setAttribute('fill', options.targetMarker.color);
2939
+ group.append(marker);
2940
+
2941
+ return {
2942
+ update(value) {
2943
+ const point = Gaugeit.geometry.polarPoint(
2944
+ options.geometry.centerX,
2945
+ options.geometry.centerY,
2946
+ options.targetMarker.distance,
2947
+ Gaugeit.geometry.valueToAngle(value, options)
2948
+ );
2949
+
2950
+ marker.setAttribute('cx', String(point.x));
2951
+ marker.setAttribute('cy', String(point.y));
2952
+ }
2953
+ };
2954
+ }
2955
+ };
2956
+
2957
+ Gaugeit.registerGaugeType('target-marker-gauge', {
2958
+ defaults: {
2959
+ targetMarker: {
2960
+ radius: 4,
2961
+ distance: 102,
2962
+ color: '#2563eb'
2963
+ }
2964
+ },
2965
+
2966
+ layers: [
2967
+ Gaugeit.layers.face,
2968
+ Gaugeit.layers.zones,
2969
+ Gaugeit.layers.ticks,
2970
+ Gaugeit.layers.labels,
2971
+ targetMarkerLayer,
2972
+ Gaugeit.layers.readout,
2973
+ Gaugeit.layers.pointer
2974
+ ]
2975
+ });
2976
+ ```
2977
+
2978
+ Use controller `value` for stable animated display semantics. Use
2979
+ `state.pointerValue` only if the custom artwork should include jitter.
2980
+
2981
+ ## 18. Destroy and recreate safely
2982
+
2983
+ ```html
2984
+ <!doctype html>
2985
+ <html lang="en">
2986
+ <head>
2987
+ <meta charset="utf-8">
2988
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2989
+ <title>Gaugeit.js lifecycle</title>
2990
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
2991
+ </head>
2992
+ <body>
2993
+ <div id="gauge"></div>
2994
+ <button id="recreate" type="button">Recreate</button>
2995
+ <button id="destroy" type="button">Destroy</button>
2996
+
2997
+ <script src="./dist/gaugeit.umd.min.js"></script>
2998
+ <script>
2999
+ const host = document.querySelector('#gauge');
3000
+ let gauge = create();
3001
+
3002
+ function create() {
3003
+ return Gaugeit.createGauge(host, {
3004
+ type: 'arc',
3005
+ min: 0,
3006
+ max: 100,
3007
+ value: 50,
3008
+ accessibility: {
3009
+ label: 'Lifecycle demonstration'
3010
+ }
3011
+ });
3012
+ }
3013
+
3014
+ document.querySelector('#recreate').addEventListener('click', () => {
3015
+ gauge.destroy();
3016
+ gauge = create();
3017
+ });
3018
+
3019
+ document.querySelector('#destroy').addEventListener('click', () => {
3020
+ gauge.destroy();
3021
+ });
3022
+ </script>
3023
+ </body>
3024
+ </html>
3025
+ ```
3026
+
3027
+ `createGauge()` is managed and already destroys an existing managed gauge on the same
3028
+ host. Explicit destruction is shown because it makes ownership clear and is required
3029
+ when the application is removing the component permanently.
3030
+
3031
+ ## 19. Listen for value and animation completion
3032
+
3033
+ ```js
3034
+ const host = document.querySelector('#gauge');
3035
+ const gauge = Gaugeit.createGauge(host, {
3036
+ type: 'arc',
3037
+ value: 10
3038
+ });
3039
+
3040
+ host.addEventListener('gaugeit:change', (event) => {
3041
+ console.log('Requested value:', event.detail.value);
3042
+ });
3043
+
3044
+ host.addEventListener('gaugeit:displaychange', (event) => {
3045
+ console.log('Displayed value:', event.detail.displayedValue);
3046
+ });
3047
+
3048
+ host.addEventListener('gaugeit:settled', (event) => {
3049
+ console.log('Animation settled:', event.detail.displayedValue);
3050
+ });
3051
+
3052
+ gauge.setValue(90);
3053
+ ```
3054
+
3055
+ Use host-scoped namespaced events. Do not install an unscoped global callback API.
3056
+
3057
+ ## 20. Switch complete configurations
3058
+
3059
+ Use `replaceOptions()` when switching instrument modes:
3060
+
3061
+ ```js
3062
+ const gauge = Gaugeit.createGauge('#instrument', {
3063
+ type: 'arc',
3064
+ min: 0,
3065
+ max: 100,
3066
+ value: 40
3067
+ });
3068
+
3069
+ function showTemperature(value) {
3070
+ gauge.replaceOptions({
3071
+ type: 'classic',
3072
+ min: -20,
3073
+ max: 120,
3074
+ value,
3075
+ readout: {
3076
+ title: {
3077
+ visible: true,
3078
+ text: 'TEMPERATURE'
3079
+ },
3080
+ value: {
3081
+ visible: true
3082
+ },
3083
+ unit: {
3084
+ visible: true,
3085
+ text: '°C'
3086
+ }
3087
+ },
3088
+ accessibility: {
3089
+ label: 'Temperature'
3090
+ }
3091
+ });
3092
+ }
3093
+
3094
+ function showBalance(value) {
3095
+ gauge.replaceOptions({
3096
+ type: 'center-zero',
3097
+ min: -100,
3098
+ max: 100,
3099
+ value,
3100
+ readout: {
3101
+ title: {
3102
+ visible: true,
3103
+ text: 'BALANCE'
3104
+ },
3105
+ value: {
3106
+ visible: true
3107
+ }
3108
+ },
3109
+ accessibility: {
3110
+ label: 'Balance'
3111
+ }
3112
+ });
3113
+ }
3114
+ ```
3115
+
3116
+ `updateOptions()` would retain earlier overrides that are omitted from the new mode.
3117
+ `replaceOptions()` resets omitted keys to the new type defaults.
3118
+
3119
+ ## Recipe verification checklist
3120
+
3121
+ Before returning generated integration code, verify:
3122
+
3123
+ - JavaScript distribution matches the environment.
3124
+ - CSS is loaded unless standalone is used.
3125
+ - The global is `Gaugeit`.
3126
+ - Package import paths use lowercase `gaugeit.js`.
3127
+ - The React component is `Gaugeit`.
3128
+ - The target exists before creation.
3129
+ - The selected built-in type name is exact.
3130
+ - Center-zero range contains its zero reference.
3131
+ - Readout child visibility is explicit.
3132
+ - Manual lights use `trigger.mode: 'manual'`.
3133
+ - Blink/pulse precedence is intentional.
3134
+ - Application timers and listeners have cleanup.
3135
+ - Component unmount destroys an owned instance.
3136
+ - CDN URLs pin `0.1.1`.
3137
+ - Accessibility label and value text are meaningful.
3138
+
3139
+ ---
3140
+
3141
+ # Part IV — Contributor architecture and workflow
3142
+
3143
+ This guide explains how to modify and extend Gaugeit.js 0.1.1 without breaking its
3144
+ public API, distribution model, responsive layout, or cross-platform reproducibility.
3145
+
3146
+ Read root `AGENTS.md` first. Existing human-oriented references remain important:
3147
+
3148
+ - `docs/architecture.md`
3149
+ - `docs/creating-gauge-types.md`
3150
+ - `docs/development.md`
3151
+ - `docs/api.md`
3152
+ - `CONTRIBUTING.md`
3153
+
3154
+ ## Architecture summary
3155
+
3156
+ Gaugeit.js separates application state from visual composition:
3157
+
3158
+ ```text
3159
+ public API
3160
+ -> managed instance creation
3161
+ -> Gauge lifecycle and animation core
3162
+ -> option resolution and selected type
3163
+ -> ordered rendering layers
3164
+ -> SVG renderer and dynamic controllers
3165
+ -> generated responsive instrument
3166
+ ```
3167
+
3168
+ The core owns values, animation, jitter, lifecycle, events, accessibility, and cleanup.
3169
+ Types own defaults and ordered layer lists. Layers own SVG. Geometry helpers own
3170
+ deterministic math. This separation is a project invariant.
3171
+
3172
+ ## Canonical source and generated output
3173
+
3174
+ Canonical files:
3175
+
3176
+ ```text
3177
+ src/index.js
3178
+ src/core/
3179
+ src/geometry/
3180
+ src/layers/
3181
+ src/renderer/
3182
+ src/types/
3183
+ src/gaugeit.css
3184
+ src/index.d.ts
3185
+ adapters/
3186
+ bin/
3187
+ scripts/
3188
+ tests/
3189
+ README.md
3190
+ docs/
3191
+ ```
3192
+
3193
+ Generated files:
3194
+
3195
+ ```text
3196
+ dist/gaugeit.cjs
3197
+ dist/gaugeit.cjs.map
3198
+ dist/gaugeit.min.cjs
3199
+ dist/gaugeit.min.cjs.map
3200
+ dist/gaugeit.esm.js
3201
+ dist/gaugeit.esm.js.map
3202
+ dist/gaugeit.esm.min.js
3203
+ dist/gaugeit.esm.min.js.map
3204
+ dist/gaugeit.umd.js
3205
+ dist/gaugeit.umd.js.map
3206
+ dist/gaugeit.umd.min.js
3207
+ dist/gaugeit.umd.min.js.map
3208
+ dist/gaugeit.standalone.js
3209
+ dist/gaugeit.standalone.js.map
3210
+ dist/gaugeit.standalone.min.js
3211
+ dist/gaugeit.standalone.min.js.map
3212
+ dist/gaugeit.css
3213
+ dist/gaugeit.min.css
3214
+ dist/gaugeit.d.ts
3215
+ dist/manifest.json
3216
+ ```
3217
+
3218
+ Never hand-edit generated files. A source change is incomplete until the generated
3219
+ distribution is rebuilt and committed.
3220
+
3221
+ ## Option resolution pipeline
3222
+
3223
+ The runtime resolves options in `src/core/options.js`.
3224
+
3225
+ ### Stage 1: determine type
3226
+
3227
+ The requested `type` is converted to a string and looked up in the registry. Unknown
3228
+ types do not receive a silent fallback because a typo would otherwise hide an
3229
+ integration error.
3230
+
3231
+ ### Stage 2: merge defaults
3232
+
3233
+ Resolution order:
3234
+
3235
+ ```text
3236
+ BASE_DEFAULTS
3237
+ + selected type defaults
3238
+ + raw instance options
3239
+ ```
3240
+
3241
+ The merge rules in `src/core/utils.js` are:
3242
+
3243
+ - plain objects merge recursively;
3244
+ - arrays are replaced;
3245
+ - callback functions are preserved;
3246
+ - unsafe keys `__proto__`, `prototype`, and `constructor` are ignored;
3247
+ - cross-realm plain objects remain accepted.
3248
+
3249
+ ### Stage 3: restore nested shapes
3250
+
3251
+ Malformed nested values are restored from fallback objects. This prevents common
3252
+ configuration mistakes such as:
3253
+
3254
+ ```js
3255
+ { pointer: null }
3256
+ ```
3257
+
3258
+ from crashing later layer code.
3259
+
3260
+ ### Stage 4: optional type configuration
3261
+
3262
+ A type may expose:
3263
+
3264
+ ```js
3265
+ configure(options, {
3266
+ requested,
3267
+ typeDefaults
3268
+ })
3269
+ ```
3270
+
3271
+ This runs after merge and before final validation.
3272
+
3273
+ Use the hook only to derive unspecified fallback values. The resizable built-in linear
3274
+ and tape types use it to scale preset geometry from requested dimensions. Do not
3275
+ overwrite coordinates explicitly provided by the instance.
3276
+
3277
+ ### Stage 5: validate and normalize
3278
+
3279
+ Validation:
3280
+
3281
+ - repairs invalid numeric ranges;
3282
+ - clamps sizes and opacity;
3283
+ - checks enum values;
3284
+ - normalizes zones;
3285
+ - swaps reversed trigger bounds;
3286
+ - derives light glow radius when null;
3287
+ - enforces minimum intervals;
3288
+ - preserves compatibility aliases.
3289
+
3290
+ Do not move ordinary defensive normalization into rendering layers. Layers should
3291
+ receive resolved runtime options.
3292
+
3293
+ ## User option ownership
3294
+
3295
+ `Gauge` stores two related configurations:
3296
+
3297
+ - `userOptions`: original declarative overrides;
3298
+ - `options`: fully resolved runtime configuration.
3299
+
3300
+ `updateOptions(patch)` deep-merges into `userOptions`.
3301
+
3302
+ `replaceOptions(options)` replaces `userOptions` with a fresh object.
3303
+
3304
+ Both re-resolve the selected type, reconfigure lifecycle observers, render static
3305
+ content again, and preserve the current target value unless an explicit new value is
3306
+ provided.
3307
+
3308
+ When adding a public option, consider both patch and replacement semantics.
3309
+
3310
+ ## Type registry
3311
+
3312
+ `GaugeTypeRegistry` stores:
3313
+
3314
+ ```js
3315
+ {
3316
+ name,
3317
+ defaults,
3318
+ layers,
3319
+ description,
3320
+ configure
3321
+ }
3322
+ ```
3323
+
3324
+ Registration validates:
3325
+
3326
+ - non-empty type name;
3327
+ - plain-object definition;
3328
+ - non-empty layer array;
3329
+ - every layer exposes `render(context)`.
3330
+
3331
+ Public registry operations:
3332
+
3333
+ ```js
3334
+ registry.register(name, definition)
3335
+ registry.get(name)
3336
+ registry.has(name)
3337
+ registry.names()
3338
+ registry.describe()
3339
+ ```
3340
+
3341
+ Built-ins are registered in `src/types/register-builtins.js` during module
3342
+ initialization.
3343
+
3344
+ When adding a built-in type:
3345
+
3346
+ 1. create `src/types/<name>.js`;
3347
+ 2. provide description, defaults, layers, and optional configure hook;
3348
+ 3. register it in `register-builtins.js`;
3349
+ 4. expose it through `builtInTypes`;
3350
+ 5. ensure all public colors and dimensions remain overridable;
3351
+ 6. add type-specific tests;
3352
+ 7. update README, API and AI docs;
3353
+ 8. rebuild `dist/`.
3354
+
3355
+ ## Type defaults
3356
+
3357
+ A type should express its visual identity through defaults, not custom branches in the
3358
+ core.
3359
+
3360
+ Prefer:
3361
+
3362
+ ```js
3363
+ export const exampleGaugeType = {
3364
+ description: '...',
3365
+ defaults: {
3366
+ geometry: { ... },
3367
+ face: { ... },
3368
+ pointer: { ... }
3369
+ },
3370
+ layers: [ ... ]
3371
+ };
3372
+ ```
3373
+
3374
+ Avoid:
3375
+
3376
+ ```js
3377
+ if (options.type === 'example') {
3378
+ // special behavior in the generic core
3379
+ }
3380
+ ```
3381
+
3382
+ A new geometry family may require new pure helpers and layers, but the lifecycle core
3383
+ should remain type-agnostic.
3384
+
3385
+ ## Layer execution and paint order
3386
+
3387
+ A layer has the public shape:
3388
+
3389
+ ```js
3390
+ {
3391
+ id: 'layer-name',
3392
+
3393
+ render(context) {
3394
+ // append static SVG
3395
+
3396
+ return {
3397
+ update(value, state) {
3398
+ // optional dynamic work
3399
+ },
3400
+
3401
+ destroy() {
3402
+ // optional cleanup
3403
+ }
3404
+ };
3405
+ }
3406
+ }
3407
+ ```
3408
+
3409
+ Render context contains:
3410
+
3411
+ ```js
3412
+ {
3413
+ gauge,
3414
+ options,
3415
+ renderer,
3416
+ registry
3417
+ }
3418
+ ```
3419
+
3420
+ Layer-array order is SVG paint order. Later groups appear above earlier groups.
3421
+
3422
+ Built-in radial types intentionally order:
3423
+
3424
+ ```text
3425
+ face
3426
+ zones
3427
+ ticks
3428
+ labels
3429
+ light
3430
+ readout
3431
+ pointer
3432
+ ```
3433
+
3434
+ so the pointer paints above the readout.
3435
+
3436
+ Built-in linear types place the indicator after readout for the same reason.
3437
+
3438
+ Do not attempt to solve SVG stacking with a numeric CSS `z-index`. Use layer order and
3439
+ the existing pointer/indicator cap `stackOrder`.
3440
+
3441
+ ## Static versus dynamic work
3442
+
3443
+ Static layer examples:
3444
+
3445
+ - face;
3446
+ - zones;
3447
+ - ticks;
3448
+ - labels;
3449
+ - fixed zero marker.
3450
+
3451
+ Dynamic layer examples:
3452
+
3453
+ - radial pointer;
3454
+ - linear indicator;
3455
+ - rolling tape slots;
3456
+ - readout value;
3457
+ - light state.
3458
+
3459
+ Static geometry should be computed once during `render()`. `update()` should mutate
3460
+ only the smallest owned set of nodes.
3461
+
3462
+ Avoid rebuilding whole SVG groups in every animation frame.
3463
+
3464
+ ## Renderer ownership
3465
+
3466
+ `SvgRenderer`:
3467
+
3468
+ - creates the generated root and SVG;
3469
+ - creates unique IDs for definitions;
3470
+ - owns layer groups;
3471
+ - owns controller registration;
3472
+ - owns a small reference map;
3473
+ - applies content clipping;
3474
+ - calculates and applies final viewBox;
3475
+ - removes generated content on destruction.
3476
+
3477
+ Use:
3478
+
3479
+ ```js
3480
+ const group = renderer.group('my-layer', {
3481
+ 'aria-hidden': 'true'
3482
+ });
3483
+ ```
3484
+
3485
+ instead of appending directly to the host or another application-owned node.
3486
+
3487
+ The renderer may apply a face clip path to groups created after the face layer. Bypass
3488
+ of `renderer.group()` can break clipping and ownership.
3489
+
3490
+ ## Renderer references
3491
+
3492
+ A layer may store generated nodes or values:
3493
+
3494
+ ```js
3495
+ renderer.setReference('name', value);
3496
+ const value = renderer.getReference('name');
3497
+ ```
3498
+
3499
+ Use references sparingly for coordination within one generated instrument. Do not use
3500
+ global registries for per-instance SVG nodes.
3501
+
3502
+ ## Dynamic controller state
3503
+
3504
+ Controller signature:
3505
+
3506
+ ```js
3507
+ update(value, state)
3508
+ ```
3509
+
3510
+ `value` is the rendered base value after animation and precision handling.
3511
+
3512
+ State fields:
3513
+
3514
+ ```js
3515
+ {
3516
+ gauge,
3517
+ targetValue,
3518
+ baseValue,
3519
+ pointerValue,
3520
+ animated,
3521
+ jitterOffset
3522
+ }
3523
+ ```
3524
+
3525
+ Choose intentionally:
3526
+
3527
+ - `targetValue`: stable application intent;
3528
+ - `value` or `baseValue`: animated displayed semantics;
3529
+ - `pointerValue`: animated value plus visual jitter.
3530
+
3531
+ Readouts and accessibility should not use `pointerValue`. A realistic moving needle or
3532
+ tape should.
3533
+
3534
+ The shared light exposes this decision as `light.trigger.source`.
3535
+
3536
+ ## Gauge lifecycle
3537
+
3538
+ Construction:
3539
+
3540
+ 1. resolve target;
3541
+ 2. resolve user options;
3542
+ 3. create renderer;
3543
+ 4. establish initial target/base/display/pointer state;
3544
+ 5. render static layers;
3545
+ 6. install lifecycle observers;
3546
+ 7. perform startup animation if enabled.
3547
+
3548
+ ### `setValue()`
3549
+
3550
+ - normalizes according to overflow;
3551
+ - updates target and resolved value;
3552
+ - updates immediately or creates one animation;
3553
+ - emits `gaugeit:change`;
3554
+ - schedules the frame loop only when needed.
3555
+
3556
+ ### Frame loop
3557
+
3558
+ One `requestAnimationFrame` loop per active gauge:
3559
+
3560
+ 1. advance base animation;
3561
+ 2. choose a new jitter target at configured frequency;
3562
+ 3. smooth jitter offset;
3563
+ 4. calculate displayed and pointer-only values;
3564
+ 5. update dynamic controllers;
3565
+ 6. update accessibility and display events;
3566
+ 7. stop when no animation or jitter remains.
3567
+
3568
+ ### Pause and lifecycle suspension
3569
+
3570
+ Manual `pause()` and `resume()` preserve elapsed animation time.
3571
+
3572
+ Page visibility and intersection observers suspend work when configured. Option
3573
+ replacement reconfigures observers.
3574
+
3575
+ ### Destruction
3576
+
3577
+ `destroy()` must:
3578
+
3579
+ - cancel animation frame;
3580
+ - clear animation;
3581
+ - disconnect observers;
3582
+ - remove listeners;
3583
+ - call controller cleanup;
3584
+ - remove generated DOM;
3585
+ - remove managed-instance mapping;
3586
+ - emit destruction event.
3587
+
3588
+ Any new layer timer, observer, or listener must be released by its controller
3589
+ `destroy()`.
3590
+
3591
+ ## Animation model
3592
+
3593
+ Application value and visual realism are separate:
3594
+
3595
+ ```text
3596
+ target value
3597
+ -> base animation
3598
+ -> displayed value
3599
+ -> optional jitter offset
3600
+ -> pointer value
3601
+ ```
3602
+
3603
+ This ensures:
3604
+
3605
+ - `getValue()` is stable;
3606
+ - `getDisplayedValue()` may animate but never jitters;
3607
+ - readout and ARIA values never jitter;
3608
+ - meaningful events never jitter;
3609
+ - moving artwork may jitter.
3610
+
3611
+ Do not merge jitter into the application value.
3612
+
3613
+ ## Easing
3614
+
3615
+ Named easing resolution belongs in `src/core/easing.js`. Public `easingNames` expose
3616
+ supported names. A caller may also provide a callback.
3617
+
3618
+ When adding an easing:
3619
+
3620
+ - keep input/output semantics compatible with progress `0 ... 1`;
3621
+ - update tests and documentation;
3622
+ - do not add an animation dependency.
3623
+
3624
+ ## Radial geometry
3625
+
3626
+ Primary helpers in `src/geometry/scale.js` include:
3627
+
3628
+ ```text
3629
+ degreesToRadians
3630
+ polarPoint
3631
+ gaugeSweep
3632
+ centeredArcAngles
3633
+ valueToAngle
3634
+ angleToValue
3635
+ arcPath
3636
+ taperedArcPath
3637
+ niceInterval
3638
+ buildScaleValues
3639
+ buildMinorValues
3640
+ resolveZoneSegments
3641
+ findZoneForValue
3642
+ ```
3643
+
3644
+ Important invariants:
3645
+
3646
+ - clockwise and counter-clockwise sweeps work;
3647
+ - sweep is limited to one full revolution;
3648
+ - complete circles use two SVG arc commands;
3649
+ - closed scales may deduplicate one endpoint;
3650
+ - inversion changes mapping, not numeric zone definitions;
3651
+ - later overlapping zones win;
3652
+ - transparent intervals remain true gaps;
3653
+ - tapered zones preserve width progression across split intervals.
3654
+
3655
+ Keep pure helpers DOM-independent.
3656
+
3657
+ ## Linear geometry
3658
+
3659
+ Primary helpers in `src/geometry/linear.js` include:
3660
+
3661
+ ```text
3662
+ linearYToSvg
3663
+ linearAxis
3664
+ valueToLinearRatio
3665
+ valueToLinearPoint
3666
+ linearPointToValue
3667
+ offsetLinearPoint
3668
+ linearNormalOffset
3669
+ linearIndicatorNormalOffset
3670
+ linearNormalSpan
3671
+ ```
3672
+
3673
+ Public Y semantics are positive-up around `linear.originY`.
3674
+
3675
+ The two-point axis supports horizontal, vertical, and diagonal instruments. Do not
3676
+ assume a horizontal axis in new layers.
3677
+
3678
+ Use tangent and normal vectors from `linearAxis()` instead of special-casing
3679
+ orientation.
3680
+
3681
+ ## Rolling-tape geometry
3682
+
3683
+ Primary helpers in `src/geometry/tape.js` include:
3684
+
3685
+ ```text
3686
+ tapeWindowBox
3687
+ resolveTapeInterval
3688
+ tapeMinorStep
3689
+ tapeMinorSpacing
3690
+ tapeValueOffset
3691
+ ```
3692
+
3693
+ The rolling-tape layer uses a bounded recyclable slot set around the current value.
3694
+ Never render one node per possible numeric value.
3695
+
3696
+ The fixed indicator remains stationary. The strip and labels move.
3697
+
3698
+ ## Light geometry and timing
3699
+
3700
+ Pure helpers in `src/geometry/light.js` resolve:
3701
+
3702
+ - logical position;
3703
+ - manual, below, above, and between trigger state.
3704
+
3705
+ The layer owns:
3706
+
3707
+ - palettes;
3708
+ - gradients and blur;
3709
+ - physical unlit lens;
3710
+ - active lens;
3711
+ - halo;
3712
+ - blink/pulse CSS state;
3713
+ - randomized flicker timer;
3714
+ - controller cleanup.
3715
+
3716
+ Version 0.1.1 separates:
3717
+
3718
+ - physical `radius`;
3719
+ - halo `glowRadius`;
3720
+ - lens `opacity`;
3721
+ - halo `glowOpacity`;
3722
+ - halo `glowSharpness`.
3723
+
3724
+ Bounds must reserve the rendered halo plus blur. If rendering math changes, update
3725
+ `includeLight()` in `src/geometry/bounds.js` and tests.
3726
+
3727
+ Blink wins over pulse. Flicker may overlay either. All moving effects respect reduced
3728
+ motion.
3729
+
3730
+ ## Intrinsic bounds
3731
+
3732
+ Bounds are a correctness feature, not cosmetic metadata.
3733
+
3734
+ `calculateGaugeBounds()` conservatively includes:
3735
+
3736
+ - configured logical geometry when requested;
3737
+ - face and shadow;
3738
+ - track and zones;
3739
+ - ticks;
3740
+ - labels;
3741
+ - pointer or indicator travel;
3742
+ - zero marker;
3743
+ - rolling-tape window;
3744
+ - readout text estimates;
3745
+ - light hardware, halo, and blur.
3746
+
3747
+ The renderer may union deterministic bounds with `getBBox()` when available.
3748
+
3749
+ When adding a visual extent:
3750
+
3751
+ 1. identify every position it may occupy;
3752
+ 2. include stroke width, blur, cap, shadow, and animation variation;
3753
+ 3. update deterministic bounds;
3754
+ 4. test finite responsive layout;
3755
+ 5. verify no clipping in intrinsic and contain modes.
3756
+
3757
+ Do not rely solely on browser `getBBox()` because Node tests and some environments do
3758
+ not provide it.
3759
+
3760
+ ## Face clipping
3761
+
3762
+ A face with `clipContent: true` defines a reusable inner-bezel clip path. Subsequent
3763
+ groups inherit it through the renderer.
3764
+
3765
+ Content-fitted faces calculate their size from conservative content bounds before the
3766
+ clip is applied.
3767
+
3768
+ When adding a new custom layer to a clipped type:
3769
+
3770
+ - create it with `renderer.group()`;
3771
+ - place it after the face layer;
3772
+ - do not let intentionally clipped geometry inflate document layout beyond the face.
3773
+
3774
+ ## Accessibility
3775
+
3776
+ The generated root is a meter:
3777
+
3778
+ ```text
3779
+ role="meter"
3780
+ aria-label
3781
+ aria-valuemin
3782
+ aria-valuemax
3783
+ aria-valuenow
3784
+ aria-valuetext, when configured
3785
+ ```
3786
+
3787
+ The SVG also receives title and optional description nodes.
3788
+
3789
+ New meaningful visual state must be represented by existing value semantics or an
3790
+ additional accessible description. Decorative layers should be `aria-hidden`.
3791
+
3792
+ Do not use visual jitter, flicker, or animation as the only carrier of information.
3793
+
3794
+ ## DOM events
3795
+
3796
+ Events emitted from the host:
3797
+
3798
+ ```text
3799
+ gaugeit:render
3800
+ gaugeit:change
3801
+ gaugeit:displaychange
3802
+ gaugeit:animationstart
3803
+ gaugeit:animationend
3804
+ gaugeit:settled
3805
+ gaugeit:typechange
3806
+ gaugeit:pause
3807
+ gaugeit:resume
3808
+ gaugeit:destroy
3809
+ ```
3810
+
3811
+ Keep event names namespaced. When changing detail payloads, consider compatibility and
3812
+ update declarations/docs/tests where applicable.
3813
+
3814
+ ## Auto mounting and managed instances
3815
+
3816
+ `createGauge()` and `autoMount()` use managed instance ownership. A new managed gauge
3817
+ on the same host destroys the old one. Repeated auto-mounting returns the existing
3818
+ instance.
3819
+
3820
+ Keep the WeakMap lifecycle safe:
3821
+
3822
+ - a destroyed instance must remove itself;
3823
+ - an old replaced instance must not remove a newer mapping;
3824
+ - custom element disconnection destroys its instance.
3825
+
3826
+ ## React adapter boundary
3827
+
3828
+ The React adapter:
3829
+
3830
+ - imports the ESM distribution;
3831
+ - creates one gauge on mount;
3832
+ - replaces options when prop identity changes;
3833
+ - applies explicit value changes separately;
3834
+ - forwards the live instance;
3835
+ - destroys on unmount.
3836
+
3837
+ The core must never import React.
3838
+
3839
+ When changing adapter behavior:
3840
+
3841
+ - preserve browser-ready ESM output;
3842
+ - update `adapters/react/Gaugeit.d.ts`;
3843
+ - test controlled values and forwarded ref;
3844
+ - avoid stale-instance cleanup races.
3845
+
3846
+ ## Composer boundary
3847
+
3848
+ The Composer package distributes prebuilt browser assets and one PHP command-line
3849
+ installer.
3850
+
3851
+ The installer:
3852
+
3853
+ - supports default, standalone, and all modes;
3854
+ - creates destination directories;
3855
+ - handles spaces in paths;
3856
+ - replaces assets atomically through temporary files;
3857
+ - cleans only known assets;
3858
+ - preserves unrelated files;
3859
+ - returns meaningful exit codes.
3860
+
3861
+ Do not add automatic public-directory writes to dependency installation.
3862
+
3863
+ ## Adding a new option
3864
+
3865
+ Use this checklist:
3866
+
3867
+ 1. Add the base default in `src/core/defaults.js` unless it is type-only.
3868
+ 2. Add validation and normalization in `src/core/options.js`.
3869
+ 3. Add TypeScript declaration in `src/index.d.ts`.
3870
+ 4. Consume the resolved value in the correct layer, geometry, renderer, or core.
3871
+ 5. Update bounds if visual extents change.
3872
+ 6. Update CSS if the option controls styling.
3873
+ 7. Add focused option tests.
3874
+ 8. Add rendering/lifecycle tests.
3875
+ 9. Update `docs/api.md`, README examples, and `docs/ai/options-reference.md`.
3876
+ 10. Rebuild all distributions.
3877
+ 11. Run `npm run check`.
3878
+ 12. Verify clean reproducibility.
3879
+
3880
+ For compatibility, use explicit aliases only when preserving an established public
3881
+ contract. Do not create undocumented aliases for convenience.
3882
+
3883
+ ## Adding a new layer
3884
+
3885
+ 1. Decide whether the layer is static or dynamic.
3886
+ 2. Create `src/layers/<name>.js`.
3887
+ 3. Use `renderer.group()`.
3888
+ 4. Use `svgElement()` or `createElementNS`; never `innerHTML`.
3889
+ 5. Return a controller only if dynamic state exists.
3890
+ 6. Clean all owned resources in controller `destroy()`.
3891
+ 7. Add pure geometry helpers if calculations can be DOM-independent.
3892
+ 8. Add bounds support.
3893
+ 9. Export through `src/index.js` if intended for custom types.
3894
+ 10. Add declarations when public.
3895
+ 11. Insert into type arrays at the intended paint position.
3896
+ 12. Add unit and DOM tests.
3897
+ 13. Document usage.
3898
+
3899
+ ## Adding a new gauge type
3900
+
3901
+ 1. Reuse an existing geometry family if possible.
3902
+ 2. Express visual identity in defaults.
3903
+ 3. Add a `configure()` hook only for derived fallback geometry.
3904
+ 4. Select layers in exact paint order.
3905
+ 5. Include the shared light layer if normal annunciator support is desired.
3906
+ 6. Keep all colors and principal dimensions overridable.
3907
+ 7. Register and expose the type.
3908
+ 8. Test default rendering, option override, finite bounds, and distribution behavior.
3909
+ 9. Add screenshot/example/documentation.
3910
+ 10. Rebuild and validate.
3911
+
3912
+ ## TypeScript declarations
3913
+
3914
+ `src/index.d.ts` is canonical for the core package API.
3915
+ `adapters/react/Gaugeit.d.ts` is canonical for React props and the forwarded ref.
3916
+
3917
+ Update `src/index.d.ts` when changing:
3918
+
3919
+ - option keys;
3920
+ - accepted enum values;
3921
+ - public methods;
3922
+ - event-facing types if added;
3923
+ - layer contexts;
3924
+ - geometry exports;
3925
+ - registry definitions.
3926
+
3927
+ Update `adapters/react/Gaugeit.d.ts` when changing React props or the forwarded ref
3928
+ type.
3929
+
3930
+ Run:
3931
+
3932
+ ```bash
3933
+ npm run typecheck
3934
+ ```
3935
+
3936
+ The build copies declarations into `dist/gaugeit.d.ts`. Do not edit that copy.
3937
+
3938
+ ## Test selection
3939
+
3940
+ Use focused tests while iterating.
3941
+
3942
+ | Change | Likely tests |
3943
+ | --- | --- |
3944
+ | defaults or normalization | `tests/options.test.js` |
3945
+ | radial geometry | `tests/geometry.test.js`, `tests/zones.test.js` |
3946
+ | linear behavior | `tests/linear.test.js`, `tests/advanced-types.test.js` |
3947
+ | light behavior | `tests/light.test.js`, `tests/advanced-types.test.js` |
3948
+ | lifecycle | `tests/lifecycle.test.js` |
3949
+ | registry/type exposure | `tests/registry.test.js` |
3950
+ | CSS hooks | `tests/css.test.js` |
3951
+ | output inventory/build | `tests/distribution.test.js` |
3952
+ | npm/Composer metadata | `tests/package-metadata.test.js` |
3953
+ | React adapter | `tests/react-adapter.test.js` |
3954
+ | examples | `tests/example-page.test.js`, `tests/framework-examples.test.js` |
3955
+ | repository rules | `tests/repository-hygiene.test.js` |
3956
+
3957
+ The main Node test command includes `tests/*.test.js` with concurrency one:
3958
+
3959
+ ```bash
3960
+ npm test
3961
+ ```
3962
+
3963
+ DOM smoke:
3964
+
3965
+ ```bash
3966
+ npm run test:browser
3967
+ ```
3968
+
3969
+ Optional real Chromium smoke:
3970
+
3971
+ ```bash
3972
+ npm run test:chromium
3973
+ ```
3974
+
3975
+ ## Build output
3976
+
3977
+ Run:
3978
+
3979
+ ```bash
3980
+ npm run clean
3981
+ npm run build
3982
+ ```
3983
+
3984
+ or use the full:
3985
+
3986
+ ```bash
3987
+ npm run check
3988
+ ```
3989
+
3990
+ The build must remain deterministic:
3991
+
3992
+ - stable file ordering;
3993
+ - no timestamps in generated content;
3994
+ - no random output;
3995
+ - normalized LF inputs;
3996
+ - no absolute local paths;
3997
+ - same source-map contents on Linux and Windows;
3998
+ - manifest sizes matching files.
3999
+
4000
+ CI performs:
4001
+
4002
+ ```bash
4003
+ npm ci
4004
+ npm run check
4005
+ git diff --exit-code -- dist package-lock.json
4006
+ ```
4007
+
4008
+ A build that passes locally but changes generated source maps on Linux is not complete.
4009
+
4010
+ ## Documentation changes
4011
+
4012
+ Update only the smallest set of authoritative documents, but do not leave contradictory
4013
+ copies.
4014
+
4015
+ When changing public options:
4016
+
4017
+ ```text
4018
+ README.md
4019
+ docs/api.md
4020
+ src/index.d.ts
4021
+ docs/ai/options-reference.md
4022
+ relevant recipes and troubleshooting
4023
+ ```
4024
+
4025
+ When changing architecture:
4026
+
4027
+ ```text
4028
+ docs/architecture.md
4029
+ docs/creating-gauge-types.md
4030
+ docs/ai/contributor-guide.md
4031
+ AGENTS.md, if repository rules changed
4032
+ ```
4033
+
4034
+ `docs/ai/ai-full.md` is the single-file context bundle and must be regenerated or
4035
+ updated whenever its component AI guides change.
4036
+
4037
+ ## Release-aware changes
4038
+
4039
+ Do not rewrite an already published npm version. A released version is immutable.
4040
+
4041
+ For a new release:
4042
+
4043
+ - update package version and public version constant consistently;
4044
+ - update changelog;
4045
+ - update exact CDN examples;
4046
+ - rebuild `dist/`;
4047
+ - run all validation;
4048
+ - create and push the Git tag only after green CI;
4049
+ - publish npm;
4050
+ - update Packagist from the Git tag.
4051
+
4052
+ Release mechanics remain explicit maintainer actions. CI validates but does not publish.
4053
+
4054
+ ## Definition of done for contributors
4055
+
4056
+ A coding agent may say a task is complete only when:
4057
+
4058
+ - behavior is implemented in canonical source;
4059
+ - public compatibility is preserved or intentionally documented;
4060
+ - focused tests and regression tests pass;
4061
+ - full `npm run check` passes;
4062
+ - declarations match runtime;
4063
+ - docs and examples match behavior;
4064
+ - visual bounds cover new extents;
4065
+ - cleanup covers all new resources;
4066
+ - generated distribution is rebuilt;
4067
+ - reproducibility diff is clean;
4068
+ - no accidental files or unrelated edits are present.
4069
+
4070
+ ---
4071
+
4072
+ # Part V — Troubleshooting
4073
+
4074
+ This guide diagnoses common Gaugeit.js 0.1.1 integration, rendering, lifecycle,
4075
+ packaging, and contribution failures. Start from the observed symptom. Do not guess at
4076
+ new API names to work around a configuration mistake.
4077
+
4078
+ ## Gauge does not appear
4079
+
4080
+ ### Check that the host exists
4081
+
4082
+ This fails when creation runs before the element exists:
4083
+
4084
+ ```js
4085
+ Gaugeit.createGauge('#gauge', {
4086
+ value: 50
4087
+ });
4088
+ ```
4089
+
4090
+ Verify:
4091
+
4092
+ ```js
4093
+ const host = document.querySelector('#gauge');
4094
+
4095
+ if (!host) {
4096
+ throw new Error('Gauge host was not found.');
4097
+ }
4098
+
4099
+ const gauge = Gaugeit.createGauge(host, {
4100
+ value: 50
4101
+ });
4102
+ ```
4103
+
4104
+ Place the script after the host, use `defer`, use a module script, or wait for
4105
+ `DOMContentLoaded`.
4106
+
4107
+ ### Check the selector
4108
+
4109
+ `createGauge()` accepts an element or valid CSS selector. A selector typo throws a
4110
+ target-not-found error. Do not pass a React component, jQuery wrapper, NodeList, or
4111
+ plain object.
4112
+
4113
+ ### Check host layout
4114
+
4115
+ The SVG is responsive to the host. Verify that the host and its ancestors are not
4116
+ collapsed, hidden by CSS, or assigned zero width/height.
4117
+
4118
+ For contain mode, provide both dimensions:
4119
+
4120
+ ```css
4121
+ #gauge {
4122
+ width: 320px;
4123
+ height: 220px;
4124
+ }
4125
+ ```
4126
+
4127
+ ```js
4128
+ layout: {
4129
+ mode: 'contain'
4130
+ }
4131
+ ```
4132
+
4133
+ ### Check CSS
4134
+
4135
+ UMD, ESM, CommonJS, React, custom element, and Composer two-file usage require Gaugeit
4136
+ CSS.
4137
+
4138
+ Browser:
4139
+
4140
+ ```html
4141
+ <link rel="stylesheet" href="./dist/gaugeit.min.css">
4142
+ ```
4143
+
4144
+ npm:
4145
+
4146
+ ```js
4147
+ import 'gaugeit.js/css';
4148
+ ```
4149
+
4150
+ Standalone already injects CSS. Missing CSS may produce incorrect sizing or animation
4151
+ behavior even when SVG nodes exist.
4152
+
4153
+ ## `Gaugeit is not defined`
4154
+
4155
+ Possible causes:
4156
+
4157
+ - UMD/standalone script failed to load;
4158
+ - application script executes before the distribution;
4159
+ - wrong global spelling;
4160
+ - ESM file was loaded as a classic script;
4161
+ - a CDN URL is wrong.
4162
+
4163
+ Correct UMD order:
4164
+
4165
+ ```html
4166
+ <script src="./dist/gaugeit.umd.min.js"></script>
4167
+ <script>
4168
+ Gaugeit.createGauge('#gauge', {
4169
+ value: 50
4170
+ });
4171
+ </script>
4172
+ ```
4173
+
4174
+ The global is exactly:
4175
+
4176
+ ```text
4177
+ Gaugeit
4178
+ ```
4179
+
4180
+ Not:
4181
+
4182
+ ```text
4183
+ GaugeIt
4184
+ GaugeIT
4185
+ gaugeit
4186
+ ```
4187
+
4188
+ ## `Gaugeit` versus `GaugeIt` filename or symbol confusion
4189
+
4190
+ Project files and exports intentionally use:
4191
+
4192
+ ```text
4193
+ Gaugeit
4194
+ ```
4195
+
4196
+ Examples:
4197
+
4198
+ ```text
4199
+ adapters/react/Gaugeit.js
4200
+ import Gaugeit from 'gaugeit.js/react'
4201
+ window.Gaugeit
4202
+ ```
4203
+
4204
+ Windows may hide case-only filename mistakes that fail on Linux CI. Preserve exact
4205
+ case in imports and package exports.
4206
+
4207
+ ## ESM import fails in a browser
4208
+
4209
+ This bare import does not work in a normal browser without a resolver:
4210
+
4211
+ ```js
4212
+ import { createGauge } from 'gaugeit.js';
4213
+ ```
4214
+
4215
+ Use one of:
4216
+
4217
+ - a bundler;
4218
+ - an import map;
4219
+ - a server resolver;
4220
+ - an explicit browser path:
4221
+
4222
+ ```js
4223
+ import { createGauge } from './dist/gaugeit.esm.js';
4224
+ ```
4225
+
4226
+ Load the file as a module:
4227
+
4228
+ ```html
4229
+ <script type="module" src="./app.js"></script>
4230
+ ```
4231
+
4232
+ Do not load ESM as a classic script.
4233
+
4234
+ ## CommonJS works but gauge is unstyled
4235
+
4236
+ The CommonJS entry resolves JavaScript only:
4237
+
4238
+ ```js
4239
+ const { createGauge } = require('gaugeit.js');
4240
+ ```
4241
+
4242
+ Add the CSS to the application bundle or page. The `require()` call does not inject it.
4243
+
4244
+ ## Standalone and separate CSS are both loaded
4245
+
4246
+ This is usually unnecessary. Standalone injects the canonical stylesheet. Loading
4247
+ separate Gaugeit CSS as well can make debugging overrides harder.
4248
+
4249
+ Choose either:
4250
+
4251
+ ```text
4252
+ UMD + CSS
4253
+ ```
4254
+
4255
+ or:
4256
+
4257
+ ```text
4258
+ standalone JavaScript
4259
+ ```
4260
+
4261
+ unless duplicate inclusion is intentional and understood.
4262
+
4263
+ ## CDN installation changes unexpectedly
4264
+
4265
+ Use an exact version:
4266
+
4267
+ ```text
4268
+ gaugeit.js@0.1.1
4269
+ ```
4270
+
4271
+ Do not use an unversioned URL in production. Verify the complete file path:
4272
+
4273
+ ```text
4274
+ dist/gaugeit.umd.min.js
4275
+ dist/gaugeit.min.css
4276
+ dist/gaugeit.standalone.min.js
4277
+ ```
4278
+
4279
+ CDN propagation may take a short time after npm publication. Check the npm package
4280
+ first when a newly published CDN file is missing.
4281
+
4282
+ ## npm import path is rejected
4283
+
4284
+ Use package exports:
4285
+
4286
+ ```js
4287
+ import { createGauge } from 'gaugeit.js';
4288
+ import 'gaugeit.js/css';
4289
+ import Gaugeit from 'gaugeit.js/react';
4290
+ import { defineGaugeitElement } from 'gaugeit.js/web-component';
4291
+ ```
4292
+
4293
+ Do not import private repository paths such as:
4294
+
4295
+ ```js
4296
+ import { createGauge } from 'gaugeit.js/src/index.js';
4297
+ ```
4298
+
4299
+ Package export maps intentionally limit supported subpaths.
4300
+
4301
+ ## React gauge keeps rebuilding
4302
+
4303
+ The React adapter replaces the complete options object when its identity changes.
4304
+ This creates a new object every render:
4305
+
4306
+ ```jsx
4307
+ <Gaugeit options={{ type: 'arc', min: 0, max: 100 }} />
4308
+ ```
4309
+
4310
+ Prefer:
4311
+
4312
+ ```jsx
4313
+ const options = useMemo(() => ({
4314
+ type: 'arc',
4315
+ min: 0,
4316
+ max: 100
4317
+ }), []);
4318
+
4319
+ return <Gaugeit options={options} />;
4320
+ ```
4321
+
4322
+ A changing options object is valid when the configuration truly changes. Stabilization
4323
+ is a performance and lifecycle decision, not a requirement for correctness.
4324
+
4325
+ ## React value does not match `options.value`
4326
+
4327
+ An explicit `value` prop is authoritative:
4328
+
4329
+ ```jsx
4330
+ <Gaugeit
4331
+ value={72}
4332
+ options={{
4333
+ value: 10
4334
+ }}
4335
+ />
4336
+ ```
4337
+
4338
+ The displayed target is 72. Avoid duplicating value in both places.
4339
+
4340
+ ## React ref is null
4341
+
4342
+ The forwarded ref becomes the live instance after mount. Do not read it during initial
4343
+ render. Use it from effects or event handlers:
4344
+
4345
+ ```jsx
4346
+ const gaugeRef = useRef(null);
4347
+
4348
+ function update() {
4349
+ gaugeRef.current?.setValue(80);
4350
+ }
4351
+ ```
4352
+
4353
+ ## Gauge updates inefficiently
4354
+
4355
+ For numeric samples, use:
4356
+
4357
+ ```js
4358
+ gauge.setValue(nextValue);
4359
+ ```
4360
+
4361
+ Do not repeatedly do:
4362
+
4363
+ ```js
4364
+ gauge.updateOptions({
4365
+ value: nextValue
4366
+ });
4367
+ ```
4368
+
4369
+ unless static configuration also needs rebuilding.
4370
+
4371
+ ## `updateOptions()` leaves old styling behind
4372
+
4373
+ `updateOptions()` deep-merges a patch. Omitted keys remain from earlier user options.
4374
+
4375
+ Use `replaceOptions()` for a complete declarative mode switch:
4376
+
4377
+ ```js
4378
+ gauge.replaceOptions({
4379
+ type: 'classic',
4380
+ value: 50
4381
+ });
4382
+ ```
4383
+
4384
+ ## Zone list does not append
4385
+
4386
+ Arrays are replaced, not concatenated:
4387
+
4388
+ ```js
4389
+ gauge.updateOptions({
4390
+ zones: newZones
4391
+ });
4392
+ ```
4393
+
4394
+ replaces the previous list. Build the complete desired array in application code.
4395
+
4396
+ ## Transparent zone still shows a track
4397
+
4398
+ Check:
4399
+
4400
+ ```js
4401
+ track: {
4402
+ showUnderZones: false
4403
+ }
4404
+ ```
4405
+
4406
+ A transparent or missing zone color produces a true gap only when the base track is not
4407
+ painted under zones.
4408
+
4409
+ Accepted gap colors include:
4410
+
4411
+ ```text
4412
+ transparent
4413
+ none
4414
+ empty or missing color
4415
+ ```
4416
+
4417
+ ## Overlapping zone color is unexpected
4418
+
4419
+ Later matching zones win. Reorder the zone list or make ranges non-overlapping.
4420
+
4421
+ This also affects color-by-zone pointer, indicator, and tape behavior.
4422
+
4423
+ ## Pointer moves in the wrong direction
4424
+
4425
+ Use:
4426
+
4427
+ ```js
4428
+ invert: true
4429
+ ```
4430
+
4431
+ Do not reverse zones manually. Inversion changes visual mapping while numeric zones
4432
+ stay attached to their values.
4433
+
4434
+ Verify the configured `startAngle` and `endAngle` if a custom radial sweep is also
4435
+ present.
4436
+
4437
+ ## Linear scale moves vertically in the wrong direction
4438
+
4439
+ Public linear Y coordinates increase upward from `linear.originY`.
4440
+
4441
+ Correct mental model:
4442
+
4443
+ ```text
4444
+ positive public Y -> visually upward
4445
+ negative public Y -> visually downward
4446
+ ```
4447
+
4448
+ Do not use raw SVG downward-positive assumptions in `startY`, `endY`,
4449
+ `labelOffset`, `zoneOffset`, readout Y, or `centerOffset`.
4450
+
4451
+ ## Linear axis disappears or has invalid geometry
4452
+
4453
+ The start and end points must differ. Runtime repairs a zero-length axis to one unit,
4454
+ but the resulting visual is unlikely to match intent.
4455
+
4456
+ Check:
4457
+
4458
+ ```js
4459
+ linear: {
4460
+ startX,
4461
+ startY,
4462
+ endX,
4463
+ endY
4464
+ }
4465
+ ```
4466
+
4467
+ ## Center-zero reference is not centered
4468
+
4469
+ Verify:
4470
+
4471
+ - range contains `centerZero.zeroValue`;
4472
+ - `symmetric: true` is enabled when equal magnitude is required;
4473
+ - selected type is `center-zero` or `classic-linear-zero`;
4474
+ - marker visibility and coordinates were not overridden incorrectly.
4475
+
4476
+ Example:
4477
+
4478
+ ```js
4479
+ {
4480
+ type: 'center-zero',
4481
+ min: -100,
4482
+ max: 100,
4483
+ centerZero: {
4484
+ zeroValue: 0,
4485
+ symmetric: true
4486
+ }
4487
+ }
4488
+ ```
4489
+
4490
+ ## Rolling tape behaves like the wrong instrument
4491
+
4492
+ Use exact type:
4493
+
4494
+ ```js
4495
+ type: 'rolling-tape'
4496
+ ```
4497
+
4498
+ The tape has a moving strip and fixed reference line. It does not use radial pointer
4499
+ options.
4500
+
4501
+ Adjust `tape.interval`, `minorSubdivisions`, and `majorSpacing`, not
4502
+ `pointer.length`.
4503
+
4504
+ ## Readout is missing
4505
+
4506
+ This is insufficient:
4507
+
4508
+ ```js
4509
+ readout: {
4510
+ visible: true
4511
+ }
4512
+ ```
4513
+
4514
+ Enable each required child:
4515
+
4516
+ ```js
4517
+ readout: {
4518
+ visible: true,
4519
+ title: {
4520
+ visible: true,
4521
+ text: 'PRESSURE'
4522
+ },
4523
+ value: {
4524
+ visible: true
4525
+ },
4526
+ unit: {
4527
+ visible: true,
4528
+ text: 'bar'
4529
+ }
4530
+ }
4531
+ ```
4532
+
4533
+ ## Readout automatic position is unexpected
4534
+
4535
+ `x: 'auto'` centers. `y: 'auto'` uses `position` and `margin`.
4536
+
4537
+ `offsetX` and `offsetY` refine automatic coordinates only. Numeric coordinates are
4538
+ final and do not inherit automatic offsets.
4539
+
4540
+ Type presets may supply different fallback coordinates. Read the selected type defaults
4541
+ when exact placement matters.
4542
+
4543
+ ## Light does not turn on
4544
+
4545
+ Check trigger mode.
4546
+
4547
+ Manual:
4548
+
4549
+ ```js
4550
+ light: {
4551
+ visible: true,
4552
+ on: true,
4553
+ trigger: {
4554
+ mode: 'manual'
4555
+ }
4556
+ }
4557
+ ```
4558
+
4559
+ Threshold:
4560
+
4561
+ ```js
4562
+ light: {
4563
+ visible: true,
4564
+ trigger: {
4565
+ mode: 'above',
4566
+ value: 80
4567
+ }
4568
+ }
4569
+ ```
4570
+
4571
+ `on` is ignored by `below`, `above`, and `between`.
4572
+
4573
+ Also verify `visible: true`.
4574
+
4575
+ ## Light does not pulse
4576
+
4577
+ Pulse is visible only while the light is active.
4578
+
4579
+ Check:
4580
+
4581
+ ```js
4582
+ light: {
4583
+ visible: true,
4584
+ pulse: true,
4585
+ blink: false,
4586
+ trigger: {
4587
+ mode: 'manual'
4588
+ },
4589
+ on: true
4590
+ }
4591
+ ```
4592
+
4593
+ Blink takes precedence over pulse. If both are true, the light blinks instead of
4594
+ pulsing.
4595
+
4596
+ Reduced-motion preference suppresses motion. Test system/browser accessibility
4597
+ settings before treating this as a rendering defect.
4598
+
4599
+ ## Light does not blink
4600
+
4601
+ Verify:
4602
+
4603
+ - active trigger;
4604
+ - `blink: true`;
4605
+ - `blinkInterval >= 100`;
4606
+ - reduced-motion is not active.
4607
+
4608
+ The off half-cycle shows the physical unlit lens rather than making the whole lamp
4609
+ transparent.
4610
+
4611
+ ## Light flicker is not visible
4612
+
4613
+ Verify:
4614
+
4615
+ ```js
4616
+ flicker: true
4617
+ ```
4618
+
4619
+ and at least one of:
4620
+
4621
+ ```js
4622
+ flickerIntensity > 0
4623
+ flickerGlowRadius > 0
4624
+ ```
4625
+
4626
+ Flicker runs only while active and is disabled under reduced motion.
4627
+
4628
+ The default effect is intentionally subtle.
4629
+
4630
+ ## Glow size does not follow lamp size
4631
+
4632
+ In 0.1.1, `glowRadius` is independent:
4633
+
4634
+ ```js
4635
+ light: {
4636
+ radius: 8,
4637
+ glowRadius: 24
4638
+ }
4639
+ ```
4640
+
4641
+ Set:
4642
+
4643
+ ```js
4644
+ glowRadius: null
4645
+ ```
4646
+
4647
+ to derive `radius * 1.82`.
4648
+
4649
+ Changing only `radius` does not automatically change an explicit numeric
4650
+ `glowRadius`.
4651
+
4652
+ ## Glow is clipped
4653
+
4654
+ This should not happen with normalized built-in options because bounds reserve halo
4655
+ and blur.
4656
+
4657
+ For repository changes, compare rendering math in `src/layers/light.js` with bounds in
4658
+ `src/geometry/bounds.js`. A new halo extent, blur formula, or flicker variation requires
4659
+ matching bounds changes and tests.
4660
+
4661
+ For application custom layers, add correct geometry within the instrument or create a
4662
+ type that provides enough layout padding/bounds support.
4663
+
4664
+ ## Animation does not run
4665
+
4666
+ Check:
4667
+
4668
+ ```js
4669
+ animation: {
4670
+ enabled: true,
4671
+ duration: 850
4672
+ }
4673
+ ```
4674
+
4675
+ Per update, do not pass:
4676
+
4677
+ ```js
4678
+ { animate: false }
4679
+ ```
4680
+
4681
+ Reduced-motion may suppress animation when `respectReducedMotion` is true.
4682
+
4683
+ No animation is visible when source and target values are identical.
4684
+
4685
+ ## Startup animation begins from the wrong point
4686
+
4687
+ Configure:
4688
+
4689
+ ```js
4690
+ animation: {
4691
+ animateOnInit: true,
4692
+ startFrom: 'min'
4693
+ }
4694
+ ```
4695
+
4696
+ Allowed values:
4697
+
4698
+ ```text
4699
+ min
4700
+ max
4701
+ value
4702
+ numeric value
4703
+ ```
4704
+
4705
+ `startFrom: 'value'` intentionally starts at the target and therefore has no visible
4706
+ startup travel.
4707
+
4708
+ ## Jitter changes application data
4709
+
4710
+ It should not. Verify application code is reading:
4711
+
4712
+ ```js
4713
+ gauge.getValue()
4714
+ gauge.getDisplayedValue()
4715
+ ```
4716
+
4717
+ and not parsing generated pointer transforms.
4718
+
4719
+ Jitter is visual-only. If readout or ARIA values jitter after a repository change, the
4720
+ dynamic layer is using `state.pointerValue` in the wrong place.
4721
+
4722
+ ## Gauge continues work offscreen
4723
+
4724
+ Defaults are:
4725
+
4726
+ ```js
4727
+ performance: {
4728
+ pauseWhenHidden: true,
4729
+ pauseWhenOffscreen: true
4730
+ }
4731
+ ```
4732
+
4733
+ IntersectionObserver may be unavailable in some environments. Manual `pause()` remains
4734
+ available.
4735
+
4736
+ Repository tests should use controlled lifecycle stubs rather than assuming browser
4737
+ observer availability.
4738
+
4739
+ ## Instance leaks after component removal
4740
+
4741
+ An application that creates a gauge manually must destroy it:
4742
+
4743
+ ```js
4744
+ const gauge = Gaugeit.createGauge(host, options);
4745
+
4746
+ // component cleanup
4747
+ gauge.destroy();
4748
+ ```
4749
+
4750
+ The React adapter and custom element already own cleanup.
4751
+
4752
+ Custom layers with timers or observers must return a controller with `destroy()`.
4753
+
4754
+ ## Auto mounting creates duplicates
4755
+
4756
+ `Gaugeit.autoMount()` is duplicate-safe for managed elements. Duplicate SVG usually
4757
+ means application code also called `createGauge()` independently or cloned generated
4758
+ content.
4759
+
4760
+ Use:
4761
+
4762
+ ```js
4763
+ Gaugeit.getMountedGauge(host)
4764
+ ```
4765
+
4766
+ to inspect ownership.
4767
+
4768
+ ## Custom element options do not update
4769
+
4770
+ Observed attributes:
4771
+
4772
+ ```text
4773
+ value
4774
+ min
4775
+ max
4776
+ type
4777
+ options
4778
+ ```
4779
+
4780
+ The `options` attribute must be valid JSON. Attribute changes replace the declarative
4781
+ option object.
4782
+
4783
+ Property assignment for value:
4784
+
4785
+ ```js
4786
+ element.value = 72;
4787
+ ```
4788
+
4789
+ ## Laravel or Twig JSON parsing fails
4790
+
4791
+ Encode data as JSON and escape it for the HTML attribute context.
4792
+
4793
+ Blade:
4794
+
4795
+ ```blade
4796
+ data-options='@json($options)'
4797
+ ```
4798
+
4799
+ Twig:
4800
+
4801
+ ```twig
4802
+ data-options="{{ options|json_encode|e('html_attr') }}"
4803
+ ```
4804
+
4805
+ Plain PHP:
4806
+
4807
+ ```php
4808
+ htmlspecialchars(json_encode($options, JSON_THROW_ON_ERROR), ENT_QUOTES, 'UTF-8')
4809
+ ```
4810
+
4811
+ Do not render PHP array syntax into a browser JSON parser.
4812
+
4813
+ ## Composer command is missing
4814
+
4815
+ Confirm installation:
4816
+
4817
+ ```bash
4818
+ composer show kasperi/gaugeit-js
4819
+ ```
4820
+
4821
+ Use the platform-appropriate vendor binary:
4822
+
4823
+ Unix-like:
4824
+
4825
+ ```bash
4826
+ vendor/bin/gaugeit-install-assets public/assets/vendor/gaugeit
4827
+ ```
4828
+
4829
+ Windows Composer normally exposes a callable wrapper under `vendor\bin`.
4830
+
4831
+ Verify PHP 8.1 or newer.
4832
+
4833
+ ## Composer assets are installed but browser returns 404
4834
+
4835
+ The installer writes to the filesystem path you provide. The web server must expose
4836
+ that path.
4837
+
4838
+ Check:
4839
+
4840
+ - destination is under the actual public document root;
4841
+ - framework asset helper matches the destination;
4842
+ - deployment included generated public assets;
4843
+ - URL casing matches filesystem casing.
4844
+
4845
+ ## `npm run test:chromium` fails with `spawn chromium ENOENT`
4846
+
4847
+ The optional test expects a globally available executable named `chromium`.
4848
+
4849
+ This does not mean the library test failed in a browser. It means the process could not
4850
+ start Chromium.
4851
+
4852
+ Options:
4853
+
4854
+ - install Chromium and add it to PATH;
4855
+ - adjust the local environment to expose the expected executable;
4856
+ - rely on `npm run check` for the portable required suite.
4857
+
4858
+ Do not weaken CI because an optional local executable is missing.
4859
+
4860
+ ## Local `npm run check` passes but GitHub CI fails at reproducibility
4861
+
4862
+ Typical symptom:
4863
+
4864
+ ```text
4865
+ git diff --exit-code -- dist package-lock.json
4866
+ ```
4867
+
4868
+ shows source-map differences.
4869
+
4870
+ Check:
4871
+
4872
+ - source files use LF, not CRLF;
4873
+ - Git `core.autocrlf` did not rewrite canonical inputs;
4874
+ - generated files do not contain absolute paths;
4875
+ - file ordering is deterministic;
4876
+ - no timestamps or randomness enter build output;
4877
+ - local `dist/` was rebuilt after the final source edit.
4878
+
4879
+ Repository policy:
4880
+
4881
+ ```text
4882
+ .editorconfig -> LF
4883
+ .gitattributes -> text eol=lf
4884
+ ```
4885
+
4886
+ Run:
4887
+
4888
+ ```bash
4889
+ npm run check
4890
+ git diff -- dist package-lock.json
4891
+ ```
4892
+
4893
+ and inspect the generated changes before pushing. To reproduce CI's exit-code check
4894
+ locally, stage the intended generated artifacts first and then run:
4895
+
4896
+ ```bash
4897
+ git diff --exit-code -- dist package-lock.json
4898
+ ```
4899
+
4900
+ CI performs this check from a clean checkout, so any post-build diff there is
4901
+ unexpected.
4902
+
4903
+ ## `dist/` changes after a documentation-only edit
4904
+
4905
+ Ordinary docs should not change runtime distribution. If `npm run check` changes
4906
+ `dist/`, either:
4907
+
4908
+ - canonical source had uncommitted changes;
4909
+ - previous generated output was stale;
4910
+ - line endings or build environment changed;
4911
+ - a build script includes documentation unexpectedly.
4912
+
4913
+ Inspect the diff before committing.
4914
+
4915
+ ## Generated files were edited manually
4916
+
4917
+ Discard manual generated edits, change canonical source, and rebuild:
4918
+
4919
+ ```bash
4920
+ git restore dist
4921
+ npm run build
4922
+ ```
4923
+
4924
+ If canonical source changes are already present, do not restore those. The goal is to
4925
+ remove only hand edits from `dist/`.
4926
+
4927
+ ## TypeScript declarations do not match runtime
4928
+
4929
+ Canonical core declarations are:
4930
+
4931
+ ```text
4932
+ src/index.d.ts
4933
+ ```
4934
+
4935
+ The React adapter has a separate canonical declaration:
4936
+
4937
+ ```text
4938
+ adapters/react/Gaugeit.d.ts
4939
+ ```
4940
+
4941
+ Generated core copy:
4942
+
4943
+ ```text
4944
+ dist/gaugeit.d.ts
4945
+ ```
4946
+
4947
+ Update the declaration that owns the affected API, then run:
4948
+
4949
+ ```bash
4950
+ npm run typecheck
4951
+ npm run build
4952
+ ```
4953
+
4954
+ and add package-usage tests when an export or option changes.
4955
+
4956
+ ## Package dry run omits expected files
4957
+
4958
+ Check `package.json` `files` and `exports`.
4959
+
4960
+ Run:
4961
+
4962
+ ```bash
4963
+ npm pack --dry-run
4964
+ ```
4965
+
4966
+ AI documentation under `docs/` is included because the package publishes `docs`.
4967
+ Top-level files not listed in `files` may be absent from the npm tarball even when they
4968
+ exist in GitHub.
4969
+
4970
+ ## npm publication returns 403 requiring 2FA
4971
+
4972
+ The package contents may be valid even though publication is rejected.
4973
+
4974
+ Use an npm account with publication 2FA or an appropriate granular token. A failed
4975
+ 403 attempt does not publish the version.
4976
+
4977
+ After successful publication, verify:
4978
+
4979
+ ```bash
4980
+ npm view gaugeit.js@0.1.1 name version
4981
+ npm view gaugeit.js dist-tags
4982
+ ```
4983
+
4984
+ ## Packagist does not show the release
4985
+
4986
+ Composer package versions come from Git tags.
4987
+
4988
+ Verify:
4989
+
4990
+ - GitHub repository is public and correct;
4991
+ - `composer.json` is valid;
4992
+ - tag exists remotely;
4993
+ - Packagist package points to the correct repository;
4994
+ - tag corresponds to the intended commit.
4995
+
4996
+ Do not add a hard-coded `version` field to `composer.json` merely to fix tag discovery.
4997
+
4998
+ ## Custom type is not found
4999
+
5000
+ Register before instance creation:
5001
+
5002
+ ```js
5003
+ Gaugeit.registerGaugeType('my-type', definition);
5004
+ Gaugeit.createGauge('#gauge', {
5005
+ type: 'my-type'
5006
+ });
5007
+ ```
5008
+
5009
+ Verify non-empty `layers`. Every layer must expose `render(context)`.
5010
+
5011
+ ## Custom layer is behind or above the wrong content
5012
+
5013
+ Layer array order is SVG paint order. Reorder layers.
5014
+
5015
+ For example, pointer above readout:
5016
+
5017
+ ```js
5018
+ layers: [
5019
+ Gaugeit.layers.face,
5020
+ Gaugeit.layers.zones,
5021
+ Gaugeit.layers.ticks,
5022
+ Gaugeit.layers.labels,
5023
+ Gaugeit.layers.readout,
5024
+ Gaugeit.layers.pointer
5025
+ ]
5026
+ ```
5027
+
5028
+ Do not use numeric CSS z-index as a substitute.
5029
+
5030
+ ## Custom layer ignores clipping
5031
+
5032
+ Create its group through:
5033
+
5034
+ ```js
5035
+ renderer.group('my-layer')
5036
+ ```
5037
+
5038
+ and place it after a face layer that establishes clipping. Appending directly to an
5039
+ unmanaged SVG node can bypass renderer ownership and clip-path application.
5040
+
5041
+ ## Custom timer survives `destroy()`
5042
+
5043
+ Return cleanup:
5044
+
5045
+ ```js
5046
+ return {
5047
+ update() {
5048
+ // ...
5049
+ },
5050
+
5051
+ destroy() {
5052
+ clearInterval(timer);
5053
+ }
5054
+ };
5055
+ ```
5056
+
5057
+ Test destruction explicitly.
5058
+
5059
+ ## A bug fix has no regression test
5060
+
5061
+ Add the smallest test that demonstrates the defect. Choose the nearest suite:
5062
+
5063
+ ```text
5064
+ options.test.js
5065
+ geometry.test.js
5066
+ zones.test.js
5067
+ linear.test.js
5068
+ light.test.js
5069
+ lifecycle.test.js
5070
+ distribution.test.js
5071
+ react-adapter.test.js
5072
+ ```
5073
+
5074
+ A bug fix is incomplete when only the implementation changed.
5075
+
5076
+ ## Final diagnostic sequence
5077
+
5078
+ For application integration:
5079
+
5080
+ 1. verify distribution and CSS;
5081
+ 2. verify exact global/import name;
5082
+ 3. verify host exists and has layout;
5083
+ 4. reduce options to `{ type, min, max, value }`;
5084
+ 5. add nested options back one section at a time;
5085
+ 6. inspect host-scoped Gaugeit events;
5086
+ 7. test destruction and recreation.
5087
+
5088
+ For repository changes:
5089
+
5090
+ 1. inspect canonical source;
5091
+ 2. run focused tests;
5092
+ 3. update declarations and docs;
5093
+ 4. run `npm run check`;
5094
+ 5. run reproducibility diff;
5095
+ 6. inspect `git diff` for accidental files.
5096
+
5097
+ ---