@zumer/orbit 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Orbit puts HTML on circles. Gauges, dials, knobs, radial menus, donut charts, wa
11
11
 
12
12
  You write plain HTML with a few classes. CSS does the geometry and the look; a small runtime keeps numbering, spacing and SVG arcs right as things change. It works the same in plain HTML, React, Vue, Svelte or anything else that renders the DOM.
13
13
 
14
- [Documentation and examples](https://zumerlab.com/orbit/) · [npm](https://www.npmjs.com/package/@zumer/orbit) · [Orbit 1.x docs](https://zumerlab.com/orbit-docs/)
14
+ [Documentation and examples](https://zumerlab.com/orbit/) · [npm](https://www.npmjs.com/package/@zumer/orbit)
15
15
 
16
16
  ## Install
17
17
 
@@ -156,16 +156,12 @@ Values cascade, so set them once on the `.gravity-spot` for the whole system or
156
156
 
157
157
  ### Which way is out
158
158
 
159
- Each satellite tells its content where it sits: `--o-dir-x` and `--o-dir-y` are the direction from the center, from `-1` to `1` (`y` grows down, and both are `0` on the center satellite). It is handy for labels that should hang outward, above the satellites of the upper half and below the rest:
159
+ Each satellite tells its content where it sits: `--o-dir-x` and `--o-dir-y` are the direction from the center, from `-1` to `1` (`y` grows down, and both are `0` on the center satellite). Two classes use them for labels. On a capsule or inside one, `.outward` sets a label just outside its satellite along that direction, and `.outward-y` only above or below: above the satellites of the upper half, below the rest. `--o-outward-gap` (4px) is the space between them.
160
160
 
161
- ```css
162
- .label {
163
- /* -1 in the upper half, 1 in the lower half */
164
- --side: clamp(-1, var(--o-dir-y) * 1000, 1);
165
- position: absolute; left: 50%;
166
- top: calc(50% + var(--side) * (50% + 4px));
167
- translate: -50% calc((var(--side) - 1) * 50%);
168
- }
161
+ ```html
162
+ <div class="satellite">
163
+ <div class="capsule"><span class="dot"></span><span class="outward-y">Mars</span></div>
164
+ </div>
169
165
  ```
170
166
 
171
167
  ## Arcs and progress
@@ -190,7 +186,19 @@ Text inside an arc is drawn along it. With `range="auto"` the arc is as long as
190
186
  </div>
191
187
  ```
192
188
 
193
- Progress rings draw filled bands. For a thin gauge line, use `variant="stroke"`: a 2px line over a gray track, colored with `--o-stroke` and `--o-back-stroke`.
189
+ `start` places an arc at a point of its ring, on the same scale as `value`. A placed arc takes no slot and moves no other arc, so labels and segments can go over a ring that already has arcs. `--o-max` on a ring or system sets the scale for everything below it, so values stay in their own units, hours for example:
190
+
191
+ ```html
192
+ <div class="orbit-9" style="--o-max: 12; --o-from: 40deg; --o-range: 320deg">
193
+ <o-arc value="12" style="--o-fill: #222a33"></o-arc>
194
+ <o-arc start="1" value="2.5" style="--o-fill: #4fd1c5"></o-arc>
195
+ <o-arc range="auto" start="0" text-anchor="end" style="--o-fill: transparent">Lights</o-arc>
196
+ </div>
197
+ ```
198
+
199
+ `class="ripple"` sends waves out of an arc to call for attention; `--o-ripple-color`, `--o-ripple-duration` and `--o-ripple-scale` tune them.
200
+
201
+ Progress rings draw filled bands. For a thin gauge line, use `variant="stroke"`: a 2px line over a gray track, colored with `--o-stroke` and `--o-back-stroke`. A `.satellite.at-value` in the same ring sits on the value of the progress and follows it, like the knob of a dial.
194
202
 
195
203
  Arcs and progress are decorative until you say otherwise. `interactive` makes the whole band clickable, transparent or not, and `hit-width="12"` adds 12px of touch area on each side:
196
204
 
@@ -294,7 +302,7 @@ Version 2 changes where size comes from. In 1.x every system was 500px across, w
294
302
  - `<o-arc>` and `<o-progress>` are placed with `rotate` instead of `transform`, and endings scale with the band. `interactive` makes the whole band clickable.
295
303
  - `variant="stroke"` on `<o-progress>` draws a 2px line over a gray track by default.
296
304
 
297
- The [1.x documentation](https://zumerlab.com/orbit-docs/) stays online.
305
+ The 1.x source and README stay on GitHub, at the [v1.6.0 tag](https://github.com/zumerlab/orbit/tree/v1.6.0#readme).
298
306
 
299
307
  ## Demos
300
308
 
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "name": "max",
28
- "description": "Value that fills the ring's range. Must be positive.",
28
+ "description": "Value that fills the ring's range. Must be positive. Without it, --o-max of the ring or system.",
29
29
  "type": {
30
30
  "text": "number"
31
31
  },
@@ -54,6 +54,13 @@
54
54
  },
55
55
  "default": "0"
56
56
  },
57
+ {
58
+ "name": "start",
59
+ "description": "Where the arc begins, on the same scale as value. A placed arc takes no slot and moves no other arc: text over a ring that already has arcs, or segments on a timeline.",
60
+ "type": {
61
+ "text": "number"
62
+ }
63
+ },
57
64
  {
58
65
  "name": "text-anchor",
59
66
  "description": "Where curved text sits on the arc. With range=\"auto\", an explicit value anchors the arc at its angle: middle centers it, end ends it there.",
@@ -104,6 +111,22 @@
104
111
  {
105
112
  "name": "--o-gap",
106
113
  "description": "Space between sibling arcs. Default 1; cascades from the ring or system."
114
+ },
115
+ {
116
+ "name": "--o-max",
117
+ "description": "Value that fills the range when there is no max attribute. Default 100; cascades from the ring or system."
118
+ },
119
+ {
120
+ "name": "--o-ripple-color",
121
+ "description": "Color of the .ripple wave. Default --o-fill."
122
+ },
123
+ {
124
+ "name": "--o-ripple-duration",
125
+ "description": "One wave of .ripple. Default 1.6s."
126
+ },
127
+ {
128
+ "name": "--o-ripple-scale",
129
+ "description": "How far a .ripple wave grows, from the center of the ring. Default 1.14."
107
130
  }
108
131
  ],
109
132
  "events": [
@@ -132,7 +155,7 @@
132
155
  },
133
156
  {
134
157
  "name": "max",
135
- "description": "Value that fills the ring's range. Must be positive.",
158
+ "description": "Value that fills the ring's range. Must be positive. Without it, --o-max of the ring or system.",
136
159
  "type": {
137
160
  "text": "number"
138
161
  },
@@ -197,6 +220,10 @@
197
220
  {
198
221
  "name": "--o-progress",
199
222
  "description": "Progress value; overrides the value attribute."
223
+ },
224
+ {
225
+ "name": "--o-max",
226
+ "description": "Value that fills the range when there is no max attribute. Default 100; cascades from the ring or system."
200
227
  }
201
228
  ],
202
229
  "events": [
@@ -245,6 +272,6 @@
245
272
  ],
246
273
  "package": {
247
274
  "name": "@zumer/orbit",
248
- "version": "2.0.0"
275
+ "version": "2.1.0"
249
276
  }
250
277
  }
package/dist/orbit.css CHANGED
@@ -1148,6 +1148,8 @@
1148
1148
  --o-gap: 1;
1149
1149
  --o-initial-orbit: 0;
1150
1150
  --o-angle: 0;
1151
+ /* The value that fills a ring's range, for arcs and progress without a max attribute. */
1152
+ --o-max: 100;
1151
1153
  width: 0;
1152
1154
  aspect-ratio: 1;
1153
1155
  position: relative;
@@ -1214,12 +1216,25 @@
1214
1216
  pointer-events: all;
1215
1217
  }
