@raylang/elements 0.5.1 → 0.6.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.
@@ -2,7 +2,8 @@
2
2
  // buttons (hold to repeat), the arrow keys (Shift: ×10), min/max/step,
3
3
  // a fixed number of decimals, a prefix and a suffix ("$", " kg") and a
4
4
  // thousands separator in the shown text. The value is a number (or empty);
5
- // the form gets its plain digits.
5
+ // the form gets its plain digits. format="duration": the value is seconds,
6
+ // shown and typed as m:ss (h:mm:ss from an hour).
6
7
  //
7
8
  // An ARIA spinbutton: aria-valuenow/min/max/valuetext on the text field.
8
9
 
@@ -60,6 +61,36 @@ export function formatNumber(n, { decimals, thousands = "", decimal = "." } = {}
60
61
  return frac !== undefined ? `${int}${decimal}${frac}` : int;
61
62
  }
62
63
 
64
+ /** Seconds as m:ss, or h:mm:ss with `hours` (or from an hour); "" if not a number. */
65
+ export function formatDuration(seconds, { hours = false } = {}) {
66
+ if (!Number.isFinite(seconds)) return "";
67
+ const sign = seconds < 0 ? "-" : "";
68
+ const t = Math.round(Math.abs(seconds));
69
+ const h = Math.floor(t / 3600);
70
+ const m = Math.floor((t % 3600) / 60);
71
+ const ss = String(t % 60).padStart(2, "0");
72
+ return hours || h > 0 ? `${sign}${h}:${String(m).padStart(2, "0")}:${ss}` : `${sign}${m}:${ss}`;
73
+ }
74
+
75
+ /** Seconds from "m:ss", "h:mm:ss" or a plain number of seconds ("3:", "3:0" while typing); NaN if none. */
76
+ export function parseDuration(text) {
77
+ const m = String(text ?? "").replace(/\s+/g, "").match(/^(-?)(\d*)(?::(\d*))?(?::(\d*))?$/);
78
+ if (!m || (m[2] === "" && !m[3])) return NaN;
79
+ const parts = [m[2], m[3], m[4]].filter((p) => p !== undefined).map((p) => Number(p || 0));
80
+ const seconds = parts.reduce((total, p) => total * 60 + p, 0);
81
+ return m[1] ? -seconds : seconds;
82
+ }
83
+
84
+ /** "3 minutes, 15 seconds" in `lang` (Intl), for aria-valuetext. */
85
+ export function durationText(seconds, lang) {
86
+ const t = Math.round(Math.abs(seconds));
87
+ const units = [["hour", Math.floor(t / 3600)], ["minute", Math.floor((t % 3600) / 60)], ["second", t % 60]];
88
+ const said = units.filter(([, n], i) => n > 0 || (t === 0 && i === 2));
89
+ const words = said.map(([unit, n]) => new Intl.NumberFormat(lang, { style: "unit", unit, unitDisplay: "long" }).format(n));
90
+ const text = new Intl.ListFormat(lang, { style: "long", type: "unit" }).format(words);
91
+ return seconds < 0 ? `-${text}` : text;
92
+ }
93
+
63
94
  export class RayNumberInput extends FieldControl {
64
95
  /** Enter submits the form (core/form.js). */
65
96
  static implicitSubmission = true;
@@ -78,6 +109,7 @@ export class RayNumberInput extends FieldControl {
78
109
  placeholder: { type: String },
79
110
  hideControls: { type: Boolean, reflect: true, attribute: "hide-controls" },
80
111
  clamp: { type: String, default: "blur" },
112
+ format: { type: String, reflect: true, default: "number" },
81
113
  readonly: { type: Boolean, reflect: true },
82
114
  };
83
115
  static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
@@ -135,6 +167,8 @@ export class RayNumberInput extends FieldControl {
135
167
  update() {
136
168
  const c = this.control;
137
169
  c.readOnly = Boolean(this.readonly);
170
+ // The number pad has no ":".
171
+ c.inputMode = this._duration ? "text" : "decimal";
138
172
  if (this.placeholder !== undefined) c.placeholder = this.placeholder;
139
173
  if (!this.#typing) this._paint();
140
174
  this.syncField();
@@ -162,10 +196,15 @@ export class RayNumberInput extends FieldControl {
162
196
  let v = n;
163
197
  if (this.min !== undefined && this.min !== null && v < this.min) v = this.min;
164
198
  if (this.max !== undefined && this.max !== null && v > this.max) v = this.max;
165
- return this.decimals !== undefined && this.decimals !== null ? Number(v.toFixed(this.decimals)) : v;
199
+ return this.decimals !== undefined && this.decimals !== null && !this._duration ? Number(v.toFixed(this.decimals)) : v;
200
+ }
201
+
202
+ get _duration() {
203
+ return this.format === "duration";
166
204
  }
167
205
 
168
206
  _parse(text) {
207
+ if (this._duration) return parseDuration(text);
169
208
  let s = String(text ?? "");
170
209
  if (this.prefix) s = s.split(this.prefix).join("");
171
210
  if (this.suffix) s = s.split(this.suffix).join("");
@@ -184,10 +223,16 @@ export class RayNumberInput extends FieldControl {
184
223
  }
185
224
  }
186
225
 
187
- // The shown text: prefix, separators, decimals, suffix.
226
+ // The shown text: prefix, separators, decimals, suffix; or m:ss.
188
227
  _text() {
189
- if (this.value === undefined || this.value === null || !Number.isFinite(this.value)) return "";
190
- const num = formatNumber(this.value, { decimals: this.decimals, thousands: this.thousandsSeparator || "", decimal: this.decimalSeparator || "." });
228
+ return this._show(this.value);
229
+ }
230
+
231
+ _show(n) {
232
+ if (n === undefined || n === null || !Number.isFinite(n)) return "";
233
+ // h:mm:ss when the range reaches an hour, so the width does not jump.
234
+ if (this._duration) return formatDuration(n, { hours: Math.abs(this.max ?? 0) >= 3600 || Math.abs(this.min ?? 0) >= 3600 });
235
+ const num = formatNumber(n, { decimals: this.decimals, thousands: this.thousandsSeparator || "", decimal: this.decimalSeparator || "." });
191
236
  return `${this.prefix ?? ""}${num}${this.suffix ?? ""}`;
192
237
  }
193
238
 
@@ -206,7 +251,8 @@ export class RayNumberInput extends FieldControl {
206
251
  set("aria-valuenow", this.value);
207
252
  set("aria-valuemin", this.min);
208
253
  set("aria-valuemax", this.max);
209
- if (this.prefix || this.suffix || this.thousandsSeparator) set("aria-valuetext", this._text() || undefined);
254
+ if (this._duration) set("aria-valuetext", Number.isFinite(this.value) ? durationText(this.value, this.closest("[lang]")?.lang || undefined) : undefined);
255
+ else if (this.prefix || this.suffix || this.thousandsSeparator) set("aria-valuetext", this._text() || undefined);
210
256
  else c.removeAttribute("aria-valuetext");
211
257
  }
212
258
 
@@ -225,8 +271,8 @@ export class RayNumberInput extends FieldControl {
225
271
  const c = this.control;
226
272
  let msg = "";
227
273
  if (this.required && (this.value === undefined || this.value === null)) msg = "Please enter a number.";
228
- else if (this.value !== undefined && this.min !== undefined && this.value < this.min) msg = `The minimum is ${this.min}.`;
229
- else if (this.value !== undefined && this.max !== undefined && this.value > this.max) msg = `The maximum is ${this.max}.`;
274
+ else if (this.value !== undefined && this.min !== undefined && this.value < this.min) msg = `The minimum is ${this._show(this.min)}.`;
275
+ else if (this.value !== undefined && this.max !== undefined && this.value > this.max) msg = `The maximum is ${this._show(this.max)}.`;
230
276
  if (msg) this.internals.setValidity(this.value === undefined ? { valueMissing: true } : this.value < (this.min ?? -Infinity) ? { rangeUnderflow: true } : { rangeOverflow: true }, msg, c);
231
277
  else this.internals.setValidity({});
232
278
  }
@@ -3,12 +3,17 @@
3
3
  // Native radios cannot group across shadow roots, so the group owns the
4
4
  // state: it is the form control (value submitted under `name`), keeps one
5
5
  // radio checked, and moves focus and selection with the arrow keys (roving
6
- // tabindex), like native radios.
6
+ // tabindex), like native radios. <ray-radio-card> is a radio drawn as a
7
+ // card (a title, a detail, an icon), for choices that need more than a line:
8
+ // plans, presets, shipping methods. Both can share a group.
7
9
 
8
10
  import { RayElement, css, define } from "../../core/core.js";
9
11
  import { applyColor, sizeStyles } from "../../core/styles.js";
10
12
  import { FormControl, fieldStyles } from "../../core/form.js";
11
13
 
14
+ /** The elements a group owns: radios and radio cards. */
15
+ const RADIOS = "ray-radio, ray-radio-card";
16
+
12
17
  const groupStyles = css`
13
18
  :host { --_rs: 1.25rem; }
14
19
  :host([size="xs"]) { --_rs: 1rem; }
@@ -53,7 +58,7 @@ export class RayRadioGroup extends FormControl {
53
58
  this.internals.role = "radiogroup";
54
59
  this.addEventListener("keydown", (e) => this._keydown(e));
55
60
  this.addEventListener("click", (e) => {
56
- const radio = e.target.closest?.("ray-radio");
61
+ const radio = e.target.closest?.(RADIOS);
57
62
  if (radio && this.radios.includes(radio)) this._choose(radio, true);
58
63
  });
59
64
  }
@@ -67,9 +72,9 @@ export class RayRadioGroup extends FormControl {
67
72
  }
68
73
  }
69
74
 
70
- /** The radios of this group (direct or nested, not in a nested group). */
75
+ /** The radios (and radio cards) of this group (direct or nested, not in a nested group). */
71
76
  get radios() {
72
- return [...this.querySelectorAll("ray-radio")].filter((r) => r.closest("ray-radio-group") === this);
77
+ return [...this.querySelectorAll(RADIOS)].filter((r) => r.closest("ray-radio-group") === this);
73
78
  }
74
79
 
75
80
  update() {
@@ -129,7 +134,7 @@ export class RayRadioGroup extends FormControl {
129
134
 
130
135
  _keydown(e) {
131
136
  const radios = this.radios.filter((r) => !r.disabled);
132
- const i = radios.indexOf(e.target.closest?.("ray-radio"));
137
+ const i = radios.indexOf(e.target.closest?.(RADIOS));
133
138
  if (i < 0) return;
134
139
  let next = null;
135
140
  if (e.key === "ArrowDown" || e.key === "ArrowRight") next = radios[(i + 1) % radios.length];
@@ -217,11 +222,28 @@ export class RayRadio extends RayElement {
217
222
  this._groupDisabled = false;
218
223
  }
219
224
 
220
- /** Whether this radio is the group's choice (read-only: set the group's value). */
225
+ /** Whether this radio is the group's choice. Setting it sets the group's
226
+ * value, like `checked` on a native radio (no events). */
221
227
  get checked() {
222
228
  return this._checked;
223
229
  }
224
230
 
231
+ set checked(on) {
232
+ on = Boolean(on);
233
+ const group = this.closest("ray-radio-group");
234
+ if (!group) {
235
+ this._setChecked(on);
236
+ return;
237
+ }
238
+ if (on) {
239
+ group.value = this.value;
240
+ for (const r of group.radios) r._setChecked(r === this);
241
+ } else if (this._checked) {
242
+ group.value = undefined;
243
+ this._setChecked(false);
244
+ }
245
+ }
246
+
225
247
  _setChecked(on) {
226
248
  this._checked = on;
227
249
  this.toggleAttribute("data-checked", on);
@@ -249,5 +271,63 @@ export class RayRadio extends RayElement {
249
271
  }
250
272
  }
251
273
 
274
+ const cardStyles = css`
275
+ :host { --_s: var(--_rs, 1.125rem); }
276
+ .root {
277
+ align-items: flex-start;
278
+ gap: var(--ray-space-3);
279
+ height: 100%;
280
+ box-sizing: border-box;
281
+ padding: var(--ray-space-3) var(--ray-space-4);
282
+ background: var(--ray-background);
283
+ border: 1px solid var(--ray-border);
284
+ border-radius: var(--ray-radius-default);
285
+ transition: background-color var(--ray-duration-fast) var(--ray-easing-standard), border-color var(--ray-duration-fast) var(--ray-easing-standard);
286
+ }
287
+ :host(:not([data-disabled])) .root:hover { background: var(--ray-default-hover); }
288
+ :host([data-checked]) .root, :host([data-checked]) .root:hover {
289
+ background: var(--_light, var(--ray-primary-light));
290
+ border-color: var(--_filled, var(--ray-primary-filled));
291
+ }
292
+ :host(:focus-visible) .root { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: var(--ray-focus-ring-offset); }
293
+ :host(:focus-visible) .indicator { outline: none; }
294
+ :host([data-disabled]) .root { background: var(--ray-muted); opacity: 0.6; }
295
+ .body { flex: 1; min-width: 0; min-height: 0; }
296
+ .label { font-weight: var(--ray-font-weight-semibold); }
297
+ .icon { display: inline-flex; flex: none; color: var(--_filled, var(--ray-primary-filled)); font-size: 1.25rem; line-height: 1; }
298
+ .extra { margin-top: var(--ray-space-2); font-size: var(--ray-font-size-sm); }
299
+ :host([hide-indicator]) .indicator { display: none; }
300
+ `;
301
+
302
+ export class RayRadioCard extends RayRadio {
303
+ static properties = {
304
+ ...RayRadio.properties,
305
+ hideIndicator: { type: Boolean, reflect: true, attribute: "hide-indicator" },
306
+ };
307
+ static styles = [radioStyles, cardStyles];
308
+ static template = `
309
+ <div class="root" part="root">
310
+ <span class="icon" part="icon" hidden><slot name="icon"></slot></span>
311
+ <span class="body">
312
+ <span class="label" part="label"><span class="label-text"></span></span>
313
+ <span class="description" part="description" hidden></span>
314
+ <span class="extra" hidden><slot></slot></span>
315
+ </span>
316
+ <span class="indicator" part="indicator"><span class="dot"></span></span>
317
+ </div>`;
318
+
319
+ constructor() {
320
+ super();
321
+ // The icon and the extra content take room only when given.
322
+ for (const slot of this.shadowRoot.querySelectorAll("slot")) {
323
+ slot.addEventListener("slotchange", () => {
324
+ const given = slot.assignedNodes().some((n) => n.nodeType === Node.ELEMENT_NODE || n.textContent.trim());
325
+ slot.parentElement.hidden = !given;
326
+ });
327
+ }
328
+ }
329
+ }
330
+
252
331
  define("ray-radio-group", RayRadioGroup);
253
332
  define("ray-radio", RayRadio);
333
+ define("ray-radio-card", RayRadioCard);
@@ -0,0 +1,127 @@
1
+ // <ray-ring-progress>: progress as a ring (Mantine's RingProgress), with
2
+ // room in the middle for content (a countdown's clock, a percentage).
3
+ //
4
+ // The arc is a stroke-dasharray on a circle with pathLength=100, drawn from
5
+ // 12 o'clock clockwise; r is a CSS length (half the box minus half the
6
+ // stroke), so any size and thickness need no viewBox arithmetic.
7
+
8
+ import { RayElement, css, define } from "../../core/core.js";
9
+ import { applyColor } from "../../core/styles.js";
10
+
11
+ const SIZES = { xs: "3rem", sm: "5rem", md: "7.5rem", lg: "10rem", xl: "12.5rem" };
12
+
13
+ const styles = css`
14
+ :host {
15
+ --_d: 7.5rem;
16
+ --_t: calc(var(--_d) / 10);
17
+ position: relative;
18
+ display: inline-grid;
19
+ /* One cell of exactly the diameter: the svg's intrinsic 300×150 must not size it. */
20
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
21
+ vertical-align: middle;
22
+ width: var(--_d);
23
+ height: var(--_d);
24
+ flex: none;
25
+ }
26
+ svg {
27
+ grid-area: 1 / 1;
28
+ width: 100%;
29
+ height: 100%;
30
+ overflow: visible;
31
+ transform: rotate(-90deg);
32
+ }
33
+ circle {
34
+ cx: 50%;
35
+ cy: 50%;
36
+ r: calc(50% - var(--_t) / 2);
37
+ fill: none;
38
+ stroke-width: var(--_t);
39
+ }
40
+ .track { stroke: var(--ray-muted); }
41
+ .bar {
42
+ stroke: var(--_filled);
43
+ stroke-dasharray: var(--_pct, 0) 100;
44
+ transition: stroke-dasharray var(--ray-duration-slow) var(--ray-easing-standard);
45
+ }
46
+ :host([round-caps]) .bar { stroke-linecap: round; }
47
+ /* A round cap draws a dot at 0. */
48
+ .bar[data-empty] { visibility: hidden; }
49
+ svg[data-indeterminate] { animation: ray-ring-spin 1.2s linear infinite; }
50
+ svg[data-indeterminate] .bar { transition: none; }
51
+ @keyframes ray-ring-spin { from { transform: rotate(-90deg); } to { transform: rotate(270deg); } }
52
+ .center {
53
+ grid-area: 1 / 1;
54
+ display: grid;
55
+ place-items: center;
56
+ padding: calc(var(--_t) * 1.5);
57
+ min-width: 0;
58
+ text-align: center;
59
+ font-size: max(var(--ray-font-size-xs), calc(var(--_d) * 0.16));
60
+ font-variant-numeric: tabular-nums;
61
+ line-height: 1.1;
62
+ }
63
+ .value { font-weight: var(--ray-font-weight-semibold); }
64
+ .value:empty { display: none; }
65
+ `;
66
+
67
+ export class RayRingProgress extends RayElement {
68
+ static properties = {
69
+ value: { type: Number },
70
+ max: { type: Number, default: 100 },
71
+ color: { type: String, reflect: true },
72
+ size: { type: String, reflect: true, default: "md" },
73
+ thickness: { type: String, reflect: true },
74
+ roundCaps: { type: Boolean, reflect: true, attribute: "round-caps" },
75
+ showValue: { type: Boolean, reflect: true, attribute: "show-value" },
76
+ label: { type: String },
77
+ };
78
+ static styles = [styles];
79
+ static template = `<svg aria-hidden="true"><circle class="track" part="track" pathLength="100"></circle><circle class="bar" part="bar" pathLength="100"></circle></svg><div class="center" part="center"><slot></slot><span class="value" aria-hidden="true"></span></div>`;
80
+
81
+ constructor() {
82
+ super();
83
+ this.internals = this.attachInternals();
84
+ this.internals.role = "progressbar";
85
+ }
86
+
87
+ update() {
88
+ const svg = this.$("svg");
89
+ const bar = this.$(".bar");
90
+ applyColor(svg, this.color);
91
+ // size: xs…xl or a length; thickness: a length (a bare number is px).
92
+ const d = SIZES[this.size] ?? this.size ?? "";
93
+ if (d) this.style.setProperty("--_d", d);
94
+ else this.style.removeProperty("--_d");
95
+ const t = this.thickness ? (/^\d*\.?\d+$/.test(this.thickness) ? `${this.thickness}px` : this.thickness) : "";
96
+ if (t) this.style.setProperty("--_t", t);
97
+ else this.style.removeProperty("--_t");
98
+
99
+ const max = this.max > 0 ? this.max : 100;
100
+ const indeterminate = this.value === undefined || this.value === null || Number.isNaN(this.value);
101
+ svg.toggleAttribute("data-indeterminate", indeterminate);
102
+ const pct = indeterminate ? 0 : Math.min(100, Math.max(0, (this.value / max) * 100));
103
+ // Indeterminate: a quarter arc that spins.
104
+ bar.style.setProperty("--_pct", String(indeterminate ? 25 : pct));
105
+ bar.toggleAttribute("data-empty", !indeterminate && pct === 0);
106
+ // The percentage in the middle only when the app puts nothing there.
107
+ const empty = this.$("slot").assignedNodes().every((n) => n.nodeType === Node.TEXT_NODE && !n.textContent.trim());
108
+ this.$(".value").textContent = this.showValue && !indeterminate && empty ? `${Math.round(pct)}%` : "";
109
+
110
+ const i = this.internals;
111
+ i.ariaValueMin = "0";
112
+ i.ariaValueMax = String(max);
113
+ i.ariaValueNow = indeterminate ? null : String(this.value);
114
+ i.ariaValueText = indeterminate ? null : `${Math.round(pct)}%`;
115
+ i.ariaLabel = this.label || this.getAttribute("aria-label") || "Progress";
116
+ }
117
+
118
+ connectedCallback() {
119
+ super.connectedCallback();
120
+ if (!this._wired) {
121
+ this._wired = true;
122
+ this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
123
+ }
124
+ }
125
+ }
126
+
127
+ define("ray-ring-progress", RayRingProgress);
@@ -0,0 +1,86 @@
1
+ // <ray-step-dots>: a row of dots, one per step: done (filled), the current
2
+ // one (a filled pill), the rest (empty). A compact progress for rounds,
3
+ // onboarding pages or a quiz. Not interactive: the carousel's dots are
4
+ // buttons, these are a progressbar.
5
+
6
+ import { RayElement, css, define } from "../../core/core.js";
7
+ import { applyColor } from "../../core/styles.js";
8
+
9
+ const styles = css`
10
+ :host {
11
+ --_dot: 0.5rem;
12
+ display: block;
13
+ }
14
+ :host([size="xs"]) { --_dot: 0.3125rem; }
15
+ :host([size="md"]) { --_dot: 0.625rem; }
16
+ :host([size="lg"]) { --_dot: 0.75rem; }
17
+ :host([size="xl"]) { --_dot: 1rem; }
18
+ .dots {
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ align-items: center;
22
+ justify-content: var(--_justify, center);
23
+ gap: calc(var(--_dot) * 0.75);
24
+ }
25
+ :host([align="start"]) { --_justify: flex-start; }
26
+ :host([align="end"]) { --_justify: flex-end; }
27
+ .dot {
28
+ flex: none;
29
+ width: var(--_dot);
30
+ height: var(--_dot);
31
+ box-sizing: border-box;
32
+ border-radius: var(--ray-radius-full);
33
+ border: 1.5px solid color-mix(in srgb, var(--ray-foreground) 28%, transparent);
34
+ transition: width var(--ray-duration-base) var(--ray-easing-standard), background-color var(--ray-duration-base) var(--ray-easing-standard), border-color var(--ray-duration-base) var(--ray-easing-standard);
35
+ }
36
+ .dot[data-state="done"] { background: var(--_filled); border-color: var(--_filled); }
37
+ .dot[data-state="current"] { width: calc(var(--_dot) * 2.5); background: var(--_filled); border-color: var(--_filled); }
38
+ `;
39
+
40
+ export class RayStepDots extends RayElement {
41
+ static properties = {
42
+ count: { type: Number, default: 0 },
43
+ value: { type: Number, default: 0 },
44
+ color: { type: String, reflect: true },
45
+ size: { type: String, reflect: true, default: "sm" },
46
+ align: { type: String, reflect: true, default: "center" },
47
+ label: { type: String },
48
+ };
49
+ static styles = [styles];
50
+ static template = `<div class="dots" part="dots" aria-hidden="true"></div>`;
51
+
52
+ constructor() {
53
+ super();
54
+ this.internals = this.attachInternals();
55
+ this.internals.role = "progressbar";
56
+ }
57
+
58
+ update() {
59
+ applyColor(this, this.color);
60
+ const count = Math.max(0, Math.floor(this.count || 0));
61
+ // value: the current step, 0-based; count (or more) is all done.
62
+ const value = Math.min(count, Math.max(0, Math.floor(this.value || 0)));
63
+ const box = this.$(".dots");
64
+ while (box.children.length < count) {
65
+ const dot = document.createElement("span");
66
+ dot.className = "dot";
67
+ dot.setAttribute("part", "dot");
68
+ box.append(dot);
69
+ }
70
+ while (box.children.length > count) box.lastElementChild.remove();
71
+ [...box.children].forEach((dot, i) => {
72
+ dot.dataset.state = i < value ? "done" : i === value ? "current" : "todo";
73
+ });
74
+
75
+ const noun = this.label || "Step";
76
+ const done = count > 0 && value >= count;
77
+ const i = this.internals;
78
+ i.ariaLabel = this.label || this.getAttribute("aria-label") || "Steps";
79
+ i.ariaValueMin = "0";
80
+ i.ariaValueMax = String(count);
81
+ i.ariaValueNow = String(value);
82
+ i.ariaValueText = count === 0 ? null : done ? `${count} of ${count}, done` : `${noun} ${value + 1} of ${count}`;
83
+ }
84
+ }
85
+
86
+ define("ray-step-dots", RayStepDots);