@raylang/elements 0.10.0 → 0.12.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.
@@ -73,6 +73,11 @@ export class RayInput extends FieldControl {
73
73
  syncComplete(this);
74
74
  }
75
75
 
76
+ /** The highlight range under a point of the viewport, or null. */
77
+ highlightAt(x, y) {
78
+ return this._textLayer?.rangeAt(x, y) ?? null;
79
+ }
80
+
76
81
  /** Runs `highlight` again (what it marks changed, not the text). */
77
82
  refreshHighlight() {
78
83
  syncHighlight(this);
@@ -142,6 +142,13 @@ export class RayMenu extends Floating {
142
142
  return this.context && this._point ? this._point : super.anchorRect();
143
143
  }
144
144
 
145
+ // A context menu's trigger is the area it serves: a press there, outside
146
+ // the panel, closes it like a system context menu (a right click reopens
147
+ // it at the new point).
148
+ triggerDismisses() {
149
+ return Boolean(this.context);
150
+ }
151
+
145
152
  toggled(open) {
146
153
  if (open) {
147
154
  this._focus(this._focusOnOpen ?? "first");
@@ -0,0 +1,140 @@
1
+ // <ray-pulse>: an indeterminate wait indicator softer than ray-spinner: up to
2
+ // five circles, concentric or in a row, breathing or expanding from a point.
3
+ // Four presets (variant); count, shape, fade and direction override them.
4
+
5
+ import { RayElement, css, define } from "../../core/core.js";
6
+ import { applyColor, srOnlyStyles } from "../../core/styles.js";
7
+
8
+ export const MAX_COUNT = 5;
9
+
10
+ /**
11
+ * The presets. `scale` is where a cycle starts (1 = the circle's full size),
12
+ * `stagger` how much of the cycle separates one circle's phase from the next
13
+ * (1 = spread evenly), `core` a static dot in the middle, `row` dots side by
14
+ * side instead of concentric.
15
+ */
16
+ export const PRESETS = {
17
+ cluster: { count: 3, shape: "filled", fade: "none", direction: "alternate", scale: 0.7, stagger: 1 },
18
+ breathe: { count: 2, shape: "filled", fade: "none", direction: "alternate", scale: 0.6, stagger: 0 },
19
+ ripple: { count: 2, shape: "ring", fade: "out", direction: "normal", scale: 0.3, stagger: 1, core: true },
20
+ dots: { count: 3, shape: "filled", fade: "out", direction: "alternate", scale: 0.6, stagger: 1, row: true },
21
+ };
22
+
23
+ const styles = css`
24
+ :host {
25
+ display: inline-flex;
26
+ vertical-align: middle;
27
+ --_pulse: 2.25rem;
28
+ --_cycle: var(--ray-pulse-cycle, 1.6s);
29
+ --_floor: var(--ray-pulse-floor, 0.2);
30
+ }
31
+ :host([size="xs"]) { --_pulse: 1.125rem; }
32
+ :host([size="sm"]) { --_pulse: 1.375rem; }
33
+ :host([size="lg"]) { --_pulse: 2.75rem; }
34
+ :host([size="xl"]) { --_pulse: 3.625rem; }
35
+ .pulse {
36
+ display: inline-grid;
37
+ place-items: center;
38
+ width: var(--_pulse);
39
+ height: var(--_pulse);
40
+ color: var(--_filled);
41
+ }
42
+ .pulse.row {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ gap: calc(var(--_pulse) * 0.16);
46
+ width: auto;
47
+ }
48
+ .dot {
49
+ grid-area: 1 / 1;
50
+ display: block;
51
+ flex: none;
52
+ width: var(--_w);
53
+ height: var(--_w);
54
+ border-radius: 50%;
55
+ background: currentColor;
56
+ opacity: var(--_o, 1);
57
+ animation: ray-pulse var(--_cycle) var(--_ease) infinite var(--_direction);
58
+ /* Negative: every circle is already mid-cycle on the first frame. */
59
+ animation-delay: calc(-1 * var(--_i) * var(--_cycle) * var(--_stagger) / var(--_n));
60
+ }
61
+ .ring > .dot { background: transparent; border: max(1.5px, calc(var(--_pulse) / 18)) solid currentColor; }
62
+ /* Concentric filled circles: the larger, the fainter (else they merge into one). */
63
+ .gradient > .dot { --_o: calc(1 - (1 - var(--_floor)) * var(--_i) / var(--_last)); }
64
+ .core { --_w: 28%; animation: none; opacity: 1; background: currentColor; border: 0; }
65
+ @keyframes ray-pulse {
66
+ from { transform: scale(var(--_scale)); opacity: var(--_o, 1); }
67
+ to { transform: scale(1); opacity: calc(var(--_o, 1) * var(--_fade-end)); }
68
+ }
69
+ @media (prefers-reduced-motion: reduce) {
70
+ .dot { animation-duration: calc(var(--_cycle) * 2) !important; }
71
+ }
72
+ `;
73
+
74
+ const clampCount = (n) => Math.max(1, Math.min(MAX_COUNT, Math.floor(n)));
75
+
76
+ export class RayPulse extends RayElement {
77
+ static properties = {
78
+ variant: { type: String, reflect: true, default: "cluster" },
79
+ count: { type: Number, reflect: true },
80
+ shape: { type: String, reflect: true },
81
+ fade: { type: String, reflect: true },
82
+ direction: { type: String, reflect: true },
83
+ color: { type: String, reflect: true },
84
+ size: { type: String, reflect: true, default: "md" },
85
+ label: { type: String, default: "Loading" },
86
+ };
87
+ static styles = [srOnlyStyles, styles];
88
+ static template = `<span class="pulse" part="pulse" aria-hidden="true"></span><span class="sr-only"></span>`;
89
+
90
+ connectedCallback() {
91
+ super.connectedCallback();
92
+ if (!this.hasAttribute("role")) this.setAttribute("role", "status");
93
+ }
94
+
95
+ update() {
96
+ const preset = PRESETS[this.variant] ?? PRESETS.cluster;
97
+ const n = this.count > 0 ? clampCount(this.count) : preset.count;
98
+ const shape = this.shape === "ring" || this.shape === "filled" ? this.shape : preset.shape;
99
+ // `fade` alone (an empty attribute) means out; "none" switches a preset's off.
100
+ const fade = this.fade == null ? preset.fade : this.fade === "none" ? "none" : "out";
101
+ const direction = this.direction === "normal" || this.direction === "alternate" ? this.direction : preset.direction;
102
+ const box = this.$(".pulse");
103
+ applyColor(box, this.color);
104
+ box.classList.toggle("row", !!preset.row);
105
+ box.classList.toggle("ring", shape === "ring");
106
+ box.classList.toggle("gradient", shape === "filled" && !preset.row);
107
+ const s = box.style;
108
+ s.setProperty("--_n", String(n));
109
+ s.setProperty("--_last", String(Math.max(1, n - 1)));
110
+ s.setProperty("--_scale", `var(--ray-pulse-scale, ${preset.scale})`);
111
+ s.setProperty("--_stagger", `var(--ray-pulse-stagger, ${preset.stagger})`);
112
+ s.setProperty("--_direction", direction);
113
+ s.setProperty("--_fade-end", fade === "out" ? "0" : "1");
114
+ s.setProperty("--_ease", direction === "normal" ? "cubic-bezier(0, 0.4, 0.5, 1)" : "ease-in-out");
115
+ const key = `${n}:${preset.row ? "row" : "ring"}:${preset.core ? "core" : ""}`;
116
+ if (box.dataset.key !== key) {
117
+ box.dataset.key = key;
118
+ const dots = [];
119
+ // Largest first, so the smaller ones paint on top of it.
120
+ for (let i = n - 1; i >= 0; i--) {
121
+ const dot = document.createElement("i");
122
+ dot.className = "dot";
123
+ dot.setAttribute("part", "dot");
124
+ dot.style.setProperty("--_i", String(i));
125
+ dot.style.setProperty("--_w", preset.row ? "calc(var(--_pulse) * 0.28)" : `${Math.round(((i + 1) / n) * 100)}%`);
126
+ dots.push(dot);
127
+ }
128
+ if (preset.core) {
129
+ const core = document.createElement("i");
130
+ core.className = "dot core";
131
+ core.setAttribute("part", "dot core");
132
+ dots.push(core);
133
+ }
134
+ box.replaceChildren(...dots);
135
+ }
136
+ this.$(".sr-only").textContent = this.label;
137
+ }
138
+ }
139
+
140
+ define("ray-pulse", RayPulse);
@@ -77,6 +77,11 @@ export class RayTextarea extends FieldControl {
77
77
  syncComplete(this);
78
78
  }
79
79
 
80
+ /** The highlight range under a point of the viewport, or null. */
81
+ highlightAt(x, y) {
82
+ return this._textLayer?.rangeAt(x, y) ?? null;
83
+ }
84
+
80
85
  /** Runs `highlight` again (what it marks changed, not the text). */
81
86
  refreshHighlight() {
82
87
  syncHighlight(this);
package/core/floating.js CHANGED
@@ -35,6 +35,7 @@ export class Floating extends RayElement {
35
35
  open: { type: Boolean, reflect: true },
36
36
  placement: { type: String, reflect: true, default: "bottom" },
37
37
  offset: { type: Number, default: 8 },
38
+ anchor: { attribute: false },
38
39
  };
39
40
  /** popover attribute of the panel ("manual": closing is handled here). */
40
41
  static popoverMode = "manual";
@@ -102,8 +103,15 @@ export class Floating extends RayElement {
102
103
  /** Subclasses attach their trigger listeners here. */
103
104
  wireTrigger(_trigger) {}
104
105
 
105
- /** The rectangle the panel is placed against (default: the trigger). */
106
+ /**
107
+ * The rectangle the panel is placed against: `anchor` when set (an element,
108
+ * a DOMRect or a function returning one: a text range, the pointer), else
109
+ * the trigger.
110
+ */
106
111
  anchorRect() {
112
+ const a = typeof this.anchor === "function" ? this.anchor() : this.anchor;
113
+ if (a instanceof Element) return a.getBoundingClientRect();
114
+ if (a && typeof a.left === "number") return a;
107
115
  return (this.trigger ?? this).getBoundingClientRect();
108
116
  }
109
117
 
@@ -132,7 +140,20 @@ export class Floating extends RayElement {
132
140
  _dismissable() {
133
141
  const ctor = /** @type {typeof Floating} */ (this.constructor);
134
142
  if (this.#undismiss || !ctor.lightDismiss || ctor.popoverMode !== "manual") return;
135
- this.#undismiss = dismissable({ panel: this.panel, owner: this, trigger: this.trigger, close: () => { this.open = false; } });
143
+ // When the trigger dismisses, so does the rest of the host's light DOM
144
+ // (the area is a child of the host): only the panel is inside then.
145
+ const area = this.triggerDismisses();
146
+ this.#undismiss = dismissable({ panel: this.panel, owner: area ? this.panel : this, trigger: area ? null : this.trigger, close: () => { this.open = false; } });
147
+ }
148
+
149
+ /**
150
+ * Whether a press on the trigger counts as outside and closes the panel.
151
+ * Default false: a button that toggles the panel handles its own click
152
+ * (closing it there too would reopen it). A context menu's trigger is the
153
+ * whole area it serves, so there it is true.
154
+ */
155
+ triggerDismisses() {
156
+ return false;
136
157
  }
137
158
 
138
159
  /** The keyframes the panel leaves with (subclasses may change them). */
@@ -166,6 +187,15 @@ export class Floating extends RayElement {
166
187
 
167
188
  _onToggle(e) {
168
189
  const opening = e.newState === "open";
190
+ // `toggle` is a task: `open` may have changed again since the state it
191
+ // reports. An open that a later `open = false` is already animating out,
192
+ // or a close that a later `open = true` re-showed, must not win over
193
+ // that newer value; only the pair of events is still emitted.
194
+ const stale = opening ? !this.open && Boolean(this.#exit) : Boolean(this.open) && this.panel.matches(":popover-open");
195
+ if (stale) {
196
+ this.emit(opening ? "ray-open" : "ray-close");
197
+ return;
198
+ }
169
199
  if (this.popupKind()) this.#trigger?.setAttribute("aria-expanded", String(opening));
170
200
  if (opening) {
171
201
  this.reposition();
package/core/textlayer.js CHANGED
@@ -176,10 +176,18 @@ export class TextLayer {
176
176
 
177
177
  /** The range under a point of the viewport, or null. */
178
178
  rangeAt(x, y) {
179
+ return this.hitAt(x, y)?.range ?? null;
180
+ }
181
+
182
+ /**
183
+ * The range under a point of the viewport and the box of the line of it
184
+ * the point is on (a range may wrap), or null.
185
+ */
186
+ hitAt(x, y) {
179
187
  const marks = [...this.text.querySelectorAll("mark")];
180
188
  for (let i = 0; i < marks.length; i++) {
181
189
  for (const r of marks[i].getClientRects()) {
182
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return this.ranges[i];
190
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return { range: this.ranges[i], rect: r };
183
191
  }
184
192
  }
185
193
  return null;
@@ -189,7 +197,9 @@ export class TextLayer {
189
197
  /**
190
198
  * Paints `host.highlight(text)` behind `host.control` (ray-input,
191
199
  * ray-textarea): the layer is made the first time a highlight function is
192
- * set. A range's `title` becomes the field's tooltip under the pointer.
200
+ * set. A range's `title` becomes the field's tooltip under the pointer, and
201
+ * `ray-highlight-hover` fires when the range under it changes (null leaving
202
+ * one), with its box, so an app can show its own popover over a range.
193
203
  */
194
204
  export function syncHighlight(host) {
195
205
  const fn = typeof host.highlight === "function" ? host.highlight : null;
@@ -197,10 +207,18 @@ export function syncHighlight(host) {
197
207
  host._textLayer ??= new TextLayer(host.control);
198
208
  if (!host._highlightTitles) {
199
209
  host._highlightTitles = true;
200
- host.control.addEventListener("pointermove", (e) => {
201
- const title = host._textLayer.rangeAt(e.clientX, e.clientY)?.title ?? "";
210
+ let over = null;
211
+ const hover = (x, y) => {
212
+ const hit = host._textLayer.hitAt(x, y);
213
+ const title = hit?.range.title ?? "";
202
214
  if (host.control.title !== title) host.control.title = title;
203
- });
215
+ const range = hit?.range ?? null;
216
+ if (range === over) return;
217
+ over = range;
218
+ host.emit("ray-highlight-hover", { range, rect: hit?.rect ?? null });
219
+ };
220
+ host.control.addEventListener("pointermove", (e) => hover(e.clientX, e.clientY));
221
+ host.control.addEventListener("pointerleave", () => hover(-1, -1));
204
222
  }
205
223
  let ranges = [];
206
224
  if (fn) {
@@ -4746,6 +4746,11 @@
4746
4746
  "name": "select",
4747
4747
  "description": "Selects the text."
4748
4748
  },
4749
+ {
4750
+ "kind": "method",
4751
+ "name": "highlightAt",
4752
+ "description": "The highlight range under a point of the viewport (clientX, clientY), or null."
4753
+ },
4749
4754
  {
4750
4755
  "kind": "method",
4751
4756
  "name": "refreshHighlight",
@@ -4760,6 +4765,13 @@
4760
4765
  },
4761
4766
  "description": "A suggestion was accepted (the text already has it)."
4762
4767
  },
4768
+ {
4769
+ "name": "ray-highlight-hover",
4770
+ "type": {
4771
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
4772
+ },
4773
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
4774
+ },
4763
4775
  {
4764
4776
  "name": "input",
4765
4777
  "type": {
@@ -5301,6 +5313,13 @@
5301
5313
  },
5302
5314
  "description": "A suggestion was accepted (the text already has it)."
5303
5315
  },
5316
+ {
5317
+ "name": "ray-highlight-hover",
5318
+ "type": {
5319
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
5320
+ },
5321
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
5322
+ },
5304
5323
  {
5305
5324
  "name": "input",
5306
5325
  "type": {
@@ -6301,6 +6320,13 @@
6301
6320
  },
6302
6321
  "description": "A suggestion was accepted (the text already has it)."
6303
6322
  },
6323
+ {
6324
+ "name": "ray-highlight-hover",
6325
+ "type": {
6326
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
6327
+ },
6328
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
6329
+ },
6304
6330
  {
6305
6331
  "name": "input",
6306
6332
  "type": {
@@ -6765,6 +6791,13 @@
6765
6791
  },
6766
6792
  "description": "A suggestion was accepted (the text already has it)."
6767
6793
  },
6794
+ {
6795
+ "name": "ray-highlight-hover",
6796
+ "type": {
6797
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
6798
+ },
6799
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
6800
+ },
6768
6801
  {
6769
6802
  "name": "input",
6770
6803
  "type": {
@@ -9540,6 +9573,11 @@
9540
9573
  "attribute": "radius",
9541
9574
  "reflects": true
9542
9575
  },
9576
+ {
9577
+ "kind": "method",
9578
+ "name": "highlightAt",
9579
+ "description": "The highlight range under a point of the viewport (clientX, clientY), or null."
9580
+ },
9543
9581
  {
9544
9582
  "kind": "method",
9545
9583
  "name": "refreshHighlight",
@@ -9554,6 +9592,13 @@
9554
9592
  },
9555
9593
  "description": "A suggestion was accepted (the text already has it)."
9556
9594
  },
9595
+ {
9596
+ "name": "ray-highlight-hover",
9597
+ "type": {
9598
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
9599
+ },
9600
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
9601
+ },
9557
9602
  {
9558
9603
  "name": "input",
9559
9604
  "type": {
@@ -13496,6 +13541,14 @@
13496
13541
  "attribute": "native",
13497
13542
  "reflects": true
13498
13543
  },
13544
+ {
13545
+ "kind": "field",
13546
+ "name": "anchor",
13547
+ "type": {
13548
+ "text": "Element | DOMRect | (() => DOMRect) | null"
13549
+ },
13550
+ "description": "What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it."
13551
+ },
13499
13552
  {
13500
13553
  "kind": "field",
13501
13554
  "name": "offset",
@@ -13571,7 +13624,7 @@
13571
13624
  "slots": [
13572
13625
  {
13573
13626
  "name": "trigger",
13574
- "description": "The button that opens it, or the area for a context menu."
13627
+ "description": "The button that opens it, or the area for a context menu (a press on the area outside the open menu closes it; a right click reopens it at the new point)."
13575
13628
  },
13576
13629
  {
13577
13630
  "name": "",
@@ -13940,6 +13993,14 @@
13940
13993
  "reflects": false,
13941
13994
  "default": "8"
13942
13995
  },
13996
+ {
13997
+ "kind": "field",
13998
+ "name": "anchor",
13999
+ "type": {
14000
+ "text": "Element | DOMRect | (() => DOMRect) | null"
14001
+ },
14002
+ "description": "What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it."
14003
+ },
13943
14004
  {
13944
14005
  "kind": "field",
13945
14006
  "name": "width",
@@ -18463,6 +18524,206 @@
18463
18524
  }
18464
18525
  ]
18465
18526
  },
18527
+ {
18528
+ "kind": "javascript-module",
18529
+ "path": "components/pulse/pulse.js",
18530
+ "declarations": [
18531
+ {
18532
+ "kind": "class",
18533
+ "name": "RayPulse",
18534
+ "tagName": "ray-pulse",
18535
+ "customElement": true,
18536
+ "description": "Four presets (variant), each a bundle of count, shape, fade and direction, which override it one by one. The rest is tuned with custom properties on the element: --ray-pulse-cycle (one cycle, default 1.6s; doubled with reduced motion), --ray-pulse-floor (opacity of the largest of concentric filled circles, 0 to 1, default 0.2), --ray-pulse-scale (where a cycle starts, 1 = full size; default per variant) and --ray-pulse-stagger (how much of the cycle separates the phases of the circles, 0 together to 1 spread evenly; default per variant).",
18537
+ "superclass": {
18538
+ "name": "RayElement"
18539
+ },
18540
+ "attributes": [
18541
+ {
18542
+ "name": "variant",
18543
+ "type": {
18544
+ "text": "\"cluster\" | \"breathe\" | \"ripple\" | \"dots\""
18545
+ },
18546
+ "description": "The figure: cluster (three concentric circles breathing out of phase), breathe (one dot over a halo), ripple (rings expanding from a dot) or dots (three in a row, in sequence).",
18547
+ "fieldName": "variant",
18548
+ "default": "cluster"
18549
+ },
18550
+ {
18551
+ "name": "count",
18552
+ "type": {
18553
+ "text": "number"
18554
+ },
18555
+ "description": "Number of circles, 1 to 5 (more are clamped). Default: the variant's.",
18556
+ "fieldName": "count"
18557
+ },
18558
+ {
18559
+ "name": "shape",
18560
+ "type": {
18561
+ "text": "\"filled\" | \"ring\""
18562
+ },
18563
+ "description": "Filled discs (concentric ones fade with size, see --ray-pulse-floor) or outlined rings. Default: the variant's.",
18564
+ "fieldName": "shape"
18565
+ },
18566
+ {
18567
+ "name": "fade",
18568
+ "type": {
18569
+ "text": "\"out\" | \"none\""
18570
+ },
18571
+ "description": "Whether a cycle ends transparent; the bare attribute means out. Default: the variant's (ripple and dots fade).",
18572
+ "fieldName": "fade"
18573
+ },
18574
+ {
18575
+ "name": "direction",
18576
+ "type": {
18577
+ "text": "\"alternate\" | \"normal\""
18578
+ },
18579
+ "description": "alternate goes there and back; normal goes one way and restarts (with fade: the ripple). Default: the variant's.",
18580
+ "fieldName": "direction"
18581
+ },
18582
+ {
18583
+ "name": "color",
18584
+ "type": {
18585
+ "text": "string"
18586
+ },
18587
+ "description": "A palette color or any CSS color. Default: the primary color.",
18588
+ "fieldName": "color"
18589
+ },
18590
+ {
18591
+ "name": "size",
18592
+ "type": {
18593
+ "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\""
18594
+ },
18595
+ "description": "Height of the figure (the dots variant is wider).",
18596
+ "fieldName": "size",
18597
+ "default": "md"
18598
+ },
18599
+ {
18600
+ "name": "label",
18601
+ "type": {
18602
+ "text": "string"
18603
+ },
18604
+ "description": "Text read by screen readers.",
18605
+ "fieldName": "label",
18606
+ "default": "Loading"
18607
+ }
18608
+ ],
18609
+ "members": [
18610
+ {
18611
+ "kind": "field",
18612
+ "name": "variant",
18613
+ "type": {
18614
+ "text": "\"cluster\" | \"breathe\" | \"ripple\" | \"dots\""
18615
+ },
18616
+ "description": "The figure: cluster (three concentric circles breathing out of phase), breathe (one dot over a halo), ripple (rings expanding from a dot) or dots (three in a row, in sequence).",
18617
+ "attribute": "variant",
18618
+ "reflects": true,
18619
+ "default": "cluster"
18620
+ },
18621
+ {
18622
+ "kind": "field",
18623
+ "name": "count",
18624
+ "type": {
18625
+ "text": "number"
18626
+ },
18627
+ "description": "Number of circles, 1 to 5 (more are clamped). Default: the variant's.",
18628
+ "attribute": "count",
18629
+ "reflects": true
18630
+ },
18631
+ {
18632
+ "kind": "field",
18633
+ "name": "shape",
18634
+ "type": {
18635
+ "text": "\"filled\" | \"ring\""
18636
+ },
18637
+ "description": "Filled discs (concentric ones fade with size, see --ray-pulse-floor) or outlined rings. Default: the variant's.",
18638
+ "attribute": "shape",
18639
+ "reflects": true
18640
+ },
18641
+ {
18642
+ "kind": "field",
18643
+ "name": "fade",
18644
+ "type": {
18645
+ "text": "\"out\" | \"none\""
18646
+ },
18647
+ "description": "Whether a cycle ends transparent; the bare attribute means out. Default: the variant's (ripple and dots fade).",
18648
+ "attribute": "fade",
18649
+ "reflects": true
18650
+ },
18651
+ {
18652
+ "kind": "field",
18653
+ "name": "direction",
18654
+ "type": {
18655
+ "text": "\"alternate\" | \"normal\""
18656
+ },
18657
+ "description": "alternate goes there and back; normal goes one way and restarts (with fade: the ripple). Default: the variant's.",
18658
+ "attribute": "direction",
18659
+ "reflects": true
18660
+ },
18661
+ {
18662
+ "kind": "field",
18663
+ "name": "color",
18664
+ "type": {
18665
+ "text": "string"
18666
+ },
18667
+ "description": "A palette color or any CSS color. Default: the primary color.",
18668
+ "attribute": "color",
18669
+ "reflects": true
18670
+ },
18671
+ {
18672
+ "kind": "field",
18673
+ "name": "size",
18674
+ "type": {
18675
+ "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\""
18676
+ },
18677
+ "description": "Height of the figure (the dots variant is wider).",
18678
+ "attribute": "size",
18679
+ "reflects": true,
18680
+ "default": "md"
18681
+ },
18682
+ {
18683
+ "kind": "field",
18684
+ "name": "label",
18685
+ "type": {
18686
+ "text": "string"
18687
+ },
18688
+ "description": "Text read by screen readers.",
18689
+ "attribute": "label",
18690
+ "reflects": false,
18691
+ "default": "Loading"
18692
+ }
18693
+ ],
18694
+ "events": [],
18695
+ "slots": [],
18696
+ "cssParts": [
18697
+ {
18698
+ "name": "pulse",
18699
+ "description": "The box holding the circles."
18700
+ },
18701
+ {
18702
+ "name": "dot",
18703
+ "description": "Each circle (the static center of ripple is also part core)."
18704
+ }
18705
+ ]
18706
+ }
18707
+ ],
18708
+ "exports": [
18709
+ {
18710
+ "kind": "js",
18711
+ "name": "RayPulse",
18712
+ "declaration": {
18713
+ "name": "RayPulse",
18714
+ "module": "components/pulse/pulse.js"
18715
+ }
18716
+ },
18717
+ {
18718
+ "kind": "custom-element-definition",
18719
+ "name": "ray-pulse",
18720
+ "declaration": {
18721
+ "name": "RayPulse",
18722
+ "module": "components/pulse/pulse.js"
18723
+ }
18724
+ }
18725
+ ]
18726
+ },
18466
18727
  {
18467
18728
  "kind": "javascript-module",
18468
18729
  "path": "components/ring-progress/ring-progress.js",
package/index.js CHANGED
@@ -58,6 +58,7 @@ export { RayAlert } from "./components/alert/alert.js";
58
58
  export { RayEmptyState } from "./components/empty-state/empty-state.js";
59
59
  export { RayNotification, RayToaster, notify, updateNotification, hideNotification, clearNotifications, statusIcon } from "./components/notification/notification.js";
60
60
  export { RayProgress } from "./components/progress/progress.js";
61
+ export { RayPulse } from "./components/pulse/pulse.js";
61
62
  export { RayRingProgress } from "./components/ring-progress/ring-progress.js";
62
63
  export { RaySkeleton } from "./components/skeleton/skeleton.js";
63
64
  export { RaySpinner } from "./components/spinner/spinner.js";
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # ray-elements
2
2
 
3
- > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.10.0.
3
+ > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.12.0.
4
4
 
5
5
  ray-elements is a design system of standard web components (custom elements
6
6
  with Shadow DOM, no dependencies): `<ray-button>`, `<ray-input>`,
@@ -918,6 +918,7 @@ Attributes (forwarded to the inner element):
918
918
  Events:
919
919
 
920
920
  - `ray-complete` (detail `{ item: CompletionItem; from: number; to: number }`): A suggestion was accepted (the text already has it).
921
+ - `ray-highlight-hover` (detail `{ range: HighlightRange | null; rect: DOMRect | null }`): The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title.
921
922
  - `input`: On every edit (value already updated).
922
923
  - `change`: When an edit is committed (blur, Enter).
923
924
 
@@ -944,6 +945,7 @@ Parts (`::part()`):
944
945
  Methods and properties:
945
946
 
946
947
  - `select(): void`: Selects the text.
948
+ - `highlightAt(x: number, y: number): HighlightRange | null`: The highlight range under a point of the viewport (clientX, clientY), or null.
947
949
  - `refreshHighlight(): void`: Runs highlight again, when what it marks changed but the text did not.
948
950
 
949
951
  Vue `v-model`: `value` (updated on input).
@@ -1480,6 +1482,7 @@ Attributes (forwarded to the inner element):
1480
1482
  Events:
1481
1483
 
1482
1484
  - `ray-complete` (detail `{ item: CompletionItem; from: number; to: number }`): A suggestion was accepted (the text already has it).
1485
+ - `ray-highlight-hover` (detail `{ range: HighlightRange | null; rect: DOMRect | null }`): The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title.
1483
1486
  - `input`: On every edit.
1484
1487
  - `change`: When an edit is committed.
1485
1488
 
@@ -1497,6 +1500,7 @@ Parts (`::part()`):
1497
1500
 
1498
1501
  Methods and properties:
1499
1502
 
1503
+ - `highlightAt(x: number, y: number): HighlightRange | null`: The highlight range under a point of the viewport (clientX, clientY), or null.
1500
1504
  - `refreshHighlight(): void`: Runs highlight again, when what it marks changed but the text did not.
1501
1505
 
1502
1506
  Vue `v-model`: `value` (updated on input).
@@ -2191,6 +2195,7 @@ Properties:
2191
2195
  - `context`: boolean, reflects. Opens on right click (contextmenu) over the trigger area, at the pointer.
2192
2196
  - `keepOpen` (attribute `keep-open`): boolean, reflects. Choosing an item does not close the menu.
2193
2197
  - `native`: boolean, reflects. With a native host (ray-elements/core/raylang.js in a std/ui window), opens as the system's context menu at the pointer: items and dividers, same ray-select.
2198
+ - `anchor` (property only): `Element | DOMRect | (() => DOMRect) | null`. What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it.
2194
2199
 
2195
2200
  Also the properties of Floating: `offset`.
2196
2201
 
@@ -2203,7 +2208,7 @@ Events:
2203
2208
 
2204
2209
  Slots:
2205
2210
 
2206
- - `trigger`: The button that opens it, or the area for a context menu.
2211
+ - `trigger`: The button that opens it, or the area for a context menu (a press on the area outside the open menu closes it; a right click reopens it at the new point).
2207
2212
  - (default): ray-menu-item, ray-menu-label and ray-menu-divider elements.
2208
2213
 
2209
2214
  Parts (`::part()`):
@@ -2294,6 +2299,7 @@ Properties:
2294
2299
  - `open`: boolean, reflects. Shown.
2295
2300
  - `placement`: `bottom` | `bottom-start` | `bottom-end` | `top` | `top-start` | `top-end` | `left` | `left-start` | `left-end` | `right` | `right-start` | `right-end`, default `bottom`, reflects. Preferred side and alignment: top/bottom/left/right are sides of the screen; on top and bottom, -start/-end follow the writing direction (right to left, "bottom-start" lines up the right edges).
2296
2301
  - `offset`: number, default `8`. Gap to the trigger in px.
2302
+ - `anchor` (property only): `Element | DOMRect | (() => DOMRect) | null`. What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it.
2297
2303
  - `width`: string. Panel width (px number or CSS length). Default: its content.
2298
2304
  - `matchWidth` (attribute `match-width`): boolean. At least as wide as the trigger.
2299
2305
  - `padding`: `0` | `xs` | `sm` | `md` | `lg`, default `md`, reflects. Panel padding.
@@ -3007,6 +3013,32 @@ Parts (`::part()`):
3007
3013
  - `bar`: The filled bar.
3008
3014
  - `value`: The percentage beside the bar (show-value on xs, sm, md).
3009
3015
 
3016
+ ### Pulse
3017
+
3018
+ An indeterminate wait indicator softer than the spinner: circles that breathe or ripple, announced as a status.
3019
+
3020
+ Import: `import "@raylang/elements/pulse";` · Category: Feedback
3021
+
3022
+ #### `<ray-pulse>` (RayPulse, extends RayElement)
3023
+
3024
+ Four presets (variant), each a bundle of count, shape, fade and direction, which override it one by one. The rest is tuned with custom properties on the element: --ray-pulse-cycle (one cycle, default 1.6s; doubled with reduced motion), --ray-pulse-floor (opacity of the largest of concentric filled circles, 0 to 1, default 0.2), --ray-pulse-scale (where a cycle starts, 1 = full size; default per variant) and --ray-pulse-stagger (how much of the cycle separates the phases of the circles, 0 together to 1 spread evenly; default per variant).
3025
+
3026
+ Properties:
3027
+
3028
+ - `variant`: `cluster` | `breathe` | `ripple` | `dots`, default `cluster`, reflects. The figure: cluster (three concentric circles breathing out of phase), breathe (one dot over a halo), ripple (rings expanding from a dot) or dots (three in a row, in sequence).
3029
+ - `count`: number, reflects. Number of circles, 1 to 5 (more are clamped). Default: the variant's.
3030
+ - `shape`: `filled` | `ring`, reflects. Filled discs (concentric ones fade with size, see --ray-pulse-floor) or outlined rings. Default: the variant's.
3031
+ - `fade`: `out` | `none`, reflects. Whether a cycle ends transparent; the bare attribute means out. Default: the variant's (ripple and dots fade).
3032
+ - `direction`: `alternate` | `normal`, reflects. alternate goes there and back; normal goes one way and restarts (with fade: the ripple). Default: the variant's.
3033
+ - `color`: string, reflects. A palette color or any CSS color. Default: the primary color.
3034
+ - `size`: `xs` | `sm` | `md` | `lg` | `xl`, default `md`, reflects. Height of the figure (the dots variant is wider).
3035
+ - `label`: string, default `Loading`. Text read by screen readers.
3036
+
3037
+ Parts (`::part()`):
3038
+
3039
+ - `pulse`: The box holding the circles.
3040
+ - `dot`: Each circle (the static center of ripple is also part core).
3041
+
3010
3042
  ### Ring progress
3011
3043
 
3012
3044
  Progress as a ring (Mantine's RingProgress), with content in the middle: a countdown, a goal, an upload. Announced as a progressbar.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # ray-elements
2
2
 
3
- > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.10.0.
3
+ > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.12.0.
4
4
 
5
5
  ray-elements is a design system of standard web components (custom elements
6
6
  with Shadow DOM, no dependencies): `<ray-button>`, `<ray-input>`,
@@ -337,6 +337,7 @@ Members whose name starts with `_` are internal.
337
337
  - [Empty state](https://elements.raylang.dev/components/empty-state.md): `<ray-empty-state>` (Feedback). What a list, table or pane shows when it is empty: icon, title, text and the action that fills it.
338
338
  - [Notification](https://elements.raylang.dev/components/notification.md): `<ray-notification>`, `<ray-toaster>` (Feedback). Toasts: notify(), updateNotification(), hideNotification(), clearNotifications(); shown in a ray-toaster (a polite live region in the top layer).
339
339
  - [Progress](https://elements.raylang.dev/components/progress.md): `<ray-progress>` (Feedback). A determinate or (without value) indeterminate progress bar, announced as a progressbar.
340
+ - [Pulse](https://elements.raylang.dev/components/pulse.md): `<ray-pulse>` (Feedback). An indeterminate wait indicator softer than the spinner: circles that breathe or ripple, announced as a status.
340
341
  - [Ring progress](https://elements.raylang.dev/components/ring-progress.md): `<ray-ring-progress>` (Feedback). Progress as a ring (Mantine's RingProgress), with content in the middle: a countdown, a goal, an upload.
341
342
  - [Skeleton](https://elements.raylang.dev/components/skeleton.md): `<ray-skeleton>` (Feedback). A placeholder while content loads: a shimmering block, a circle or text lines; with loaded it shows its content.
342
343
  - [Spinner](https://elements.raylang.dev/components/spinner.md): `<ray-spinner>` (Feedback). An indeterminate loading indicator, announced as a status.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raylang/elements",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Web components for any app, with raylang extras (shadcn/ui + Mantine inspired)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": ["web-components", "custom-elements", "design-system", "ui", "raylang"],
@@ -252,6 +252,10 @@
252
252
  "types": "./ray-elements.d.ts",
253
253
  "default": "./components/progress/progress.js"
254
254
  },
255
+ "./pulse": {
256
+ "types": "./ray-elements.d.ts",
257
+ "default": "./components/pulse/pulse.js"
258
+ },
255
259
  "./ring-progress": {
256
260
  "types": "./ray-elements.d.ts",
257
261
  "default": "./components/ring-progress/ring-progress.js"
package/ray-elements.d.ts CHANGED
@@ -855,6 +855,8 @@ export declare class RayInput extends FieldControl {
855
855
  suggestionsLabel: string;
856
856
  /** Selects the text. */
857
857
  select(): void;
858
+ /** The highlight range under a point of the viewport (clientX, clientY), or null. */
859
+ highlightAt(x: number, y: number): HighlightRange | null;
858
860
  /** Runs highlight again, when what it marks changed but the text did not. */
859
861
  refreshHighlight(): void;
860
862
  addEventListener<K extends keyof RayInputEventMap>(type: K, listener: (this: RayInput, ev: RayInputEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
@@ -865,6 +867,8 @@ export declare class RayInput extends FieldControl {
865
867
  export interface RayInputEventMap extends HTMLElementEventMap {
866
868
  /** A suggestion was accepted (the text already has it). */
867
869
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
870
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
871
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
868
872
  }
869
873
 
870
874
  // --- Mode input (mode-input)
@@ -900,6 +904,8 @@ export interface RayModeInputEventMap extends HTMLElementEventMap {
900
904
  "ray-mode-change": CustomEvent<{ mode: string; previous: string | null; option: ModeOption }>;
901
905
  /** A suggestion was accepted (the text already has it). */
902
906
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
907
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
908
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
903
909
  }
904
910
 
905
911
  // --- Number input (number-input)
@@ -960,6 +966,8 @@ export interface RayPasswordInputEventMap extends HTMLElementEventMap {
960
966
  "ray-visibility-change": CustomEvent<{ visible: boolean }>;
961
967
  /** A suggestion was accepted (the text already has it). */
962
968
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
969
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
970
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
963
971
  }
964
972
 
965
973
  // --- Path input (path-input)
@@ -991,6 +999,8 @@ export interface RayPathInputEventMap extends HTMLElementEventMap {
991
999
  "ray-error": CustomEvent<{ error: Error }>;
992
1000
  /** A suggestion was accepted (the text already has it). */
993
1001
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
1002
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
1003
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
994
1004
  }
995
1005
 
996
1006
  // --- Radio group (radio)
@@ -1250,6 +1260,8 @@ export declare class RayTextarea extends FieldControl {
1250
1260
  complete: ((text: string, caret: number) => CompletionResult | null | Promise<CompletionResult | null>) | null;
1251
1261
  /** Accessible name of the suggestion list. */
1252
1262
  suggestionsLabel: string;
1263
+ /** The highlight range under a point of the viewport (clientX, clientY), or null. */
1264
+ highlightAt(x: number, y: number): HighlightRange | null;
1253
1265
  /** Runs highlight again, when what it marks changed but the text did not. */
1254
1266
  refreshHighlight(): void;
1255
1267
  addEventListener<K extends keyof RayTextareaEventMap>(type: K, listener: (this: RayTextarea, ev: RayTextareaEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
@@ -1260,6 +1272,8 @@ export declare class RayTextarea extends FieldControl {
1260
1272
  export interface RayTextareaEventMap extends HTMLElementEventMap {
1261
1273
  /** A suggestion was accepted (the text already has it). */
1262
1274
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
1275
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
1276
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
1263
1277
  }
1264
1278
 
1265
1279
  // --- Bottom nav (bottom-nav)
@@ -1747,6 +1761,8 @@ export declare class RayMenu extends Floating {
1747
1761
  keepOpen: boolean | undefined;
1748
1762
  /** With a native host (ray-elements/core/raylang.js in a std/ui window), opens as the system's context menu at the pointer: items and dividers, same ray-select. */
1749
1763
  native: boolean | undefined;
1764
+ /** What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it. */
1765
+ anchor: Element | DOMRect | (() => DOMRect) | null;
1750
1766
  /** Opens the items as the system's context menu (needs a native host); resolves to the chosen item, or null. */
1751
1767
  popupNative(): Promise<RayMenuItem | null>;
1752
1768
  /** Opens it. */
@@ -1807,6 +1823,8 @@ export declare class RayPopover extends Floating {
1807
1823
  placement: "bottom" | "bottom-start" | "bottom-end" | "top" | "top-start" | "top-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end";
1808
1824
  /** Gap to the trigger in px. */
1809
1825
  offset: number;
1826
+ /** What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it. */
1827
+ anchor: Element | DOMRect | (() => DOMRect) | null;
1810
1828
  /** Panel width (px number or CSS length). Default: its content. */
1811
1829
  width: string | undefined;
1812
1830
  /** At least as wide as the trigger. */
@@ -2367,6 +2385,27 @@ export declare class RayProgress extends RayElement {
2367
2385
  label: string | undefined;
2368
2386
  }
2369
2387
 
2388
+ // --- Pulse (pulse)
2389
+ /** <ray-pulse> Four presets (variant), each a bundle of count, shape, fade and direction, which override it one by one. The rest is tuned with custom properties on the element: --ray-pulse-cycle (one cycle, default 1.6s; doubled with reduced motion), --ray-pulse-floor (opacity of the largest of concentric filled circles, 0 to 1, default 0.2), --ray-pulse-scale (where a cycle starts, 1 = full size; default per variant) and --ray-pulse-stagger (how much of the cycle separates the phases of the circles, 0 together to 1 spread evenly; default per variant). */
2390
+ export declare class RayPulse extends RayElement {
2391
+ /** The figure: cluster (three concentric circles breathing out of phase), breathe (one dot over a halo), ripple (rings expanding from a dot) or dots (three in a row, in sequence). */
2392
+ variant: "cluster" | "breathe" | "ripple" | "dots";
2393
+ /** Number of circles, 1 to 5 (more are clamped). Default: the variant's. */
2394
+ count: number | undefined;
2395
+ /** Filled discs (concentric ones fade with size, see --ray-pulse-floor) or outlined rings. Default: the variant's. */
2396
+ shape: "filled" | "ring" | undefined;
2397
+ /** Whether a cycle ends transparent; the bare attribute means out. Default: the variant's (ripple and dots fade). */
2398
+ fade: "out" | "none" | undefined;
2399
+ /** alternate goes there and back; normal goes one way and restarts (with fade: the ripple). Default: the variant's. */
2400
+ direction: "alternate" | "normal" | undefined;
2401
+ /** A palette color or any CSS color. Default: the primary color. */
2402
+ color: string | undefined;
2403
+ /** Height of the figure (the dots variant is wider). */
2404
+ size: "xs" | "sm" | "md" | "lg" | "xl";
2405
+ /** Text read by screen readers. */
2406
+ label: string;
2407
+ }
2408
+
2370
2409
  // --- Ring progress (ring-progress)
2371
2410
  /** <ray-ring-progress> The arc starts at 12 o'clock and runs clockwise; a countdown is a value that goes down. Without value it spins (indeterminate). The middle holds the default slot (a clock), or the percentage with show-value. */
2372
2411
  export declare class RayRingProgress extends RayElement {
@@ -2764,6 +2803,7 @@ declare global {
2764
2803
  "ray-notification": RayNotification;
2765
2804
  "ray-toaster": RayToaster;
2766
2805
  "ray-progress": RayProgress;
2806
+ "ray-pulse": RayPulse;
2767
2807
  "ray-ring-progress": RayRingProgress;
2768
2808
  "ray-skeleton": RaySkeleton;
2769
2809
  "ray-spinner": RaySpinner;
@@ -6645,6 +6645,99 @@
6645
6645
  }
6646
6646
  ]
6647
6647
  },
6648
+ {
6649
+ "name": "ray-pulse",
6650
+ "description": "Four presets (variant), each a bundle of count, shape, fade and direction, which override it one by one. The rest is tuned with custom properties on the element: --ray-pulse-cycle (one cycle, default 1.6s; doubled with reduced motion), --ray-pulse-floor (opacity of the largest of concentric filled circles, 0 to 1, default 0.2), --ray-pulse-scale (where a cycle starts, 1 = full size; default per variant) and --ray-pulse-stagger (how much of the cycle separates the phases of the circles, 0 together to 1 spread evenly; default per variant).",
6651
+ "attributes": [
6652
+ {
6653
+ "name": "variant",
6654
+ "description": "The figure: cluster (three concentric circles breathing out of phase), breathe (one dot over a halo), ripple (rings expanding from a dot) or dots (three in a row, in sequence).",
6655
+ "values": [
6656
+ {
6657
+ "name": "cluster"
6658
+ },
6659
+ {
6660
+ "name": "breathe"
6661
+ },
6662
+ {
6663
+ "name": "ripple"
6664
+ },
6665
+ {
6666
+ "name": "dots"
6667
+ }
6668
+ ]
6669
+ },
6670
+ {
6671
+ "name": "count",
6672
+ "description": "Number of circles, 1 to 5 (more are clamped). Default: the variant's."
6673
+ },
6674
+ {
6675
+ "name": "shape",
6676
+ "description": "Filled discs (concentric ones fade with size, see --ray-pulse-floor) or outlined rings. Default: the variant's.",
6677
+ "values": [
6678
+ {
6679
+ "name": "filled"
6680
+ },
6681
+ {
6682
+ "name": "ring"
6683
+ }
6684
+ ]
6685
+ },
6686
+ {
6687
+ "name": "fade",
6688
+ "description": "Whether a cycle ends transparent; the bare attribute means out. Default: the variant's (ripple and dots fade).",
6689
+ "values": [
6690
+ {
6691
+ "name": "out"
6692
+ },
6693
+ {
6694
+ "name": "none"
6695
+ }
6696
+ ]
6697
+ },
6698
+ {
6699
+ "name": "direction",
6700
+ "description": "alternate goes there and back; normal goes one way and restarts (with fade: the ripple). Default: the variant's.",
6701
+ "values": [
6702
+ {
6703
+ "name": "alternate"
6704
+ },
6705
+ {
6706
+ "name": "normal"
6707
+ }
6708
+ ]
6709
+ },
6710
+ {
6711
+ "name": "color",
6712
+ "description": "A palette color or any CSS color. Default: the primary color."
6713
+ },
6714
+ {
6715
+ "name": "size",
6716
+ "description": "Height of the figure (the dots variant is wider).",
6717
+ "values": [
6718
+ {
6719
+ "name": "xs"
6720
+ },
6721
+ {
6722
+ "name": "sm"
6723
+ },
6724
+ {
6725
+ "name": "md"
6726
+ },
6727
+ {
6728
+ "name": "lg"
6729
+ },
6730
+ {
6731
+ "name": "xl"
6732
+ }
6733
+ ]
6734
+ },
6735
+ {
6736
+ "name": "label",
6737
+ "description": "Text read by screen readers."
6738
+ }
6739
+ ]
6740
+ },
6648
6741
  {
6649
6742
  "name": "ray-ring-progress",
6650
6743
  "description": "The arc starts at 12 o'clock and runs clockwise; a countdown is a value that goes down. Without value it spins (indeterminate). The middle holds the default slot (a clock), or the percentage with show-value.",