1216
1218
 
1219
+ /* The angle of the value of the <o-progress> in a ring, from the start of its
1220
+ range. The progress writes it on its ring. */
1221
+ @property --o-value-angle {
1222
+ syntax: "<angle>";
1223
+ inherits: true;
1224
+ initial-value: 0deg;
1225
+ }
1217
1226
  .satellite.at-center {
1218
1227
  --o-transform: rotate(0deg);
1219
1228
  --o-dir-x: 0;
1220
1229
  --o-dir-y: 0;
1221
1230
  }
1222
1231
 
1232
+ /* Sits on the value of the <o-progress> in the same ring, like the knob of a
1233
+ dial. Progress always fills clockwise, so this ignores .ccw. */
1234
+ .satellite.at-value {
1235
+ --o-angle-composite: calc(var(--o-value-angle) + 270deg);
1236
+ }
1237
+
1223
1238
  .satellite.circle {
1224
1239
  border-radius: 50%;
1225
1240
  }
@@ -9638,6 +9653,33 @@ Can be used in parent elements or in each orbiter class
9638
9653
  margin-top: unset;
9639
9654
  }
9640
9655
 
9656
+ /*
9657
+ Labels outside their satellite, away from the center (--o-dir-x/-y). Put
9658
+ the class on a capsule or on an element inside one. 50cqw and 50cqh are half
9659
+ the satellite; --o-outward-gap is the space between it and the label.
9660
+ */
9661
+ .outward, .outward-y {
9662
+ position: absolute;
9663
+ width: max-content;
9664
+ }
9665
+
9666
+ /* Along the direction. The translate scales the direction so its larger
9667
+ component is 1: the label's nearest edge or corner touches the circle. */
9668
+ .outward {
9669
+ --o-out: max(var(--o-dir-x), -1 * var(--o-dir-x), var(--o-dir-y), -1 * var(--o-dir-y), .001);
9670
+ left: calc(50% + var(--o-dir-x) * (50cqw + var(--o-outward-gap, 4px)));
9671
+ top: calc(50% + var(--o-dir-y) * (50cqh + var(--o-outward-gap, 4px)));
9672
+ translate: calc(-50% + 50% * var(--o-dir-x) / var(--o-out)) calc(-50% + 50% * var(--o-dir-y) / var(--o-out));
9673
+ }
9674
+
9675
+ /* Only up or down: above the satellites of the upper half, below the rest. */
9676
+ .outward-y {
9677
+ --o-out: clamp(-1, (var(--o-dir-y) + .01) * 1000, 1);
9678
+ left: 50%;
9679
+ top: calc(50% + var(--o-out) * (50cqh + var(--o-outward-gap, 4px)));
9680
+ translate: -50% calc((var(--o-out) - 1) * 50%);
9681
+ }
9682
+
9641
9683
  /* Satellite gyro */
