@zumer/orbit 1.6.0 → 2.0.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/dist/orbit.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * orbit
3
- * v.1.6.0
3
+ * v.2.0.0
4
4
  * Author Juan Martin Muda - Zumerlab
5
5
  * License MIT
6
6
  */
@@ -18,6 +18,56 @@
18
18
  }
19
19
  return ((_a = element.classList) == null ? void 0 : _a.contains("orbit")) ? null : void 0;
20
20
  }
21
+ var WARNED = Symbol.for("zumer.orbit.warned.v1");
22
+ function warnOnce(element, key, message) {
23
+ const seen = element[WARNED] || (element[WARNED] = /* @__PURE__ */ new Set());
24
+ if (seen.has(key)) return;
25
+ seen.add(key);
26
+ console.warn(`[orbit] ${message}`);
27
+ }
28
+ function composedClosest(element, selector) {
29
+ var _a, _b;
30
+ for (let current = element; current; current = current.parentElement || ((_a = current.getRootNode) == null ? void 0 : _a.call(current).host)) {
31
+ if ((_b = current.matches) == null ? void 0 : _b.call(current, selector)) return current;
32
+ }
33
+ return null;
34
+ }
35
+ var KINDS = ".satellite, .vector, .side, o-arc, o-progress";
36
+ function describe(element) {
37
+ const classes = [...element.classList].slice(0, 3).map((name) => "." + name).join("");
38
+ return `<${element.localName}${classes ? " " + classes : ""}>`;
39
+ }
40
+ function diagnose(ring) {
41
+ const spot = ring.parentElement;
42
+ const universe = composedClosest(spot.parentElement, ".bigbang, .satellite");
43
+ if (!universe) {
44
+ warnOnce(spot, "universe", 'A .gravity-spot is not inside a .bigbang, so it measures the viewport. Wrap it: <div class="bigbang"><div class="gravity-spot">\u2026</div></div>.');
45
+ } else if (universe.getClientRects().length && (!universe.clientWidth || !universe.clientHeight)) {
46
+ warnOnce(universe, "empty", `${describe(universe)} has no size (${universe.clientWidth}\xD7${universe.clientHeight}px), so its system is invisible. Give it a width, or place it in a parent with a width.`);
47
+ }
48
+ const visual = ring.querySelector(":scope > o-arc, :scope > o-progress");
49
+ if (visual) {
50
+ const style = ring.ownerDocument.defaultView.getComputedStyle(ring);
51
+ const x = style.getPropertyValue("--o-ellipse-x").trim(), y = style.getPropertyValue("--o-ellipse-y").trim();
52
+ if (x && Number(x) !== 1 || y && Number(y) !== 1) {
53
+ warnOnce(ring, "ellipse-visual", `${describe(visual)} is on ${describe(ring)}, which is stretched into an ellipse (--o-ellipse-x: ${x || 1}, --o-ellipse-y: ${y || 1}). Arcs and progress are drawn on circles only, so it is hidden. Put it on a round ring.`);
54
+ }
55
+ }
56
+ if (!composedClosest(ring, ".dev-orbit")) return;
57
+ for (const child of ring.children) {
58
+ if (orbitNumber(child) !== void 0) {
59
+ warnOnce(child, "nested-ring", `${describe(child)} is inside another ring. Rings must be direct children of .gravity-spot; to nest a system, put a .gravity-spot directly inside a .satellite.`);
60
+ } else if (!child.matches(KINDS)) {
61
+ warnOnce(child, "ring-child", `${describe(child)} is not placed by its ring. A ring holds only .satellite, .vector, .side, <o-arc> and <o-progress>; wrap content as <div class="satellite"><div class="capsule">\u2026</div></div>.`);
62
+ } else if (child.classList.contains("satellite")) {
63
+ for (const inner of child.children) {
64
+ if (!inner.matches(".capsule, .gravity-spot, .bigbang")) {
65
+ warnOnce(inner, "satellite-child", `${describe(inner)} sits directly in a .satellite. Put content in a .capsule so it stays upright and centered.`);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
21
71
  function setLayoutProperty(element, name, value) {
22
72
  const text = String(value);
23
73
  if (element.style.getPropertyValue(name) !== text) element.style.setProperty(name, text);
@@ -62,7 +112,8 @@
62
112
  else if (child.localName === "o-progress") setLayoutProperty(child, "--o-layout-index", 0);
63
113
  }
64
114
  const count = Math.max(counts.arc, counts.satellite, counts.vector, 1);
65
- const divisor = counts.side ? String(counts.side) : `max(1, ${count} - var(--o-fit-range, 0))`;
115
+ const points = counts.satellite || counts.vector;
116
+ const divisor = counts.side ? String(counts.side) : points ? `max(1, ${count} - var(--o-fit-range, 0))` : String(count);
66
117
  setLayoutProperty(ring, "--o-layout-angle", `calc(var(--o-range, 360deg) / ${divisor})`);
67
118
  }
68
119
  function isScope(root) {
@@ -88,10 +139,19 @@
88
139
  });
89
140
  return doc[REGISTRY];
90
141
  }
142
+ function cancel(state) {
143
+ if (state.frame < 0) state.view.clearTimeout(-state.frame);
144
+ else if (state.frame) state.view.cancelAnimationFrame(state.frame);
145
+ state.frame = 0;
146
+ }
147
+ function settle(state) {
148
+ for (const resolve of state.waiters.splice(0)) resolve();
149
+ }
91
150
  function dispose(state) {
92
151
  if (state.disposed) return;
93
152
  state.disposed = true;
94
- state.view.cancelAnimationFrame(state.frame);
153
+ cancel(state);
154
+ settle(state);
95
155
  state.observer.disconnect();
96
156
  state.resize.disconnect();
97
157
  for (const remove of state.listeners) remove();
@@ -146,17 +206,20 @@
146
206
  all: true,
147
207
  frame: 0,
148
208
  disposed: false,
149
- listeners: []
209
+ listeners: [],
210
+ waiters: []
150
211
  };
151
212
  const listen = (target, event, listener, options) => {
152
213
  target == null ? void 0 : target.addEventListener(event, listener, options);
153
214
  state.listeners.push(() => target == null ? void 0 : target.removeEventListener(event, listener, options));
154
215
  };
155
216
  const queue = () => {
156
- if (!state.disposed && !state.frame) state.frame = view.requestAnimationFrame(() => {
217
+ if (state.disposed || state.frame) return;
218
+ const run = () => {
157
219
  state.frame = 0;
158
220
  state.flush();
159
- });
221
+ };
222
+ state.frame = doc.visibilityState === "hidden" ? -view.setTimeout(run, 0) : view.requestAnimationFrame(run);
160
223
  };
161
224
  state.queue = queue;
162
225
  state.flush = () => flush(state);
@@ -196,6 +259,12 @@
196
259
  invalidateDescendants(state);
197
260
  queue();
198
261
  };
262
+ listen(doc, "visibilitychange", () => {
263
+ if (doc.visibilityState === "hidden" && state.frame > 0) {
264
+ cancel(state);
265
+ queue();
266
+ }
267
+ });
199
268
  if (scope === doc) {
200
269
  listen(view, "resize", invalidateAll);
201
270
  listen(doc.fonts, "loadingdone", invalidateAll);
@@ -255,13 +324,13 @@
255
324
  state.scan = false;
256
325
  }
257
326
  function flush(state) {
258
- var _a;
259
327
  if (state.disposed) return;
260
328
  if (state.scope.host && !state.scope.host.isConnected) {
261
329
  dispose(state);
262
330
  return;
263
331
  }
264
332
  state.observer.disconnect();
333
+ const drawn = [];
265
334
  try {
266
335
  if (state.scan) discover(state);
267
336
  const dirty = state.all ? /* @__PURE__ */ new Set([...state.rings, ...state.visuals]) : state.dirty;
@@ -270,15 +339,21 @@
270
339
  if (!target.isConnected) continue;
271
340
  if (state.rings.has(target)) {
272
341
  layoutRing(target);
342
+ diagnose(target);
273
343
  for (const visual of target.querySelectorAll(VISUALS)) visuals.add(visual);
274
344
  } else if (state.visuals.has(target)) visuals.add(target);
275
345
  }
276
- for (const visual of state.visuals) if (visuals.has(visual)) (_a = visual.update) == null ? void 0 : _a.call(visual);
346
+ for (const visual of state.visuals) if (visuals.has(visual) && visual.update) {
347
+ visual.update();
348
+ drawn.push(visual);
349
+ }
277
350
  state.dirty = /* @__PURE__ */ new Set();
278
351
  state.all = false;
279
352
  } finally {
280
353
  if (!state.disposed) state.observer.observe(state.scope, OBSERVE);
281
354
  }
355
+ for (const visual of drawn) visual.dispatchEvent(new state.view.Event("o-rendered", { bubbles: true, composed: true }));
356
+ if (!state.frame) settle(state);
282
357
  }
