wake-marquee 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/CHANGELOG.md ADDED
@@ -0,0 +1,81 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ While the version is below `1.0.0` the API may still change in a minor release.
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Fixed
12
+
13
+ - The row no longer jumps as it starts. Three separate causes, all of them
14
+ the same shape: work spread across frames that the browser paints one by
15
+ one.
16
+ - Measuring, cloning and the first transform were left to three different
17
+ observer callbacks. They now happen together, in the task that starts the
18
+ row, for any row already on screen.
19
+ - The track's overhang was applied at construction while the transform that
20
+ cancels it was written on first sight. Between the two, the row sat a full
21
+ wake amplitude off to one side. Both now happen in the same frame.
22
+ - The first animated frame landed on the loop's zero point rather than on
23
+ the position the static row already occupied. It is now aligned to it,
24
+ which matters most for a row reached by anchor link or a restored scroll
25
+ position, where the two are furthest apart.
26
+ - A lane that resizes under a running row, from a late web font or an image
27
+ with no `width` and `height`, no longer shifts the row by the difference.
28
+ The offset is restated in the new period, holding the phase.
29
+
30
+ ### Changed
31
+
32
+ - `wake-marquee/astro` renders `--wake-gap` and `--wake-fade` as inline custom
33
+ properties, so the static row is laid out correctly before the script runs.
34
+ The `data-wake-*` attributes still carry the same values for the script.
35
+
36
+ ## [0.1.0] - 2026-09-01
37
+
38
+ First release.
39
+
40
+ ### Added
41
+
42
+ - `createMarquee(element, options)` turns an element and its children into an
43
+ endless marquee and hands back a handle with `play`, `pause`, `refresh` and
44
+ `destroy`. `destroy` restores the original DOM exactly, which is what makes
45
+ the React integration a ref and an effect rather than an adapter package.
46
+ - The wake: the layer holding the content is built `wake` percent wider than
47
+ its container on each side, and the scroll spends exactly that reserve as
48
+ the element crosses the viewport. The displacement is bounded by the
49
+ overhang, so the ends of the row can never come into view.
50
+ - The turn: scrolling up reverses travel, eased from `+1` to `-1` rather than
51
+ switched. The easing is framerate independent, so the same reversal takes
52
+ the same time at 60 and at 120 Hz.
53
+ - `initMarquees()` and `wake-marquee/auto` start every `[data-wake-marquee]`
54
+ on the page from its own `data-wake-*` attributes, so a page with no build
55
+ step needs one script tag and no JavaScript of its own.
56
+ - One shared `requestAnimationFrame` loop for every instance, reading all
57
+ geometry before writing any transform, so a frame costs one layout rather
58
+ than one per row. The loop stops when nothing is on screen.
59
+ - Rows are measured and cloned on first sight, not at construction, so a page
60
+ of ten marquees does no work for the nine nobody has scrolled to.
61
+ - `wake-marquee/css`: the loop geometry in an `@layer wake-marquee` cascade
62
+ layer, 407 B gzipped. It owns nothing about how items look.
63
+ - `wake-marquee/astro`: Astro component rendering the same attributes.
64
+ - Accessibility: clones are `inert` and `aria-hidden`, with a `tabindex="-1"`
65
+ fallback where `inert` is missing, so the content is announced once rather
66
+ than once per lane. Lazy images inside clones are switched to eager, because
67
+ a clone starts off screen and would otherwise arrive as a hole in the row.
68
+ - `prefers-reduced-motion: reduce` is honoured live: the row never starts, and
69
+ turning the preference on mid-session stops and resets it.
70
+
71
+ ### Notes
72
+
73
+ - The spacing between items is a `margin-inline-end` on the item rather than
74
+ `gap` on the row. The lane's width is the loop period, so the space after
75
+ the last item has to belong to the lane; with `gap` there is none between
76
+ one lane and the next and the seam shows on every pass.
77
+ - Right-to-left writing modes are not supported. Flex rows follow the writing
78
+ direction while the transforms are physical, so the two disagree.
79
+
80
+ [Unreleased]: https://github.com/robin-gogolok/wake-marquee/compare/v0.1.0...HEAD
81
+ [0.1.0]: https://github.com/robin-gogolok/wake-marquee/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robin Gogolok
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # wake-marquee
2
+
3
+ An endless marquee that answers to the scroll. It reverses when the reader scrolls back, and drags against its own direction of travel as it crosses the viewport.
4
+
5
+ **[Live demo →](https://robin-gogolok.github.io/wake-marquee/)**
6
+
7
+ ```
8
+ 3.0 kB gzipped JS
9
+ 0.4 kB gzipped CSS
10
+ 0 runtime dependencies
11
+ 1 rAF loop, however many rows are on the page
12
+ ```
13
+
14
+ A row of logos that plays on a loop is wallpaper. The reader learns in two seconds that it will never do anything, and stops looking. This makes the row part of the page instead: it has momentum, it turns around when they do, and it is visibly being dragged through the scroll.
15
+
16
+ ---
17
+
18
+ ## Why this exists
19
+
20
+ A CSS marquee is four lines and costs nothing, and for a lot of pages that is the right answer. It also has nothing to say to the reader. Nothing about it changes whether they are moving, still, or heading back up.
21
+
22
+ The scroll-linked half of this cannot be done in CSS today. `animation-timeline: scroll()` can drive an animation from the scroll position, but not an animation that also runs on its own, in a direction the scroll only influences. That needs a frame loop.
23
+
24
+ So this is a frame loop, kept as small as one can honestly be:
25
+
26
+ - **One `requestAnimationFrame` for the whole page.** Ten rows share it. It reads every element's geometry before it writes any transform, so a frame costs one layout rather than one per row.
27
+ - **It stops when nothing is on screen.** An idle rAF loop still keeps the compositor awake, and that shows up on a battery.
28
+ - **Nothing is measured or cloned until a row is actually seen.** A page with a row in the footer does no work for it until the reader gets there.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ npm i wake-marquee
34
+ ```
35
+
36
+ ## Use
37
+
38
+ Import the stylesheet once, anywhere in your app:
39
+
40
+ ```js
41
+ import 'wake-marquee/css'
42
+ ```
43
+
44
+ Write the container and its items. The direct children become the items; the library builds everything else.
45
+
46
+ ```html
47
+ <div data-wake-marquee data-wake-speed="55" data-wake="10" data-wake-fade="6rem">
48
+ <img src="/logos/a.svg" alt="Nordlicht">
49
+ <img src="/logos/b.svg" alt="Kvist">
50
+ <img src="/logos/c.svg" alt="Halden & Co">
51
+ </div>
52
+ ```
53
+
54
+ Then either start every row on the page in one line:
55
+
56
+ ```js
57
+ import 'wake-marquee/auto'
58
+ ```
59
+
60
+ or take a handle and drive it yourself:
61
+
62
+ ```js
63
+ import { createMarquee } from 'wake-marquee'
64
+
65
+ const row = createMarquee(document.querySelector('#logos'), {
66
+ direction: 'left',
67
+ speed: 55,
68
+ wake: 10,
69
+ })
70
+ ```
71
+
72
+ ### Astro
73
+
74
+ ```astro
75
+ ---
76
+ import WakeMarquee from 'wake-marquee/astro'
77
+ ---
78
+ <WakeMarquee speed={55} wake={10} fade="6rem" aria-label="Brands we stock">
79
+ {brands.map((b) => <img src={b.logo} alt={b.name} />)}
80
+ </WakeMarquee>
81
+ ```
82
+
83
+ ### React
84
+
85
+ The library takes an element, so a ref and an effect are the whole integration. No adapter package.
86
+
87
+ ```jsx
88
+ import { useEffect, useRef } from 'react'
89
+ import { createMarquee } from 'wake-marquee'
90
+ import 'wake-marquee/css'
91
+
92
+ function Marquee({ children, ...options }) {
93
+ const ref = useRef(null)
94
+
95
+ useEffect(() => {
96
+ const row = createMarquee(ref.current, options)
97
+ return () => row.destroy()
98
+ // Options are read once at construction; change the key to rebuild.
99
+ }, [])
100
+
101
+ return <div ref={ref} data-wake-marquee>{children}</div>
102
+ }
103
+ ```
104
+
105
+ `destroy()` puts the DOM back exactly as it found it, which is what makes this safe under React 18's double-invoked effects in development.
106
+
107
+ ## What the wake is
108
+
109
+ The row does two things at once.
110
+
111
+ The **loop** is the ordinary part: lanes of content translate sideways by up to one lane width, then snap back. The snap is invisible because it lands exactly one repeat of the content later.
112
+
113
+ The **wake** is the part worth having. The layer holding those lanes is built wider than its container by `wake` percent on each side, and the scroll spends precisely that reserve as the element crosses the viewport:
114
+
115
+ ```
116
+ element enters centre element leaves
117
+ │ │ │
118
+ track ├────┼──────────────────────────┼──────────────────────────┼────┤
119
+ └ overhang overhang ┘
120
+ → → → · ← ← ←
121
+ held back against its travel zero pulled the other way
122
+ ```
123
+
124
+ Because the displacement is bounded by exactly the overhang the track was given, the ends of the row can never come into view, at any scroll position or scroll speed. That bound is a unit test, not a hope.
125
+
126
+ Set `wake: 0` and you have a plain loop. Around `8` is a row that feels attached to the page; past `20` it reads as an effect in its own right.
127
+
128
+ ## The turn
129
+
130
+ When the reader scrolls up, the row turns around. Not by flipping a sign, which looks like a video played backwards, but by easing the direction factor from `+1` to `-1` over `ease` seconds. The row decelerates, stops, and picks up the other way, so it reads as something with mass.
131
+
132
+ The easing is framerate independent, so the same turn takes the same time on a 60 Hz laptop and a 120 Hz phone. (`current += (target - current) * 0.1` per frame, the usual lerp, would be twice as fast on the phone.)
133
+
134
+ Set `reverse: false` to keep a fixed heading and keep the wake.
135
+
136
+ ## Starting without a flash
137
+
138
+ A row has two lives: the static one the server sends, and the running one the
139
+ script builds. The handover between them is where marquees usually give
140
+ themselves away, jumping once or twice before they settle.
141
+
142
+ The library does its part. Measuring, cloning and the first transform all
143
+ happen in one task, so no half-built state is ever painted, and that first
144
+ frame is positioned to land on the same pixel the static row occupied rather
145
+ than wherever the loop's zero point falls. A row reached by anchor link, or a
146
+ reload at a restored scroll position, starts as cleanly as one scrolled to.
147
+
148
+ Two things are left to you, because both change the layout of the static row
149
+ before any script has run:
150
+
151
+ **Declare the spacing in CSS, not only in the attribute.** `data-wake-gap` is
152
+ read by the script, which means the static row is laid out with the default
153
+ `2rem` until then and the items shift when it runs.
154
+
155
+ ```html
156
+ <div data-wake-marquee data-wake-gap="4.5rem" style="--wake-gap:4.5rem">
157
+ ```
158
+
159
+ Same for `data-wake-fade` and `--wake-fade`. The library leaves a property
160
+ that is already set alone, so there is no conflict. `wake-marquee/astro` does
161
+ this for you.
162
+
163
+ **Give images `width` and `height`.** An unsized image changes the lane width
164
+ when it decodes, and the lane width is the loop period. The library restates
165
+ the offset in the new period so the row does not leap, but the items around it
166
+ still move: only the attributes prevent that.
167
+
168
+ ## API
169
+
170
+ ### `createMarquee(element, options?)`
171
+
172
+ Turns an element and its children into a marquee. Returns a handle.
173
+
174
+ | Option | Default | |
175
+ |---|---|---|
176
+ | `direction` | `'left'` | `'left'` or `'right'`, while scrolling down |
177
+ | `speed` | `60` | Travel speed in pixels per second |
178
+ | `wake` | `8` | Scroll displacement, as a percentage of the container width. `0` turns it off |
179
+ | `reverse` | `true` | Whether scrolling up reverses travel |
180
+ | `ease` | `5` | How sharply a reversal settles, per second. Around `1` is a long, heavy turn |
181
+ | `gap` | `2rem` | Space between two items, any CSS length |
182
+ | `fade` | `false` | Soft edges left and right, any CSS length |
183
+ | `pauseOnHover` | `false` | Hold still while a real pointer rests on the row |
184
+ | `scroller` | `window` | What to read the scroll direction from |
185
+ | `respectMotionPreference` | `true` | Stay still under `prefers-reduced-motion: reduce` |
186
+
187
+ The handle:
188
+
189
+ | Member | |
190
+ |---|---|
191
+ | `element` | The element you passed in |
192
+ | `options` | The normalised options |
193
+ | `play()` | Resume. Does nothing under a reduced-motion preference |
194
+ | `pause()` | Hold the row where it is. The wake stops with it |
195
+ | `refresh()` | Re-measure and top up the clones. Call it after changing the items yourself |
196
+ | `destroy()` | Undo everything: clones, wrappers, observers, listeners, attributes |
197
+
198
+ ### `initMarquees({ root?, defaults? })`
199
+
200
+ Finds every `[data-wake-marquee]` under `root` and starts it, reading each element's own attributes for its options. Skips rows that are already running, so it is safe to call again after new content arrives. Returns only the handles it created.
201
+
202
+ ```js
203
+ import { initMarquees } from 'wake-marquee'
204
+
205
+ initMarquees() // the whole document
206
+ initMarquees({ root: panel, defaults: { wake: 4 } }) // scoped, with a house default
207
+ ```
208
+
209
+ `defaults` sits under each element's own attributes, so the markup always wins.
210
+
211
+ ### Attributes
212
+
213
+ Every option except `scroller` and `respectMotionPreference` can be declared in markup, which is what makes `wake-marquee/auto` a complete integration on its own.
214
+
215
+ | Attribute | Option |
216
+ |---|---|
217
+ | `data-wake-marquee` | Marks the container. Also the stylesheet's hook |
218
+ | `data-wake-direction="right"` | `direction` |
219
+ | `data-wake-speed="55"` | `speed` |
220
+ | `data-wake="10"` | `wake` |
221
+ | `data-wake-ease="2.5"` | `ease` |
222
+ | `data-wake-gap="4rem"` | `gap` |
223
+ | `data-wake-fade="6rem"` | `fade` |
224
+ | `data-wake-reverse="false"` | `reverse` |
225
+ | `data-wake-pause-on-hover` | `pauseOnHover`, presence is the value |
226
+
227
+ The library writes two attributes back, for you to hang CSS off:
228
+
229
+ | | |
230
+ |---|---|
231
+ | `data-wake-active` | Present while the row is on screen and running |
232
+ | `data-wake-travel` | `forward` or `reversed`, following the reader |
233
+
234
+ ```css
235
+ [data-wake-travel="reversed"] .item { color: var(--accent); }
236
+ ```
237
+
238
+ ### Without a bundler
239
+
240
+ ```html
241
+ <link rel="stylesheet" href="https://unpkg.com/wake-marquee/dist/wake-marquee.css">
242
+ <script src="https://unpkg.com/wake-marquee/dist/wake-marquee.auto.js" defer></script>
243
+ ```
244
+
245
+ Starts every `[data-wake-marquee]` on the page and leaves the API on `window.WakeMarquee`.
246
+
247
+ ## Styling
248
+
249
+ The library owns the loop geometry and nothing else. Items look however you style them.
250
+
251
+ | Custom property | Default | |
252
+ |---|---|---|
253
+ | `--wake-gap` | `2rem` | Space after each item |
254
+ | `--wake-fade` | — | Width of the soft edge, when `fade` is on |
255
+
256
+ The spacing is a `margin-inline-end` on each item rather than `gap` on the row, and that is load-bearing rather than a style choice: **the lane's width is the loop's repeat distance**, so the space after the last item has to be part of the lane. With `gap`, there is no space between the last item of one lane and the first of the next, and the seam is visible on every pass.
257
+
258
+ All rules live in an `@layer wake-marquee` cascade layer, so your own unlayered CSS wins without `!important`.
259
+
260
+ ### Tailwind CSS v4
261
+
262
+ Declare the layer order before the imports, or the library will outrank every utility:
263
+
264
+ ```css
265
+ @layer theme, base, components, wake-marquee, utilities;
266
+
267
+ @import "tailwindcss";
268
+ @import "wake-marquee/css";
269
+ ```
270
+
271
+ A layer registered late wins, and an `@import` after `tailwindcss` registers `wake-marquee` after `utilities`. Declared up front, it sits where it belongs: your utilities still win.
272
+
273
+ ## Accessibility
274
+
275
+ - **The content is announced once.** Clones are `inert` and `aria-hidden`, so a screen reader hears eight logos, not the forty that fill the track. Where `inert` is missing, focusable elements inside clones get `tabindex="-1"` instead, so they never become invisible tab stops.
276
+ - **Reduced motion is honoured live.** Under `prefers-reduced-motion: reduce` the row does not start, and if the reader turns the preference on while the page is open, it stops and resets. What is left is a static, clipped row: the same thing a reader with JavaScript off sees.
277
+ - **Only `transform` is animated**, on a layer that is promoted while the row is on screen and dropped when it leaves.
278
+ - Give the container an `aria-label` if the row means something (`aria-label="Brands we stock"`), or leave it off if it is decoration.
279
+
280
+ ## Browser support
281
+
282
+ | | |
283
+ |---|---|
284
+ | Chrome / Edge | 99+ |
285
+ | Safari / iOS | 15.4+ |
286
+ | Firefox | 97+ |
287
+
288
+ The floor is cascade layers, which is what `@layer wake-marquee` needs. Everything else the library uses, `IntersectionObserver`, `ResizeObserver` and `matchMedia` change events, shipped well before that.
289
+
290
+ One exception: `inert` reached Firefox in 112. Below that, clones fall back to a `tabindex="-1"` sweep, which keeps them out of the tab order but not out of the accessibility tree.
291
+
292
+ Without JavaScript, or before it arrives, the row is a static line of items clipped at the container edge. That is the intended resting state, not a broken one, and it needs no script.
293
+
294
+ ## Known limitations
295
+
296
+ ### It needs JavaScript
297
+
298
+ There is no build-time or CSS-only version of this, and there cannot be one while the row has to both run on its own and be steered by the scroll. If a completely static row is unacceptable as the no-JS state, this is the wrong library.
299
+
300
+ ### Items are cloned
301
+
302
+ Filling the track means copying the row until it covers the container, typically two to four times. That is cheap for images and text and expensive for anything with its own runtime: iframes, videos, canvases and framework components with state get copied along with everything else. Rows of that kind want a different approach.
303
+
304
+ ### Horizontal, and left-to-right
305
+
306
+ Vertical marquees are not supported. Neither is a right-to-left writing mode: the flex rows follow the writing direction while the transforms are physical, so the two disagree and the loop runs the wrong way. An `rtl` page can still use this inside an `ltr` container.
307
+
308
+ ### One measurement, one period
309
+
310
+ The lane is measured once per resize, and its width is the loop period. Content that changes its own width while the row is running, a counter ticking from 9 to 10, say, drifts the seam until the next `refresh()`. Call it yourself after changing the items.
311
+
312
+ ## How it compares
313
+
314
+ | | Size | Scroll-linked | Needs JS |
315
+ |---|---|---|---|
316
+ | **wake-marquee** | 3.4 kB | yes, direction and displacement | yes |
317
+ | CSS `@keyframes` marquee | 0 | no | no |
318
+ | [Marquee3k](https://github.com/ezekielaquino/Marquee3000) | ~2 kB | no | yes |
319
+ | [react-fast-marquee](https://github.com/justin-chu/react-fast-marquee) | ~4 kB | no | yes, React only |
320
+ | GSAP + ScrollTrigger | ~50 kB | yes, with a full timeline API | yes |
321
+
322
+ If a plain loop is all you need, write the four lines of CSS. If you need a timeline, keyframes and scrubbing across a whole page, use GSAP. If you want a row that answers to the reader and nothing else, use this.
323
+
324
+ ## Licence
325
+
326
+ MIT © Robin Gogolok
@@ -0,0 +1 @@
1
+ var WakeMarquee=(()=>{var l=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var b=i=>{throw TypeError(i)};var Q=(i,e)=>{for(var t in e)l(i,t,{get:e[t],enumerable:!0})},_=(i,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of H(e))!B.call(i,n)&&n!==t&&l(i,n,{get:()=>e[n],enumerable:!(s=D(e,n))||s.enumerable});return i};var G=i=>_(l({},"__esModule",{value:!0}),i);var U=(i,e,t)=>e.has(i)||b("Cannot "+t);var y=(i,e,t)=>e.has(i)?b("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,t);var o=(i,e,t)=>(U(i,e,"access private method"),t);var ie={};Q(ie,{initMarquees:()=>f});function j(i,e,t){return i<e?e:i>t?t:i}function x(i,e,t=1){return!(e>0)||!Number.isFinite(i)?1:Math.ceil(i/e)+1+t}function E(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function M(i,e,t,s){return i+(e-i)*(1-Math.exp(-t*s))}function A(i,e,t){let s=t+e;return s>0?j((t-i)/s,0,1):0}function C(i,e,t){return-t*e*(1-2*i)}function S(i,e=.1){return!Number.isFinite(i)||i<=0?0:Math.min(i/1e3,e)}var K=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),L="data-wake-marquee",V="wake-lane",O="wake-track",Y=1,J="200px 0px";function X(i){return i.firstElementChild?.classList.contains(O)===!0}var u=new Set,p=new Map,m=0,c=0;function q(i){return i===window?window.scrollY:i.scrollTop}function Z(i){let e=p.get(i);if(!e){p.set(i,{last:q(i),sign:1});return}let t=q(i);Math.abs(t-e.last)>.5&&(e.sign=t>e.last?1:-1,e.last=t)}function P(i){return p.get(i)?.sign??1}function F(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function W(i={}){let e={...K};for(let[t,s]of Object.entries(i))s!==void 0&&(e[t]=s);if(e.direction!=="left"&&e.direction!=="right")throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${e.direction}"`);if(!Number.isFinite(e.speed)||e.speed<0)throw new RangeError(`wake-marquee: speed must be a non-negative number, received ${e.speed}`);if(!Number.isFinite(e.wake)||e.wake<0)throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${e.wake}`);if(!Number.isFinite(e.ease)||e.ease<=0)throw new RangeError(`wake-marquee: ease must be a positive number, received ${e.ease}`);return e.scroller=e.scroller??(typeof window>"u"?null:window),e}function ee(i){let e=i.dataset,t={};return e.wakeDirection&&(t.direction=e.wakeDirection),e.wakeSpeed&&(t.speed=Number(e.wakeSpeed)),e.wake&&(t.wake=Number(e.wake)),e.wakeEase&&(t.ease=Number(e.wakeEase)),e.wakeGap&&(t.gap=e.wakeGap),e.wakeFade&&(t.fade=e.wakeFade),e.wakePauseOnHover!==void 0&&(t.pauseOnHover=!0),e.wakeReverse!==void 0&&(t.reverse=e.wakeReverse!=="false"),t}var r,R,N,g,I,v,T,$,k,w=class{constructor(e,t){y(this,r);this.element=e,this.options=W(t),this.dirSign=this.options.direction==="right"?1:-1,this.dirFactor=this.dirSign,this.offset=0,this.period=0,this.wakePx=0,this.amplitude=0,this.aligned=!1,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",o(this,r,R).call(this),o(this,r,I).call(this),u.add(this),o(this,r,v).call(this)}prime(){if(this.destroyed||this.measured)return this;let e=window.innerHeight,t=this.element.getBoundingClientRect(),s=200;return t.bottom<-s||t.top>e+s?this:(this.visible=!0,this.element.toggleAttribute("data-wake-active",!0),o(this,r,g).call(this),this)}refresh(){if(this.destroyed)return;let e=this.lane.getBoundingClientRect().width;if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.period=e,this.measured=!0,o(this,r,N).call(this);let t=x(this.track.getBoundingClientRect().width,e,Y);for(;this.lanes.length<t;){let s=this.lane.cloneNode(!0);s.inert=!0,s.setAttribute("aria-hidden","true"),"inert"in s||s.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(n=>n.setAttribute("tabindex","-1")),s.querySelectorAll('img[loading="lazy"]').forEach(n=>n.setAttribute("loading","eager")),this.track.appendChild(s),this.lanes.push(s)}for(;this.lanes.length>t;)this.lanes.pop()?.remove();d()}read(e){if(!o(this,r,k).call(this))return;let t=this.element.getBoundingClientRect(),s=A(t.top,t.height,e);this.amplitude=this.options.wake*t.width/100,this.wakePx=C(s,this.amplitude,this.dirSign)}write(e,t){if(!o(this,r,k).call(this))return;this.aligned||o(this,r,$).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=M(this.dirFactor,s,e,this.options.ease),this.offset=E(this.offset,this.dirFactor*this.options.speed,e,this.period);let a=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let z of this.lanes)z.style.transform=a;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let h=t===1?"forward":"reversed";h!==this.status&&(this.status=h,this.element.setAttribute("data-wake-travel",h))}play(){return this.destroyed?this:this.options.respectMotionPreference&&F()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,u.delete(this),this.intersection?.disconnect(),this.resize?.disconnect(),this.onEnter&&this.element.removeEventListener("pointerenter",this.onEnter),this.onLeave&&this.element.removeEventListener("pointerleave",this.onLeave),this.motionQuery&&this.onMotionChange&&this.motionQuery.removeEventListener("change",this.onMotionChange);for(let e of this.lanes.slice(1))e.remove();for(;this.lane.firstChild;)this.element.appendChild(this.lane.firstChild);this.track.remove(),this.element.removeAttribute("data-wake-travel"),this.element.removeAttribute("data-wake-active");for(let e of this.undo)e();this.undo=[],this.lanes=[]}}};r=new WeakSet,R=function(){let e=this.element,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=O,this.lane=t.createElement("div"),this.lane.className=V;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(a,h)=>{e.hasAttribute(a)||(e.setAttribute(a,h),this.undo.push(()=>e.removeAttribute(a)))},n=(a,h)=>{e.style.getPropertyValue(a)||(e.style.setProperty(a,h),this.undo.push(()=>e.style.removeProperty(a)))};s(L,""),this.options.gap&&n("--wake-gap",this.options.gap),this.options.fade&&(n("--wake-fade",this.options.fade),s("data-wake-fade",""))},N=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},g=function(){this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,P(this.options.scroller)))},I=function(){let e=this.element;this.intersection=new IntersectionObserver(t=>{for(let s of t)this.visible=s.isIntersecting,e.toggleAttribute("data-wake-active",s.isIntersecting),s.isIntersecting&&!this.measured&&o(this,r,g).call(this);d()},{rootMargin:J,threshold:0}),this.intersection.observe(e),this.resize=new ResizeObserver(()=>{this.measured&&this.refresh()}),this.resize.observe(this.lane),this.resize.observe(e),this.options.pauseOnHover&&matchMedia("(hover: hover)").matches&&(this.onEnter=()=>{this.hovered=!0},this.onLeave=()=>{this.hovered=!1,d()},e.addEventListener("pointerenter",this.onEnter),e.addEventListener("pointerleave",this.onLeave)),this.options.respectMotionPreference&&typeof matchMedia=="function"&&(this.motionQuery=matchMedia("(prefers-reduced-motion: reduce)"),this.onMotionChange=()=>o(this,r,v).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},v=function(){if(!this.options.respectMotionPreference){d();return}F()?(this.paused=!0,o(this,r,T).call(this)):(this.paused=!1,d())},T=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},$=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},k=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function te(i){let e=c===0?0:S(i-c);c=i;let t=window.innerHeight;for(let s of u)s.options.reverse&&Z(s.options.scroller),s.read(t);for(let s of u)s.write(e,s.options.reverse?P(s.options.scroller):1);m=0,d()}function d(){if(m!==0)return;let i=!1;for(let e of u)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){c=0;return}m=requestAnimationFrame(te)}function f({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${L}]`))X(s)||t.push(new w(s,{...e,...ee(s)}));for(let s of t)s.prime();return t}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>f(),{once:!0}):f());return G(ie);})();
@@ -0,0 +1 @@
1
+ /*! wake-marquee | MIT | https://github.com/robin-gogolok/wake-marquee */@layer wake-marquee{[data-wake-marquee]{display:flex;position:relative;overflow:hidden;min-width:0}@supports (overflow: clip){[data-wake-marquee]{overflow-x:clip;overflow-y:visible}}[data-wake-marquee]>:not(.wake-track),.wake-lane>*{flex:none;margin-inline-end:var(--wake-gap, 2rem)}.wake-track{display:flex;flex:none;flex-wrap:nowrap;width:100%}.wake-lane{display:flex;flex:none;flex-wrap:nowrap;align-items:center}[data-wake-active]>.wake-track{will-change:transform}[data-wake-fade]{-webkit-mask-image:linear-gradient(to right,transparent,#000 var(--wake-fade, 0px),#000 calc(100% - var(--wake-fade, 0px)),transparent);mask-image:linear-gradient(to right,transparent,#000 var(--wake-fade, 0px),#000 calc(100% - var(--wake-fade, 0px)),transparent)}@media(prefers-reduced-motion:reduce){[data-wake-active]>.wake-track{will-change:auto}}}
@@ -0,0 +1 @@
1
+ var v=i=>{throw TypeError(i)};var z=(i,e,t)=>e.has(i)||v("Cannot "+t);var k=(i,e,t)=>e.has(i)?v("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,t);var n=(i,e,t)=>(z(i,e,"access private method"),t);function D(i,e,t){return i<e?e:i>t?t:i}function b(i,e,t=1){return!(e>0)||!Number.isFinite(i)?1:Math.ceil(i/e)+1+t}function y(i,e,t,s){return s>0?((i+e*t)%s+s)%s:0}function x(i,e,t,s){return i+(e-i)*(1-Math.exp(-t*s))}function E(i,e,t){let s=t+e;return s>0?D((t-i)/s,0,1):0}function A(i,e,t){return-t*e*(1-2*i)}function M(i,e=.1){return!Number.isFinite(i)||i<=0?0:Math.min(i/1e3,e)}var H=Object.freeze({direction:"left",speed:60,wake:8,reverse:!0,ease:5,gap:null,fade:!1,pauseOnHover:!1,scroller:null,respectMotionPreference:!0}),q="data-wake-marquee",B="wake-lane",F="wake-track",Q=1,_="200px 0px";function P(i){return i.firstElementChild?.classList.contains(F)===!0}var u=new Set,l=new Map,p=0,c=0;function C(i){return i===window?window.scrollY:i.scrollTop}function G(i){let e=l.get(i);if(!e){l.set(i,{last:C(i),sign:1});return}let t=C(i);Math.abs(t-e.last)>.5&&(e.sign=t>e.last?1:-1,e.last=t)}function R(i){return l.get(i)?.sign??1}function S(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function U(i={}){let e={...H};for(let[t,s]of Object.entries(i))s!==void 0&&(e[t]=s);if(e.direction!=="left"&&e.direction!=="right")throw new RangeError(`wake-marquee: direction must be "left" or "right", received "${e.direction}"`);if(!Number.isFinite(e.speed)||e.speed<0)throw new RangeError(`wake-marquee: speed must be a non-negative number, received ${e.speed}`);if(!Number.isFinite(e.wake)||e.wake<0)throw new RangeError(`wake-marquee: wake must be a non-negative number, received ${e.wake}`);if(!Number.isFinite(e.ease)||e.ease<=0)throw new RangeError(`wake-marquee: ease must be a positive number, received ${e.ease}`);return e.scroller=e.scroller??(typeof window>"u"?null:window),e}function j(i){let e=i.dataset,t={};return e.wakeDirection&&(t.direction=e.wakeDirection),e.wakeSpeed&&(t.speed=Number(e.wakeSpeed)),e.wake&&(t.wake=Number(e.wake)),e.wakeEase&&(t.ease=Number(e.wakeEase)),e.wakeGap&&(t.gap=e.wakeGap),e.wakeFade&&(t.fade=e.wakeFade),e.wakePauseOnHover!==void 0&&(t.pauseOnHover=!0),e.wakeReverse!==void 0&&(t.reverse=e.wakeReverse!=="false"),t}var r,L,O,w,N,m,I,T,g,f=class{constructor(e,t){k(this,r);this.element=e,this.options=U(t),this.dirSign=this.options.direction==="right"?1:-1,this.dirFactor=this.dirSign,this.offset=0,this.period=0,this.wakePx=0,this.amplitude=0,this.aligned=!1,this.visible=!1,this.measured=!1,this.paused=!1,this.hovered=!1,this.destroyed=!1,this.status="",n(this,r,L).call(this),n(this,r,N).call(this),u.add(this),n(this,r,m).call(this)}prime(){if(this.destroyed||this.measured)return this;let e=window.innerHeight,t=this.element.getBoundingClientRect(),s=200;return t.bottom<-s||t.top>e+s?this:(this.visible=!0,this.element.toggleAttribute("data-wake-active",!0),n(this,r,w).call(this),this)}refresh(){if(this.destroyed)return;let e=this.lane.getBoundingClientRect().width;if(!(e>0))return;this.period>0&&e!==this.period&&(this.offset=this.offset/this.period*e),this.period=e,this.measured=!0,n(this,r,O).call(this);let t=b(this.track.getBoundingClientRect().width,e,Q);for(;this.lanes.length<t;){let s=this.lane.cloneNode(!0);s.inert=!0,s.setAttribute("aria-hidden","true"),"inert"in s||s.querySelectorAll("a, button, input, select, textarea, [tabindex]").forEach(a=>a.setAttribute("tabindex","-1")),s.querySelectorAll('img[loading="lazy"]').forEach(a=>a.setAttribute("loading","eager")),this.track.appendChild(s),this.lanes.push(s)}for(;this.lanes.length>t;)this.lanes.pop()?.remove();d()}read(e){if(!n(this,r,g).call(this))return;let t=this.element.getBoundingClientRect(),s=E(t.top,t.height,e);this.amplitude=this.options.wake*t.width/100,this.wakePx=A(s,this.amplitude,this.dirSign)}write(e,t){if(!n(this,r,g).call(this))return;this.aligned||n(this,r,T).call(this);let s=this.hovered?0:this.options.reverse?this.dirSign*t:this.dirSign;this.dirFactor=x(this.dirFactor,s,e,this.options.ease),this.offset=y(this.offset,this.dirFactor*this.options.speed,e,this.period);let o=`translate3d(${(this.offset-this.period).toFixed(2)}px, 0, 0)`;for(let $ of this.lanes)$.style.transform=o;this.track.style.transform=`translate3d(${this.wakePx.toFixed(2)}px, 0, 0)`;let h=t===1?"forward":"reversed";h!==this.status&&(this.status=h,this.element.setAttribute("data-wake-travel",h))}play(){return this.destroyed?this:this.options.respectMotionPreference&&S()?this:(this.paused=!1,d(),this)}pause(){return this.paused=!0,this}destroy(){if(!this.destroyed){this.destroyed=!0,u.delete(this),this.intersection?.disconnect(),this.resize?.disconnect(),this.onEnter&&this.element.removeEventListener("pointerenter",this.onEnter),this.onLeave&&this.element.removeEventListener("pointerleave",this.onLeave),this.motionQuery&&this.onMotionChange&&this.motionQuery.removeEventListener("change",this.onMotionChange);for(let e of this.lanes.slice(1))e.remove();for(;this.lane.firstChild;)this.element.appendChild(this.lane.firstChild);this.track.remove(),this.element.removeAttribute("data-wake-travel"),this.element.removeAttribute("data-wake-active");for(let e of this.undo)e();this.undo=[],this.lanes=[]}}};r=new WeakSet,L=function(){let e=this.element,t=e.ownerDocument;for(this.track=t.createElement("div"),this.track.className=F,this.lane=t.createElement("div"),this.lane.className=B;e.firstChild;)this.lane.appendChild(e.firstChild);this.track.appendChild(this.lane),e.appendChild(this.track),this.lanes=[this.lane],this.undo=[];let s=(o,h)=>{e.hasAttribute(o)||(e.setAttribute(o,h),this.undo.push(()=>e.removeAttribute(o)))},a=(o,h)=>{e.style.getPropertyValue(o)||(e.style.setProperty(o,h),this.undo.push(()=>e.style.removeProperty(o)))};s(q,""),this.options.gap&&a("--wake-gap",this.options.gap),this.options.fade&&(a("--wake-fade",this.options.fade),s("data-wake-fade",""))},O=function(){this.track.style.marginInlineStart=`-${this.options.wake}%`,this.track.style.width=`${100+this.options.wake*2}%`},w=function(){this.refresh(),this.period>0&&!this.paused&&(this.read(window.innerHeight),this.write(0,R(this.options.scroller)))},N=function(){let e=this.element;this.intersection=new IntersectionObserver(t=>{for(let s of t)this.visible=s.isIntersecting,e.toggleAttribute("data-wake-active",s.isIntersecting),s.isIntersecting&&!this.measured&&n(this,r,w).call(this);d()},{rootMargin:_,threshold:0}),this.intersection.observe(e),this.resize=new ResizeObserver(()=>{this.measured&&this.refresh()}),this.resize.observe(this.lane),this.resize.observe(e),this.options.pauseOnHover&&matchMedia("(hover: hover)").matches&&(this.onEnter=()=>{this.hovered=!0},this.onLeave=()=>{this.hovered=!1,d()},e.addEventListener("pointerenter",this.onEnter),e.addEventListener("pointerleave",this.onLeave)),this.options.respectMotionPreference&&typeof matchMedia=="function"&&(this.motionQuery=matchMedia("(prefers-reduced-motion: reduce)"),this.onMotionChange=()=>n(this,r,m).call(this),this.motionQuery.addEventListener("change",this.onMotionChange))},m=function(){if(!this.options.respectMotionPreference){d();return}S()?(this.paused=!0,n(this,r,I).call(this)):(this.paused=!1,d())},I=function(){this.offset=0,this.wakePx=0,this.aligned=!1;for(let e of this.lanes)e.style.transform="";this.track.style.transform=""},T=function(){this.aligned=!0;let e=this.amplitude-this.wakePx;this.offset=(e%this.period+this.period)%this.period},g=function(){return!this.destroyed&&!this.paused&&this.visible&&this.period>0};function K(i){let e=c===0?0:M(i-c);c=i;let t=window.innerHeight;for(let s of u)s.options.reverse&&G(s.options.scroller),s.read(t);for(let s of u)s.write(e,s.options.reverse?R(s.options.scroller):1);p=0,d()}function d(){if(p!==0)return;let i=!1;for(let e of u)if(!e.destroyed&&!e.paused&&e.visible){i=!0;break}if(!i){c=0;return}p=requestAnimationFrame(K)}function Z(i,e={}){if(!i||i.nodeType!==1)throw new TypeError("wake-marquee: createMarquee expects an element");if(P(i))throw new Error("wake-marquee: this element is already a marquee, call destroy() first");return new f(i,e).prime()}function W({root:i=document,defaults:e={}}={}){let t=[];for(let s of i.querySelectorAll(`[${q}]`))P(s)||t.push(new f(s,{...e,...j(s)}));for(let s of t)s.prime();return t}export{f as Marquee,Z as createMarquee,H as defaults,W as initMarquees,U as normalizeOptions,j as readOptions};
@@ -0,0 +1,80 @@
1
+ ---
2
+ /**
3
+ * Astro wrapper around wake-marquee.
4
+ *
5
+ * Import the stylesheet once in your layout:
6
+ * import 'wake-marquee/css'
7
+ *
8
+ * Options are rendered as `data-wake-*` attributes and read back by the
9
+ * script, so the server sends a plain clipped row and the client enhances it.
10
+ * The row is on screen and correctly laid out before any JavaScript arrives.
11
+ */
12
+ interface Props {
13
+ /** Travel direction while scrolling down. Scrolling up reverses it. */
14
+ direction?: 'left' | 'right';
15
+ /** Travel speed in pixels per second. */
16
+ speed?: number;
17
+ /** Scroll-driven displacement, in percent of the container width. 0 turns it off. */
18
+ wake?: number;
19
+ /** Whether scrolling up reverses travel. */
20
+ reverse?: boolean;
21
+ /** How sharply a reversal settles, per second. */
22
+ ease?: number;
23
+ /** Space between two items, any CSS length. */
24
+ gap?: string;
25
+ /** Soft edges left and right, any CSS length. */
26
+ fade?: string;
27
+ /** Hold still while a real pointer rests on the row. */
28
+ pauseOnHover?: boolean;
29
+ class?: string;
30
+ 'aria-label'?: string;
31
+ }
32
+
33
+ /**
34
+ * Spacing and fade width are rendered as custom properties here rather than
35
+ * left to the script. They change the layout of the static row, so a value
36
+ * that only arrives with the JavaScript makes the items visibly shift the
37
+ * moment it runs. The data-* attributes carry the same values for the script
38
+ * to read back; it leaves a property that is already set alone.
39
+ */
40
+ const {
41
+ direction,
42
+ speed,
43
+ wake,
44
+ reverse,
45
+ ease,
46
+ gap,
47
+ fade,
48
+ pauseOnHover = false,
49
+ class: className,
50
+ 'aria-label': ariaLabel,
51
+ } = Astro.props;
52
+
53
+ const style = [gap && `--wake-gap:${gap}`, fade && `--wake-fade:${fade}`]
54
+ .filter(Boolean)
55
+ .join(';');
56
+ ---
57
+
58
+ <div
59
+ class={className}
60
+ style={style || undefined}
61
+ data-wake-marquee
62
+ data-wake-direction={direction}
63
+ data-wake-speed={speed?.toString()}
64
+ data-wake={wake?.toString()}
65
+ data-wake-ease={ease?.toString()}
66
+ data-wake-gap={gap}
67
+ data-wake-fade={fade}
68
+ data-wake-reverse={reverse === undefined ? undefined : String(reverse)}
69
+ data-wake-pause-on-hover={pauseOnHover ? '' : undefined}
70
+ aria-label={ariaLabel}
71
+ >
72
+ <slot />
73
+ </div>
74
+
75
+ <script>
76
+ // Astro hoists and deduplicates this, so a page with ten marquees still
77
+ // ships one copy of the library and runs one initialisation pass.
78
+ import { initMarquees } from 'wake-marquee';
79
+ initMarquees();
80
+ </script>
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "wake-marquee",
3
+ "version": "0.1.0",
4
+ "description": "An endless marquee that answers to the scroll: it reverses when the reader scrolls back, and drags against its own direction of travel as it crosses the viewport.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Robin Gogolok (https://robin-gogolok.de)",
8
+ "homepage": "https://robin-gogolok.github.io/wake-marquee/",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/robin-gogolok/wake-marquee.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/robin-gogolok/wake-marquee/issues"
15
+ },
16
+ "keywords": [
17
+ "marquee",
18
+ "ticker",
19
+ "logo-ticker",
20
+ "infinite-scroll",
21
+ "scroll-animation",
22
+ "parallax",
23
+ "scroll-linked",
24
+ "astro",
25
+ "vanilla-js",
26
+ "zero-dependency"
27
+ ],
28
+ "exports": {
29
+ ".": "./src/marquee.js",
30
+ "./css": "./dist/wake-marquee.css",
31
+ "./css/source": "./src/wake-marquee.css",
32
+ "./auto": "./src/auto.js",
33
+ "./motion": "./src/motion.js",
34
+ "./astro": "./integrations/astro/WakeMarquee.astro",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "main": "./src/marquee.js",
38
+ "files": [
39
+ "src",
40
+ "dist",
41
+ "integrations",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE"
45
+ ],
46
+ "sideEffects": [
47
+ "./src/auto.js",
48
+ "**/*.css"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "scripts": {
54
+ "build": "node scripts/build.js && node demo/build.js",
55
+ "test": "npm run test:unit && npm run test:browser",
56
+ "test:unit": "node --test test/*.test.js",
57
+ "pretest:browser": "npm run build",
58
+ "test:browser": "playwright test",
59
+ "serve": "node scripts/serve.js",
60
+ "prepublishOnly": "npm run build && npm run test:unit"
61
+ },
62
+ "devDependencies": {
63
+ "@playwright/test": "^1.50.0",
64
+ "esbuild": "^0.25.0"
65
+ }
66
+ }
package/src/auto.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Side-effect entry point: start every `[data-wake-marquee]` on the page.
3
+ *
4
+ * import 'wake-marquee/auto'
5
+ *
6
+ * Each element is configured by its own `data-wake-*` attributes, so this is
7
+ * the whole integration for a page that has no build step to speak of. Reach
8
+ * for `createMarquee` instead when you want a handle to pause, refresh or
9
+ * tear down.
10
+ *
11
+ * @module wake-marquee/auto
12
+ */
13
+
14
+ import { initMarquees } from './marquee.js';
15
+
16
+ if (typeof document !== 'undefined') {
17
+ if (document.readyState === 'loading') {
18
+ document.addEventListener('DOMContentLoaded', () => initMarquees(), { once: true });
19
+ } else {
20
+ initMarquees();
21
+ }
22
+ }
23
+
24
+ export { initMarquees };