9642
9684
  .satellite.spin-lock {
9643
9685
  transform: var(--o-transform) rotate(calc(var(--o-angle-composite)));
package/dist/orbit.d.ts CHANGED
@@ -18,8 +18,13 @@ type BooleanAttribute = boolean | '' | undefined;
18
18
  export interface OrbitArcAttributes {
19
19
  /** Share of the ring's range, relative to `max`. Without it, sibling arcs split the range equally. */
20
20
  value?: Numeric;
21
- /** Value that fills the whole range. Default `100`. */
21
+ /** Value that fills the whole range. Default `--o-max` of the ring or system, else `100`. */
22
22
  max?: Numeric;
23
+ /**
24
+ * Where the arc begins, on the same scale as `value` (0 is the start of the range).
25
+ * A placed arc takes no slot and moves no other arc, so it can sit over a ring that already has arcs.
26
+ */
27
+ start?: Numeric;
23
28
  shape?: OrbitShape;
24
29
  /**
25
30
  * Where curved text sits on the arc. Default `middle`.
@@ -43,7 +48,7 @@ export interface OrbitArcAttributes {
43
48
  export interface OrbitProgressAttributes {
44
49
  /** Progress relative to `max`. `--o-progress` overrides it. */
45
50
  value?: Numeric;
46
- /** Value that fills the whole range. Default `100`. */
51
+ /** Value that fills the whole range. Default `--o-max` of the ring or system, else `100`. */
47
52
  max?: Numeric;
48
53
  shape?: OrbitShape;
49
54
  /** `stroke` draws open lines colored by `--o-stroke` and `--o-back-stroke`. */
@@ -68,7 +73,10 @@ export declare class OrbitProgress extends HTMLElement {
68
73
  value: number | null;
69
74
  /** Mirrors the `max` attribute (default 100). Setting it redraws. */
70
75
  max: number;
71
- /** Redraw now. The runtime calls this when the progress or its layout changes. */
76
+ /**
77
+ * Redraw now. The runtime calls this when the progress or its layout changes.
78
+ * It also writes `--o-value-angle` on its ring, where a `.satellite.at-value` sits.
79
+ */
72
80
  update(): void;
73
81
  }
74
82
 
package/dist/orbit.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * orbit
3
- * v.2.0.0
3
+ * v.2.1.0
4
4
  * Author Juan Martin Muda - Zumerlab
5
5
  * License MIT
6
6
  */
@@ -100,7 +100,7 @@
100
100
  return Number.isFinite(resolved) ? resolved : fallback;
101
101
  }
102
102
  function group(element) {
103
- if (element.localName === "o-arc") return "arc";
103
+ if (element.localName === "o-arc") return element.hasAttribute("start") ? null : "arc";
104
104
  for (const name of ["satellite", "vector", "side"]) if (element.classList.contains(name)) return name;
105
105
  return null;
106
106
  }
@@ -445,10 +445,22 @@
445
445
  }
