remarqueeble 0.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/LICENSE.md ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright © 2026 Rémino Rem <https://remino.net>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Remarqueeble
2
+
3
+ A tiny custom element tribute to the cursed glory of `<marquee>`, exposed as
4
+ `<re-marquee>` and `<re-marquee-ble>`.
5
+
6
+ By Rémino Rem
7
+ <https://remino.net/>
8
+
9
+ [Docs](https://remino.net/remarqueeble/) |
10
+ [Code Repo](https://github.com/remino/remarqueeble) |
11
+ [npm Package](https://www.npmjs.com/package/remarqueeble)
12
+
13
+ ---
14
+
15
+ <!-- mtoc-start -->
16
+
17
+ - [Installation](#installation)
18
+ - [Usage](#usage)
19
+ - [Attributes](#attributes)
20
+ - [API](#api)
21
+ - [Development](#development)
22
+ - [Contributing](#contributing)
23
+ - [Licence](#licence)
24
+
25
+ <!-- mtoc-end -->
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```sh
32
+ npm install remarqueeble
33
+ ```
34
+
35
+ [Back to top](#)
36
+
37
+ ---
38
+
39
+ ## Usage
40
+
41
+ Register the custom elements automatically:
42
+
43
+ ```js
44
+ import 'remarqueeble/auto'
45
+ ```
46
+
47
+ Or register them explicitly:
48
+
49
+ ```js
50
+ import { defineRemarqueebleElements } from 'remarqueeble'
51
+
52
+ defineRemarqueebleElements()
53
+ ```
54
+
55
+ Then use either element name:
56
+
57
+ ```html
58
+ <re-marquee>Default marquee behaviour.</re-marquee>
59
+ <re-marquee-ble direction="right">Rightward marquee behaviour.</re-marquee-ble>
60
+ ```
61
+
62
+ [Back to top](#)
63
+
64
+ ---
65
+
66
+ ## Attributes
67
+
68
+ Remarqueeble follows the legacy marquee attribute names where practical:
69
+
70
+ - `behavior`: `scroll`, `slide`, or `alternate`.
71
+ - `direction`: `left`, `right`, `up`, or `down`.
72
+ - `scrollamount`: step size in pixels.
73
+ - `scrolldelay`: delay between steps in milliseconds.
74
+ - `truespeed`: keeps delays under 60ms instead of clamping them.
75
+ - `loop`: positive loop count, or `-1` for infinite scrolling.
76
+ - `bgcolor`, `width`, `height`, `hspace`, `vspace`: presentational hints mapped
77
+ to CSS.
78
+
79
+ [Back to top](#)
80
+
81
+ ---
82
+
83
+ ## API
84
+
85
+ Each element exposes the legacy methods:
86
+
87
+ ```js
88
+ const marquee = document.querySelector('re-marquee')
89
+
90
+ marquee.stop()
91
+ marquee.start()
92
+ ```
93
+
94
+ [Back to top](#)
95
+
96
+ ---
97
+
98
+ ## Development
99
+
100
+ ```sh
101
+ npm install
102
+ npm run dev
103
+ npm run build
104
+ ```
105
+
106
+ The library source lives in `src/lib`. The documentation site is built with
107
+ Astro and lives in the rest of `src`.
108
+
109
+ ---
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork the repository.
114
+ 2. Create a feature branch: `git checkout -b feature/amazing-feature`.
115
+ 3. Make your changes.
116
+ 4. Run `npm run build` and `npm test`.
117
+ 5. Commit, push, and open a pull request.
118
+
119
+ Issues and ideas are welcome—please star the project if you enjoy it!
120
+
121
+ [Back to top](#)
122
+
123
+ ---
124
+
125
+ ## Licence
126
+
127
+ Licensed under the ISC licence. See `LICENSE.md`.
128
+
129
+ [Back to top](#)
package/dist/auto.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { defineRemarqueebleElements } from './remarqueeble';
@@ -0,0 +1,307 @@
1
+ /*! remarqueeble v0.1.0 | (c) 2026 Rémino Rem <https://remino.net/> | ISC Licence */
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ //#region src/lib/remarqueeble.ts
4
+ var DEFAULT_DIRECTION = "left";
5
+ var DEFAULT_BEHAVIOR = "scroll";
6
+ var DEFAULT_SCROLL_AMOUNT = 6;
7
+ var DEFAULT_SCROLL_DELAY = 85;
8
+ var MIN_SCROLL_DELAY = 60;
9
+ var DEFAULT_VERTICAL_HEIGHT = "200px";
10
+ var DEFAULT_HOST_WIDTH = "calc(100% - (var(--attr-hspace, 0px) * 2))";
11
+ var ATTR_DIRECTION = "direction";
12
+ var ATTR_BEHAVIOR = "behavior";
13
+ var ATTR_SCROLL_AMOUNT = "scrollamount";
14
+ var ATTR_SCROLL_DELAY = "scrolldelay";
15
+ var ATTR_TRUE_SPEED = "truespeed";
16
+ var ATTR_LOOP = "loop";
17
+ var ATTR_BG_COLOR = "bgcolor";
18
+ var ATTR_WIDTH = "width";
19
+ var ATTR_HEIGHT = "height";
20
+ var ATTR_HSPACE = "hspace";
21
+ var ATTR_VSPACE = "vspace";
22
+ var CSS_VAR_WIDTH = "--attr-width";
23
+ var CSS_VAR_HEIGHT = "--attr-height";
24
+ var CSS_VAR_HSPACE = "--attr-hspace";
25
+ var CSS_VAR_VSPACE = "--attr-vspace";
26
+ var CSS_VAR_BG_COLOR = "--attr-bgcolor";
27
+ var HTMLElementBase = globalThis.HTMLElement ?? class {};
28
+ var parsePresentationalDimension = (value) => {
29
+ if (value === null) return null;
30
+ const trimmed = value.trim();
31
+ if (trimmed === "") return null;
32
+ if (/^[+-]?(?:\d+|\d*\.\d+)$/.test(trimmed)) return `${trimmed}px`;
33
+ return globalThis.CSS?.supports("width", trimmed) ? trimmed : null;
34
+ };
35
+ var parseLegacyColor = (value) => {
36
+ if (value === null) return null;
37
+ const trimmed = value.trim();
38
+ if (trimmed === "") return null;
39
+ return globalThis.CSS?.supports("background-color", trimmed) ? trimmed : null;
40
+ };
41
+ var ATTRIBUTE_HINTS = [
42
+ {
43
+ attribute: ATTR_WIDTH,
44
+ cssVar: CSS_VAR_WIDTH,
45
+ parser: parsePresentationalDimension
46
+ },
47
+ {
48
+ attribute: ATTR_HEIGHT,
49
+ cssVar: CSS_VAR_HEIGHT,
50
+ parser: parsePresentationalDimension,
51
+ fallback(element) {
52
+ return element.isVerticalDirection && !element.hasAttribute(ATTR_HEIGHT) ? DEFAULT_VERTICAL_HEIGHT : null;
53
+ }
54
+ },
55
+ {
56
+ attribute: ATTR_HSPACE,
57
+ cssVar: CSS_VAR_HSPACE,
58
+ parser: parsePresentationalDimension
59
+ },
60
+ {
61
+ attribute: ATTR_VSPACE,
62
+ cssVar: CSS_VAR_VSPACE,
63
+ parser: parsePresentationalDimension
64
+ },
65
+ {
66
+ attribute: ATTR_BG_COLOR,
67
+ cssVar: CSS_VAR_BG_COLOR,
68
+ parser: parseLegacyColor
69
+ }
70
+ ];
71
+ var RemarqueebleElement = class extends HTMLElementBase {
72
+ static observedAttributes = [
73
+ ATTR_DIRECTION,
74
+ ATTR_BEHAVIOR,
75
+ ATTR_SCROLL_AMOUNT,
76
+ ATTR_SCROLL_DELAY,
77
+ ATTR_TRUE_SPEED,
78
+ ATTR_LOOP,
79
+ ATTR_BG_COLOR,
80
+ ATTR_WIDTH,
81
+ ATTR_HEIGHT,
82
+ ATTR_HSPACE,
83
+ ATTR_VSPACE
84
+ ];
85
+ track;
86
+ running = false;
87
+ position = 0;
88
+ lastTime = null;
89
+ loopsDone = 0;
90
+ forward = true;
91
+ rafId = null;
92
+ constructor() {
93
+ super();
94
+ const shadowRoot = this.attachShadow({ mode: "open" });
95
+ shadowRoot.innerHTML = `
96
+ <style>
97
+ :host {
98
+ display: inline-block;
99
+ text-align: initial;
100
+ overflow: hidden !important;
101
+ white-space: nowrap;
102
+ width: var(${CSS_VAR_WIDTH}, ${DEFAULT_HOST_WIDTH});
103
+ height: var(${CSS_VAR_HEIGHT}, auto);
104
+ margin-inline: var(${CSS_VAR_HSPACE}, 0px);
105
+ margin-block: var(${CSS_VAR_VSPACE}, 0px);
106
+ background-color: var(${CSS_VAR_BG_COLOR}, transparent);
107
+ box-sizing: border-box;
108
+ }
109
+
110
+ :host([direction="up"]),
111
+ :host([direction="down"]) {
112
+ white-space: normal;
113
+ }
114
+
115
+ .track {
116
+ display: inline-block;
117
+ will-change: transform;
118
+ }
119
+ </style>
120
+
121
+ <span class="track"><slot></slot></span>
122
+ `;
123
+ const track = shadowRoot.querySelector(".track");
124
+ if (!track) throw new Error("Remarqueeble track element was not created.");
125
+ this.track = track;
126
+ }
127
+ connectedCallback() {
128
+ this.running = true;
129
+ this.syncPresentationalHints();
130
+ requestAnimationFrame(() => {
131
+ if (!this.isConnected || !this.running) return;
132
+ this.reset();
133
+ this.tick();
134
+ });
135
+ }
136
+ disconnectedCallback() {
137
+ this.running = false;
138
+ this.lastTime = null;
139
+ if (this.rafId !== null) {
140
+ cancelAnimationFrame(this.rafId);
141
+ this.rafId = null;
142
+ }
143
+ }
144
+ attributeChangedCallback(_name, oldValue, newValue) {
145
+ if (oldValue === newValue) return;
146
+ this.syncPresentationalHints();
147
+ if (this.isConnected) this.reset();
148
+ }
149
+ get direction() {
150
+ return this.getAttribute(ATTR_DIRECTION) || DEFAULT_DIRECTION;
151
+ }
152
+ get behavior() {
153
+ return this.getAttribute(ATTR_BEHAVIOR) || DEFAULT_BEHAVIOR;
154
+ }
155
+ get scrollAmount() {
156
+ const raw = this.getAttribute(ATTR_SCROLL_AMOUNT);
157
+ const value = raw === null || raw.trim() === "" ? NaN : Number(raw);
158
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_SCROLL_AMOUNT;
159
+ }
160
+ get scrollDelay() {
161
+ const raw = this.getAttribute(ATTR_SCROLL_DELAY);
162
+ const value = raw === null || raw.trim() === "" ? NaN : Number(raw);
163
+ const delay = Number.isFinite(value) && value >= 0 ? value : DEFAULT_SCROLL_DELAY;
164
+ if (this.hasAttribute(ATTR_TRUE_SPEED)) return delay;
165
+ return Math.max(delay, MIN_SCROLL_DELAY);
166
+ }
167
+ get loop() {
168
+ const value = this.getAttribute(ATTR_LOOP);
169
+ return value === null ? -1 : Number(value);
170
+ }
171
+ get directionSign() {
172
+ return this.direction === "right" || this.direction === "down" ? 1 : -1;
173
+ }
174
+ get isVerticalDirection() {
175
+ return this.direction === "up" || this.direction === "down";
176
+ }
177
+ start() {
178
+ if (this.running) return;
179
+ this.running = true;
180
+ this.lastTime = null;
181
+ this.tick();
182
+ }
183
+ stop() {
184
+ this.running = false;
185
+ if (this.rafId !== null) {
186
+ cancelAnimationFrame(this.rafId);
187
+ this.rafId = null;
188
+ }
189
+ }
190
+ syncPresentationalHints() {
191
+ for (const hint of ATTRIBUTE_HINTS) this.syncVar(hint);
192
+ }
193
+ syncVar(hint) {
194
+ const raw = this.getAttribute(hint.attribute);
195
+ const value = hint.parser(raw);
196
+ const fallback = hint.fallback ? hint.fallback(this) : null;
197
+ if (value == null) {
198
+ if (fallback == null) this.style.removeProperty(hint.cssVar);
199
+ else this.style.setProperty(hint.cssVar, fallback);
200
+ return;
201
+ }
202
+ this.style.setProperty(hint.cssVar, value);
203
+ }
204
+ reset() {
205
+ const hostSize = this.getHostSize();
206
+ const trackSize = this.getTrackSize();
207
+ this.loopsDone = 0;
208
+ this.forward = true;
209
+ this.position = this.behavior === "alternate" ? this.getAlternateStartPosition(hostSize, trackSize) : this.getStartPosition(hostSize, trackSize);
210
+ this.render();
211
+ }
212
+ getHostSize() {
213
+ return this.isVerticalDirection ? this.clientHeight : this.clientWidth;
214
+ }
215
+ getTrackSize() {
216
+ return this.isVerticalDirection ? this.track.offsetHeight : this.track.offsetWidth;
217
+ }
218
+ getStartPosition(hostSize, trackSize) {
219
+ return this.directionSign < 0 ? hostSize : -trackSize;
220
+ }
221
+ getFlushEndPosition(hostSize, trackSize) {
222
+ return this.directionSign < 0 ? 0 : hostSize - trackSize;
223
+ }
224
+ getOffEndPosition(hostSize, trackSize) {
225
+ return this.directionSign < 0 ? -trackSize : hostSize;
226
+ }
227
+ getSlideEndPosition(hostSize, trackSize) {
228
+ return this.directionSign < 0 ? 0 : hostSize - trackSize;
229
+ }
230
+ getAlternateStartPosition(hostSize, trackSize) {
231
+ return this.directionSign < 0 ? hostSize - trackSize : 0;
232
+ }
233
+ tick(time = performance.now()) {
234
+ if (!this.running) return;
235
+ if (this.lastTime === null) this.lastTime = time;
236
+ if (time - this.lastTime >= this.scrollDelay) {
237
+ this.step();
238
+ this.lastTime = time;
239
+ }
240
+ if (!this.running) return;
241
+ this.rafId = requestAnimationFrame((nextTime) => this.tick(nextTime));
242
+ }
243
+ step() {
244
+ const hostSize = this.getHostSize();
245
+ const trackSize = this.getTrackSize();
246
+ const startPosition = this.getStartPosition(hostSize, trackSize);
247
+ const flushEndPosition = this.getFlushEndPosition(hostSize, trackSize);
248
+ const offEndPosition = this.getOffEndPosition(hostSize, trackSize);
249
+ const slideEndPosition = this.getSlideEndPosition(hostSize, trackSize);
250
+ const alternateStartPosition = this.getAlternateStartPosition(hostSize, trackSize);
251
+ const amount = this.scrollAmount;
252
+ const delta = this.directionSign * amount;
253
+ if (this.behavior === "alternate") {
254
+ this.position += this.forward ? delta : -delta;
255
+ if (this.forward) {
256
+ if (this.directionSign < 0 && this.position <= flushEndPosition || this.directionSign > 0 && this.position >= flushEndPosition) {
257
+ this.position = flushEndPosition;
258
+ this.forward = false;
259
+ this.incrementLoopCount();
260
+ if (this.shouldStopAfterLoop()) this.stop();
261
+ }
262
+ } else if (this.directionSign < 0 && this.position >= alternateStartPosition || this.directionSign > 0 && this.position <= alternateStartPosition) {
263
+ this.position = alternateStartPosition;
264
+ this.forward = true;
265
+ this.incrementLoopCount();
266
+ if (this.shouldStopAfterLoop()) this.stop();
267
+ }
268
+ } else if (this.behavior === "slide") {
269
+ this.position += delta;
270
+ if (this.directionSign < 0 && this.position <= slideEndPosition || this.directionSign > 0 && this.position >= slideEndPosition) {
271
+ this.position = slideEndPosition;
272
+ this.incrementLoopCount();
273
+ if (!this.hasAttribute(ATTR_LOOP) || this.loop <= 0) this.stop();
274
+ else if (this.shouldStopAfterLoop()) this.stop();
275
+ else this.position = startPosition;
276
+ }
277
+ } else {
278
+ this.position += delta;
279
+ if (this.directionSign < 0 && this.position <= offEndPosition || this.directionSign > 0 && this.position >= offEndPosition) {
280
+ this.position = startPosition;
281
+ this.incrementLoopCount();
282
+ if (this.shouldStopAfterLoop()) this.stop();
283
+ }
284
+ }
285
+ this.render();
286
+ }
287
+ incrementLoopCount() {
288
+ this.loopsDone++;
289
+ }
290
+ shouldStopAfterLoop() {
291
+ return this.hasAttribute(ATTR_LOOP) && this.loop > 0 && this.loopsDone >= this.loop;
292
+ }
293
+ render() {
294
+ if (this.isVerticalDirection) this.track.style.transform = `translateY(${this.position}px)`;
295
+ else this.track.style.transform = `translateX(${this.position}px)`;
296
+ }
297
+ };
298
+ var defineRemarqueebleElements = () => {
299
+ if (typeof customElements === "undefined") return;
300
+ if (!customElements.get("re-marquee")) customElements.define("re-marquee", RemarqueebleElement);
301
+ if (!customElements.get("re-marquee-ble")) customElements.define("re-marquee-ble", RemarqueebleElement);
302
+ };
303
+ //#endregion
304
+ //#region src/lib/auto.ts
305
+ defineRemarqueebleElements();
306
+ //#endregion
307
+ exports.defineRemarqueebleElements = defineRemarqueebleElements;