@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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <o-color-wheel>: a circular color picker. A hue ring around a
|
|
3
|
+
* saturation / brightness square, with an optional ring of swatches.
|
|
4
|
+
*
|
|
5
|
+
* .orbit-0 > .satellite.at-center > .ok-cw-ring the hue ring (a conic gradient)
|
|
6
|
+
* .orbit-12 > .satellite hue handle, at the hue's angle
|
|
7
|
+
* .orbit-11 > .satellite > button × n swatches, spread by Orbit
|
|
8
|
+
* .orbit-0 > .satellite.at-center > .ok-cw-area saturation (x) and brightness (y)
|
|
9
|
+
*
|
|
10
|
+
* The hue handle is one satellite on a ring whose --o-from is the hue, so
|
|
11
|
+
* Orbit places it. It is a form control (FormControl): `name`, `disabled`,
|
|
12
|
+
* form reset/restore; it submits the color as hex, rgb() or hsl() (`format`).
|
|
13
|
+
* Emits `input` while changing and `change` when a gesture commits.
|
|
14
|
+
*/
|
|
15
|
+
import { OrbitKitElement, FormControl, define, injectStyleOnce, universe, hub, fixed, escapeHTML as e, FORM_ATTRS, withShared } from './base.js';
|
|
16
|
+
import { parseColor, rgbToHex, hsvToRgb, rgbToHsv, colorName, formatColor } from './color.js';
|
|
17
|
+
|
|
18
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
19
|
+
const FORMATS = ['hex', 'rgb', 'hsl'];
|
|
20
|
+
|
|
21
|
+
const CW_CSS = `
|
|
22
|
+
o-color-wheel { -webkit-tap-highlight-color: transparent; }
|
|
23
|
+
o-color-wheel :where(.satellite) { border: none; background: transparent; }
|
|
24
|
+
o-color-wheel > .bigbang > .gravity-spot { font-size: var(--ok-font-size, 8cqmin); color: var(--ok-text, inherit); }
|
|
25
|
+
o-color-wheel .ok-cw-ring {
|
|
26
|
+
width: 100%; aspect-ratio: 1; border-radius: 50%;
|
|
27
|
+
background: conic-gradient(#f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
|
|
28
|
+
-webkit-mask: radial-gradient(closest-side, #0000 calc(var(--ok-cw-hole) - .5px), #000 var(--ok-cw-hole), #000 calc(100% - .75px), #0000 100%);
|
|
29
|
+
mask: radial-gradient(closest-side, #0000 calc(var(--ok-cw-hole) - .5px), #000 var(--ok-cw-hole), #000 calc(100% - .75px), #0000 100%);
|
|
30
|
+
}
|
|
31
|
+
o-color-wheel .ok-cw-area {
|
|
32
|
+
position: relative; width: 100%; aspect-ratio: 1; border-radius: var(--ok-cw-radius, .3em); touch-action: none;
|
|
33
|
+
background: linear-gradient(to top, #000, #0000), linear-gradient(to right, #fff, hsl(var(--ok-cw-hue, 0) 100% 50%));
|
|
34
|
+
box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
|
|
35
|
+
}
|
|
36
|
+
o-color-wheel .ok-cw-thumb {
|
|
37
|
+
box-sizing: border-box; display: block; border-radius: 50%; background: var(--ok-cw-color, #fff);
|
|
38
|
+
border: .1em solid var(--ok-cw-thumb-border, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,.35), 0 .05em .25em rgba(0,0,0,.45);
|
|
39
|
+
outline: none; cursor: grab; touch-action: none;
|
|
40
|
+
}
|
|
41
|
+
o-color-wheel .ok-cw-hue-thumb { width: 100%; aspect-ratio: 1; }
|
|
42
|
+
o-color-wheel .ok-cw-area-thumb { position: absolute; width: .82em; aspect-ratio: 1; translate: -50% -50%; }
|
|
43
|
+
o-color-wheel .ok-cw-thumb:focus-visible,
|
|
44
|
+
o-color-wheel .ok-cw-swatch:focus-visible { outline: 2px solid var(--ok-focus, #3da9fc); outline-offset: 2px; }
|
|
45
|
+
o-color-wheel .ok-cw-swatch {
|
|
46
|
+
font: inherit; display: block; width: 100%; aspect-ratio: 1; padding: 0; margin: 0; border-radius: 50%; cursor: pointer;
|
|
47
|
+
background: var(--ok-cw-swatch); border: .07em solid var(--ok-cw-swatch-border, rgba(255,255,255,.14));
|
|
48
|
+
transition: scale .15s;
|
|
49
|
+
}
|
|
50
|
+
o-color-wheel .ok-cw-swatch:hover { scale: 1.12; background: var(--ok-cw-swatch); }
|
|
51
|
+
o-color-wheel .ok-cw-swatch[aria-pressed="true"] { border-color: var(--ok-cw-thumb-border, #fff); box-shadow: 0 0 0 .07em rgba(0,0,0,.4); }
|
|
52
|
+
o-color-wheel[data-disabled] > .bigbang { opacity: .5; }
|
|
53
|
+
o-color-wheel[data-disabled] :is(.ok-cw-thumb, .ok-cw-swatch) { cursor: default; }
|
|
54
|
+
@media (prefers-reduced-motion: reduce) { o-color-wheel .ok-cw-swatch { transition: none; } }
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
/** "#1e90ff" for any parsable color, else null. */
|
|
58
|
+
export const toHex = (value) => { const rgb = parseColor(value); return rgb ? rgbToHex(rgb) : null; };
|
|
59
|
+
|
|
60
|
+
export class OrbitColorWheel extends FormControl(OrbitKitElement) {
|
|
61
|
+
static get observedAttributes() {
|
|
62
|
+
return withShared(['value', 'size', 'format', 'swatches', 'label', 'thickness'], FORM_ATTRS);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
constructor() {
|
|
66
|
+
super();
|
|
67
|
+
for (const name of ['_onDown', '_onMove', '_onUp', '_onKey']) this[name] = this[name].bind(this);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
readConfig() {
|
|
71
|
+
const swatches = (Array.isArray(this._json('swatches', [])) ? this._json('swatches', []) : []).slice(0, 36).map((s) => {
|
|
72
|
+
const item = typeof s === 'string' ? { color: s } : (s && typeof s === 'object' ? s : {});
|
|
73
|
+
const hex = toHex(item.color);
|
|
74
|
+
return hex ? { hex, label: item.label == null ? null : String(item.label) } : null;
|
|
75
|
+
}).filter(Boolean);
|
|
76
|
+
const format = this._str('format', 'hex');
|
|
77
|
+
const band = clamp(this._num('thickness', swatches.length ? 1.7 : 1.9), .8, 3);
|
|
78
|
+
const outer = swatches.length ? 9.6 : 11.6;
|
|
79
|
+
return {
|
|
80
|
+
size: this._size('size', '240px'), swatches, format: FORMATS.includes(format) ? format : 'hex',
|
|
81
|
+
label: this._str('label', 'Color picker'), disabled: this._isDisabled(),
|
|
82
|
+
outer, band, center: outer - band / 2,
|
|
83
|
+
// The square fits inside the hole with a little air.
|
|
84
|
+
half: (outer - band - .6) / Math.SQRT2,
|
|
85
|
+
value: toHex(this.getAttribute('value')) || '#4fd1c5'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
structuralSignature({ value, label, disabled, format, ...rest }) { return JSON.stringify(rest); }
|
|
90
|
+
|
|
91
|
+
build(cfg) {
|
|
92
|
+
injectStyleOnce('orbit-kit-color-wheel', CW_CSS);
|
|
93
|
+
const hole = fixed((cfg.outer - cfg.band) / cfg.outer * 100, 2);
|
|
94
|
+
const sw = cfg.swatches.length ? Math.min(1.7, 2 * Math.PI * 11 / cfg.swatches.length * .62) : 0;
|
|
95
|
+
const swatches = cfg.swatches.length ? `<div class="orbit-11 ok-cw-swatches" role="group" aria-label="Swatches" style="--o-size-ratio:${fixed(sw)}">${cfg.swatches.map((s, i) => {
|
|
96
|
+
const name = s.label ?? colorName(s.hex);
|
|
97
|
+
return `<div class="satellite"><div class="capsule"><button type="button" class="ok-cw-swatch" data-index="${i}" data-hex="${s.hex}" tabindex="-1" aria-pressed="false" aria-label="${e(`${name}, ${s.hex}`)}" title="${e(name)}" style="--ok-cw-swatch:${s.hex}"></button></div></div>`;
|
|
98
|
+
}).join('')}</div>` : '';
|
|
99
|
+
this.innerHTML = universe(cfg.size, `
|
|
100
|
+
${hub(2 * cfg.outer, `<div class="ok-cw-ring" aria-hidden="true" style="--ok-cw-hole:${hole}%"></div>`, 'ok-cw-ring-cap')}
|
|
101
|
+
<div class="orbit-12 ok-cw-hue-ring"><div class="satellite" style="--o-size-ratio:${fixed(cfg.band + .45)};--o-alignment:calc(var(--o-radius) / 12 * ${fixed(12 - cfg.center)})"><div class="capsule"><span class="ok-cw-thumb ok-cw-hue-thumb" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="359" data-part="hue"></span></div></div></div>
|
|
102
|
+
${hub(2 * cfg.half, '<div class="ok-cw-area"><span class="ok-cw-thumb ok-cw-area-thumb" role="slider" aria-label="Saturation and brightness" aria-roledescription="2D slider" aria-valuemin="0" aria-valuemax="100" data-part="area"></span></div>', 'ok-cw-area-cap')}
|
|
103
|
+
${swatches}`);
|
|
104
|
+
this._box = this.querySelector('.bigbang');
|
|
105
|
+
this._hueRing = this.querySelector('.ok-cw-hue-ring');
|
|
106
|
+
this._hueThumb = this.querySelector('.ok-cw-hue-thumb');
|
|
107
|
+
this._area = this.querySelector('.ok-cw-area');
|
|
108
|
+
this._areaThumb = this.querySelector('.ok-cw-area-thumb');
|
|
109
|
+
this._swatches = [...this.querySelectorAll('.ok-cw-swatch')];
|
|
110
|
+
this._painted = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sync(cfg) {
|
|
114
|
+
this._cfg = cfg;
|
|
115
|
+
// A new value attribute moves the picker; the hue survives grays and black.
|
|
116
|
+
if (!this._hsv || cfg.value !== rgbToHex(hsvToRgb(this._hsv))) this._hsv = this._adopt(cfg.value);
|
|
117
|
+
this._describe(cfg.label, 'group');
|
|
118
|
+
for (const thumb of [this._hueThumb, this._areaThumb]) if (thumb) thumb.tabIndex = cfg.disabled ? -1 : 0;
|
|
119
|
+
this._roving(this._swatches.find((s) => s.tabIndex === 0) || this._swatches[0], cfg.disabled);
|
|
120
|
+
this.style.touchAction = cfg.disabled ? 'auto' : 'none';
|
|
121
|
+
if (cfg.disabled) this._stopDrag();
|
|
122
|
+
this._paint();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** HSV for a hex, keeping the current hue (and saturation) where the hex has none. */
|
|
126
|
+
_adopt(hex) {
|
|
127
|
+
const next = rgbToHsv(parseColor(hex) || { r: 0, g: 0, b: 0 });
|
|
128
|
+
const prev = this._hsv;
|
|
129
|
+
if (prev && (next.s === 0 || next.v === 0)) next.h = prev.h;
|
|
130
|
+
if (prev && next.v === 0) next.s = prev.s;
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_paint() {
|
|
135
|
+
const { h, s, v } = this._hsv;
|
|
136
|
+
const rgb = hsvToRgb(this._hsv);
|
|
137
|
+
const hex = rgbToHex(rgb);
|
|
138
|
+
const key = `${fixed(h, 2)}|${fixed(s, 4)}|${fixed(v, 4)}`;
|
|
139
|
+
if (key === this._painted) return;
|
|
140
|
+
this._painted = key;
|
|
141
|
+
const name = colorName(rgb);
|
|
142
|
+
if (this._hueRing) this._hueRing.style.setProperty('--o-from', `${fixed(h, 2)}deg`);
|
|
143
|
+
if (this._hueThumb) {
|
|
144
|
+
this._hueThumb.style.setProperty('--ok-cw-color', `hsl(${fixed(h, 2)} 100% 50%)`);
|
|
145
|
+
this._hueThumb.setAttribute('aria-valuenow', String(Math.round(h) % 360));
|
|
146
|
+
this._hueThumb.setAttribute('aria-valuetext', `${Math.round(h) % 360} degrees, ${colorName(hsvToRgb({ h, s: 1, v: 1 }))}`);
|
|
147
|
+
}
|
|
148
|
+
if (this._area) this._area.style.setProperty('--ok-cw-hue', fixed(h, 2));
|
|
149
|
+
if (this._areaThumb) {
|
|
150
|
+
this._areaThumb.style.left = `${fixed(s * 100, 2)}%`;
|
|
151
|
+
this._areaThumb.style.top = `${fixed((1 - v) * 100, 2)}%`;
|
|
152
|
+
this._areaThumb.style.setProperty('--ok-cw-color', hex);
|
|
153
|
+
this._areaThumb.setAttribute('aria-valuenow', String(Math.round(s * 100)));
|
|
154
|
+
this._areaThumb.setAttribute('aria-valuetext', `Saturation ${Math.round(s * 100)}%, brightness ${Math.round(v * 100)}%, ${name}, ${hex}`);
|
|
155
|
+
}
|
|
156
|
+
for (const sw of this._swatches) sw.setAttribute('aria-pressed', String(sw.dataset.hex === hex));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get value() { return this._hsv ? rgbToHex(hsvToRgb(this._hsv)) : this.readConfig().value; }
|
|
160
|
+
/** Any CSS color; it is stored as hex. Setting it fires no event. */
|
|
161
|
+
set value(color) {
|
|
162
|
+
const hex = toHex(color);
|
|
163
|
+
if (!hex) return;
|
|
164
|
+
if (!this.isConnected) { this.setAttribute('value', hex); return; }
|
|
165
|
+
this._hsv = this._adopt(hex);
|
|
166
|
+
this._reflectValue(hex);
|
|
167
|
+
this._paint();
|
|
168
|
+
this._updateForm();
|
|
169
|
+
}
|
|
170
|
+
get rgb() { return formatColor(parseColor(this.value), 'rgb'); }
|
|
171
|
+
get hsl() { return formatColor(parseColor(this.value), 'hsl'); }
|
|
172
|
+
/** A plain-English name of the color, like "dark blue". */
|
|
173
|
+
get colorName() { return colorName(this.value); }
|
|
174
|
+
|
|
175
|
+
_formValue() {
|
|
176
|
+
const format = this.getAttribute('format');
|
|
177
|
+
return formatColor(parseColor(this.value), FORMATS.includes(format) ? format : 'hex');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_formAnchor() { return this._areaThumb || undefined; }
|
|
181
|
+
|
|
182
|
+
onConnected() {
|
|
183
|
+
this.addEventListener('pointerdown', this._onDown);
|
|
184
|
+
this.addEventListener('lostpointercapture', this._onUp);
|
|
185
|
+
this.addEventListener('keydown', this._onKey);
|
|
186
|
+
this.addEventListener('click', this._onSwatch);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
onDisconnected() {
|
|
190
|
+
this.removeEventListener('pointerdown', this._onDown);
|
|
191
|
+
this.removeEventListener('lostpointercapture', this._onUp);
|
|
192
|
+
this.removeEventListener('keydown', this._onKey);
|
|
193
|
+
this.removeEventListener('click', this._onSwatch);
|
|
194
|
+
this._stopDrag();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Set the color from a gesture: `input` on each change, `change` with { change: true }. */
|
|
198
|
+
_commit(hsv, { change = false } = {}) {
|
|
199
|
+
const before = this.value;
|
|
200
|
+
this._hsv = { h: ((hsv.h % 360) + 360) % 360, s: clamp(hsv.s, 0, 1), v: clamp(hsv.v, 0, 1) };
|
|
201
|
+
const hex = this.value;
|
|
202
|
+
this._reflectValue(hex);
|
|
203
|
+
this._paint();
|
|
204
|
+
this._updateForm();
|
|
205
|
+
if (hex !== before) {
|
|
206
|
+
this._emit('input');
|
|
207
|
+
if (change) this._emit('change');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_emit(type) {
|
|
212
|
+
const rgb = parseColor(this.value);
|
|
213
|
+
this._emitValue(type, this.value, { rgb: formatColor(rgb, 'rgb'), hsl: formatColor(rgb, 'hsl'), name: colorName(rgb) });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Where a pointer is, in lanes from the center (a lane is a twelfth of the radius). */
|
|
217
|
+
_lanes(event) {
|
|
218
|
+
const r = this._box?.getBoundingClientRect?.();
|
|
219
|
+
if (!r?.width) return null;
|
|
220
|
+
const lane = Math.min(r.width, r.height) / 24;
|
|
221
|
+
return { x: (event.clientX - (r.left + r.width / 2)) / lane, y: (event.clientY - (r.top + r.height / 2)) / lane };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_onDown(event) {
|
|
225
|
+
if (this._isDisabled() || event.button !== 0 || this._pointerId != null || event.target?.closest?.('.ok-cw-swatch')) return;
|
|
226
|
+
const p = this._lanes(event);
|
|
227
|
+
const cfg = this._cfg;
|
|
228
|
+
if (!p || !cfg) return;
|
|
229
|
+
const d = Math.hypot(p.x, p.y);
|
|
230
|
+
if (d >= cfg.outer - cfg.band - .5 && d <= cfg.outer + .5) this._mode = 'hue';
|
|
231
|
+
else if (Math.abs(p.x) <= cfg.half + .5 && Math.abs(p.y) <= cfg.half + .5) this._mode = 'area';
|
|
232
|
+
else return;
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
(this._mode === 'hue' ? this._hueThumb : this._areaThumb)?.focus({ preventScroll: true });
|
|
235
|
+
this._pointerId = event.pointerId;
|
|
236
|
+
this._startValue = this.value;
|
|
237
|
+
this.setPointerCapture?.(event.pointerId);
|
|
238
|
+
window.addEventListener('pointermove', this._onMove);
|
|
239
|
+
window.addEventListener('pointerup', this._onUp);
|
|
240
|
+
window.addEventListener('pointercancel', this._onUp);
|
|
241
|
+
this._move(p);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_onMove(event) {
|
|
245
|
+
if (event.pointerId !== this._pointerId || this._isDisabled()) return;
|
|
246
|
+
const p = this._lanes(event);
|
|
247
|
+
if (p) this._move(p);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_move(p) {
|
|
251
|
+
const { half } = this._cfg;
|
|
252
|
+
if (this._mode === 'hue') this._commit({ ...this._hsv, h: Math.atan2(p.x, -p.y) * 180 / Math.PI });
|
|
253
|
+
else this._commit({ ...this._hsv, s: (p.x + half) / (2 * half), v: (half - p.y) / (2 * half) });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_stopDrag() {
|
|
257
|
+
if (typeof window === 'undefined') return;
|
|
258
|
+
if (this._pointerId != null && this.hasPointerCapture?.(this._pointerId)) this.releasePointerCapture(this._pointerId);
|
|
259
|
+
this._pointerId = null;
|
|
260
|
+
window.removeEventListener('pointermove', this._onMove);
|
|
261
|
+
window.removeEventListener('pointerup', this._onUp);
|
|
262
|
+
window.removeEventListener('pointercancel', this._onUp);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_onUp(event) {
|
|
266
|
+
if (event.pointerId !== this._pointerId) return;
|
|
267
|
+
const changed = this.value !== this._startValue;
|
|
268
|
+
this._stopDrag();
|
|
269
|
+
if (changed && !this._isDisabled()) this._emit('change');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_onSwatch = (event) => {
|
|
273
|
+
const sw = event.target?.closest?.('.ok-cw-swatch');
|
|
274
|
+
if (!sw || this._isDisabled()) return;
|
|
275
|
+
this._roving(sw);
|
|
276
|
+
this._commit(rgbToHsv(parseColor(sw.dataset.hex)), { change: true });
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
_roving(active, disabled = this._isDisabled()) {
|
|
280
|
+
for (const sw of this._swatches || []) sw.tabIndex = !disabled && sw === active ? 0 : -1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_onKey(event) {
|
|
284
|
+
if (this._isDisabled()) return;
|
|
285
|
+
const part = event.target?.dataset?.part;
|
|
286
|
+
const big = event.shiftKey ? 10 : 1;
|
|
287
|
+
const { h, s, v } = this._hsv;
|
|
288
|
+
let next = null;
|
|
289
|
+
if (event.target?.classList?.contains('ok-cw-swatch')) {
|
|
290
|
+
const i = this._swatches.indexOf(event.target), n = this._swatches.length;
|
|
291
|
+
const to = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: n - 1 }[event.key];
|
|
292
|
+
if (to == null) return;
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
const sw = this._swatches[(to + n) % n];
|
|
295
|
+
this._roving(sw);
|
|
296
|
+
sw.focus();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (part === 'hue') {
|
|
300
|
+
const moves = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1, PageUp: 10, PageDown: -10 };
|
|
301
|
+
if (Object.hasOwn(moves, event.key)) next = { h: (Math.round(h) + moves[event.key] * (event.key.startsWith('Page') ? 1 : big) + 360) % 360, s, v };
|
|
302
|
+
else if (event.key === 'Home') next = { h: 0, s, v };
|
|
303
|
+
else if (event.key === 'End') next = { h: 359, s, v };
|
|
304
|
+
} else if (part === 'area') {
|
|
305
|
+
const step = big / 100;
|
|
306
|
+
const r = (n) => Math.round(n * 100) / 100;
|
|
307
|
+
if (event.key === 'ArrowRight') next = { h, s: r(s) + step, v };
|
|
308
|
+
else if (event.key === 'ArrowLeft') next = { h, s: r(s) - step, v };
|
|
309
|
+
else if (event.key === 'ArrowUp') next = { h, s, v: r(v) + step };
|
|
310
|
+
else if (event.key === 'ArrowDown') next = { h, s, v: r(v) - step };
|
|
311
|
+
else if (event.key === 'PageUp') next = { h, s, v: r(v) + .1 };
|
|
312
|
+
else if (event.key === 'PageDown') next = { h, s, v: r(v) - .1 };
|
|
313
|
+
else if (event.key === 'Home') next = { h, s: 0, v };
|
|
314
|
+
else if (event.key === 'End') next = { h, s: 1, v };
|
|
315
|
+
}
|
|
316
|
+
if (!next) return;
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
this._commit(next, { change: true });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
define('o-color-wheel', OrbitColorWheel);
|
package/src/o-compass.js
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <o-compass> — heading indicator.
|
|
3
3
|
*
|
|
4
|
-
* A fixed dial with a
|
|
5
|
-
*
|
|
6
|
-
* the way an aircraft heading indicator reads.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* A fixed dial with a lubber index at the top; the compass card (36 ticks and
|
|
5
|
+
* the cardinal letters) turns so the current `heading` sits under the index,
|
|
6
|
+
* the way an aircraft heading indicator reads. Turning the card is Orbit's own
|
|
7
|
+
* `--o-from` on its rings, so changing `heading` only re-rotates. Capsules
|
|
8
|
+
* stay upright on their own.
|
|
9
9
|
*
|
|
10
10
|
* .bigbang > .gravity-spot
|
|
11
|
-
* ├── .orbit-
|
|
12
|
-
* ├── .orbit-
|
|
13
|
-
* ├── .orbit-
|
|
14
|
-
* ├── .orbit-
|
|
15
|
-
* └── .orbit-0
|
|
11
|
+
* ├── .orbit-12 (filled) dial face + bezel
|
|
12
|
+
* ├── .orbit-12 (turns) > .vector × 36 tick scale
|
|
13
|
+
* ├── .orbit-10 (turns) > .satellite × n cardinals
|
|
14
|
+
* ├── .orbit-12 > .satellite.angle-0 lubber index (fixed)
|
|
15
|
+
* └── .orbit-0 > heading readout
|
|
16
16
|
*
|
|
17
17
|
* <o-compass heading="45"></o-compass>
|
|
18
18
|
* <o-compass heading="120" labels="N,NE,E,SE,S,SW,W,NW"></o-compass>
|
|
19
19
|
*/
|
|
20
|
-
import { OrbitKitElement, define, injectStyleOnce } from './base.js';
|
|
20
|
+
import { OrbitKitElement, define, injectStyleOnce, universe, escapeHTML as e, withShared, FORMAT_ATTRS } from './base.js';
|
|
21
21
|
|
|
22
22
|
const CMP_CSS = `
|
|
23
|
+
o-compass { --ok-font-size: 7cqmin; }
|
|
23
24
|
o-compass .ok-cmp-dial {
|
|
24
|
-
background: var(--ok-cmp-face, radial-gradient(circle at 50% 35%, #1b212b, #0a0d12));
|
|
25
|
-
box-shadow: inset 0 0 0 4px var(--ok-cmp-bezel, #2a3340), inset 0 4px 18px rgba(0,0,0,.65);
|
|
25
|
+
background: var(--ok-cmp-face, var(--ok-face, radial-gradient(circle at 50% 35%, #1b212b, #0a0d12)));
|
|
26
|
+
box-shadow: inset 0 0 0 4px var(--ok-cmp-bezel, var(--ok-bezel, #2a3340)), inset 0 4px 18px rgba(0,0,0,.65);
|
|
26
27
|
}
|
|
27
|
-
o-compass .ok-cmp-
|
|
28
|
-
o-compass .ok-cmp-tick
|
|
29
|
-
o-compass .ok-cmp-
|
|
30
|
-
o-compass .ok-cmp-card
|
|
31
|
-
o-compass .ok-cmp-card.north { color: var(--ok-cmp-accent, #ef4444); }
|
|
28
|
+
o-compass .ok-cmp-tick { background: var(--ok-cmp-tick, var(--ok-tick, rgba(203,213,225,.4))); height: 1px; }
|
|
29
|
+
o-compass .ok-cmp-tick-M { background: var(--ok-cmp-tick-M, var(--ok-hand, #e6edf3)); height: max(2px, .9cqmin); border-radius: 1px; }
|
|
30
|
+
o-compass .ok-cmp-card { font-weight: 800; color: var(--ok-cmp-text, var(--ok-text, #e6edf3)); }
|
|
31
|
+
o-compass .ok-cmp-card.north { color: var(--ok-cmp-accent, var(--ok-accent, #ef4444)); }
|
|
32
32
|
o-compass .ok-cmp-lubber {
|
|
33
|
-
|
|
34
|
-
border-
|
|
33
|
+
filter: drop-shadow(0 0 var(--ok-cmp-glow, 0px) var(--ok-cmp-accent, var(--ok-accent, #ef4444)));
|
|
34
|
+
width: 0; height: 0; border-left: .45em solid transparent; border-right: .45em solid transparent;
|
|
35
|
+
border-top: .8em solid var(--ok-cmp-accent, var(--ok-accent, #ef4444)); translate: 0 40%;
|
|
35
36
|
}
|
|
36
37
|
o-compass .ok-cmp-readout { display: flex; flex-direction: column; align-items: center; line-height: 1.05; }
|
|
37
|
-
o-compass .ok-cmp-deg { font-weight: 800; font-size: 1.5em; font-variant-numeric: tabular-nums; color: #f8fafc; }
|
|
38
|
-
o-compass .ok-cmp-sub { font-size: .
|
|
38
|
+
o-compass .ok-cmp-deg { font-weight: 800; font-size: 1.5em; font-variant-numeric: tabular-nums; color: var(--ok-cmp-text, var(--ok-text, #f8fafc)); }
|
|
39
|
+
o-compass .ok-cmp-sub { font-size: .65em; opacity: .6; }
|
|
39
40
|
`;
|
|
40
41
|
|
|
41
42
|
// Compass point abbreviations for the heading readout.
|
|
@@ -43,7 +44,7 @@ const POINTS = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
|
|
|
43
44
|
|
|
44
45
|
export class OrbitCompass extends OrbitKitElement {
|
|
45
46
|
static get observedAttributes() {
|
|
46
|
-
return ['heading', 'size', 'labels', 'fill', '
|
|
47
|
+
return withShared(['heading', 'size', 'labels', 'fill', 'accent', 'glow'], FORMAT_ATTRS);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
readConfig() {
|
|
@@ -51,50 +52,41 @@ export class OrbitCompass extends OrbitKitElement {
|
|
|
51
52
|
return {
|
|
52
53
|
heading: ((this._num('heading', 0) % 360) + 360) % 360,
|
|
53
54
|
labels,
|
|
54
|
-
size: this.
|
|
55
|
-
fill: this.
|
|
56
|
-
unit: this.
|
|
57
|
-
glow: this.
|
|
55
|
+
size: this._size('size', '220px'),
|
|
56
|
+
fill: this._color('accent', this._color('fill', null)),
|
|
57
|
+
unit: this._unitText('°'),
|
|
58
|
+
glow: this.getAttribute('glow')
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
structuralSignature(cfg) {
|
|
62
|
-
const { heading, ...rest } = cfg;
|
|
63
|
+
const { heading, unit, fill, ...rest } = cfg;
|
|
63
64
|
return JSON.stringify(rest);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
build(cfg) {
|
|
67
68
|
injectStyleOnce('orbit-kit-compass', CMP_CSS);
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
// orbit-6 is the outermost ring; scale --o-force so it fills `size`.
|
|
71
|
-
const dialPx = parseFloat(cfg.size) || 220;
|
|
72
|
-
const force = (dialPx * 12 / 6).toFixed(1);
|
|
73
|
-
const baseFont = (dialPx * 0.07).toFixed(1);
|
|
69
|
+
this._applyTheme({ '--ok-cmp-glow': cfg.glow == null ? null : `${Math.max(0, this._num('glow', 6))}px` });
|
|
74
70
|
|
|
75
71
|
// 36 ticks every 10°; every 9th (90°) is a long cardinal tick.
|
|
76
72
|
const ticks = Array.from({ length: 36 }, (_, i) =>
|
|
77
|
-
`<div class="vector inner-orbit ${i % 9 === 0 ? '
|
|
73
|
+
`<div class="vector inner-orbit ${i % 9 === 0 ? 'ok-cmp-tick-M" style="--o-size-ratio:1.5' : 'ok-cmp-tick" style="--o-size-ratio:.8'}"></div>`).join('');
|
|
78
74
|
|
|
79
75
|
// Cardinals distributed by count (NO fit-range — that overlaps on a 360 loop).
|
|
80
76
|
// Plain satellites (NOT spin-lock): the capsule stays upright by default
|
|
81
77
|
// while the ring's --o-from rotates only their POSITION. spin-lock would
|
|
82
78
|
// over-rotate the letters once we drive --o-from for the heading.
|
|
83
79
|
const cards = cfg.labels.map((l, i) =>
|
|
84
|
-
`<div class="satellite"><div class="capsule ok-cmp-card${i === 0 ? ' north' : ''}">${l}</div></div>`).join('');
|
|
80
|
+
`<div class="satellite"><div class="capsule ok-cmp-card${i === 0 ? ' north' : ''}">${e(l)}</div></div>`).join('');
|
|
85
81
|
|
|
86
|
-
this.innerHTML = `
|
|
87
|
-
<div class="
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<span class="ok-cmp-deg"></span><span class="ok-cmp-sub"></span>
|
|
95
|
-
</div></div></div>
|
|
96
|
-
</div>
|
|
97
|
-
</div>`;
|
|
82
|
+
this.innerHTML = universe(cfg.size, `
|
|
83
|
+
<div class="orbit-12 ok-cmp-dial ok-surface"></div>
|
|
84
|
+
<div class="orbit-12 ok-cmp-rotor o-cmp-ticks" style="--o-from:0deg">${ticks}</div>
|
|
85
|
+
<div class="orbit-10 ok-cmp-rotor o-cmp-cards" style="--o-from:0deg">${cards}</div>
|
|
86
|
+
<div class="orbit-12"><div class="satellite angle-0"><div class="capsule ok-cmp-lubber"></div></div></div>
|
|
87
|
+
<div class="orbit-0"><div class="satellite at-center"><div class="capsule ok-cmp-readout">
|
|
88
|
+
<span class="ok-cmp-deg"></span><span class="ok-cmp-sub"></span>
|
|
89
|
+
</div></div></div>`);
|
|
98
90
|
|
|
99
91
|
this._ticks = this.querySelector('.o-cmp-ticks');
|
|
100
92
|
this._cards = this.querySelector('.o-cmp-cards');
|
|
@@ -107,8 +99,12 @@ export class OrbitCompass extends OrbitKitElement {
|
|
|
107
99
|
const rot = `${-cfg.heading}deg`;
|
|
108
100
|
if (this._ticks) this._ticks.style.setProperty('--o-from', rot);
|
|
109
101
|
if (this._cards) this._cards.style.setProperty('--o-from', rot);
|
|
110
|
-
|
|
102
|
+
const range = { min: 0, max: 360 };
|
|
103
|
+
const degrees = (h) => String(Math.round(h) % 360).padStart(3, '0');
|
|
104
|
+
if (this._deg) this._deg.textContent = this._formatText(cfg.heading, { ...range, fallback: degrees }, cfg.unit);
|
|
111
105
|
if (this._sub) this._sub.textContent = POINTS[Math.round(cfg.heading / 45) % 8];
|
|
106
|
+
this._describe(`Heading ${this._formatText(cfg.heading, { ...range, kind: 'aria', fallback: (h) => String(Math.round(h) % 360) }, cfg.unit)}, ${POINTS[Math.round(cfg.heading / 45) % 8]}`);
|
|
107
|
+
this._applyTheme({ '--ok-cmp-accent': cfg.fill });
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
110
|
|
package/src/o-donut.js
CHANGED
|
@@ -1,100 +1,88 @@
|
|
|
1
|
+
import { OrbitKitElement, define, injectStyleOnce, universe, hub, fixed, finiteNumber, cssValue, escapeHTML as e, withShared, FORMAT_ATTRS } from './base.js';
|
|
2
|
+
|
|
3
|
+
const PALETTE = ['#6366f1', '#22c55e', '#eab308', '#f97316', '#ef4444', '#06b6d4', '#a855f7', '#ec4899'];
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
* <o-donut
|
|
3
|
-
*
|
|
4
|
-
* Collapses the Orbit recipe:
|
|
5
|
-
*
|
|
6
|
-
* .bigbang > .gravity-spot
|
|
7
|
-
* ├── .orbit-N > <o-arc> × segments (each arc stacks onto the previous)
|
|
8
|
-
* └── .orbit-0 > .satellite.at-center > .capsule (center label)
|
|
6
|
+
* <o-donut>: a donut chart from a JSON array. Segments are sibling <o-arc>s
|
|
7
|
+
* with a `value`, so Orbit stacks them one after another around one ring.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* {"value":28,"color":"#eab308"},
|
|
14
|
-
* {"value":20,"color":"#22c55e"}]'
|
|
15
|
-
* label="$2.4k" unit=""></o-donut>
|
|
16
|
-
*
|
|
17
|
-
* Segments are normalized to the sum of their values (so they fill the ring),
|
|
18
|
-
* unless `total` is given — then values are taken as a fraction of `total`,
|
|
19
|
-
* leaving an empty remainder (partial donut).
|
|
9
|
+
* .orbit-12 > o-arc track
|
|
10
|
+
* .orbit-12 > o-arc[value] × n segments (stacked by Orbit)
|
|
11
|
+
* .orbit-0 > .satellite.at-center label
|
|
20
12
|
*/
|
|
21
|
-
import { OrbitKitElement, define } from './base.js';
|
|
22
|
-
|
|
23
13
|
export class OrbitDonut extends OrbitKitElement {
|
|
24
14
|
static get observedAttributes() {
|
|
25
|
-
return ['data', 'size', 'orbit', 'stroke-width', 'gap', 'track',
|
|
26
|
-
'range', 'from', 'label', 'unit', 'total', 'glow'];
|
|
15
|
+
return withShared(['data', 'size', 'orbit', 'thickness', 'stroke-width', 'gap', 'track', 'range', 'from', 'label', 'total', 'shape', 'glow'], FORMAT_ATTRS);
|
|
27
16
|
}
|
|
28
17
|
|
|
29
18
|
readConfig() {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
color: d.color || null,
|
|
34
|
-
label: d.label != null ? String(d.label) : null
|
|
19
|
+
const data = this._array('data').map((d, i) => ({
|
|
20
|
+
value: Math.max(0, finiteNumber(d.value)), color: cssValue(d.color, PALETTE[i % PALETTE.length]),
|
|
21
|
+
label: d.label == null ? null : String(d.label)
|
|
35
22
|
}));
|
|
36
|
-
|
|
37
|
-
const
|
|
23
|
+
// Normalize through a scaled sum to avoid overflow on large finite values.
|
|
24
|
+
const largest = data.reduce((n, d) => Math.max(n, d.value), 1);
|
|
25
|
+
const sum = data.reduce((n, d) => n + d.value / largest, 0) * largest;
|
|
26
|
+
const requestedTotal = this._num('total', sum);
|
|
27
|
+
const total = requestedTotal > 0 ? requestedTotal : (sum || 1);
|
|
28
|
+
let remaining = 100;
|
|
29
|
+
const weightSum = data.reduce((n, d) => n + d.value / largest, 0) || 1;
|
|
30
|
+
for (const d of data) {
|
|
31
|
+
const pct = Number.isFinite(total) ? d.value / total * 100 : (d.value / largest) / weightSum * 100;
|
|
32
|
+
d.pct = Math.max(0, Math.min(remaining, pct));
|
|
33
|
+
remaining = Math.max(0, remaining - d.pct);
|
|
34
|
+
}
|
|
38
35
|
return {
|
|
39
|
-
data,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
size: this._str('size', '200px'),
|
|
45
|
-
strokeWidth: this._num('stroke-width', 14),
|
|
46
|
-
gap: this._num('gap', 2),
|
|
47
|
-
shape: this._str('shape', 'none'),
|
|
48
|
-
track: this._str('track', 'rgba(255,255,255,.05)'),
|
|
49
|
-
unit: this._str('unit', ''),
|
|
50
|
-
glow: this.getAttribute('glow'),
|
|
51
|
-
label: this.hasAttribute('label') ? this.getAttribute('label') : null
|
|
36
|
+
data, total, range: Math.max(0, Math.min(360, this._num('range', 360))), from: this._num('from', 0),
|
|
37
|
+
thickness: this._thickness(3), size: this._size('size', '200px'),
|
|
38
|
+
strokeWidth: Math.max(0, this._num('stroke-width', 0)), gap: this._gap(1.5),
|
|
39
|
+
shape: this._str('shape', 'none'), track: this._color('track', null),
|
|
40
|
+
unit: this._unitText(), glow: this.hasAttribute('glow') ? this._glowPx() : null, label: this.getAttribute('label')
|
|
52
41
|
};
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return JSON.stringify(cfg);
|
|
44
|
+
/** A segment value as text; "percent" is its share of the total. */
|
|
45
|
+
_segmentText(cfg, d, i) {
|
|
46
|
+
return `${d.label ?? `Segment ${i + 1}`}: ${this._formatText(d.value, { min: 0, max: cfg.total })}`;
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const segments = cfg.data.map((d, i) => {
|
|
66
|
-
const pct = (d.value / cfg.total) * 100;
|
|
67
|
-
const color = d.color || palette[i % palette.length];
|
|
68
|
-
const style = [
|
|
69
|
-
`--o-fill:${color}`,
|
|
70
|
-
`--o-stroke:${color}`,
|
|
71
|
-
`--o-stroke-width:${cfg.strokeWidth}`,
|
|
72
|
-
`--o-gap:${cfg.gap}`,
|
|
73
|
-
this._glow(color)
|
|
74
|
-
].join(';');
|
|
75
|
-
const title = d.label ? ` title="${d.label}: ${d.value}"` : '';
|
|
76
|
-
return `<o-arc value="${pct.toFixed(4)}" shape="${cfg.shape}" style="${style}"${title}></o-arc>`;
|
|
77
|
-
}).join('');
|
|
78
|
-
|
|
79
|
-
// Faint full-circle track underneath the segments (rounded to match).
|
|
80
|
-
const track = cfg.track && cfg.track !== 'transparent'
|
|
81
|
-
? `<o-arc value="100" style="--o-fill:${cfg.track};--o-stroke:${cfg.track};--o-stroke-width:${cfg.strokeWidth}"></o-arc>`
|
|
82
|
-
: '';
|
|
49
|
+
structuralSignature({ data, total, label, unit, track, ...structure }) {
|
|
50
|
+
return JSON.stringify({ ...structure, count: data.length });
|
|
51
|
+
}
|
|
83
52
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
53
|
+
build(cfg) {
|
|
54
|
+
const ring = `--o-range:${cfg.range}deg;--o-from:${cfg.from}deg;--o-size-ratio:${fixed(cfg.thickness)};`;
|
|
55
|
+
const stroke = cfg.strokeWidth ? `--o-stroke-width:${cfg.strokeWidth};` : '';
|
|
56
|
+
const segments = cfg.data.map((d, i) =>
|
|
57
|
+
`<o-arc class="inner-orbit ok-donut-segment" value="${d.pct.toFixed(4)}" shape="${e(cfg.shape)}" style="${e(`--o-fill:${d.color};${stroke}${cfg.glow == null ? '' : `filter:drop-shadow(0 0 ${cfg.glow}px ${d.color});`}`)}" title="${e(this._segmentText(cfg, d, i))}"></o-arc>`).join('');
|
|
58
|
+
this.innerHTML = universe(cfg.size, `
|
|
59
|
+
<div class="orbit-12" style="${ring}--o-gap:0"><o-arc class="inner-orbit ok-track" value="100"></o-arc></div>
|
|
60
|
+
<div class="orbit-12" style="${ring}--o-gap:${fixed(cfg.gap * 6)}">${segments}</div>
|
|
61
|
+
${hub(2 * (12 - cfg.thickness) - 1, `${e(cfg.label ?? '')}${cfg.label == null ? '' : e(cfg.unit)}`)}`);
|
|
62
|
+
this._segments = [...this.querySelectorAll('.ok-donut-segment')];
|
|
63
|
+
this._center = this.querySelector('.ok-hub');
|
|
64
|
+
}
|
|
87
65
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
66
|
+
sync(cfg) {
|
|
67
|
+
this._applyTheme({ '--ok-track': cfg.track });
|
|
68
|
+
this._segments?.forEach((arc, i) => {
|
|
69
|
+
const d = cfg.data[i];
|
|
70
|
+
arc.setAttribute('value', d.pct.toFixed(4));
|
|
71
|
+
arc.setAttribute('title', this._segmentText(cfg, d, i));
|
|
72
|
+
arc.style.setProperty('--o-fill', d.color);
|
|
73
|
+
if (cfg.glow != null) arc.style.setProperty('filter', `drop-shadow(0 0 ${cfg.glow}px ${d.color})`);
|
|
74
|
+
else arc.style.removeProperty?.('filter');
|
|
75
|
+
});
|
|
76
|
+
if (this._center) {
|
|
77
|
+
this._center.textContent = cfg.label == null ? '' : `${cfg.label}${cfg.unit}`;
|
|
78
|
+
this._center.hidden = cfg.label == null;
|
|
79
|
+
}
|
|
80
|
+
this._describe([cfg.label == null ? null : `${cfg.label}${cfg.unit}`, ...cfg.data.map((d, i) => this._segmentText(cfg, d, i))].filter(Boolean).join('; ') || 'Empty donut chart');
|
|
97
81
|
}
|
|
98
82
|
}
|
|
99
83
|
|
|
84
|
+
injectStyleOnce('orbit-kit-donut', `
|
|
85
|
+
o-donut .ok-track { --o-fill: var(--ok-track, rgba(127,127,127,.12)); --o-stroke: var(--o-fill); }
|
|
86
|
+
o-donut .ok-donut-segment { --o-stroke: var(--o-fill); }
|
|
87
|
+
`);
|
|
100
88
|
define('o-donut', OrbitDonut);
|