446
446
  return value;
447
447
  }
448
- /** The max attribute (default 100). It must be a positive number. */
449
- readMax() {
448
+ /**
449
+ * The max attribute, else --o-max from the ring or system (default 100).
450
+ * It must be a positive number.
451
+ */
452
+ readMax(style) {
450
453
  const raw = this.getAttribute("max");
451
- if (raw === null) return 100;
454
+ if (raw === null) {
455
+ const css = style == null ? void 0 : style.getPropertyValue("--o-max").trim();
456
+ if (!css) return 100;
457
+ const max2 = cssNumber(this, css, NaN);
458
+ if (!(max2 > 0)) {
459
+ warnOnce(this, "o-max:" + css, `--o-max: ${css} on <${this.localName}> must be a positive number, so nothing is drawn.`);
460
+ return 0;
461
+ }
462
+ return max2;
463
+ }
452
464
  const max = Number(raw);
453
465
  if (!(max > 0)) {
454
466
  warnOnce(this, "max:" + raw, `<${this.localName} max="${raw}"> must be a positive number, so nothing is drawn.`);
@@ -668,14 +680,15 @@
668
680
  }
669
681
  /**
670
682
  * 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.
683
+ * stroke and touch area, and as far as a ripple reaches (attrs.reach).
684
+ * Some engines (WebKit) neither paint reliably nor hit-test SVG content that
685
+ * overflows its box.
673
686
  */
674
687
  fitCanvas(attrs, extra) {
675
688
  const svg = this.shadowRoot.querySelector("svg");
676
689
  const scale = attrs.orbitRadius / 50;
677
690
  if (!svg || !(scale > 0)) return;
678
- const outer = (attrs.realRadius + attrs.arcHeightPercentage) * scale + attrs.strokeWidth + extra;
691
+ const outer = ((attrs.realRadius + attrs.arcHeightPercentage) * scale + attrs.strokeWidth) * (attrs.reach || 1) + extra;
679
692
  const pad = Math.max(0, Math.ceil(outer - attrs.orbitRadius));
680
693
  const units = pad / scale;
681
694
  svg.setAttribute("viewBox", `${-units} ${-units} ${100 + 2 * units} ${100 + 2 * units}`);
@@ -776,6 +789,16 @@
776
789
  this.shadowRoot.querySelector(".progress-bg").setAttribute("d", dBg);
777
790
  this.shadowRoot.querySelector(".progress-bar").setAttribute("d", dBar);
778
791
  this.updateHitArea(this.shadowRoot.querySelector(".hit"), attrs, 0, attrs.range);
792
+ const ring = this.parentElement;
793
+ if (this._valueRing && this._valueRing !== ring) this._valueRing.style.removeProperty("--o-value-angle");
794
+ this._valueRing = ring;
795
+ if (ring) setLayoutProperty(ring, "--o-value-angle", `${this.getProgressAngle(attrs, false)}deg`);
796
+ }
797
+ disconnectedCallback() {
798
+ var _a;
799
+ super.disconnectedCallback();
800
+ (_a = this._valueRing) == null ? void 0 : _a.style.removeProperty("--o-value-angle");
801
+ this._valueRing = null;
779
802
  }
780
803
  /**
781
804
  * variant="stroke": open arc along the orbit radius (no closed band),
@@ -793,7 +816,7 @@
793
816
  const range = Math.max(0, Math.min(360, this.readAngle(common.style, "--o-range", 360)));
794
817
  const rawValue = common.style.getPropertyValue("--o-progress").trim();
795
818
  const progress = rawValue ? this.readNumber(common.style, "--o-progress", 0) : this.readValue();
796
- const maxValue = this.readMax();
819
+ const maxValue = this.readMax(common.style);
797
820
  return {
798
821
  ...common,
799
822
  range,
@@ -850,6 +873,12 @@
850
873
  :host([interactive]) svg * {
851
874
  pointer-events: visiblePainted;
852
875
  }
876
+ /* Text lets the pointer through to the band under it: WebKit fires no
877
+ click on SVG text inside a shadow root. */
878
+ :host([interactive]) svg text,
879
+ :host([interactive]) svg textPath {
880
+ pointer-events: none;
881
+ }
853
882
  /* The touch area catches the pointer even where the band is transparent. */
854
883
  #orbitHit {
855
884
  fill: none;
@@ -876,9 +905,32 @@
876
905
  stroke: none;
877
906
  pointer-events: none;
878
907
  }
908
+ /* .ripple: a copy of the band grows out from the center of the ring and
909
+ fades, over and over. The band itself stays. */
910
+ #orbitWave {
911
+ display: none;
912
+ fill: var(--o-ripple-color, var(--o-fill));
913
+ stroke: none;
914
+ pointer-events: none;
915
+ transform-origin: 50px 50px;
916
+ }
917
+ :host(.ripple) #orbitWave {
918
+ display: inline;
919
+ animation: orbit-ripple var(--o-ripple-duration, 1.6s) ease-out infinite;
920
+ }
921
+ @keyframes orbit-ripple {
922
+ from { transform: scale(1); opacity: .9; }
923
+ to { transform: scale(var(--o-ripple-scale, 1.14)); opacity: 0; }
924
+ }
925
+ @media (prefers-reduced-motion: reduce) {
926
+ :host(.ripple) #orbitWave {
927
+ display: none;
928
+ }
929
+ }
879
930
  </style>
