@zumer/orbit-kit 0.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -316
- package/package.json +20 -8
- package/src/base.js +606 -41
- package/src/color.js +139 -0
- package/src/o-clock.js +106 -129
- package/src/o-color-wheel.js +322 -0
- package/src/o-compass.js +45 -49
- package/src/o-donut.js +67 -79
- package/src/o-gauge.js +51 -117
- package/src/o-instrument.js +116 -105
- package/src/o-knob.js +137 -129
- package/src/o-menu.js +271 -183
- package/src/o-meter.js +35 -89
- package/src/o-radar-chart.js +391 -0
- package/src/o-radar.js +80 -123
- package/src/o-range.js +509 -0
- package/src/o-rings.js +66 -77
- package/src/o-spinner.js +214 -0
- package/src/o-status.js +209 -0
- package/src/o-sunburst.js +569 -0
- package/src/o-time-picker.js +447 -0
- package/src/o-timer.js +240 -0
- package/src/o-wheel.js +436 -0
- package/src/orbit-kit.js +23 -5
package/src/o-gauge.js
CHANGED
|
@@ -1,135 +1,69 @@
|
|
|
1
|
+
import { OrbitKitElement, define, injectStyleOnce, universe, hub, fixed, escapeHTML as e, withShared, RANGE_ATTRS, FORMAT_ATTRS } from './base.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* <o-gauge
|
|
3
|
-
*
|
|
4
|
-
* Collapses the canonical Orbit gauge recipe:
|
|
5
|
-
*
|
|
6
|
-
* .bigbang > .gravity-spot
|
|
7
|
-
* ├── .orbit-N (range/from) > <o-progress>
|
|
8
|
-
* └── .orbit-0 > .satellite.at-center > .capsule (center label)
|
|
9
|
-
*
|
|
10
|
-
* ...into a single declarative tag:
|
|
11
|
-
*
|
|
12
|
-
* <o-gauge value="72" unit="%"></o-gauge>
|
|
4
|
+
* <o-gauge>: one value on an arc, with a readout in the middle.
|
|
13
5
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* source" honest: the output is exactly the documented Orbit structure.
|
|
6
|
+
* .bigbang (width = size)
|
|
7
|
+
* └── .gravity-spot
|
|
8
|
+
* ├── .orbit-12 > o-arc.inner-orbit track (thickness lanes, flush with the edge)
|
|
9
|
+
* ├── .orbit-12 > o-arc.inner-orbit value
|
|
10
|
+
* └── .orbit-0 > .satellite.at-center readout
|
|
20
11
|
*
|
|
21
|
-
*
|
|
22
|
-
* <o-progress> is registered. orbit-kit declares it as a peer dependency.
|
|
12
|
+
* Value and label updates patch the existing arc; nothing is rebuilt.
|
|
23
13
|
*/
|
|
24
|
-
|
|
25
|
-
// outline) so a standalone <o-gauge> looks right without the rest of the kit.
|
|
26
|
-
import './base.js';
|
|
27
|
-
|
|
28
|
-
export class OrbitGauge extends HTMLElement {
|
|
14
|
+
export class OrbitGauge extends OrbitKitElement {
|
|
29
15
|
static get observedAttributes() {
|
|
30
|
-
return [
|
|
31
|
-
'value', 'max', 'range', 'from', 'orbit', 'size',
|
|
32
|
-
'label', 'unit', 'fill', 'track', 'stroke-width', 'shape', 'glow'
|
|
33
|
-
];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
connectedCallback() {
|
|
37
|
-
this.render();
|
|
16
|
+
return withShared(['value', 'range', 'from', 'orbit', 'thickness', 'size', 'label', 'fill', 'track', 'stroke-width', 'shape', 'glow'], RANGE_ATTRS, FORMAT_ATTRS);
|
|
38
17
|
}
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/** Parse a numeric attribute with a fallback. */
|
|
45
|
-
_num(name, fallback) {
|
|
46
|
-
const raw = this.getAttribute(name);
|
|
47
|
-
if (raw === null || raw === '') return fallback;
|
|
48
|
-
const n = parseFloat(raw);
|
|
49
|
-
return Number.isFinite(n) ? n : fallback;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
_readConfig() {
|
|
53
|
-
const range = this._num('range', 270);
|
|
19
|
+
readConfig() {
|
|
20
|
+
const range = Math.max(0, Math.min(360, this._num('range', 270)));
|
|
21
|
+
const { value, min, max, step, fraction } = this._range({ min: 0, max: 100, step: 0 });
|
|
54
22
|
return {
|
|
55
|
-
value
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? this._num('from', 0)
|
|
62
|
-
: ((360 - range / 2) % 360 + 360) % 360,
|
|
63
|
-
orbit: Math.round(this._num('orbit', 4)),
|
|
64
|
-
size: this.getAttribute('size') || '200px',
|
|
65
|
-
fill: this.getAttribute('fill') || '#6366f1',
|
|
66
|
-
track: this.getAttribute('track') || '#262d38',
|
|
67
|
-
strokeWidth: this._num('stroke-width', 8),
|
|
68
|
-
shape: this.getAttribute('shape') || 'rounded',
|
|
69
|
-
unit: this.getAttribute('unit') || '',
|
|
70
|
-
label: this.hasAttribute('label') ? this.getAttribute('label') : null
|
|
23
|
+
value, min, max, step, fraction, range,
|
|
24
|
+
from: this.hasAttribute('from') ? this._num('from', 0) : this._autoFrom(range),
|
|
25
|
+
thickness: this._thickness(2.4), size: this._size('size', '200px'),
|
|
26
|
+
fill: this._color('fill', null), track: this._color('track', null),
|
|
27
|
+
strokeWidth: Math.max(0, this._num('stroke-width', 0)), shape: this._str('shape', 'rounded'),
|
|
28
|
+
unit: this._unitText(), label: this.getAttribute('label'), glow: this.hasAttribute('glow') ? this._glowPx() : null
|
|
71
29
|
};
|
|
72
30
|
}
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// inner <o-progress> isn't torn down and recreated on every frame.
|
|
79
|
-
const sig = [
|
|
80
|
-
cfg.range, cfg.from, cfg.orbit, cfg.size,
|
|
81
|
-
cfg.fill, cfg.track, cfg.strokeWidth, cfg.shape,
|
|
82
|
-
this.getAttribute('glow')
|
|
83
|
-
].join('|');
|
|
84
|
-
|
|
85
|
-
if (!this._built || this._sig !== sig) {
|
|
86
|
-
this._build(cfg);
|
|
87
|
-
this._sig = sig;
|
|
88
|
-
this._built = true;
|
|
89
|
-
}
|
|
90
|
-
this._sync(cfg);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
_build(cfg) {
|
|
94
|
-
const glow = this.hasAttribute('glow')
|
|
95
|
-
? `filter:drop-shadow(0 0 ${this._num('glow', 6) || 6}px ${cfg.fill});`
|
|
96
|
-
: '';
|
|
97
|
-
// Track + value as two stacked <o-arc>s (each in its own orbit so they
|
|
98
|
-
// don't chain): o-arc FILLS solid, unlike o-progress's back which only
|
|
99
|
-
// strokes a hollow outline (the ugly border + center seam).
|
|
100
|
-
const ring = `--o-range:${cfg.range}deg;--o-from:${cfg.from}deg;`;
|
|
101
|
-
const trackStyle = `--o-fill:${cfg.track};--o-stroke:${cfg.track};--o-stroke-width:${cfg.strokeWidth}`;
|
|
102
|
-
const fillStyle = `--o-fill:${cfg.fill};--o-stroke:${cfg.fill};--o-stroke-width:${cfg.strokeWidth};${glow}`;
|
|
103
|
-
|
|
104
|
-
this.innerHTML = `
|
|
105
|
-
<div class="bigbang" style="width:${cfg.size};aspect-ratio:1;">
|
|
106
|
-
<div class="gravity-spot">
|
|
107
|
-
<div class="orbit-${cfg.orbit}" style="${ring}"><o-arc value="100" shape="${cfg.shape}" style="${trackStyle}"></o-arc></div>
|
|
108
|
-
<div class="orbit-${cfg.orbit}" style="${ring}"><o-arc class="o-gauge-fill" value="0" shape="${cfg.shape}" style="${fillStyle}"></o-arc></div>
|
|
109
|
-
<div class="orbit-0">
|
|
110
|
-
<div class="satellite at-center">
|
|
111
|
-
<div class="capsule ok-hub"></div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
</div>`;
|
|
32
|
+
structuralSignature({ value, fraction, min, max, step, label, unit, fill, track, ...structure }) { return JSON.stringify(structure); }
|
|
33
|
+
// The 0.x helper names keep working.
|
|
34
|
+
_readConfig() { return this.readConfig(); }
|
|
35
|
+
render() { this._render(); }
|
|
116
36
|
|
|
37
|
+
build(cfg) {
|
|
38
|
+
const ring = `--o-range:${cfg.range}deg;--o-from:${cfg.from}deg;--o-size-ratio:${fixed(cfg.thickness)};--o-gap:0`;
|
|
39
|
+
// stroke-width (0.x): an outline in the band's own color, in px.
|
|
40
|
+
const stroke = cfg.strokeWidth ? `--o-stroke-width:${cfg.strokeWidth};` : '';
|
|
41
|
+
const glow = cfg.glow == null ? '' : `filter:drop-shadow(0 0 ${cfg.glow}px var(--ok-fill, #6366f1));`;
|
|
42
|
+
this.innerHTML = universe(cfg.size, `
|
|
43
|
+
<div class="orbit-12" style="${ring}"><o-arc class="inner-orbit ok-track" value="100" shape="${e(cfg.shape)}"${stroke ? ` style="${stroke}"` : ''}></o-arc></div>
|
|
44
|
+
<div class="orbit-12" style="${ring}"><o-arc class="inner-orbit ok-value o-gauge-fill" value="0" shape="${e(cfg.shape)}" ${stroke || glow ? ` style="${stroke}${glow}"` : ''}></o-arc></div>
|
|
45
|
+
${hub(2 * (12 - cfg.thickness) - 1)}`);
|
|
117
46
|
this._fill = this.querySelector('.o-gauge-fill');
|
|
118
|
-
this._center = this.querySelector('.
|
|
47
|
+
this._center = this.querySelector('.ok-hub');
|
|
119
48
|
}
|
|
120
49
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
50
|
+
sync(cfg) {
|
|
51
|
+
this._applyTheme({ '--ok-fill': cfg.fill, '--ok-track': cfg.track });
|
|
52
|
+
const range = { min: cfg.min, max: cfg.max };
|
|
53
|
+
const reading = this._formatText(cfg.value, range);
|
|
54
|
+
const text = cfg.label ?? reading;
|
|
55
|
+
this._fill?.setAttribute('value', (cfg.fraction * 100).toFixed(2));
|
|
56
|
+
if (this._center) this._center.textContent = text;
|
|
57
|
+
this._describe(`${text}; ${this._format(cfg.value, range)} of ${this._formatText(cfg.max, range)}`, 'meter');
|
|
58
|
+
this._ariaRange(cfg);
|
|
130
59
|
}
|
|
60
|
+
_build(cfg) { this.build(cfg); }
|
|
61
|
+
_sync(cfg) { this.sync(cfg); }
|
|
131
62
|
}
|
|
132
63
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
64
|
+
const GAUGE_CSS = `
|
|
65
|
+
o-gauge .ok-track { --o-fill: var(--ok-track, #262d38); --o-stroke: var(--o-fill); }
|
|
66
|
+
o-gauge .ok-value { --o-fill: var(--ok-fill, #6366f1); --o-stroke: var(--o-fill); transition: filter .2s; }
|
|
67
|
+
`;
|
|
68
|
+
injectStyleOnce('orbit-kit-gauge', GAUGE_CSS);
|
|
69
|
+
define('o-gauge', OrbitGauge);
|
package/src/o-instrument.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* preset-driven needle gauge for automotive, aviation and computer readouts.
|
|
4
4
|
*
|
|
5
5
|
* Anatomy (all in the light DOM, aligned in one --o-from/--o-range frame so
|
|
6
|
-
* ticks, labels, color zones and the needle agree):
|
|
6
|
+
* ticks, labels, color zones and the needle agree). The dial is the universe:
|
|
7
7
|
*
|
|
8
8
|
* case > .bigbang > .gravity-spot
|
|
9
|
-
* ├── full sweep: .orbit-
|
|
10
|
-
* │ partial(≤180): .orbit-
|
|
11
|
-
* ├── .orbit-
|
|
12
|
-
* ├── .orbit-
|
|
13
|
-
* ├── .orbit-
|
|
14
|
-
* ├── .orbit-
|
|
15
|
-
* ├── .orbit-0 > .vector needle (
|
|
9
|
+
* ├── full sweep: .orbit-12 (filled) dial face + bezel
|
|
10
|
+
* │ partial(≤180): .orbit-11 > <o-arc> track band (no round case)
|
|
11
|
+
* ├── .orbit-11 fit-range > .vector × minor ticks
|
|
12
|
+
* ├── .orbit-11 fit-range > .vector × major ticks
|
|
13
|
+
* ├── .orbit-11 (per zone, own --o-from) > <o-arc> color/redline zones
|
|
14
|
+
* ├── .orbit-9 fit-range > .satellite × labels numeric scale
|
|
15
|
+
* ├── .orbit-0 > .vector needle (rotates to value)
|
|
16
16
|
* └── .orbit-4 > readout (value + unit + caption)
|
|
17
17
|
*
|
|
18
18
|
* <o-instrument preset="tachometer" value="6.2"></o-instrument>
|
|
@@ -25,80 +25,84 @@
|
|
|
25
25
|
* visible, even at zero) — pointed by `--o-angle-composite`. Only `value` is
|
|
26
26
|
* cheap to change; the dial rebuilds when the scale, zones, case or theme change.
|
|
27
27
|
*/
|
|
28
|
-
import { OrbitKitElement, define, injectStyleOnce } from './base.js';
|
|
28
|
+
import { OrbitKitElement, define, injectStyleOnce, finiteNumber, arrayData, cssValue, fixed, escapeHTML as e, withShared, RANGE_ATTRS, FORMAT_ATTRS } from './base.js';
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// WHT follows the theme's strong color (--ok-hand), so a white needle stays visible on a light face.
|
|
31
|
+
const RED = '#ef4444', YEL = '#eab308', GRN = '#22c55e', AMB = '#f59e0b', WHT = 'var(--ok-hand, #e5e7eb)';
|
|
31
32
|
|
|
32
33
|
// Real instruments as preset bundles; any explicit attribute overrides them.
|
|
33
34
|
const PRESETS = {
|
|
34
35
|
tachometer: { min: 0, max: 8, range: 270, ticks: 9, minor: 5, unit: '', label: 'RPM x1000',
|
|
35
36
|
zones: [{ from: 6.5, to: 8, color: RED }], value: 0.8, accent: RED },
|
|
36
|
-
speedometer: { min: 0, max: 240, range: 270, ticks: 9, minor: 2, unit: 'km/h', label: '
|
|
37
|
-
fuel: { min: 0, max: 1, range: 90, from: 270, ticks: 3, minor: 4, unit: '', label: '
|
|
37
|
+
speedometer: { min: 0, max: 240, range: 270, ticks: 9, minor: 2, unit: 'km/h', label: 'Speed', value: 0, accent: WHT },
|
|
38
|
+
fuel: { min: 0, max: 1, range: 90, from: 270, ticks: 3, minor: 4, unit: '', label: 'Fuel', digital: false,
|
|
38
39
|
tickLabels: ['E', '½', 'F'], zones: [{ from: 0, to: 0.12, color: RED }], value: 0.66, accent: AMB },
|
|
39
|
-
temp: { min: 0, max: 1, range: 90, from: 270, ticks: 3, minor: 4, unit: '', label: '
|
|
40
|
+
temp: { min: 0, max: 1, range: 90, from: 270, ticks: 3, minor: 4, unit: '', label: 'Temp', digital: false,
|
|
40
41
|
tickLabels: ['C', '', 'H'], zones: [{ from: 0.82, to: 1, color: RED }], value: 0.45, accent: RED },
|
|
41
|
-
boost: { min: -1, max: 2, range: 240, ticks: 7, minor: 5, unit: 'bar', label: '
|
|
42
|
+
boost: { min: -1, max: 2, range: 240, ticks: 7, minor: 5, unit: 'bar', label: 'Boost',
|
|
42
43
|
zones: [{ from: 1.2, to: 2, color: RED }], value: 0, accent: '#06b6d4' },
|
|
43
|
-
voltmeter: { min: 8, max: 16, range: 180, ticks: 5, minor: 5, unit: 'V', label: '
|
|
44
|
+
voltmeter: { min: 8, max: 16, range: 180, ticks: 5, minor: 5, unit: 'V', label: 'Volts',
|
|
44
45
|
zones: [{ from: 8, to: 11, color: RED }, { from: 11, to: 12, color: YEL }], value: 13.8, accent: GRN },
|
|
45
|
-
airspeed: { min: 0, max: 200, range: 300, ticks: 11, minor: 5, unit: 'kt', label: '
|
|
46
|
+
airspeed: { min: 0, max: 200, range: 300, ticks: 11, minor: 5, unit: 'kt', label: 'Airspeed',
|
|
46
47
|
zones: [{ from: 40, to: 140, color: GRN }, { from: 140, to: 170, color: YEL }, { from: 170, to: 200, color: RED }],
|
|
47
48
|
value: 0, accent: WHT },
|
|
48
|
-
altimeter: { min: 0, max: 10, range: 360, ticks: 10, minor: 5, unit: 'x1000 ft', label: '
|
|
49
|
+
altimeter: { min: 0, max: 10, range: 360, ticks: 10, minor: 5, unit: 'x1000 ft', label: 'Altitude', value: 0, accent: WHT },
|
|
49
50
|
cpu: { min: 0, max: 100, range: 270, ticks: 6, minor: 5, unit: '%', label: 'CPU',
|
|
50
51
|
zones: [{ from: 85, to: 100, color: RED }], value: 0, accent: GRN },
|
|
51
52
|
gpu: { min: 0, max: 100, range: 270, ticks: 6, minor: 5, unit: '%', label: 'GPU',
|
|
52
53
|
zones: [{ from: 85, to: 100, color: RED }], value: 0, accent: '#a855f7' },
|
|
53
|
-
vu: { min: -
|
|
54
|
+
vu: { min: -21, max: 3, range: 120, ticks: 5, minor: 5, unit: 'dB', label: 'VU',
|
|
54
55
|
zones: [{ from: 0, to: 3, color: RED }], value: -8, accent: YEL }
|
|
55
56
|
};
|
|
56
57
|
const CASES = ['round', 'square', 'rounded', 'bare'];
|
|
57
58
|
|
|
58
59
|
const INST_CSS = `
|
|
60
|
+
o-instrument { --ok-font-size: 7.2cqmin; }
|
|
59
61
|
o-instrument .ok-inst-case { box-sizing: border-box; display: flex; align-items: center; justify-content: center; }
|
|
62
|
+
o-instrument .ok-inst-case--square, o-instrument .ok-inst-case--rounded { padding: 7%; }
|
|
60
63
|
o-instrument .ok-inst-case--round { border-radius: 50%; }
|
|
61
64
|
o-instrument .ok-inst-case--square { border-radius: 9%; }
|
|
62
65
|
o-instrument .ok-inst-case--rounded { border-radius: 22%; }
|
|
63
66
|
o-instrument .ok-inst-case--bare { background: transparent; }
|
|
64
67
|
o-instrument .ok-inst-case--square,
|
|
65
68
|
o-instrument .ok-inst-case--rounded {
|
|
66
|
-
background: var(--ok-inst-case-bg, linear-gradient(160deg, #232a34, #11151b));
|
|
69
|
+
background: var(--ok-inst-case-bg, var(--ok-case, linear-gradient(160deg, #232a34, #11151b)));
|
|
67
70
|
box-shadow: 0 10px 30px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.06);
|
|
68
71
|
}
|
|
69
|
-
o-instrument .ok-inst-face { color: var(--ok-inst-label, #cbd5e1); }
|
|
72
|
+
o-instrument .ok-inst-face { color: var(--ok-inst-label, var(--ok-text, #cbd5e1)); }
|
|
70
73
|
o-instrument .ok-inst-dial {
|
|
71
|
-
background: var(--ok-inst-face, radial-gradient(circle at 50% 35%, #1b212b, #0a0d12));
|
|
72
|
-
box-shadow: inset 0 0 0 4px var(--ok-inst-bezel, #2a3340), inset 0 4px 18px rgba(0,0,0,.65);
|
|
74
|
+
background: var(--ok-inst-face, var(--ok-face, radial-gradient(circle at 50% 35%, #1b212b, #0a0d12)));
|
|
75
|
+
box-shadow: inset 0 0 0 4px var(--ok-inst-bezel, var(--ok-bezel, #2a3340)), inset 0 4px 18px rgba(0,0,0,.65);
|
|
73
76
|
}
|
|
74
|
-
o-instrument .ok-inst-
|
|
75
|
-
o-instrument .ok-inst-tick
|
|
76
|
-
o-instrument .ok-inst-
|
|
77
|
-
o-instrument .ok-inst-
|
|
77
|
+
o-instrument .ok-inst-track { --o-fill: var(--ok-inst-track, var(--ok-track, #171c24)); --o-stroke: var(--o-fill); }
|
|
78
|
+
o-instrument .ok-inst-tick { background: var(--ok-inst-tick, var(--ok-tick, rgba(203,213,225,.45))); height: 1px; }
|
|
79
|
+
o-instrument .ok-inst-tick-M { background: var(--ok-inst-tick-M, var(--ok-hand, #e6edf3)); height: 2px; border-radius: 1px; }
|
|
80
|
+
o-instrument .ok-inst-num { font-weight: 700; font-variant-numeric: tabular-nums; font-size: .8em; white-space: nowrap; }
|
|
81
|
+
o-instrument .ok-inst-needle { background: var(--ok-inst-accent, var(--ok-accent, #ef4444)); height: max(2px, 1.2cqmin); border-radius: 0 3px 3px 0; transition: transform .25s cubic-bezier(.3,.7,.4,1); }
|
|
82
|
+
@media (prefers-reduced-motion: reduce) { o-instrument .ok-inst-needle { transition: none; } }
|
|
78
83
|
o-instrument .ok-inst-readout {
|
|
79
84
|
display: flex; flex-direction: column; align-items: center; line-height: 1.05;
|
|
80
85
|
}
|
|
81
|
-
o-instrument .ok-inst-value { font-weight: 800; font-variant-numeric: tabular-nums; font-size: 1.5em; color: var(--ok-inst-readout-color, #f8fafc); }
|
|
82
|
-
o-instrument .ok-inst-unit { font-size: .62em; opacity: .7;
|
|
83
|
-
o-instrument .ok-inst-cap-label { font-size: .
|
|
84
|
-
o-instrument .ok-inst-cap { background: var(--ok-inst-accent, #
|
|
85
|
-
o-instrument.ok-glow .ok-inst-needle { filter: drop-shadow(0 0 5px var(--ok-inst-accent, #ef4444)); }
|
|
86
|
+
o-instrument .ok-inst-value { font-weight: 800; font-variant-numeric: tabular-nums; font-size: 1.5em; white-space: nowrap; color: var(--ok-inst-readout-color, var(--ok-hand, #f8fafc)); }
|
|
87
|
+
o-instrument .ok-inst-unit { font-size: .62em; opacity: .7; white-space: nowrap; }
|
|
88
|
+
o-instrument .ok-inst-cap-label { font-size: .6em; font-weight: 700; opacity: .65; margin-top: .15em; white-space: nowrap; }
|
|
89
|
+
o-instrument .ok-inst-cap { background: var(--ok-inst-accent, var(--ok-accent, #ef4444)); box-shadow: 0 0 0 3px var(--ok-inst-hub, var(--ok-surface, #0a0d12)); }
|
|
90
|
+
o-instrument.ok-glow .ok-inst-needle { filter: drop-shadow(0 0 var(--ok-inst-glow, 5px) var(--ok-inst-accent, #ef4444)); }
|
|
86
91
|
`;
|
|
87
92
|
|
|
88
93
|
export class OrbitInstrument extends OrbitKitElement {
|
|
89
94
|
static get observedAttributes() {
|
|
90
|
-
return ['value', '
|
|
91
|
-
'tick-labels', '
|
|
92
|
-
'digital', 'face', 'bezel', 'accent', 'needle-color', 'tick-color', 'glow'];
|
|
95
|
+
return withShared(['value', 'range', 'from', 'ticks', 'minor', 'zones',
|
|
96
|
+
'tick-labels', 'label', 'redline', 'preset', 'case', 'size',
|
|
97
|
+
'digital', 'face', 'bezel', 'accent', 'needle-color', 'tick-color', 'glow'], RANGE_ATTRS, FORMAT_ATTRS);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
_preset() { return PRESETS[this.getAttribute('preset')] || {}; }
|
|
96
101
|
|
|
97
102
|
_pNum(name, key, dflt) {
|
|
98
103
|
const v = this.getAttribute(name);
|
|
99
|
-
if (v != null && v !== '') return parseFloat(v);
|
|
100
104
|
const p = this._preset();
|
|
101
|
-
return p[key] != null ? p[key] : dflt;
|
|
105
|
+
return finiteNumber(v, p[key] != null ? p[key] : dflt);
|
|
102
106
|
}
|
|
103
107
|
_pStr(name, key, dflt) {
|
|
104
108
|
const v = this.getAttribute(name);
|
|
@@ -108,22 +112,24 @@ export class OrbitInstrument extends OrbitKitElement {
|
|
|
108
112
|
}
|
|
109
113
|
_pArr(name, key, dflt) {
|
|
110
114
|
const v = this.getAttribute(name);
|
|
111
|
-
if (v != null && v.trim() !== '') { try {
|
|
115
|
+
if (v != null && v.trim() !== '') { try { const parsed = JSON.parse(v); if (Array.isArray(parsed)) return parsed; } catch { /* fall through */ } }
|
|
112
116
|
const p = this._preset();
|
|
113
117
|
return p[key] != null ? p[key] : dflt;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
readConfig() {
|
|
117
|
-
const range = this._pNum('range', 'range', 270);
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const range = Math.max(0, Math.min(360, this._pNum('range', 'range', 270)));
|
|
122
|
+
const p = this._preset();
|
|
123
|
+
const r = this._range({ min: p.min ?? 0, max: p.max ?? 100, step: 0, value: p.value });
|
|
124
|
+
const { min, max } = r;
|
|
125
|
+
const span = r.span || 1;
|
|
126
|
+
const zones = arrayData(this._pArr('zones', 'zones', [])).map((z) => ({
|
|
127
|
+
from: Math.max(min, finiteNumber(z.from, min)), to: Math.min(max, finiteNumber(z.to, min)), color: cssValue(z.color, RED)
|
|
128
|
+
})).filter((z) => z.to > z.from);
|
|
124
129
|
// `redline=` is sugar for a red zone from that value to max.
|
|
125
130
|
const redline = this.getAttribute('redline');
|
|
126
|
-
|
|
131
|
+
const redlineValue = finiteNumber(redline, max);
|
|
132
|
+
if (redline != null && redlineValue < max) zones.push({ from: Math.max(min, redlineValue), to: max, color: RED });
|
|
127
133
|
|
|
128
134
|
// Partial-sweep gauges (fuel/temp/…) read as an arc band, not a round watch,
|
|
129
135
|
// so they default to a "bare" case (no round bezel) unless told otherwise.
|
|
@@ -133,54 +139,50 @@ export class OrbitInstrument extends OrbitKitElement {
|
|
|
133
139
|
if (!CASES.includes(caseShape)) caseShape = partial ? 'bare' : 'round';
|
|
134
140
|
|
|
135
141
|
return {
|
|
136
|
-
value:
|
|
142
|
+
value: r.value, fraction: r.fraction, step: r.step,
|
|
137
143
|
min, max, span, range, zones, caseShape, partial,
|
|
138
|
-
from: this.hasAttribute('from') ? this._pNum('from', null, 0) : (this._preset().from ?? this._autoFrom(range)),
|
|
139
|
-
ticks: Math.max(2, Math.round(this._pNum('ticks', 'ticks', 7))),
|
|
140
|
-
minor: Math.max(0, Math.round(this._pNum('minor', 'minor', 5))),
|
|
144
|
+
from: this.hasAttribute('from') ? this._pNum('from', null, 0) : (this._preset().from ?? (range === 360 ? 0 : this._autoFrom(range))),
|
|
145
|
+
ticks: Math.max(2, Math.min(360, Math.round(this._pNum('ticks', 'ticks', 7)))),
|
|
146
|
+
minor: Math.max(0, Math.min(60, Math.round(this._pNum('minor', 'minor', 5)))),
|
|
141
147
|
tickLabels: this._pArr('tick-labels', 'tickLabels', null),
|
|
142
|
-
unit: this._pStr('unit', 'unit', ''),
|
|
148
|
+
unit: this.getAttribute('format') === 'unit' ? '' : this._pStr('unit', 'unit', ''),
|
|
143
149
|
label: this._pStr('label', 'label', ''),
|
|
144
150
|
digital: this.hasAttribute('digital')
|
|
145
151
|
? this.getAttribute('digital') !== 'false'
|
|
146
152
|
: (this._preset().digital !== false),
|
|
147
|
-
size: this.
|
|
148
|
-
glow: this.
|
|
149
|
-
accent: this._pStr('accent', 'accent',
|
|
150
|
-
theme: ['face', 'bezel', 'needle-color', 'tick-color'].map((a) => this.getAttribute(a)).join('|')
|
|
153
|
+
size: this._size('size', '240px'),
|
|
154
|
+
glow: this.getAttribute('glow'),
|
|
155
|
+
accent: cssValue(this._pStr('accent', 'accent', null), null),
|
|
156
|
+
theme: ['face', 'bezel', 'needle-color', 'tick-color'].map((a) => this.getAttribute(a)).join('|'),
|
|
157
|
+
// Tick labels are part of the dial, so the number format rebuilds it.
|
|
158
|
+
format: [...['format', 'locale', 'decimals', 'currency'].map((a) => this.getAttribute(a)), this._formatter ? 'fn' : '', this.getAttribute('format') === 'unit' ? this.getAttribute('unit') : ''].join('|')
|
|
151
159
|
};
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
structuralSignature(cfg) {
|
|
155
|
-
const { value, ...rest } = cfg;
|
|
163
|
+
const { value, fraction, step, unit, label, ...rest } = cfg;
|
|
156
164
|
return JSON.stringify(rest);
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
build(cfg) {
|
|
160
168
|
injectStyleOnce('orbit-kit-instrument', INST_CSS);
|
|
161
|
-
this.classList.toggle('ok-glow', cfg.glow);
|
|
169
|
+
this.classList.toggle('ok-glow', cfg.glow != null);
|
|
162
170
|
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
171
|
+
this._applyTheme({
|
|
172
|
+
'--ok-inst-accent': this.getAttribute('needle-color') ?? cfg.accent,
|
|
173
|
+
'--ok-inst-face': this.getAttribute('face'), '--ok-inst-bezel': this.getAttribute('bezel'),
|
|
174
|
+
'--ok-inst-tick': this.getAttribute('tick-color'),
|
|
175
|
+
'--ok-inst-glow': cfg.glow == null ? null : `${Math.max(0, this._num('glow', 5))}px`
|
|
176
|
+
});
|
|
170
177
|
|
|
171
|
-
const dialPx = parseFloat(cfg.size) || 240;
|
|
172
|
-
const force = (dialPx * 12 / 11).toFixed(1);
|
|
173
|
-
const frame = (cfg.caseShape === 'round' || cfg.caseShape === 'bare') ? 0 : Math.round(dialPx * 0.07);
|
|
174
|
-
const box = dialPx + frame * 2;
|
|
175
|
-
const baseFont = (dialPx * 0.072).toFixed(1);
|
|
176
178
|
const fromVar = `--o-range:${cfg.range}deg;--o-from:${cfg.from}deg;`;
|
|
177
179
|
|
|
178
180
|
// Partial gauges (a 90° fuel arc) otherwise sit crammed in one quadrant.
|
|
179
|
-
// Center the content by
|
|
180
|
-
//
|
|
181
|
+
// Center the content by moving the gravity-spot so the arc's bounding box
|
|
182
|
+
// (plus the center pivot) is centered in the square box.
|
|
181
183
|
let centering = '';
|
|
182
|
-
if (cfg.partial) {
|
|
183
|
-
const R =
|
|
184
|
+
if (cfg.partial && cfg.caseShape === 'bare') {
|
|
185
|
+
const R = 50 * 11 / 12; // the tick ring, in cqmin of the universe
|
|
184
186
|
let minX = 0, maxX = 0, minY = 0, maxY = 0; // seed with the center pivot
|
|
185
187
|
for (let k = 0; k <= 24; k++) {
|
|
186
188
|
const p = (cfg.from + (cfg.range * k) / 24) * Math.PI / 180;
|
|
@@ -188,64 +190,63 @@ export class OrbitInstrument extends OrbitKitElement {
|
|
|
188
190
|
minX = Math.min(minX, x); maxX = Math.max(maxX, x);
|
|
189
191
|
minY = Math.min(minY, y); maxY = Math.max(maxY, y);
|
|
190
192
|
}
|
|
191
|
-
|
|
192
|
-
const dy = (-(minY + maxY) / 2 * R).toFixed(1);
|
|
193
|
-
centering = `transform:translate(${dx}px,${dy}px);`;
|
|
193
|
+
centering = `translate:${fixed(-(minX + maxX) / 2 * R, 1)}cqmin ${fixed(-(minY + maxY) / 2 * R, 1)}cqmin;`;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// Face: a round dial for full sweeps, or a neutral track band for partial
|
|
197
197
|
// gauges (so a quarter-arc fuel gauge isn't trapped in a round bezel).
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
const band = `--o-size-ratio:.55;--o-gap:0;`;
|
|
199
|
+
const face = cfg.caseShape === 'bare'
|
|
200
|
+
? `<div class="orbit-11" style="${fromVar}${band}"><o-arc class="inner-orbit ok-inst-track" value="100"></o-arc></div>`
|
|
201
|
+
: '<div class="orbit-12 ok-inst-dial ok-surface"></div>';
|
|
201
202
|
|
|
202
203
|
// --- ticks: major (count) + minor (between) over the range ---
|
|
203
204
|
const majorN = cfg.ticks;
|
|
204
|
-
const
|
|
205
|
+
const closed = cfg.range === 360;
|
|
206
|
+
const fit = closed ? '' : 'fit-range';
|
|
207
|
+
const minorN = cfg.minor > 0 ? (closed ? majorN * (cfg.minor + 1) : (majorN - 1) * (cfg.minor + 1) + 1) : 0;
|
|
205
208
|
const minorTicks = minorN
|
|
206
|
-
? `<div class="orbit-
|
|
207
|
-
'<div class="vector inner-orbit
|
|
209
|
+
? `<div class="orbit-11 ${fit}" style="${fromVar}--o-size-ratio:.3">${Array.from({ length: minorN }, () =>
|
|
210
|
+
'<div class="vector inner-orbit ok-inst-tick"></div>').join('')}</div>`
|
|
208
211
|
: '';
|
|
209
|
-
const majorTicks = `<div class="orbit-
|
|
210
|
-
'<div class="vector inner-orbit
|
|
212
|
+
const majorTicks = `<div class="orbit-11 ${fit}" style="${fromVar}--o-size-ratio:.6">${Array.from({ length: majorN }, () =>
|
|
213
|
+
'<div class="vector inner-orbit ok-inst-tick-M"></div>').join('')}</div>`;
|
|
211
214
|
|
|
212
215
|
// --- numeric / custom scale labels ---
|
|
213
216
|
const labels = Array.from({ length: majorN }, (_, i) => {
|
|
214
217
|
const txt = cfg.tickLabels ? (cfg.tickLabels[i] ?? '')
|
|
215
|
-
: this.
|
|
216
|
-
return `<div class="satellite"><div class="capsule ok-inst-num">${txt}</div></div>`;
|
|
218
|
+
: this._format(cfg.min + (i * cfg.span) / (closed ? majorN : majorN - 1), { kind: 'tick', min: cfg.min, max: cfg.max, fallback: (v) => this._fmtTick(v) });
|
|
219
|
+
return `<div class="satellite"><div class="capsule ok-inst-num">${e(txt)}</div></div>`;
|
|
217
220
|
}).join('');
|
|
218
|
-
const labelRing = `<div class="orbit-
|
|
221
|
+
const labelRing = `<div class="orbit-9 ${fit}" style="${fromVar}">${labels}</div>`;
|
|
219
222
|
|
|
220
|
-
// --- color zones: each
|
|
223
|
+
// --- color zones: each on its own ring so non-contiguous zones land right ---
|
|
221
224
|
const zones = cfg.zones.map((z) => {
|
|
222
225
|
const startA = cfg.from + ((z.from - cfg.min) / cfg.span) * cfg.range;
|
|
223
226
|
const pct = ((z.to - z.from) / cfg.span) * 100;
|
|
224
|
-
const style = `--o-range:${cfg.range}deg;--o-from:${startA}deg`;
|
|
225
|
-
const arc = `--o-fill:${z.color};--o-stroke:${z.color}
|
|
226
|
-
return `<div class="orbit-
|
|
227
|
+
const style = `--o-range:${cfg.range}deg;--o-from:${fixed(startA)}deg;${band}`;
|
|
228
|
+
const arc = `--o-fill:${z.color};--o-stroke:${z.color}${cfg.glow != null ? `;filter:drop-shadow(0 0 ${Math.max(0, this._num('glow', 4))}px ${z.color})` : ''}`;
|
|
229
|
+
return `<div class="orbit-11" style="${style}"><o-arc class="inner-orbit ok-inst-zone" value="${pct.toFixed(3)}" style="${e(arc)}"></o-arc></div>`;
|
|
227
230
|
}).join('');
|
|
228
231
|
|
|
229
|
-
const readout =
|
|
230
|
-
?
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</div></div></div>`
|
|
235
|
-
: '';
|
|
232
|
+
const readout = `<div class="orbit-4"><div class="satellite angle-180"><div class="capsule ok-inst-readout">
|
|
233
|
+
${cfg.digital ? '<span class="ok-inst-value"></span>' : ''}
|
|
234
|
+
<span class="ok-inst-unit">${e(cfg.unit)}</span>
|
|
235
|
+
<span class="ok-inst-cap-label">${e(cfg.label)}</span>
|
|
236
|
+
</div></div></div>`;
|
|
236
237
|
|
|
237
238
|
this.innerHTML = `
|
|
238
|
-
<div class="ok-inst-case ok-inst-case--${cfg.caseShape}" style="width:${
|
|
239
|
-
<div class="bigbang" style="width:100
|
|
240
|
-
<div class="gravity-spot ok-inst-face" style="
|
|
239
|
+
<div class="ok-inst-case ok-inst-case--${cfg.caseShape}${cfg.caseShape === 'square' || cfg.caseShape === 'rounded' ? ' ok-surface' : ''}" style="width:${e(cfg.size)};aspect-ratio:1">
|
|
240
|
+
<div class="bigbang" style="width:100%">
|
|
241
|
+
<div class="gravity-spot ok-inst-face"${centering ? ` style="${centering}"` : ''}>
|
|
241
242
|
${face}
|
|
242
243
|
${minorTicks}${majorTicks}
|
|
243
244
|
${zones}
|
|
244
245
|
${labelRing}
|
|
245
246
|
${readout}
|
|
246
247
|
<div class="orbit-0">
|
|
247
|
-
<div class="vector outer-orbit
|
|
248
|
-
<div class="satellite at-center
|
|
248
|
+
<div class="vector outer-orbit ok-inst-needle" style="--o-size-ratio:9.6"></div>
|
|
249
|
+
<div class="satellite at-center ok-inst-cap" style="--o-size-ratio:.8"></div>
|
|
249
250
|
</div>
|
|
250
251
|
</div>
|
|
251
252
|
</div>
|
|
@@ -253,16 +254,26 @@ export class OrbitInstrument extends OrbitKitElement {
|
|
|
253
254
|
|
|
254
255
|
this._needle = this.querySelector('.ok-inst-needle');
|
|
255
256
|
this._value = this.querySelector('.ok-inst-value');
|
|
257
|
+
this._unit = this.querySelector('.ok-inst-unit');
|
|
258
|
+
this._caption = this.querySelector('.ok-inst-cap-label');
|
|
259
|
+
this._readout = this.querySelector('.ok-inst-readout');
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
sync(cfg) {
|
|
259
|
-
const
|
|
260
|
-
const frac = (v - cfg.min) / cfg.span;
|
|
263
|
+
const frac = cfg.fraction;
|
|
261
264
|
// The arc start (value 0) sits at screen angle `from + 270`; the needle is
|
|
262
265
|
// a full-length vector rotated to the value's screen angle (always visible).
|
|
263
266
|
const deg = (((cfg.from + 270 + frac * cfg.range) % 360) + 360) % 360;
|
|
264
267
|
if (this._needle) this._needle.style.setProperty('--o-angle-composite', `${deg.toFixed(2)}deg`);
|
|
265
|
-
|
|
268
|
+
const range = { min: cfg.min, max: cfg.max };
|
|
269
|
+
const reading = this._format(cfg.value, { ...range, fallback: (v) => this._fmtValue(v, cfg.span) });
|
|
270
|
+
if (this._value) this._value.textContent = reading;
|
|
271
|
+
if (this._unit) { this._unit.textContent = cfg.unit; this._unit.hidden = !cfg.unit; }
|
|
272
|
+
if (this._caption) { this._caption.textContent = cfg.label; this._caption.hidden = !cfg.label; }
|
|
273
|
+
if (this._readout) this._readout.hidden = !cfg.digital && !cfg.label && !cfg.unit;
|
|
274
|
+
const tick = (v) => this._format(v, { ...range, kind: 'tick', fallback: String });
|
|
275
|
+
this._describe(`${cfg.label ? cfg.label + ': ' : ''}${reading}${cfg.unit}; range ${tick(cfg.min)} to ${tick(cfg.max)}`, 'meter');
|
|
276
|
+
this._ariaRange(cfg, cfg.value, this._formatText(cfg.value, { ...range, kind: 'aria', fallback: (v) => this._fmtValue(v, cfg.span) }, cfg.unit));
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
/** Tick-scale label: integers bare, otherwise one decimal. */
|