@zumer/orbit 1.6.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/orbit.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * orbit
3
- * v.1.6.0
3
+ * v.2.0.1
4
4
  * Author Juan Martin Muda - Zumerlab
5
5
  * License MIT
6
6
  */
@@ -18,6 +18,56 @@ function orbitNumber(element) {
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 @@ function layoutRing(ring) {
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 @@ function registryFor(doc) {
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 @@ function createState(scope) {
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 @@ function createState(scope) {
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 @@ function discover(state) {
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 @@ function flush(state) {
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 @@ function refreshLayout(root = globalThis.document) {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitBase = class extends (globalThis.HTMLElement || class {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ var OrbitProgress = class extends OrbitBase {
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 @@ if (template) template.innerHTML = `
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,20 @@ if (template) template.innerHTML = `
738
850
  :host([interactive]) svg * {
739
851
  pointer-events: visiblePainted;
740
852
  }
853
+ /* Text lets the pointer through to the band under it: WebKit fires no
854
+ click on SVG text inside a shadow root. */
855
+ :host([interactive]) svg text,
856
+ :host([interactive]) svg textPath {
857
+ pointer-events: none;
858
+ }
859
+ /* The touch area catches the pointer even where the band is transparent. */
860
+ #orbitHit {
861
+ fill: none;
862
+ stroke: transparent;
863
+ }
864
+ :host([interactive]) #orbitHit {
865
+ pointer-events: stroke;
866
+ }
741
867
  :host([interactive]) {
742
868
  cursor: pointer;
743
869
  }
@@ -750,18 +876,21 @@ if (template) template.innerHTML = `
750
876
  text {
751
877
  fill: var(--o-color);
752
878
  }
753
- #orbitPath {
754
- fill: transparent;
879
+ /* Guides for text: never painted and never under the pointer. */
880
+ #orbitPath, #measure {
881
+ fill: none;
755
882
  stroke: none;
756
- stroke-width: 0;
883
+ pointer-events: none;
757
884
  }
758
885
  </style>
759
886
  <svg viewBox="0 0 100 100">
887
+ <path id="orbitHit" vector-effect="non-scaling-stroke"></path>
760
888
  <path id="orbitShape" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
761
889
  <path id="orbitPath" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
762
890
  <text>
763
891
  <textPath href="#orbitPath" alignment-baseline="middle"></textPath>
764
892
  </text>
893
+ <text id="measure" visibility="hidden" aria-hidden="true"></text>
765
894
  </svg>
766
895
  `;
767
896
  var OrbitArc = class extends OrbitBase {
@@ -772,9 +901,10 @@ var OrbitArc = class extends OrbitBase {
772
901
  }
773
902
  update() {
774
903
  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`);
904
+ const { fitRange, stacked } = attrs;
905
+ if (stacked) {
906
+ const anchor = attrs.auto && this.hasAttribute("text-anchor") ? { middle: -0.5, end: -1 }[attrs.textAnchor] || 0 : 0;
907
+ setLayoutProperty(this, "--o-arc-start", `${attrs.stackOffset + anchor * attrs.arcAngle}deg`);
778
908
  setLayoutProperty(this, "--o_stack", attrs.stackOffset + attrs.arcAngle);
779
909
  } else {
780
910
  this.style.removeProperty("--o-arc-start");
@@ -785,6 +915,8 @@ var OrbitArc = class extends OrbitBase {
785
915
  const textPath = this.shadowRoot.querySelector("textPath");
786
916
  orbitShape.setAttribute("d", this.calculateArcParameters(attrs).dShape);
787
917
  orbitPath.setAttribute("d", this.calculateTextArcParameters(attrs).dPath);
918
+ this.updateHitArea(this.shadowRoot.querySelector("#orbitHit"), attrs, 0, attrs.arcAngle);
919
+ const textAnchor = attrs.textAnchor;
788
920
  if (textAnchor === "start") {
789
921
  textPath.setAttribute("startOffset", "0%");
790
922
  textPath.setAttribute("text-anchor", "start");
@@ -800,9 +932,9 @@ var OrbitArc = class extends OrbitBase {
800
932
  } else {
801
933
  textPath.parentElement.removeAttribute("textLength");
802
934
  }
803
- textPath.parentElement.style.fontSize = `calc(${fontSize} * (100 / (${length}) * (12 / var(--o-orbit-number)))`;
935
+ textPath.parentElement.style.fontSize = attrs.svgFontSize;
804
936
  textPath.textContent = this.textContent;
805
- this.warnIfTextOverflows(orbitPath, textPath, fitRange);
937
+ if (!attrs.auto) this.warnIfTextOverflows(orbitPath, textPath, fitRange);
806
938
  }
807
939
  /**
808
940
  * Curved text has an explicit angular budget: glyphs past the end of the
@@ -823,7 +955,7 @@ var OrbitArc = class extends OrbitBase {
823
955
  if (pathLen > 0 && textLen > pathLen && this._truncWarned !== raw) {
824
956
  this._truncWarned = raw;
825
957
  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.`
958
+ `[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
959
  );
828
960
  }
829
961
  } catch (e) {
@@ -840,23 +972,43 @@ var OrbitArc = class extends OrbitBase {
840
972
  const textAnchor = this.getAttribute("text-anchor") || "middle";
841
973
  const fontSize = style.fontSize || "16px";
842
974
  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)));
975
+ const svgFontSize = `calc(${fontSize} * (100 / (${length}) * (12 / var(--o-orbit-number)))`;
976
+ const auto = this.getAttribute("range") === "auto";
977
+ const stacked = auto || this.hasAttribute("value");
978
+ let arcAngle;
979
+ if (auto) arcAngle = this.measureTextAngle(svgFontSize, common.realRadius, gap);
980
+ else if (this.hasAttribute("value")) arcAngle = super.getProgressAngle(range, this.readValue(), this.readMax());
981
+ else arcAngle = Math.max(0, Math.min(360, this.readAngle(style, "--o-angle", 0)));
982
+ this._autoAngle = auto ? arcAngle : 0;
847
983
  let stackOffset = 0;
848
- if (this.hasAttribute("value")) {
984
+ if (stacked) {
849
985
  for (let prev = this.previousElementSibling; prev; prev = prev.previousElementSibling) {
850
986
  if (prev.localName !== "o-arc") continue;
851
987
  const previousStyle = this.ownerDocument.defaultView.getComputedStyle(prev);
852
- if (prev.hasAttribute("value")) {
988
+ if (prev.getAttribute("range") === "auto") stackOffset += prev._autoAngle || 0;
989
+ else if (prev.hasAttribute("value")) {
853
990
  const prevMax = prev.hasAttribute("max") ? Number(prev.getAttribute("max")) : 100;
854
991
  const prevRange = cssNumber(prev, previousStyle.getPropertyValue("--o-range"), range, "angle");
855
992
  stackOffset += super.getProgressAngle(prevRange, Number(prev.getAttribute("value")), prevMax);
856
993
  } else stackOffset += cssNumber(prev, previousStyle.getPropertyValue("--o-angle"), 0, "angle");
857
994
  }
858
995
  }
859
- return { ...common, gap, arcAngle, stackOffset, flip, fitRange, length, fontSize, textAnchor };
996
+ return { ...common, gap, arcAngle, stackOffset, flip, fitRange, length, fontSize, svgFontSize, textAnchor, auto, stacked };
997
+ }
998
+ /** range="auto": the angle that fits this arc's text, with half an em of room at each end. */
999
+ measureTextAngle(svgFontSize, radius, gap) {
1000
+ const text = (this.textContent || "").trim();
1001
+ if (!text || !(radius > 0)) return 0;
1002
+ const measure = this.shadowRoot.querySelector("#measure");
1003
+ measure.style.fontSize = svgFontSize;
1004
+ measure.textContent = text;
1005
+ let length = 0;
1006
+ try {
1007
+ length = measure.getComputedTextLength() + parseFloat(this.ownerDocument.defaultView.getComputedStyle(measure).fontSize);
1008
+ } catch (e) {
1009
+ }
1010
+ if (!(length > 0)) return 0;
1011
+ return Math.min(360, length / radius * 180 / Math.PI + gap);
860
1012
  }
861
1013
  calculateArcParameters(attrs) {
862
1014
  const { arcAngle, realRadius, arcHeightPercentage, orbitNumber: orbitNumber2, shape, strokeWidth, arcHeight, gap } = attrs;
@@ -871,32 +1023,38 @@ var OrbitArc = class extends OrbitBase {
871
1023
  arcHeight,
872
1024
  gap
873
1025
  );
874
- const dShape = super.generatePathData(shape, params, arcHeight, orbitNumber2);
1026
+ const dShape = super.generatePathData(shape, params);
875
1027
  return { dShape };
876
1028
  }
877
1029
  calculateTextArcParameters(attrs) {
878
1030
  const { arcAngle, realRadius, gap, flip } = attrs;
879
1031
  if (!(arcAngle > 0)) return { dPath: "" };
880
1032
  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 };
1033
+ const start = (adjustedGap - 90) * Math.PI / 180;
1034
+ const end = (arcAngle - adjustedGap - 90) * Math.PI / 180;
1035
+ return { dPath: flip ? this.linePath(realRadius, end, start) : this.linePath(realRadius, start, end) };
890
1036
  }
891
- calcularExpresionCSS(cssExpression) {
1037
+ resolveAngle(cssExpression) {
892
1038
  return cssNumber(this, cssExpression, 0, "angle");
893
1039
  }
1040
+ /** @deprecated Use resolveAngle(). */
1041
+ calcularExpresionCSS(cssExpression) {
1042
+ return this.resolveAngle(cssExpression);
1043
+ }
894
1044
  };
895
1045
 
896
1046
  // src/js/orbit-resize.js
897
1047
  var resizing = /* @__PURE__ */ new WeakMap();
1048
+ var FIXED = /^[+-]?(?:\d+\.?\d*|\.\d+)px$/;
898
1049
  var Orbit = {
899
1050
  refresh: refreshLayout,
1051
+ /** A promise that resolves when pending layouts are drawn. */
1052
+ ready: layoutReady,
1053
+ /**
1054
+ * Scale systems that use a fixed px --o-force with their container's width
1055
+ * (500px wide = ratio 1). Systems on the default --o-force already follow
1056
+ * their .bigbang, so they are left alone.
1057
+ */
900
1058
  resize(parentElementSelector) {
901
1059
  var _a, _b;
902
1060
  const parent = typeof parentElementSelector === "string" ? (_a = globalThis.document) == null ? void 0 : _a.querySelector(parentElementSelector) : parentElementSelector;
@@ -909,10 +1067,16 @@ var Orbit = {
909
1067
  const view = parent.ownerDocument.defaultView;
910
1068
  const applyRatio = (width) => {
911
1069
  if (!(width > 0)) return;
1070
+ let fixed = 0;
912
1071
  for (const element of parent.querySelectorAll(".gravity-spot")) {
1072
+ if (!FIXED.test(view.getComputedStyle(element).getPropertyValue("--o-force").trim())) continue;
1073
+ fixed++;
913
1074
  const ratio = String(width / 500);
914
1075
  if (element.style.getPropertyValue("--o-force-ratio") !== ratio) element.style.setProperty("--o-force-ratio", ratio);
915
1076
  }
1077
+ if (!fixed) {
1078
+ 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.");
1079
+ }
916
1080
  refreshLayout(parent);
917
1081
  };
918
1082
  const observer = new view.ResizeObserver((entries) => {