880
931
  <svg viewBox="0 0 100 100">
881
932
  <path id="orbitHit" vector-effect="non-scaling-stroke"></path>
933
+ <path id="orbitWave" shape-rendering="geometricPrecision"></path>
882
934
  <path id="orbitShape" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
883
935
  <path id="orbitPath" shape-rendering="geometricPrecision" vector-effect="non-scaling-stroke"></path>
884
936
  <text>
@@ -907,7 +959,9 @@
907
959
  const orbitPath = this.shadowRoot.querySelector("#orbitPath");
908
960
  const orbitShape = this.shadowRoot.querySelector("#orbitShape");
909
961
  const textPath = this.shadowRoot.querySelector("textPath");
910
- orbitShape.setAttribute("d", this.calculateArcParameters(attrs).dShape);
962
+ const { dShape } = this.calculateArcParameters(attrs);
963
+ orbitShape.setAttribute("d", dShape);
964
+ this.shadowRoot.querySelector("#orbitWave").setAttribute("d", dShape);
911
965
  orbitPath.setAttribute("d", this.calculateTextArcParameters(attrs).dPath);
912
966
  this.updateHitArea(this.shadowRoot.querySelector("#orbitHit"), attrs, 0, attrs.arcAngle);
913
967
  const textAnchor = attrs.textAnchor;