283
358
  function requestLayout(element = globalThis.document) {
284
359
  var _a;
@@ -299,11 +374,22 @@
299
374
  if (current !== state && (!current.scope.host || !composedContains(root, current.scope.host))) continue;
300
375
  current.scan = true;
301
376
  current.all = true;
302
- current.view.cancelAnimationFrame(current.frame);
303
- current.frame = 0;
377
+ cancel(current);
304
378
  current.flush();
305
379
  }
306
380
  }
381
+ function layoutReady(root = globalThis.document) {
382
+ const scope = scopeFor(root);
383
+ const state = scope && ensureState(scope);
384
+ if (!state) return Promise.resolve();
385
+ pruneDisconnected(state.registry);
386
+ const pending = [...state.registry.states].filter((current) => current === state || current.scope.host && composedContains(root, current.scope.host));
387
+ return Promise.all(pending.map((current) => new Promise((resolve) => {
388
+ current.waiters.push(resolve);
389
+ current.queue();
390
+ }))).then(() => {
391
+ });
392
+ }
307
393
 
308
394
  // src/js/orbit-base.js
309
395
  var OrbitBase = class extends (globalThis.HTMLElement || class {
@@ -312,8 +398,32 @@
312
398
  super();
313
399
  }
314
400
  connectedCallback() {
401
+ for (const name of ["value", "max"]) {
402
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
403
+ const pending = this[name];
404
+ delete this[name];
405
+ this[name] = pending;
406
+ }
407
+ }
315
408
  requestLayout(this);
316
409
  }