@@ -968,26 +1022,45 @@
968
1022
  const gap = Math.max(0, this.readNumber(style, "--o-gap", 1));
969
1023
  const svgFontSize = `calc(${fontSize} * (100 / (${length}) * (12 / var(--o-orbit-number)))`;
970
1024
  const auto = this.getAttribute("range") === "auto";
971
- const stacked = auto || this.hasAttribute("value");
1025
+ const valued = this.hasAttribute("value");
1026
+ const placed = this.hasAttribute("start");
1027
+ const stacked = auto || valued || placed;
1028
+ const max = this.readMax(style);
972
1029
  let arcAngle;
973
1030
  if (auto) arcAngle = this.measureTextAngle(svgFontSize, common.realRadius, gap);
974
- else if (this.hasAttribute("value")) arcAngle = super.getProgressAngle(range, this.readValue(), this.readMax());
1031
+ else if (valued) arcAngle = super.getProgressAngle(range, this.readValue(), max);
975
1032
  else arcAngle = Math.max(0, Math.min(360, this.readAngle(style, "--o-angle", 0)));
976
1033
  this._autoAngle = auto ? arcAngle : 0;
977
1034
  let stackOffset = 0;
978
- if (stacked) {
1035
+ if (placed) {
1036
+ stackOffset = max > 0 ? this.readStart() / max * range : 0;
1037
+ if (!auto && range < 360) {
1038
+ const end = Math.min(range, stackOffset + arcAngle);
1039
+ stackOffset = Math.max(0, stackOffset);
1040
+ arcAngle = Math.max(0, end - stackOffset);
1041
+ }
1042
+ } else if (stacked) {
979
1043
  for (let prev = this.previousElementSibling; prev; prev = prev.previousElementSibling) {
980
- if (prev.localName !== "o-arc") continue;
1044
+ if (prev.localName !== "o-arc" || prev.hasAttribute("start")) continue;
981
1045
  const previousStyle = this.ownerDocument.defaultView.getComputedStyle(prev);
982
1046
  if (prev.getAttribute("range") === "auto") stackOffset += prev._autoAngle || 0;
983
1047
  else if (prev.hasAttribute("value")) {
984
- const prevMax = prev.hasAttribute("max") ? Number(prev.getAttribute("max")) : 100;
1048
+ const prevMax = this.readMax.call(prev, previousStyle);
985
1049
  const prevRange = cssNumber(prev, previousStyle.getPropertyValue("--o-range"), range, "angle");
986
1050
  stackOffset += super.getProgressAngle(prevRange, Number(prev.getAttribute("value")), prevMax);
987
1051
  } else stackOffset += cssNumber(prev, previousStyle.getPropertyValue("--o-angle"), 0, "angle");
988
1052
  }
989
1053
  }
990
- return { ...common, gap, arcAngle, stackOffset, flip, fitRange, length, fontSize, svgFontSize, textAnchor, auto, stacked };
1054
+ const reach = this.classList.contains("ripple") ? Math.max(1, this.readNumber(style, "--o-ripple-scale", 1.14)) : 1;
1055
+ return { ...common, gap, max, arcAngle, stackOffset, flip, fitRange, length, fontSize, svgFontSize, textAnchor, auto, placed, stacked, reach };
1056
+ }
1057
+ /** The start attribute: where a placed arc begins, on the ring's scale. */
1058
+ readStart() {
1059
+ const raw = this.getAttribute("start");
1060
+ const start = Number(raw);
1061
+ if (Number.isFinite(start)) return start;
1062
+ warnOnce(this, "start:" + raw, `<o-arc start="${raw}"> is not a number, so the arc starts at 0.`);
1063
+ return 0;
991
1064
  }
992
1065
  /** range="auto": the angle that fits this arc's text, with half an em of room at each end. */
993
1066
  measureTextAngle(svgFontSize, radius, gap) {