410
+ /** value and max mirror their attributes, so property bindings redraw too. */
411
+ get value() {
412
+ const raw = this.getAttribute("value");
413
+ return raw === null || raw.trim() === "" ? null : Number(raw);
414
+ }
415
+ set value(next) {
416
+ if (next === null || next === void 0 || next === "") this.removeAttribute("value");
417
+ else this.setAttribute("value", String(next));
418
+ }
419
+ get max() {
420
+ const raw = this.getAttribute("max");
421
+ return raw === null ? 100 : Number(raw);
422
+ }
423
+ set max(next) {
424
+ if (next === null || next === void 0 || next === "") this.removeAttribute("max");
425
+ else this.setAttribute("max", String(next));
426
+ }
317
427
  disconnectedCallback() {
318
428
  if (this._textFrame) this.ownerDocument.defaultView.cancelAnimationFrame(this._textFrame);
319
429
  this._textFrame = 0;
@@ -324,6 +434,28 @@
324
434
  readAngle(style, name, fallback = 0) {
325
435
  return cssNumber(this, style.getPropertyValue(name), fallback, "angle");
326
436
  }
437
+ /** The value attribute as a number. Invalid values draw nothing, and say why. */
438
+ readValue() {
439
+ const raw = this.getAttribute("value");
440
+ if (raw === null || raw.trim() === "") return 0;
441
+ const value = Number(raw);
442
+ if (!Number.isFinite(value)) {
443
+ warnOnce(this, "value:" + raw, `<${this.localName} value="${raw}"> is not a number, so nothing is drawn.`);
444
+ return 0;
445
+ }
446
+ return value;
447
+ }
448
+ /** The max attribute (default 100). It must be a positive number. */
449
+ readMax() {
450
+ const raw = this.getAttribute("max");
451
+ if (raw === null) return 100;
452
+ const max = Number(raw);
453
+ if (!(max > 0)) {
454
+ warnOnce(this, "max:" + raw, `<${this.localName} max="${raw}"> must be a positive number, so nothing is drawn.`);
455
+ return 0;
456
+ }
457
+ return max;
458
+ }
327
459
  getCommonAttributes(element) {
328
460
  var _a;
329
461
  const style = element.ownerDocument.defaultView.getComputedStyle(element);
@@ -351,7 +483,7 @@
351
483
  arcHeight,
352
484
  realRadius,
353
485
  arcAngle: 0,
354
- // Se sobrescribe en cada componente
486
+ // set by each component
355
487
  shape,
356
488
  arcHeightPercentage,
357
489
  orbitNumber: orbitNumber2,
@@ -364,22 +496,6 @@
364
496
  if (!Number.isFinite(value) || !Number.isFinite(maxValue) || maxValue <= 0) return 0;
365
497
  return Math.min(1, Math.max(0, value / maxValue)) * Math.max(0, Math.min(360, maxAngle));
366
498
  }
367
- getControlPoint(x, y, x1, y1, direction = "clockwise") {
368
- const xm = (x + x1) / 2;
369
- const ym = (y + y1) / 2;
370
- const dx = x1 - x;
371
- const dy = y1 - y;
372
- if (direction === "clockwise") {
373
- return {
374
- xc: xm + dy * 0.4,
375
- yc: ym - dx * 0.4
376
- };
377
- }
378
- return {
379
- xc: xm - dy * 0.4,
380
- yc: ym + dx * 0.4
381
- };
382
- }
383
499
  arcPoint(radius, angle, radiusAdjustment = 0, angleOffsetDegrees = 0) {
384
500
  const adjustedRadius = radius + radiusAdjustment;
385
501
  const adjustedAngle = angle + angleOffsetDegrees * Math.PI / 180;
@@ -390,7 +506,7 @@
390
506
  }
391
507
  calculateCommonArcParameters(arcAngle, radius, arcHeightPercentage, orbitNumber2, shape, strokeWidth, arcHeight, gap = 0) {
392
508
  const offset = Math.PI / 2;
393
- const fangle = Math.max(0, Math.min(359.999999, arcAngle)) * Math.PI / 180;
509
+ const fangle = Math.max(0, Math.min(360, arcAngle)) * Math.PI / 180;
394
510
  const bigRadius = radius + arcHeightPercentage;
395
511
  const smallRadius = Math.max(1e-3, radius - arcHeightPercentage);
396
512
  const bigGap = Math.min(fangle * 0.49, (gap + strokeWidth * 1.25) / orbitNumber2 / bigRadius);
@@ -403,19 +519,11 @@
403
519
  const upperArcEnd = this.arcPoint(bigRadius, upperAngleEnd);
404
520
  const innerArcStart = this.arcPoint(smallRadius, innerAngleStart);
405
521
  const innerArcEnd = this.arcPoint(smallRadius, innerAngleEnd);
406
- const upperSweep = upperAngleEnd - upperAngleStart;
407
- const innerSweep = innerAngleEnd - innerAngleStart;
408
- const largeArcFlagUpper = upperSweep > Math.PI ? 1 : 0;
409
- const largeArcFlagInner = innerSweep > Math.PI ? 1 : 0;
410
522
  return {
411
523
  upperArcStart,
412
524
  upperArcEnd,
413
525
  innerArcStart,
414
526
  innerArcEnd,
415
- largeArcFlag: largeArcFlagUpper,
416
- // back-compat alias; prefer the two below
417
- largeArcFlagUpper,
418
- largeArcFlagInner,
419
527
  bigRadius,
420
528
  smallRadius,
421
529
  radius,
@@ -425,143 +533,153 @@
425
533
  innerAngleEnd
426
534
  };
427
535
  }
428
- generatePathData(shape, params, arcHeight, orbitNumber2) {
429
- let d = "";
536
+ generatePathData(shape, params) {
430
537
  switch (shape) {
431
538
  case "rounded":
432
- d = this.generateRoundedPath(params, arcHeight, orbitNumber2);
433
- break;
539
+ return this.generateRoundedPath(params);
434
540
  case "circle":
435
541
  case "circle-a":
436
542
  case "bullet":
437
- d = this.generateCirclePath(params, shape);
438
- break;
543
+ return this.generateCirclePath(params, shape);
439
544
  case "circle-b":
440
- d = this.generateCircleBPath(params, arcHeight, orbitNumber2);
441
- break;
545
+ return this.generateCircleBPath(params);
442
546
  case "arrow":
443
- d = this.generateArrowPath(params, orbitNumber2);
444
- break;
547
+ return this.generateArrowPath(params);
445
548
  case "backslash":
446
549
  case "slash":
447
- d = this.generateSlashPath(params, shape, orbitNumber2);
448
- break;
550
+ return this.generateSlashPath(params, shape);
449
551
  case "zigzag":
450
- d = this.generateZigzagPath(params, arcHeight, orbitNumber2);
451
- break;
552
+ return this.generateZigzagPath(params);
452
553
  default:
453
- d = this.generateDefaultPath(params);
554
+ return this.generateDefaultPath(params);
555
+ }
556
+ }
557
+ /** "x,y" of a point at radius and angle (radians), in viewBox units. */
558
+ pt(radius, angle) {
559
+ const point = this.arcPoint(radius, angle);
560
+ return `${point.x},${point.y}`;
561
+ }
562
+ /**
563
+ * SVG arc commands from the current point (at `from`) to `to`, along one
564
+ * circle. Every arc in Orbit goes through here. Splitting into pieces of at
565
+ * most 90° means no large-arc flag to get wrong and no arc whose endpoints
566
+ * coincide, so a full 360° circle is just another arc.
567
+ */
568
+ arcTo(radius, from, to) {
569
+ const steps = Math.max(1, Math.ceil(Math.abs(to - from) / (Math.PI / 2) - 1e-9));
570
+ const sweep = to >= from ? 1 : 0;
571
+ let d = "";
572
+ for (let i = 1; i <= steps; i++) {
573
+ d += ` A ${radius},${radius} 0 0 ${sweep} ${this.pt(radius, from + (to - from) * i / steps)}`;
454
574
  }
455
575
  return d;
456
576
  }
457
- generateRoundedPath(params, arcHeight, orbitNumber2) {
577
+ /** An open arc along one radius: strokes, curved text and touch areas. */
578
+ linePath(radius, from, to) {
579
+ return `M ${this.pt(radius, from)}${this.arcTo(radius, from, to)}`;
580
+ }
581
+ /**
582
+ * Shape endings are sized from the band itself (h = half its thickness, in
583
+ * viewBox units), so they look the same on every ring and at every size.
584
+ * Angles are in radians, as in calculateCommonArcParameters().
585
+ */
586
+ ending(params, factor) {
458
587
  const { bigRadius, smallRadius } = params;
459
- const available = Math.min(params.upperAngleEnd - params.upperAngleStart, params.innerAngleEnd - params.innerAngleStart);
460
- const curve = Math.min(arcHeight < 5 ? 2.5 : arcHeight < 10 ? 5 : 10, available * 180 / Math.PI * orbitNumber2 * 0.49);
461
- const capRad = curve / orbitNumber2 * Math.PI / 180;
462
- const flagU = params.upperAngleEnd - params.upperAngleStart - 2 * capRad > Math.PI ? 1 : 0;
463
- const flagI = params.innerAngleEnd - params.innerAngleStart - 2 * capRad > Math.PI ? 1 : 0;
464
- const newUpperStart = this.arcPoint(bigRadius, params.upperAngleStart, 0, curve / orbitNumber2);
465
- const newUpperEnd = this.arcPoint(bigRadius, params.upperAngleEnd, 0, -curve / orbitNumber2);
466
- const newInnerStart = this.arcPoint(smallRadius, params.innerAngleStart, 0, curve / orbitNumber2);
467
- const newInnerEnd = this.arcPoint(smallRadius, params.innerAngleEnd, 0, -curve / orbitNumber2);
468
- const upperPointStart = this.arcPoint(bigRadius, params.upperAngleStart, -(curve / 2) / orbitNumber2, 0);
469
- const upperPointEnd = this.arcPoint(bigRadius, params.upperAngleEnd, -(curve / 2) / orbitNumber2, 0);
470
- const innerPointStart = this.arcPoint(smallRadius, params.innerAngleStart, curve / 2 / orbitNumber2, 0);
471
- const innerPointEnd = this.arcPoint(smallRadius, params.innerAngleEnd, curve / 2 / orbitNumber2, 0);
472
- const Q = this.getControlPoint(newUpperEnd.x, newUpperEnd.y, upperPointEnd.x, upperPointEnd.y);
473
- const Q1 = this.getControlPoint(innerPointEnd.x, innerPointEnd.y, newInnerEnd.x, newInnerEnd.y);
474
- const Q2 = this.getControlPoint(newInnerStart.x, newInnerStart.y, innerPointStart.x, innerPointStart.y);
475
- const Q3 = this.getControlPoint(upperPointStart.x, upperPointStart.y, newUpperStart.x, newUpperStart.y);
476
- let d = `M ${newUpperStart.x},${newUpperStart.y} A ${bigRadius},${bigRadius} 0 ${flagU} 1 ${newUpperEnd.x},${newUpperEnd.y}`;
477
- d += `Q ${Q.xc},${Q.yc} ${upperPointEnd.x},${upperPointEnd.y} L ${innerPointEnd.x},${innerPointEnd.y}`;
478
- d += `Q ${Q1.xc},${Q1.yc} ${newInnerEnd.x},${newInnerEnd.y}`;
479
- d += `A ${smallRadius},${smallRadius} 0 ${flagI} 0 ${newInnerStart.x},${newInnerStart.y}`;
480
- d += `Q ${Q2.xc},${Q2.yc} ${innerPointStart.x},${innerPointStart.y} L ${upperPointStart.x},${upperPointStart.y}`;
481
- d += ` Q ${Q3.xc},${Q3.yc} ${newUpperStart.x},${newUpperStart.y}`;
482
- d += ` Z`;
588
+ const h = (bigRadius - smallRadius) / 2;
589
+ const middle = (bigRadius + smallRadius) / 2;
590
+ const span = Math.min(params.upperAngleEnd - params.upperAngleStart, params.innerAngleEnd - params.innerAngleStart);
591
+ const length = Math.max(0, Math.min(h * factor, span * smallRadius * 0.49));
592
+ return { h, middle, length, angle: length / middle };
593
+ }
594
+ generateDefaultPath(params) {
595
+ const { bigRadius: R, smallRadius: r, upperAngleStart: uS, upperAngleEnd: uE, innerAngleStart: iS, innerAngleEnd: iE } = params;
596
+ return `M ${this.pt(R, uS)}${this.arcTo(R, uS, uE)} L ${this.pt(r, iE)}${this.arcTo(r, iE, iS)} Z`;
597
+ }
598
+ generateRoundedPath(params) {
599
+ const { bigRadius: R, smallRadius: r, upperAngleStart: uS, upperAngleEnd: uE, innerAngleStart: iS, innerAngleEnd: iE } = params;
600
+ const { length: c } = this.ending(params, 0.5);
601
+ const corner = (radius, angle) => ` A ${c},${c} 0 0 1 ${this.pt(radius, angle)}`;
602
+ let d = `M ${this.pt(R, uS + c / R)}${this.arcTo(R, uS + c / R, uE - c / R)}`;
603
+ d += `${corner(R - c, uE)} L ${this.pt(r + c, iE)}${corner(r, iE - c / r)}`;
604
+ d += this.arcTo(r, iE - c / r, iS + c / r);
605
+ d += `${corner(r + c, iS)} L ${this.pt(R - c, uS)}${corner(R, uS + c / R)} Z`;
483
606
  return d;
484
607
  }
485
- // Dentro de la clase OrbitCommon en orbit-common.js
486
608
  generateCirclePath(params, shape) {
487
- const { upperArcStart, upperArcEnd, innerArcStart, innerArcEnd, bigRadius, smallRadius, largeArcFlagUpper, largeArcFlagInner } = params;
488
- let d = `M ${upperArcStart.x},${upperArcStart.y} A ${bigRadius},${bigRadius} 0 ${largeArcFlagUpper} 1 ${upperArcEnd.x},${upperArcEnd.y}`;
489
- d += ` A 1,1 0 0 1 ${innerArcEnd.x},${innerArcEnd.y} `;
490
- d += ` A ${smallRadius},${smallRadius} 0 ${largeArcFlagInner} 0 ${innerArcStart.x},${innerArcStart.y}`;
491
- d += ` A 1,1 0 0 ${shape === "circle" || shape === "circle-a" ? 1 : 0} ${upperArcStart.x},${upperArcStart.y} `;
492
- d += ` Z`;
609
+ const { bigRadius: R, smallRadius: r, upperAngleStart: uS, upperAngleEnd: uE, innerAngleStart: iS, innerAngleEnd: iE } = params;
610
+ const startSweep = shape === "bullet" ? 0 : 1;
611
+ let d = `M ${this.pt(R, uS)}${this.arcTo(R, uS, uE)} A 1,1 0 0 1 ${this.pt(r, iE)}`;
612
+ d += `${this.arcTo(r, iE, iS)} A 1,1 0 0 ${startSweep} ${this.pt(R, uS)} Z`;
493
613
  return d;
494
614
  }
495
- generateCircleBPath(params, arcHeight, orbitNumber2) {
496
- const { upperAngleStart, upperAngleEnd, innerAngleStart, innerAngleEnd, bigRadius, smallRadius } = params;
497
- const available = Math.min(upperAngleEnd - upperAngleStart, innerAngleEnd - innerAngleStart);
498
- const segment = Math.min(arcHeight * 1.36, available * 180 / Math.PI * orbitNumber2 * 0.49);
499
- const capRad = segment / orbitNumber2 * Math.PI / 180;
500
- const flagU = upperAngleEnd - upperAngleStart - 2 * capRad > Math.PI ? 1 : 0;
501
- const flagI = innerAngleEnd - innerAngleStart - 2 * capRad > Math.PI ? 1 : 0;
502
- const newUpperStart = this.arcPoint(bigRadius, upperAngleStart, 0, segment / orbitNumber2);
503
- const newUpperEnd = this.arcPoint(bigRadius, upperAngleEnd, 0, -segment / orbitNumber2);
504
- const newInnerStart = this.arcPoint(smallRadius, innerAngleStart, 0, segment / orbitNumber2);
505
- const newInnerEnd = this.arcPoint(smallRadius, innerAngleEnd, 0, -segment / orbitNumber2);
506
- let d = `M ${newUpperStart.x},${newUpperStart.y} A ${bigRadius},${bigRadius} 0 ${flagU} 1 ${newUpperEnd.x},${newUpperEnd.y}`;
507
- d += ` A 1,1 0 0 1 ${newInnerEnd.x},${newInnerEnd.y} `;
508
- d += ` A ${smallRadius},${smallRadius} 0 ${flagI} 0 ${newInnerStart.x},${newInnerStart.y}`;
509
- d += ` A 1,1 0 0 1 ${newUpperStart.x},${newUpperStart.y} `;
510
- d += ` Z`;
615
+ generateCircleBPath(params) {
616
+ const { bigRadius: R, smallRadius: r } = params;
617
+ const { h, angle } = this.ending(params, 1);
618
+ const a0 = params.upperAngleStart + angle;
619
+ const a1 = params.upperAngleEnd - angle;
620
+ let d = `M ${this.pt(R, a0)}${this.arcTo(R, a0, a1)} A ${h},${h} 0 0 1 ${this.pt(r, a1)}`;
621
+ d += `${this.arcTo(r, a1, a0)} A ${h},${h} 0 0 1 ${this.pt(R, a0)} Z`;
511
622
  return d;
512
623
  }
513
- generateArrowPath(params, orbitNumber2) {
514
- const { upperArcStart, upperArcEnd, innerArcStart, innerArcEnd, bigRadius, smallRadius, largeArcFlagUpper, largeArcFlagInner, radius } = params;
515
- const middleEnd = this.arcPoint(radius, params.upperAngleEnd, 0, 24 / orbitNumber2 / 2);
516
- const middleStart = this.arcPoint(radius, params.upperAngleStart, 0, 24 / orbitNumber2 / 2);
517
- let d = `M ${upperArcStart.x},${upperArcStart.y} A ${bigRadius},${bigRadius} 0 ${largeArcFlagUpper} 1 ${upperArcEnd.x},${upperArcEnd.y}`;
518
- d += `L ${middleEnd.x} ${middleEnd.y}`;
519
- d += `L ${innerArcEnd.x} ${innerArcEnd.y}`;
520
- d += `A ${smallRadius},${smallRadius} 0 ${largeArcFlagInner} 0 ${innerArcStart.x}, ${innerArcStart.y}`;
521
- d += `L ${middleStart.x} ${middleStart.y}`;
522
- d += `Z`;
624
+ generateArrowPath(params) {
625
+ const { bigRadius: R, smallRadius: r } = params;
626
+ const { middle, angle } = this.ending(params, 1.6);
627
+ const a0 = params.upperAngleStart;
628
+ const a1 = params.upperAngleEnd;
629
+ let d = `M ${this.pt(R, a0)}${this.arcTo(R, a0, a1 - angle)}`;
630
+ d += ` L ${this.pt(middle, a1)} L ${this.pt(r, a1 - angle)}`;
631
+ d += `${this.arcTo(r, a1 - angle, a0)} L ${this.pt(middle, a0 + angle)} Z`;
523
632
  return d;
524
633
  }
525
- generateSlashPath(params, shape, orbitNumber2) {
526
- const { upperAngleStart, upperAngleEnd, innerAngleStart, innerAngleEnd, bigRadius, smallRadius, largeArcFlagUpper, largeArcFlagInner } = params;
527
- const newUpperStart = this.arcPoint(bigRadius, upperAngleStart, 0, shape === "backslash" ? 0 : 24 / orbitNumber2 / 2);
528
- const newUpperEnd = this.arcPoint(bigRadius, upperAngleEnd, 0, shape === "backslash" ? 0 : 24 / orbitNumber2 / 2);
529
- const newInnerStart = this.arcPoint(smallRadius, innerAngleStart, 0, shape === "backslash" ? 24 / orbitNumber2 / 2 : 0);
530
- const newInnerEnd = this.arcPoint(smallRadius, innerAngleEnd, 0, shape === "backslash" ? 24 / orbitNumber2 / 2 : 0);
531
- let d = `M ${newUpperStart.x},${newUpperStart.y} A ${bigRadius},${bigRadius} 0 ${largeArcFlagUpper} 1 ${newUpperEnd.x},${newUpperEnd.y}`;
532
- d += `L ${newInnerEnd.x} ${newInnerEnd.y}`;
533
- d += `A ${smallRadius},${smallRadius} 0 ${largeArcFlagInner} 0 ${newInnerStart.x}, ${newInnerStart.y}`;
534
- d += `Z`;
535
- return d;
634
+ generateSlashPath(params, shape) {
635
+ const { bigRadius: R, smallRadius: r } = params;
636
+ const { angle } = this.ending(params, 1);
637
+ const a0 = params.upperAngleStart;
638
+ const a1 = params.upperAngleEnd;
639
+ const [outer0, outer1, inner0, inner1] = shape === "backslash" ? [a0, a1 - angle, a0 + angle, a1] : [a0 + angle, a1, a0, a1 - angle];
640
+ return `M ${this.pt(R, outer0)}${this.arcTo(R, outer0, outer1)} L ${this.pt(r, inner1)}${this.arcTo(r, inner1, inner0)} Z`;
536
641
  }
537
- generateZigzagPath(params, arcHeight, orbitNumber2) {
538
- const { upperArcStart, upperArcEnd, innerArcStart, innerArcEnd, bigRadius, smallRadius, largeArcFlagUpper, largeArcFlagInner, radius } = params;
539
- const h2 = arcHeight / orbitNumber2 / 2;
540
- const s2 = this.arcPoint(radius, params.upperAngleStart, -h2, 3);
541
- const s3 = this.arcPoint(radius, params.upperAngleStart, 0, 0);
542
- const s4 = this.arcPoint(radius, params.upperAngleStart, h2, 3);
543
- const e2 = this.arcPoint(radius, params.innerAngleEnd, h2, 3);
544
- const e3 = this.arcPoint(radius, params.innerAngleEnd, 0, 0);
545
- const e4 = this.arcPoint(radius, params.innerAngleEnd, -h2, 3);
546
- let d = `M ${upperArcStart.x},${upperArcStart.y} A ${bigRadius},${bigRadius} 0 ${largeArcFlagUpper} 1 ${upperArcEnd.x},${upperArcEnd.y}`;
547
- d += `L ${e2.x} ${e2.y}`;
548
- d += `L ${e3.x} ${e3.y}`;
549
- d += `L ${e4.x} ${e4.y}`;
550
- d += `L ${innerArcEnd.x} ${innerArcEnd.y}`;
551
- d += `A ${smallRadius},${smallRadius} 0 ${largeArcFlagInner} 0 ${innerArcStart.x}, ${innerArcStart.y}`;
552
- d += `L ${s2.x} ${s2.y}`;
553
- d += `L ${s3.x} ${s3.y}`;
554
- d += `L ${s4.x} ${s4.y}`;
555
- d += `Z`;
642
+ generateZigzagPath(params) {
643
+ const { bigRadius: R, smallRadius: r } = params;
644
+ const { middle, angle } = this.ending(params, 1.6);
645
+ const a0 = params.upperAngleStart;
646
+ const a1 = params.upperAngleEnd;
647
+ let d = `M ${this.pt(R, a0 + angle)}${this.arcTo(R, a0 + angle, a1)}`;
648
+ d += ` L ${this.pt(middle, a1 - angle)} L ${this.pt(r, a1)}`;
649
+ d += `${this.arcTo(r, a1, a0 + angle)} L ${this.pt(middle, a0)} Z`;
556
650
  return d;
557
651
  }
558
- generateDefaultPath(params) {
559
- const { upperArcStart, upperArcEnd, innerArcStart, innerArcEnd, bigRadius, smallRadius, largeArcFlagUpper, largeArcFlagInner } = params;
560
- let d = `M ${upperArcStart.x},${upperArcStart.y} A ${bigRadius},${bigRadius} 0 ${largeArcFlagUpper} 1 ${upperArcEnd.x},${upperArcEnd.y}`;
561
- d += `L ${innerArcEnd.x} ${innerArcEnd.y}`;
562
- d += `A ${smallRadius},${smallRadius} 0 ${largeArcFlagInner} 0 ${innerArcStart.x}, ${innerArcStart.y}`;
563
- d += `Z`;
564
- return d;
652
+ /**
653
+ * The touch area of an interactive arc or progress: an invisible stroke
654
+ * along the middle of the band, from `from` to `to` degrees (0 = start of
655
+ * the element). It is as wide as the band plus `hit-width` px on each side,
656
+ * and catches the pointer whatever the fill, so transparent bands work.
657
+ */
658
+ updateHitArea(hit, attrs, from, to) {
659
+ const extra = Math.max(0, Number(this.getAttribute("hit-width")) || 0);
660
+ this.fitCanvas(attrs, this.hasAttribute("interactive") ? extra : 0);
661
+ if (!this.hasAttribute("interactive") || !(to > from) || !(attrs.orbitRadius > 0)) {
662
+ hit.removeAttribute("d");
663
+ return;
664
+ }
665
+ const radians = (degrees) => (degrees - 90) * Math.PI / 180;
666
+ hit.setAttribute("d", this.linePath(attrs.realRadius, radians(from), radians(Math.min(to, from + 360))));
667
+ hit.style.strokeWidth = `${attrs.arcHeight + attrs.strokeWidth + 2 * extra}px`;
668
+ }
669
+ /**
670
+ * Grow the SVG canvas past the ring's box so it covers the whole band, its
671
+ * stroke and touch area. Some engines (WebKit) neither paint reliably nor
672
+ * hit-test SVG content that overflows its box.
673
+ */
674
+ fitCanvas(attrs, extra) {
675
+ const svg = this.shadowRoot.querySelector("svg");
676
+ const scale = attrs.orbitRadius / 50;
677
+ if (!svg || !(scale > 0)) return;
678
+ const outer = (attrs.realRadius + attrs.arcHeightPercentage) * scale + attrs.strokeWidth + extra;
679
+ const pad = Math.max(0, Math.ceil(outer - attrs.orbitRadius));
680
+ const units = pad / scale;
681
+ svg.setAttribute("viewBox", `${-units} ${-units} ${100 + 2 * units} ${100 + 2 * units}`);
682
+ svg.style.inset = `${-pad}px`;
565
683
  }
566
684
  };
567
685
 
@@ -580,17 +698,11 @@
580
698
  --o-back-stroke: none;
581
699
  --o-back-stroke-width: 1;
582
700
  }
583
- :host(:hover) {
584
- --o-fill: var(--o-gray-light);
585
- --o-stroke: var(--o-fill);
586
- --o-stroke-width: 1;
587
- --o-back-fill: transparent;
588
- --o-back-stroke: none;
589
- --o-back-stroke-width: 1;
590
- }
591
701
  svg {
592
- width: 100%;
593
- height: 100%;
702
+ position: absolute;
703
+ inset: 0;
704
+ width: auto;
705
+ height: auto;
594
706
  overflow: visible;
595
707
  pointer-events: none;
596
708
  }
@@ -602,6 +714,14 @@
602
714
  :host([interactive]) svg * {
603
715
  pointer-events: visiblePainted;
604
716
  }
717
+ /* The touch area covers the whole track, whatever its colors. */
718
+ .hit {
719
+ fill: none;
720
+ stroke: transparent;
721
+ }
722
+ :host([interactive]) .hit {
723
+ pointer-events: stroke;
724
+ }
605
725
  :host([interactive]) {
606
726
  cursor: pointer;
607
727
  }
@@ -617,11 +737,17 @@
617
737
  stroke: var(--o-back-stroke);
618
738
  stroke-width: var(--o-back-stroke-width);
619
739
  }
620
- /* variant="stroke": the thin, legible gauge (registro "medidor").
740
+ /* variant="stroke": the thin, legible gauge.
621
741
  The default band is a FILLED donut wedge, so everyone building a
622
742
  thin gauge tripped on --o-fill/--o-back-fill. With this variant the
623
743
  paths are open arcs and the data color lives where you expect it:
624
744
  --o-stroke for the bar, --o-back-stroke for the track. */
745
+ /* The stroke variant draws a 2px bar over a gray 1px track unless you
746
+ set your own values on the element. */
747
+ :host([variant="stroke"]) {
748
+ --o-stroke-width: 2;
749
+ --o-back-stroke: var(--o-gray-light);
750
+ }
625
751
  :host([variant="stroke"]) .progress-bar {
626
752
  fill: none;
627
753
  stroke: var(--o-stroke);
@@ -636,6 +762,7 @@
636
762
  }
637
763
  </style>
638
764
  <svg viewBox="0 0 100 100">
765
+ <path class="hit" vector-effect="non-scaling-stroke"></path>
639
766
  <path class="progress-bg" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
640
767
  <path class="progress-bar" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
641
768
  </svg>
@@ -648,6 +775,7 @@
648
775
  const dBar = isStroke ? this.calculateStrokeArc(attrs, false) : this.calculateArcParameters(attrs, false);
649
776
  this.shadowRoot.querySelector(".progress-bg").setAttribute("d", dBg);
650
777
  this.shadowRoot.querySelector(".progress-bar").setAttribute("d", dBar);
778
+ this.updateHitArea(this.shadowRoot.querySelector(".hit"), attrs, 0, attrs.range);
651
779
  }
652
780
  /**
653
781
  * variant="stroke": open arc along the orbit radius (no closed band),
@@ -657,27 +785,15 @@
657
785
  const { realRadius } = attrs;
658
786
  const arcAngle = Math.max(0, Math.min(this.getProgressAngle(attrs, full), 360));
659
787
  if (!(arcAngle > 0) || !(attrs.orbitRadius > 0)) return "";
660
- if (arcAngle >= 359.9) {
661
- const top = 50 - realRadius;
662
- const bottom = 50 + realRadius;
663
- return `M 50,${top} A ${realRadius},${realRadius} 0 1 1 50,${bottom} A ${realRadius},${realRadius} 0 1 1 50,${top}`;
664
- }
665
- const a0 = -90 * (Math.PI / 180);
666
- const a1 = (-90 + arcAngle) * (Math.PI / 180);
667
- const x0 = 50 + realRadius * Math.cos(a0);
668
- const y0 = 50 + realRadius * Math.sin(a0);
669
- const x1 = 50 + realRadius * Math.cos(a1);
670
- const y1 = 50 + realRadius * Math.sin(a1);
671
- const largeArcFlag = arcAngle > 180 ? 1 : 0;
672
- return `M ${x0},${y0} A ${realRadius},${realRadius} 0 ${largeArcFlag} 1 ${x1},${y1}`;
788
+ const start = -Math.PI / 2;
789
+ return this.linePath(realRadius, start, start + arcAngle * Math.PI / 180);
673
790
  }
674
791
  getAttributes() {
675
792
  const common = super.getCommonAttributes(this);
676
793
  const range = Math.max(0, Math.min(360, this.readAngle(common.style, "--o-range", 360)));
677
794
  const rawValue = common.style.getPropertyValue("--o-progress").trim();
678
- const progress = rawValue ? this.readNumber(common.style, "--o-progress", 0) : Number(this.getAttribute("value") || 0);
679
- const rawMax = this.getAttribute("max");
680
- const maxValue = rawMax === null ? 100 : Number(rawMax);
795
+ const progress = rawValue ? this.readNumber(common.style, "--o-progress", 0) : this.readValue();
796
+ const maxValue = this.readMax();
681
797
  return {
682
798
  ...common,
683
799
  range,
@@ -702,7 +818,7 @@
702
818
  strokeWidth,
703
819
  arcHeight
704
820
  );
705
- return super.generatePathData(shape, params, arcHeight, orbitNumber2);
821
+ return super.generatePathData(shape, params);
706
822
  }
707
823
  };
708
824
 
@@ -716,15 +832,11 @@
716
832
  --o-stroke-width: 1;
717
833
  --o-color: currentcolor;
718
834
  }
719
- :host(:hover) {
720
- --o-fill: var(--o-gray-light);
721
- --o-stroke: var(--o-fill);
722
- --o-stroke-width: 1;
723
- --o-color: currentcolor;
724
- }
725
835
  svg {
726
- width: 100%;
727
- height: 100%;
836
+ position: absolute;
837
+ inset: 0;
838
+ width: auto;
839
+ height: auto;
728
840
  overflow: visible;
729
841
  pointer-events: none;
730
842
  }
@@ -738,6 +850,14 @@
738
850
  :host([interactive]) svg * {
739
851
  pointer-events: visiblePainted;
740
852
  }
853
+ /* The touch area catches the pointer even where the band is transparent. */
854
+ #orbitHit {
855
+ fill: none;
856
+ stroke: transparent;
857
+ }
858
+ :host([interactive]) #orbitHit {
859
+ pointer-events: stroke;
860
+ }
741
861
  :host([interactive]) {
742
862
  cursor: pointer;
743
863
  }
@@ -750,18 +870,21 @@
750
870
  text {
751
871
  fill: var(--o-color);
752
872
  }
753
- #orbitPath {
754
- fill: transparent;
873
+ /* Guides for text: never painted and never under the pointer. */
874
+ #orbitPath, #measure {
875
+ fill: none;
755
876
  stroke: none;
756
- stroke-width: 0;
877
+ pointer-events: none;
757
878
  }
758
879
  </style>
759
880
  <svg viewBox="0 0 100 100">
881
+ <path id="orbitHit" vector-effect="non-scaling-stroke"></path>
760
882
  <path id="orbitShape" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
761
883
  <path id="orbitPath" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
762
884
  <text>
763
885
  <textPath href="#orbitPath" alignment-baseline="middle"></textPath>
764
886
  </text>
887
+ <text id="measure" visibility="hidden" aria-hidden="true"></text>
765
888
  </svg>
766
889
  `;
767
890
  var OrbitArc = class extends OrbitBase {
@@ -772,9 +895,10 @@
772
895
  }
773
896
  update() {
774
897
  const attrs = this.getAttributes();
775
- const { length, fontSize, textAnchor, fitRange } = attrs;
776
- if (this.hasAttribute("value")) {
777
- setLayoutProperty(this, "--o-arc-start", `${attrs.stackOffset}deg`);
898
+ const { fitRange, stacked } = attrs;
899
+ if (stacked) {
900
+ const anchor = attrs.auto && this.hasAttribute("text-anchor") ? { middle: -0.5, end: -1 }[attrs.textAnchor] || 0 : 0;
901
+ setLayoutProperty(this, "--o-arc-start", `${attrs.stackOffset + anchor * attrs.arcAngle}deg`);
778
902
  setLayoutProperty(this, "--o_stack", attrs.stackOffset + attrs.arcAngle);
779
903
  } else {
780
904
  this.style.removeProperty("--o-arc-start");
@@ -785,6 +909,8 @@
785
909
  const textPath = this.shadowRoot.querySelector("textPath");
786
910
  orbitShape.setAttribute("d", this.calculateArcParameters(attrs).dShape);
787
911
  orbitPath.setAttribute("d", this.calculateTextArcParameters(attrs).dPath);
912
+ this.updateHitArea(this.shadowRoot.querySelector("#orbitHit"), attrs, 0, attrs.arcAngle);
913
+ const textAnchor = attrs.textAnchor;
788
914
  if (textAnchor === "start") {
789
915
  textPath.setAttribute("startOffset", "0%");
790
916
  textPath.setAttribute("text-anchor", "start");
@@ -800,9 +926,9 @@
800
926
  } else {
801
927
  textPath.parentElement.removeAttribute("textLength");
802
928
  }
803
- textPath.parentElement.style.fontSize = `calc(${fontSize} * (100 / (${length}) * (12 / var(--o-orbit-number)))`;
929
+ textPath.parentElement.style.fontSize = attrs.svgFontSize;
804
930
  textPath.textContent = this.textContent;
805
- this.warnIfTextOverflows(orbitPath, textPath, fitRange);
931
+ if (!attrs.auto) this.warnIfTextOverflows(orbitPath, textPath, fitRange);
806
932
  }
807
933
  /**
808
934
  * Curved text has an explicit angular budget: glyphs past the end of the
@@ -823,7 +949,7 @@
823
949
  if (pathLen > 0 && textLen > pathLen && this._truncWarned !== raw) {
824
950
  this._truncWarned = raw;
825
951
  console.warn(
826
- `[orbit] <o-arc> text "${raw.length > 34 ? raw.slice(0, 34) + "\u2026" : raw}" overflows its arc (${Math.round(textLen)} > ${Math.round(pathLen)} units) and will clip. Shorten the text, widen --o-range, or use fit-range to squeeze it.`
952
+ `[orbit] <o-arc> text "${raw.length > 34 ? raw.slice(0, 34) + "\u2026" : raw}" overflows its arc (${Math.round(textLen)} > ${Math.round(pathLen)} units) and will clip. Shorten the text, widen --o-range, add range="auto" to fit the arc to the text, or fit-range to squeeze it.`
827
953
  );
828
954
  }
829
955
  } catch (e) {
@@ -840,23 +966,43 @@
840
966
  const textAnchor = this.getAttribute("text-anchor") || "middle";
841
967
  const fontSize = style.fontSize || "16px";
842
968
  const gap = Math.max(0, this.readNumber(style, "--o-gap", 1));
843
- const value = Number(this.getAttribute("value"));
844
- const rawMax = this.getAttribute("max");
845
- const max = rawMax === null ? 100 : Number(rawMax);
846
- const arcAngle = this.hasAttribute("value") ? super.getProgressAngle(range, value, max) : Math.max(0, Math.min(360, this.readAngle(style, "--o-angle", 0)));
969
+ const svgFontSize = `calc(${fontSize} * (100 / (${length}) * (12 / var(--o-orbit-number)))`;
970
+ const auto = this.getAttribute("range") === "auto";
971
+ const stacked = auto || this.hasAttribute("value");
972
+ let arcAngle;
973
+ if (auto) arcAngle = this.measureTextAngle(svgFontSize, common.realRadius, gap);
974
+ else if (this.hasAttribute("value")) arcAngle = super.getProgressAngle(range, this.readValue(), this.readMax());
975
+ else arcAngle = Math.max(0, Math.min(360, this.readAngle(style, "--o-angle", 0)));
976
+ this._autoAngle = auto ? arcAngle : 0;
847
977
  let stackOffset = 0;
848
- if (this.hasAttribute("value")) {
978
+ if (stacked) {
849
979
  for (let prev = this.previousElementSibling; prev; prev = prev.previousElementSibling) {
850
980
  if (prev.localName !== "o-arc") continue;
851
981
  const previousStyle = this.ownerDocument.defaultView.getComputedStyle(prev);
852
- if (prev.hasAttribute("value")) {
982
+ if (prev.getAttribute("range") === "auto") stackOffset += prev._autoAngle || 0;
983
+ else if (prev.hasAttribute("value")) {
853
984
  const prevMax = prev.hasAttribute("max") ? Number(prev.getAttribute("max")) : 100;
854
985
  const prevRange = cssNumber(prev, previousStyle.getPropertyValue("--o-range"), range, "angle");
855
986
  stackOffset += super.getProgressAngle(prevRange, Number(prev.getAttribute("value")), prevMax);
856
987
  } else stackOffset += cssNumber(prev, previousStyle.getPropertyValue("--o-angle"), 0, "angle");
857
988
  }
858
989
  }
859
- return { ...common, gap, arcAngle, stackOffset, flip, fitRange, length, fontSize, textAnchor };
990
+ return { ...common, gap, arcAngle, stackOffset, flip, fitRange, length, fontSize, svgFontSize, textAnchor, auto, stacked };
991
+ }
992
+ /** range="auto": the angle that fits this arc's text, with half an em of room at each end. */
993
+ measureTextAngle(svgFontSize, radius, gap) {
994
+ const text = (this.textContent || "").trim();
995
+ if (!text || !(radius > 0)) return 0;
996
+ const measure = this.shadowRoot.querySelector("#measure");
997
+ measure.style.fontSize = svgFontSize;
998
+ measure.textContent = text;
999
+ let length = 0;
1000
+ try {
1001
+ length = measure.getComputedTextLength() + parseFloat(this.ownerDocument.defaultView.getComputedStyle(measure).fontSize);
1002
+ } catch (e) {
1003
+ }
1004
+ if (!(length > 0)) return 0;
1005
+ return Math.min(360, length / radius * 180 / Math.PI + gap);
860
1006
  }
861
1007
  calculateArcParameters(attrs) {
862
1008
  const { arcAngle, realRadius, arcHeightPercentage, orbitNumber: orbitNumber2, shape, strokeWidth, arcHeight, gap } = attrs;
@@ -871,32 +1017,38 @@
871
1017
  arcHeight,
872
1018
  gap
873
1019
  );
874
- const dShape = super.generatePathData(shape, params, arcHeight, orbitNumber2);
1020
+ const dShape = super.generatePathData(shape, params);
875
1021
  return { dShape };
876
1022
  }
877
1023
  calculateTextArcParameters(attrs) {
878
1024
  const { arcAngle, realRadius, gap, flip } = attrs;
879
1025
  if (!(arcAngle > 0)) return { dPath: "" };
880
1026
  const adjustedGap = Math.min(gap * 0.5, arcAngle * 0.49);
881
- const sweepFlag = flip ? 0 : 1;
882
- const largeArcFlag = arcAngle <= 180 ? 0 : 1;
883
- let coordX1 = 50 + realRadius * Math.cos((-90 + adjustedGap) * (Math.PI / 180));
884
- let coordY1 = 50 + realRadius * Math.sin((-90 + adjustedGap) * (Math.PI / 180));
885
- let coordX2 = 50 + realRadius * Math.cos((arcAngle - 90 - adjustedGap) * Math.PI / 180);
886
- let coordY2 = 50 + realRadius * Math.sin((arcAngle - 90 - adjustedGap) * Math.PI / 180);
887
- const [startX, startY, endX, endY] = flip ? [coordX2, coordY2, coordX1, coordY1] : [coordX1, coordY1, coordX2, coordY2];
888
- const dPath = `M ${startX},${startY} A ${realRadius},${realRadius} 0 ${largeArcFlag} ${sweepFlag} ${endX},${endY}`;
889
- return { dPath };
1027
+ const start = (adjustedGap - 90) * Math.PI / 180;
1028
+ const end = (arcAngle - adjustedGap - 90) * Math.PI / 180;
1029
+ return { dPath: flip ? this.linePath(realRadius, end, start) : this.linePath(realRadius, start, end) };
890
1030
  }
891
- calcularExpresionCSS(cssExpression) {
1031
+ resolveAngle(cssExpression) {
892
1032
  return cssNumber(this, cssExpression, 0, "angle");
893
1033
  }
1034
+ /** @deprecated Use resolveAngle(). */
1035
+ calcularExpresionCSS(cssExpression) {
1036
+ return this.resolveAngle(cssExpression);
1037
+ }
894
1038
  };
895
1039
 
896
1040
  // src/js/orbit-resize.js
897
1041
  var resizing = /* @__PURE__ */ new WeakMap();
1042
+ var FIXED = /^[+-]?(?:\d+\.?\d*|\.\d+)px$/;
898
1043
  var Orbit = {
899
1044
  refresh: refreshLayout,
1045
+ /** A promise that resolves when pending layouts are drawn. */
1046
+ ready: layoutReady,
1047
+ /**
1048
+ * Scale systems that use a fixed px --o-force with their container's width
1049
+ * (500px wide = ratio 1). Systems on the default --o-force already follow
1050
+ * their .bigbang, so they are left alone.
1051
+ */
900
1052
  resize(parentElementSelector) {
901
1053
  var _a, _b;
902
1054
  const parent = typeof parentElementSelector === "string" ? (_a = globalThis.document) == null ? void 0 : _a.querySelector(parentElementSelector) : parentElementSelector;
@@ -909,10 +1061,16 @@
909
1061
  const view = parent.ownerDocument.defaultView;
910
1062
  const applyRatio = (width) => {
911
1063
  if (!(width > 0)) return;
1064
+ let fixed = 0;
912
1065
  for (const element of parent.querySelectorAll(".gravity-spot")) {
1066
+ if (!FIXED.test(view.getComputedStyle(element).getPropertyValue("--o-force").trim())) continue;
1067
+ fixed++;
913
1068
  const ratio = String(width / 500);
914
1069
  if (element.style.getPropertyValue("--o-force-ratio") !== ratio) element.style.setProperty("--o-force-ratio", ratio);
915
1070
  }
1071
+ if (!fixed) {
1072
+ warnOnce(parent, "resize", "Orbit.resize() has nothing to do here: every .bigbang already scales with its container. It only scales systems that set a fixed px --o-force.");
1073
+ }
916
1074
  refreshLayout(parent);
917
1075
  };
918
1076
  const observer = new view.ResizeObserver((entries) => {