@marcwiest/midday.js 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 +21 -0
- package/README.md +423 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.mjs +194 -0
- package/dist/engine.d.ts +2 -0
- package/dist/headless.d.ts +2 -0
- package/dist/index.d.ts +24 -0
- package/dist/midday.mjs +45 -0
- package/dist/midday.umd.js +1 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.mjs +15 -0
- package/dist/solid.d.ts +21 -0
- package/dist/solid.mjs +21 -0
- package/dist/svelte.d.ts +12 -0
- package/dist/svelte.mjs +15 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils.d.ts +27 -0
- package/dist/vue.d.ts +14 -0
- package/dist/vue.mjs +25 -0
- package/package.json +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Wiest
|
|
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,423 @@
|
|
|
1
|
+
# midday.js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@marcwiest/midday.js)
|
|
4
|
+
[](https://bundlephobia.com/package/@marcwiest/midday.js)
|
|
5
|
+
[](https://github.com/marcwiest/midday.js/actions/workflows/ci.yml)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
A modern, zero-dependency vanilla JS plugin for fixed headers that change style as you scroll through page sections. The spiritual successor to [midnight.js](https://github.com/Aerolab/midnight.js).
|
|
9
|
+
|
|
10
|
+
**~1 kB gzipped** (auto mode) | TypeScript | Framework adapters (React, Vue, Svelte, Solid) included
|
|
11
|
+
|
|
12
|
+
**[Live Demo](https://marcwiest.github.io/midday.js/)**
|
|
13
|
+
|
|
14
|
+
## Background
|
|
15
|
+
|
|
16
|
+
[midnight.js](https://aerolab.github.io/midnight.js/) (2014) introduced a great UI pattern: a fixed header that smoothly transitions between visual styles as page sections scroll beneath it. The transition is a pixel-perfect wipe that follows the section boundary, not an abrupt class swap. midday.js implements the same effect using the browser APIs available today:
|
|
17
|
+
|
|
18
|
+
- **`clip-path: inset()`** for GPU-composited clipping (replaces the nested `overflow: hidden` + opposing `translateY` technique)
|
|
19
|
+
- **`ResizeObserver`** to track section dimensions reactively (replaces interval-based polling)
|
|
20
|
+
- **Scroll-triggered `requestAnimationFrame`** that idles when the user isn't scrolling
|
|
21
|
+
- **`aria-hidden` + `inert`** on cloned variants for screen reader and keyboard accessibility
|
|
22
|
+
- Full **`destroy()` / `refresh()`** lifecycle for clean teardown and dynamic content
|
|
23
|
+
- Zero dependencies, ~1 kB gzipped, TypeScript, framework adapters for React / Vue / Svelte / Solid
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @marcwiest/midday.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or via CDN (UMD):
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script src="https://unpkg.com/@marcwiest/midday.js/dist/midday.umd.js"></script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
### 1. Write one header, mark your sections
|
|
40
|
+
|
|
41
|
+
You write a single header. Each section declares which header style it wants via `data-midday-section`:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<header data-midday>
|
|
45
|
+
<nav>
|
|
46
|
+
<a href="/" class="logo">Logo</a>
|
|
47
|
+
<a href="/about">About</a>
|
|
48
|
+
<a href="/contact">Contact</a>
|
|
49
|
+
</nav>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<section data-midday-section="dark">
|
|
53
|
+
<!-- Dark hero — header should have white text here -->
|
|
54
|
+
</section>
|
|
55
|
+
|
|
56
|
+
<section>
|
|
57
|
+
<!-- No attribute — header uses its default style -->
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section data-midday-section="accent">
|
|
61
|
+
<!-- Purple section — header should match -->
|
|
62
|
+
</section>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Initialize
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { midday } from '@marcwiest/midday.js';
|
|
69
|
+
|
|
70
|
+
const instance = midday(document.querySelector('[data-midday]'));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. What happens next
|
|
74
|
+
|
|
75
|
+
The plugin reads every unique `data-midday-section` value on the page (`"dark"`, `"accent"`) and clones your header once per variant. Each clone is wrapped in a container with a `data-midday-variant` attribute. Your original HTML stays as-is — the clones are created at runtime:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
<header data-midday> ← your element (position: fixed)
|
|
79
|
+
<div data-midday-variant="default"> ← default style (original content)
|
|
80
|
+
<nav>Logo, About, Contact</nav>
|
|
81
|
+
</div>
|
|
82
|
+
<div data-midday-variant="dark"> ← clone for "dark" sections
|
|
83
|
+
<nav>Logo, About, Contact</nav>
|
|
84
|
+
</div>
|
|
85
|
+
<div data-midday-variant="accent"> ← clone for "accent" sections
|
|
86
|
+
<nav>Logo, About, Contact</nav>
|
|
87
|
+
</div>
|
|
88
|
+
</header>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
As you scroll, the plugin shows and hides portions of each clone using `clip-path`, creating a smooth wipe transition at every section boundary.
|
|
92
|
+
|
|
93
|
+
### 4. Style each variant
|
|
94
|
+
|
|
95
|
+
Target variant wrappers with `[data-midday-variant="..."]`. Style them however you want:
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
header {
|
|
99
|
+
position: fixed;
|
|
100
|
+
top: 0;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
z-index: 100;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Shown over sections without data-midday-section */
|
|
107
|
+
[data-midday-variant="default"] {
|
|
108
|
+
background: white;
|
|
109
|
+
color: #111;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Shown over data-midday-section="dark" */
|
|
113
|
+
[data-midday-variant="dark"] {
|
|
114
|
+
background: #111;
|
|
115
|
+
color: white;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Shown over data-midday-section="accent" */
|
|
119
|
+
[data-midday-variant="accent"] {
|
|
120
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
121
|
+
color: white;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
That's it. Scroll through your sections and the header transitions smoothly from one section to another.
|
|
126
|
+
|
|
127
|
+
## API
|
|
128
|
+
|
|
129
|
+
### `midday(header, options?)` — Auto mode
|
|
130
|
+
|
|
131
|
+
Clones your header content once per variant and manages everything. Sections are discovered automatically via `data-midday-section` attributes.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const instance = midday(document.querySelector('[data-midday]'));
|
|
135
|
+
|
|
136
|
+
// With optional onChange callback:
|
|
137
|
+
const instance = midday(document.querySelector('[data-midday]'), {
|
|
138
|
+
onChange: (variants) => console.log(variants),
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `middayHeadless(options)` — Headless mode
|
|
143
|
+
|
|
144
|
+
You provide pre-rendered variant elements. The plugin only manages `clip-path` values. No DOM cloning. Use this when you need **different markup** (not just different styles) per variant.
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { middayHeadless } from '@marcwiest/midday.js';
|
|
148
|
+
|
|
149
|
+
const instance = middayHeadless({
|
|
150
|
+
header: document.querySelector('header'),
|
|
151
|
+
variants: {
|
|
152
|
+
default: document.querySelector('.header-default'),
|
|
153
|
+
dark: document.querySelector('.header-dark'),
|
|
154
|
+
},
|
|
155
|
+
defaultVariant: 'default', // Which key in `variants` is the fallback (optional, defaults to 'default')
|
|
156
|
+
onChange: (variants) => {}, // Optional
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Instance methods
|
|
161
|
+
|
|
162
|
+
Both modes return the same instance:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
instance.refresh(); // Re-scan sections and recalculate (call after DOM changes)
|
|
166
|
+
instance.destroy(); // Full teardown — removes clones, listeners, observers
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `onChange` callback
|
|
170
|
+
|
|
171
|
+
Fires whenever the set of visible variants changes:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
midday(header, {
|
|
175
|
+
onChange: (variants) => {
|
|
176
|
+
// variants: Array<{ name: string, progress: number }>
|
|
177
|
+
// progress: 0–1, how much of the header this variant covers
|
|
178
|
+
console.log(variants);
|
|
179
|
+
// e.g. [{ name: 'dark', progress: 0.7 }, { name: 'default', progress: 0.3 }]
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Framework Adapters
|
|
185
|
+
|
|
186
|
+
Each adapter is a separate tree-shakable entry point (~0.2 kB gzipped). Import only the one you need — the others are never bundled.
|
|
187
|
+
|
|
188
|
+
The adapters wrap **auto mode** — your component renders a single header element, and cloning happens client-side on mount. This means your server-rendered HTML always contains just one header, keeping SEO clean (see [SSR & SEO](#ssr--seo) below).
|
|
189
|
+
|
|
190
|
+
### React
|
|
191
|
+
|
|
192
|
+
```jsx
|
|
193
|
+
import { useRef } from 'react';
|
|
194
|
+
import { useMidday } from '@marcwiest/midday.js/react';
|
|
195
|
+
|
|
196
|
+
function Header() {
|
|
197
|
+
const headerRef = useRef(null);
|
|
198
|
+
useMidday(headerRef);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<header ref={headerRef} style={{ position: 'fixed', top: 0, left: 0, right: 0 }}>
|
|
202
|
+
<Nav />
|
|
203
|
+
</header>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Vue
|
|
209
|
+
|
|
210
|
+
Composable:
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<script setup>
|
|
214
|
+
import { ref } from 'vue';
|
|
215
|
+
import { useMidday } from '@marcwiest/midday.js/vue';
|
|
216
|
+
|
|
217
|
+
const headerRef = ref(null);
|
|
218
|
+
useMidday(headerRef);
|
|
219
|
+
</script>
|
|
220
|
+
|
|
221
|
+
<template>
|
|
222
|
+
<header ref="headerRef">
|
|
223
|
+
<Nav />
|
|
224
|
+
</header>
|
|
225
|
+
</template>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or as a directive (import as `vMidday` for auto-registration in `<script setup>`):
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<script setup>
|
|
232
|
+
import { vMidday } from '@marcwiest/midday.js/vue';
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<template>
|
|
236
|
+
<header v-midday>
|
|
237
|
+
<Nav />
|
|
238
|
+
</header>
|
|
239
|
+
</template>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Svelte
|
|
243
|
+
|
|
244
|
+
```html
|
|
245
|
+
<script>
|
|
246
|
+
import { midday } from '@marcwiest/midday.js/svelte';
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
<header use:midday>
|
|
250
|
+
<Nav />
|
|
251
|
+
</header>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Solid
|
|
255
|
+
|
|
256
|
+
Primitive:
|
|
257
|
+
|
|
258
|
+
```jsx
|
|
259
|
+
import { createMidday } from '@marcwiest/midday.js/solid';
|
|
260
|
+
|
|
261
|
+
function Header() {
|
|
262
|
+
let headerEl;
|
|
263
|
+
createMidday(() => headerEl);
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<header ref={headerEl} style={{ position: 'fixed', top: '0', left: '0', right: '0' }}>
|
|
267
|
+
<Nav />
|
|
268
|
+
</header>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Or as a directive:
|
|
274
|
+
|
|
275
|
+
```jsx
|
|
276
|
+
import { midday } from '@marcwiest/midday.js/solid';
|
|
277
|
+
|
|
278
|
+
function Header() {
|
|
279
|
+
return (
|
|
280
|
+
<header use:midday style={{ position: 'fixed', top: '0', left: '0', right: '0' }}>
|
|
281
|
+
<Nav />
|
|
282
|
+
</header>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Passing options
|
|
288
|
+
|
|
289
|
+
All adapters accept `onChange`:
|
|
290
|
+
|
|
291
|
+
```jsx
|
|
292
|
+
// React
|
|
293
|
+
useMidday(headerRef, { onChange: (v) => console.log(v) });
|
|
294
|
+
|
|
295
|
+
// Vue (composable)
|
|
296
|
+
useMidday(headerRef, { onChange: (v) => console.log(v) });
|
|
297
|
+
|
|
298
|
+
// Vue (directive)
|
|
299
|
+
<header v-midday="{ onChange: (v) => console.log(v) }"></header>
|
|
300
|
+
|
|
301
|
+
// Svelte
|
|
302
|
+
<header use:midday={{ onChange: (v) => console.log(v) }}></header>
|
|
303
|
+
|
|
304
|
+
// Solid (primitive)
|
|
305
|
+
createMidday(() => headerEl, { onChange: (v) => console.log(v) });
|
|
306
|
+
|
|
307
|
+
// Solid (directive)
|
|
308
|
+
<header use:midday={{ onChange: (v) => console.log(v) }}></header>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Multiple Instances
|
|
312
|
+
|
|
313
|
+
midday.js supports multiple independent fixed elements on the same page (e.g., a top header and a bottom app-bar). Name each instance via the `data-midday` attribute and use `data-midday-target` on sections to control which instance they affect.
|
|
314
|
+
|
|
315
|
+
```html
|
|
316
|
+
<header data-midday="top">...</header>
|
|
317
|
+
<nav class="app-bar" data-midday="bottom">...</nav>
|
|
318
|
+
|
|
319
|
+
<!-- Targets only the top header -->
|
|
320
|
+
<section data-midday-section="accent" data-midday-target="top">...</section>
|
|
321
|
+
|
|
322
|
+
<!-- Targets both (space-separated) -->
|
|
323
|
+
<section data-midday-section="inverted" data-midday-target="top bottom">...</section>
|
|
324
|
+
|
|
325
|
+
<!-- No target — applies to ALL instances -->
|
|
326
|
+
<section data-midday-section="dark">...</section>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
import { midday } from '@marcwiest/midday.js';
|
|
331
|
+
|
|
332
|
+
const top = midday(document.querySelector('[data-midday="top"]'));
|
|
333
|
+
const bottom = midday(document.querySelector('[data-midday="bottom"]'));
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Each instance runs its own engine and only reacts to its own sections. The instance name defaults to the element's `data-midday` attribute value, or you can set it explicitly via `options.name`.
|
|
337
|
+
|
|
338
|
+
## SSR & SEO
|
|
339
|
+
|
|
340
|
+
midday.js is designed to be SSR-safe by default.
|
|
341
|
+
|
|
342
|
+
**Auto mode** (including all framework adapters) clones header content client-side on mount. The server-rendered HTML always contains a single, clean header element — no duplicate navigation links, no hidden clones. Search engine crawlers see exactly one header with one set of nav links.
|
|
343
|
+
|
|
344
|
+
After hydration, the plugin creates variant clones in the browser. These clones are marked with `aria-hidden="true"` and `inert`, so they're invisible to screen readers and excluded from keyboard navigation. The original header content remains the accessible version.
|
|
345
|
+
|
|
346
|
+
**Headless mode** is different — since you provide the variant elements yourself, they exist in your markup. If you're using headless mode with SSR, render non-default variants client-side only to avoid duplicate content in the server HTML:
|
|
347
|
+
|
|
348
|
+
```jsx
|
|
349
|
+
// React (headless + SSR)
|
|
350
|
+
import { useState, useEffect } from 'react';
|
|
351
|
+
import { middayHeadless } from '@marcwiest/midday.js';
|
|
352
|
+
|
|
353
|
+
function Header() {
|
|
354
|
+
const [mounted, setMounted] = useState(false);
|
|
355
|
+
const headerRef = useRef(null);
|
|
356
|
+
const defaultRef = useRef(null);
|
|
357
|
+
const darkRef = useRef(null);
|
|
358
|
+
|
|
359
|
+
useEffect(() => setMounted(true), []);
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (!mounted || !headerRef.current) return;
|
|
363
|
+
const instance = middayHeadless({
|
|
364
|
+
header: headerRef.current,
|
|
365
|
+
variants: { default: defaultRef.current, dark: darkRef.current },
|
|
366
|
+
});
|
|
367
|
+
return () => instance.destroy();
|
|
368
|
+
}, [mounted]);
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<header ref={headerRef}>
|
|
372
|
+
<div ref={defaultRef} className="header-default"><Nav /></div>
|
|
373
|
+
{mounted && (
|
|
374
|
+
<div ref={darkRef} className="header-dark" aria-hidden="true" inert="">
|
|
375
|
+
<Nav />
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</header>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For most use cases, the framework adapters (which use auto mode) are simpler and SSR-safe without any extra work.
|
|
384
|
+
|
|
385
|
+
## How It Works
|
|
386
|
+
|
|
387
|
+
midday.js uses `clip-path: inset()` to reveal and hide variant header elements as sections scroll past.
|
|
388
|
+
|
|
389
|
+
1. **Auto mode** clones the header content once per unique variant found in `data-midday-section` attributes. Each clone is wrapped in an absolutely-positioned container inside the header element.
|
|
390
|
+
|
|
391
|
+
2. On each scroll frame, the plugin calculates which sections overlap the header's viewport position and by how many pixels.
|
|
392
|
+
|
|
393
|
+
3. Each variant's container gets a `clip-path: inset(topPx 0 bottomPx 0)` that reveals exactly the portion corresponding to its section's overlap with the header. Every variant — including the default — is clipped to only its own region. Nothing is used as a backdrop, so transparent backgrounds work fine.
|
|
394
|
+
|
|
395
|
+
The result is a pixel-perfect wipe transition at every section boundary.
|
|
396
|
+
|
|
397
|
+
## Styling Guide
|
|
398
|
+
|
|
399
|
+
- The header element should be `position: fixed` or `position: sticky`
|
|
400
|
+
- In auto mode, variant wrappers get `data-midday-variant="<name>"` — target them with `[data-midday-variant="dark"]` or however you prefer
|
|
401
|
+
- Transparent variant backgrounds work — each variant is clipped independently, so page content shows through where intended
|
|
402
|
+
- In headless mode, you're responsible for positioning variant elements absolutely within the header and setting `aria-hidden`/`inert` on non-default variants
|
|
403
|
+
- In headless mode, variant elements can have different heights than the header. The clip-path will track section boundaries exactly, with extra height revealing only when the section extends past the header edge
|
|
404
|
+
|
|
405
|
+
## Browser Support
|
|
406
|
+
|
|
407
|
+
Requires `clip-path: inset()` (97%+ global support), `ResizeObserver` (97%+), and `requestAnimationFrame`. Works in all modern browsers. No IE support.
|
|
408
|
+
|
|
409
|
+
## Development
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
pnpm install # Install dependencies
|
|
413
|
+
pnpm dev # Vite dev server (serves demo/ with HMR)
|
|
414
|
+
pnpm build # Full build: ESM + UMD + .d.ts
|
|
415
|
+
pnpm test # Run tests once
|
|
416
|
+
pnpm test:watch # Watch mode
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Tests use [Vitest](https://vitest.dev/) + [happy-dom](https://github.com/nicedayfor/happy-dom). Global mocks for `ResizeObserver` and `requestAnimationFrame` are in `tests/setup.ts` — see the comments there for details on the mock strategy.
|
|
420
|
+
|
|
421
|
+
## License
|
|
422
|
+
|
|
423
|
+
MIT
|
package/dist/core.d.ts
ADDED
package/dist/core.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
function k(t) {
|
|
2
|
+
const i = t.getBoundingClientRect();
|
|
3
|
+
return {
|
|
4
|
+
top: i.top + window.scrollY,
|
|
5
|
+
height: i.height
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function z(t) {
|
|
9
|
+
const i = t.getBoundingClientRect();
|
|
10
|
+
return {
|
|
11
|
+
top: i.top,
|
|
12
|
+
height: i.height
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function P(t) {
|
|
16
|
+
for (const i of t) {
|
|
17
|
+
const l = k(i.el);
|
|
18
|
+
i.top = l.top, i.height = l.height;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const G = "[data-midday-section]", K = "data-midday-section", J = "data-midday-target";
|
|
22
|
+
function Y(t) {
|
|
23
|
+
const i = document.querySelectorAll(G), l = [];
|
|
24
|
+
for (const s of i) {
|
|
25
|
+
const d = s.getAttribute(K);
|
|
26
|
+
if (!d) continue;
|
|
27
|
+
const v = s.getAttribute(J);
|
|
28
|
+
v && (!t || !v.split(" ").includes(t)) || l.push({ el: s, variant: d, top: 0, height: 0 });
|
|
29
|
+
}
|
|
30
|
+
return P(l), l;
|
|
31
|
+
}
|
|
32
|
+
function Q(t) {
|
|
33
|
+
let { header: i, variants: l, sections: s } = t;
|
|
34
|
+
const { defaultName: d, onChange: v } = t;
|
|
35
|
+
let a = null, p = !1, E = "", c = null;
|
|
36
|
+
function L() {
|
|
37
|
+
g();
|
|
38
|
+
}
|
|
39
|
+
function S() {
|
|
40
|
+
P(s), g();
|
|
41
|
+
}
|
|
42
|
+
function g() {
|
|
43
|
+
p || (p = !0, a = requestAnimationFrame(r));
|
|
44
|
+
}
|
|
45
|
+
function r() {
|
|
46
|
+
p = !1, h();
|
|
47
|
+
}
|
|
48
|
+
function h() {
|
|
49
|
+
const C = z(i), o = C.height;
|
|
50
|
+
if (o <= 0) return;
|
|
51
|
+
const W = window.scrollY, F = C.top, q = F + o, R = [], _ = /* @__PURE__ */ new Map();
|
|
52
|
+
let N = o, $ = 0, j = 0;
|
|
53
|
+
const U = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const e of s) {
|
|
55
|
+
const T = e.top - W, m = T + e.height, M = Math.max(F, T), u = Math.min(q, m), B = Math.max(0, u - M);
|
|
56
|
+
if (B > 0) {
|
|
57
|
+
j += B;
|
|
58
|
+
const A = M - F, x = q - u;
|
|
59
|
+
N = Math.min(N, A), $ = Math.max($, o - x);
|
|
60
|
+
const I = _.get(e.variant);
|
|
61
|
+
I ? (I.topInset = Math.min(I.topInset, A), I.bottomInset = Math.min(I.bottomInset, x)) : _.set(e.variant, { topInset: A, bottomInset: x });
|
|
62
|
+
}
|
|
63
|
+
let f = U.get(e.variant);
|
|
64
|
+
f || (f = [], U.set(e.variant, f)), f.push({ viewTop: T, viewBottom: m });
|
|
65
|
+
}
|
|
66
|
+
let b = null;
|
|
67
|
+
for (const e of l) {
|
|
68
|
+
if (e.name === d) {
|
|
69
|
+
b = e.wrapper;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const T = U.get(e.name);
|
|
73
|
+
if (!T) {
|
|
74
|
+
e.wrapper.style.clipPath = "inset(0 0 100% 0)";
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const m = e.wrapper.getBoundingClientRect(), M = m.height || o;
|
|
78
|
+
let u = M, B = M;
|
|
79
|
+
for (const f of T) {
|
|
80
|
+
const A = Math.max(m.top, f.viewTop), x = Math.min(m.bottom, f.viewBottom);
|
|
81
|
+
x <= A || (u = Math.min(u, A - m.top), B = Math.min(B, m.bottom - x));
|
|
82
|
+
}
|
|
83
|
+
if (u + B < M) {
|
|
84
|
+
e.wrapper.style.clipPath = `inset(${u}px 0 ${B}px 0)`;
|
|
85
|
+
const f = _.get(e.name), A = f ? (o - f.topInset - f.bottomInset) / o : 0;
|
|
86
|
+
R.push({ name: e.name, progress: A });
|
|
87
|
+
} else
|
|
88
|
+
e.wrapper.style.clipPath = "inset(0 0 100% 0)";
|
|
89
|
+
}
|
|
90
|
+
if (b) {
|
|
91
|
+
const e = b.getBoundingClientRect().height || o;
|
|
92
|
+
if (j >= o)
|
|
93
|
+
b.style.clipPath = "inset(0 0 100% 0)";
|
|
94
|
+
else if (j <= 0)
|
|
95
|
+
b.style.clipPath = "inset(0)", R.unshift({
|
|
96
|
+
name: d,
|
|
97
|
+
progress: 1
|
|
98
|
+
});
|
|
99
|
+
else {
|
|
100
|
+
const T = N;
|
|
101
|
+
if (o - $ >= T) {
|
|
102
|
+
const u = e * ($ / o);
|
|
103
|
+
b.style.clipPath = `inset(${u}px 0 0 0)`;
|
|
104
|
+
} else {
|
|
105
|
+
const u = e * ((o - N) / o);
|
|
106
|
+
b.style.clipPath = `inset(0 0 ${u}px 0)`;
|
|
107
|
+
}
|
|
108
|
+
const M = o - j;
|
|
109
|
+
R.unshift({
|
|
110
|
+
name: d,
|
|
111
|
+
progress: M / o
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const D = R.map((e) => `${e.name}:${e.progress.toFixed(3)}`).join("|");
|
|
116
|
+
D !== E && (E = D, v == null || v(R));
|
|
117
|
+
}
|
|
118
|
+
function w() {
|
|
119
|
+
c == null || c.disconnect(), c = new ResizeObserver(() => {
|
|
120
|
+
P(s), g();
|
|
121
|
+
});
|
|
122
|
+
for (const C of s)
|
|
123
|
+
c.observe(C.el);
|
|
124
|
+
}
|
|
125
|
+
function n() {
|
|
126
|
+
window.addEventListener("scroll", L, { passive: !0 }), window.addEventListener("resize", S, { passive: !0 }), w(), g();
|
|
127
|
+
}
|
|
128
|
+
function V() {
|
|
129
|
+
P(s), g();
|
|
130
|
+
}
|
|
131
|
+
function H(C, o) {
|
|
132
|
+
l = C, s = o, P(s), w(), g();
|
|
133
|
+
}
|
|
134
|
+
function y() {
|
|
135
|
+
a !== null && cancelAnimationFrame(a), window.removeEventListener("scroll", L), window.removeEventListener("resize", S), c == null || c.disconnect(), c = null;
|
|
136
|
+
}
|
|
137
|
+
return n(), { recalculate: V, update: H, destroy: y };
|
|
138
|
+
}
|
|
139
|
+
const X = "data-midday-variant", O = "default";
|
|
140
|
+
function Z(t, i = {}) {
|
|
141
|
+
const { onChange: l } = i, s = i.name ?? (t.getAttribute("data-midday") || void 0), d = t.innerHTML, v = t.style.overflow;
|
|
142
|
+
let a = null, p = [];
|
|
143
|
+
function E() {
|
|
144
|
+
const r = Y(s), h = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const y of r)
|
|
146
|
+
h.add(y.variant);
|
|
147
|
+
t.style.overflow = "visible";
|
|
148
|
+
const w = document.createDocumentFragment();
|
|
149
|
+
for (; t.firstChild; )
|
|
150
|
+
w.appendChild(t.firstChild);
|
|
151
|
+
const n = [];
|
|
152
|
+
for (const y of h)
|
|
153
|
+
n.push(c(y, w, !0));
|
|
154
|
+
const V = c(O, w, !1);
|
|
155
|
+
t.appendChild(V.wrapper);
|
|
156
|
+
const H = [V];
|
|
157
|
+
for (const y of n)
|
|
158
|
+
t.appendChild(y.wrapper), H.push(y);
|
|
159
|
+
return H;
|
|
160
|
+
}
|
|
161
|
+
function c(r, h, w) {
|
|
162
|
+
const n = document.createElement("div");
|
|
163
|
+
return n.setAttribute(X, r), n.style.position = "absolute", n.style.top = "0", n.style.left = "0", n.style.right = "0", n.style.bottom = "0", n.style.willChange = "clip-path", n.style.clipPath = "inset(0 0 100% 0)", w ? (n.setAttribute("aria-hidden", "true"), n.setAttribute("inert", ""), n.style.pointerEvents = "none", n.appendChild(h.cloneNode(!0))) : n.appendChild(h), { wrapper: n, name: r };
|
|
164
|
+
}
|
|
165
|
+
function L() {
|
|
166
|
+
const r = Y(s);
|
|
167
|
+
p = E(), a = Q({
|
|
168
|
+
header: t,
|
|
169
|
+
variants: p,
|
|
170
|
+
defaultName: O,
|
|
171
|
+
sections: r,
|
|
172
|
+
onChange: l
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function S() {
|
|
176
|
+
for (const h of p)
|
|
177
|
+
h.wrapper.remove();
|
|
178
|
+
t.innerHTML = d;
|
|
179
|
+
const r = Y(s);
|
|
180
|
+
p = E(), a == null || a.update(p, r);
|
|
181
|
+
}
|
|
182
|
+
function g() {
|
|
183
|
+
a == null || a.destroy(), a = null;
|
|
184
|
+
for (const r of p)
|
|
185
|
+
r.wrapper.remove();
|
|
186
|
+
p = [], t.innerHTML = d, t.style.overflow = v;
|
|
187
|
+
}
|
|
188
|
+
return L(), { refresh: S, destroy: g };
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
Z as a,
|
|
192
|
+
Q as c,
|
|
193
|
+
Y as s
|
|
194
|
+
};
|
package/dist/engine.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MiddayOptions, MiddayHeadlessOptions, MiddayInstance, ActiveVariant } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Auto mode — Initialize midday.js on a fixed header element.
|
|
4
|
+
* Automatically clones header content for each variant and manages the DOM.
|
|
5
|
+
*
|
|
6
|
+
* @param header - The fixed/sticky header element
|
|
7
|
+
* @param options - Optional configuration
|
|
8
|
+
* @returns Instance with refresh() and destroy() methods
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```js
|
|
12
|
+
* const instance = midday(document.querySelector('[data-midday]'));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function midday(header: HTMLElement, options?: MiddayOptions): MiddayInstance;
|
|
16
|
+
/**
|
|
17
|
+
* Headless mode — Bring your own variant elements.
|
|
18
|
+
* Manages only clip-paths on pre-rendered elements. No DOM cloning.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Header, variant elements, and optional config
|
|
21
|
+
* @returns Instance with refresh() and destroy() methods
|
|
22
|
+
*/
|
|
23
|
+
export declare function middayHeadless(options: MiddayHeadlessOptions): MiddayInstance;
|
|
24
|
+
export type { MiddayOptions, MiddayHeadlessOptions, MiddayInstance, ActiveVariant };
|
package/dist/midday.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { s as i, c as y, a as p } from "./core.mjs";
|
|
2
|
+
function h(a) {
|
|
3
|
+
const {
|
|
4
|
+
header: e,
|
|
5
|
+
variants: c,
|
|
6
|
+
defaultVariant: o = "default",
|
|
7
|
+
name: r,
|
|
8
|
+
onChange: d
|
|
9
|
+
} = a;
|
|
10
|
+
let t = null;
|
|
11
|
+
function s() {
|
|
12
|
+
return Object.entries(c).map(([n, m]) => ({
|
|
13
|
+
name: n,
|
|
14
|
+
wrapper: m
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function u() {
|
|
18
|
+
const n = i(r);
|
|
19
|
+
t = y({
|
|
20
|
+
header: e,
|
|
21
|
+
variants: s(),
|
|
22
|
+
defaultName: o,
|
|
23
|
+
sections: n,
|
|
24
|
+
onChange: d
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function f() {
|
|
28
|
+
const n = i(r);
|
|
29
|
+
t == null || t.update(s(), n);
|
|
30
|
+
}
|
|
31
|
+
function l() {
|
|
32
|
+
t == null || t.destroy(), t = null;
|
|
33
|
+
}
|
|
34
|
+
return u(), { refresh: f, destroy: l };
|
|
35
|
+
}
|
|
36
|
+
function M(a, e) {
|
|
37
|
+
return p(a, e);
|
|
38
|
+
}
|
|
39
|
+
function H(a) {
|
|
40
|
+
return h(a);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
M as midday,
|
|
44
|
+
H as middayHeadless
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(C,I){typeof exports=="object"&&typeof module<"u"?I(exports):typeof define=="function"&&define.amd?define(["exports"],I):(C=typeof globalThis<"u"?globalThis:C||self,I(C.midday={}))})(this,(function(C){"use strict";function I(t){const o=t.getBoundingClientRect();return{top:o.top+window.scrollY,height:o.height}}function G(t){const o=t.getBoundingClientRect();return{top:o.top,height:o.height}}function V(t){for(const o of t){const l=I(o.el);o.top=l.top,o.height=l.height}}const K="[data-midday-section]",J="data-midday-section",Q="data-midday-target";function H(t){const o=document.querySelectorAll(K),l=[];for(const s of o){const u=s.getAttribute(J);if(!u)continue;const m=s.getAttribute(Q);m&&(!t||!m.split(" ").includes(t))||l.push({el:s,variant:u,top:0,height:0})}return V(l),l}function D(t){let{header:o,variants:l,sections:s}=t;const{defaultName:u,onChange:m}=t;let n=null,c=!1,B="",r=null;function R(){v()}function h(){V(s),v()}function v(){c||(c=!0,n=requestAnimationFrame(d))}function d(){c=!1,g()}function g(){const P=G(o),a=P.height;if(a<=0)return;const ot=window.scrollY,U=P.top,k=U+a,L=[],Y=new Map;let F=a,_=0,O=0;const q=new Map;for(const e of s){const M=e.top-ot,w=M+e.height,b=Math.max(U,M),p=Math.min(k,w),E=Math.max(0,p-b);if(E>0){O+=E;const A=b-U,S=k-p;F=Math.min(F,A),_=Math.max(_,a-S);const N=Y.get(e.variant);N?(N.topInset=Math.min(N.topInset,A),N.bottomInset=Math.min(N.bottomInset,S)):Y.set(e.variant,{topInset:A,bottomInset:S})}let f=q.get(e.variant);f||(f=[],q.set(e.variant,f)),f.push({viewTop:M,viewBottom:w})}let x=null;for(const e of l){if(e.name===u){x=e.wrapper;continue}const M=q.get(e.name);if(!M){e.wrapper.style.clipPath="inset(0 0 100% 0)";continue}const w=e.wrapper.getBoundingClientRect(),b=w.height||a;let p=b,E=b;for(const f of M){const A=Math.max(w.top,f.viewTop),S=Math.min(w.bottom,f.viewBottom);S<=A||(p=Math.min(p,A-w.top),E=Math.min(E,w.bottom-S))}if(p+E<b){e.wrapper.style.clipPath=`inset(${p}px 0 ${E}px 0)`;const f=Y.get(e.name),A=f?(a-f.topInset-f.bottomInset)/a:0;L.push({name:e.name,progress:A})}else e.wrapper.style.clipPath="inset(0 0 100% 0)"}if(x){const e=x.getBoundingClientRect().height||a;if(O>=a)x.style.clipPath="inset(0 0 100% 0)";else if(O<=0)x.style.clipPath="inset(0)",L.unshift({name:u,progress:1});else{const M=F;if(a-_>=M){const p=e*(_/a);x.style.clipPath=`inset(${p}px 0 0 0)`}else{const p=e*((a-F)/a);x.style.clipPath=`inset(0 0 ${p}px 0)`}const b=a-O;L.unshift({name:u,progress:b/a})}}const z=L.map(e=>`${e.name}:${e.progress.toFixed(3)}`).join("|");z!==B&&(B=z,m==null||m(L))}function y(){r==null||r.disconnect(),r=new ResizeObserver(()=>{V(s),v()});for(const P of s)r.observe(P.el)}function i(){window.addEventListener("scroll",R,{passive:!0}),window.addEventListener("resize",h,{passive:!0}),y(),v()}function j(){V(s),v()}function $(P,a){l=P,s=a,V(s),y(),v()}function T(){n!==null&&cancelAnimationFrame(n),window.removeEventListener("scroll",R),window.removeEventListener("resize",h),r==null||r.disconnect(),r=null}return i(),{recalculate:j,update:$,destroy:T}}const X="data-midday-variant",W="default";function Z(t,o={}){const{onChange:l}=o,s=o.name??(t.getAttribute("data-midday")||void 0),u=t.innerHTML,m=t.style.overflow;let n=null,c=[];function B(){const d=H(s),g=new Set;for(const T of d)g.add(T.variant);t.style.overflow="visible";const y=document.createDocumentFragment();for(;t.firstChild;)y.appendChild(t.firstChild);const i=[];for(const T of g)i.push(r(T,y,!0));const j=r(W,y,!1);t.appendChild(j.wrapper);const $=[j];for(const T of i)t.appendChild(T.wrapper),$.push(T);return $}function r(d,g,y){const i=document.createElement("div");return i.setAttribute(X,d),i.style.position="absolute",i.style.top="0",i.style.left="0",i.style.right="0",i.style.bottom="0",i.style.willChange="clip-path",i.style.clipPath="inset(0 0 100% 0)",y?(i.setAttribute("aria-hidden","true"),i.setAttribute("inert",""),i.style.pointerEvents="none",i.appendChild(g.cloneNode(!0))):i.appendChild(g),{wrapper:i,name:d}}function R(){const d=H(s);c=B(),n=D({header:t,variants:c,defaultName:W,sections:d,onChange:l})}function h(){for(const g of c)g.wrapper.remove();t.innerHTML=u;const d=H(s);c=B(),n==null||n.update(c,d)}function v(){n==null||n.destroy(),n=null;for(const d of c)d.wrapper.remove();c=[],t.innerHTML=u,t.style.overflow=m}return R(),{refresh:h,destroy:v}}function tt(t){const{header:o,variants:l,defaultVariant:s="default",name:u,onChange:m}=t;let n=null;function c(){return Object.entries(l).map(([h,v])=>({name:h,wrapper:v}))}function B(){const h=H(u);n=D({header:o,variants:c(),defaultName:s,sections:h,onChange:m})}function r(){const h=H(u);n==null||n.update(c(),h)}function R(){n==null||n.destroy(),n=null}return B(),{refresh:r,destroy:R}}function et(t,o){return Z(t,o)}function nt(t){return tt(t)}C.midday=et,C.middayHeadless=nt,Object.defineProperty(C,Symbol.toStringTag,{value:"Module"})}));
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MiddayOptions, MiddayInstance } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* React hook for midday.js (auto mode).
|
|
4
|
+
* Initializes on mount, destroys on unmount.
|
|
5
|
+
* Cloning happens client-side — safe for SSR.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useMidday(headerRef: React.RefObject<HTMLElement | null>, options?: MiddayOptions): React.RefObject<MiddayInstance | null>;
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRef as u, useEffect as c } from "react";
|
|
2
|
+
import { a as o } from "./core.mjs";
|
|
3
|
+
function s(t, n) {
|
|
4
|
+
const r = u(null);
|
|
5
|
+
return c(() => {
|
|
6
|
+
if (t.current)
|
|
7
|
+
return r.current = o(t.current, n), () => {
|
|
8
|
+
var e;
|
|
9
|
+
(e = r.current) == null || e.destroy(), r.current = null;
|
|
10
|
+
};
|
|
11
|
+
}, []), r;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
s as useMidday
|
|
15
|
+
};
|
package/dist/solid.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MiddayOptions, MiddayInstance } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Solid primitive for midday.js (auto mode).
|
|
4
|
+
* Initializes on mount, cleans up on disposal.
|
|
5
|
+
* Cloning happens client-side — safe for SSR.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createMidday(headerAccessor: () => HTMLElement | null, options?: MiddayOptions): () => MiddayInstance | null;
|
|
8
|
+
/**
|
|
9
|
+
* Solid directive for midday.js (auto mode).
|
|
10
|
+
* Usage: <header use:midday> or <header use:midday={{ onChange }}>
|
|
11
|
+
*
|
|
12
|
+
* TypeScript: extend JSX.DirectiveFunctions in your app:
|
|
13
|
+
* declare module "solid-js" {
|
|
14
|
+
* namespace JSX {
|
|
15
|
+
* interface DirectiveFunctions {
|
|
16
|
+
* midday: typeof import('midday.js/solid').midday;
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export declare function midday(el: HTMLElement, accessor: () => MiddayOptions | undefined): void;
|
package/dist/solid.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { onMount as d, onCleanup as e } from "solid-js";
|
|
2
|
+
import { a } from "./core.mjs";
|
|
3
|
+
function c(n, o) {
|
|
4
|
+
let t = null;
|
|
5
|
+
return d(() => {
|
|
6
|
+
const r = n();
|
|
7
|
+
r && (t = a(r, o));
|
|
8
|
+
}), e(() => {
|
|
9
|
+
t == null || t.destroy(), t = null;
|
|
10
|
+
}), () => t;
|
|
11
|
+
}
|
|
12
|
+
function l(n, o) {
|
|
13
|
+
const t = a(n, o());
|
|
14
|
+
e(() => {
|
|
15
|
+
t.destroy();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
c as createMidday,
|
|
20
|
+
l as midday
|
|
21
|
+
};
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MiddayOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Svelte action for midday.js (auto mode).
|
|
4
|
+
* Initializes when the element mounts, destroys when it unmounts.
|
|
5
|
+
* Cloning happens client-side — safe for SSR.
|
|
6
|
+
*
|
|
7
|
+
* Usage: <header use:midday> or <header use:midday={{ onChange }}>
|
|
8
|
+
*/
|
|
9
|
+
export declare function midday(node: HTMLElement, options?: MiddayOptions): {
|
|
10
|
+
update: (opts?: MiddayOptions) => void;
|
|
11
|
+
destroy: () => void;
|
|
12
|
+
};
|
package/dist/svelte.mjs
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface ActiveVariant {
|
|
2
|
+
/** The variant name (from data-midday-section value) */
|
|
3
|
+
name: string;
|
|
4
|
+
/** How much of the header this variant covers (0 to 1) */
|
|
5
|
+
progress: number;
|
|
6
|
+
}
|
|
7
|
+
export interface MiddayInstance {
|
|
8
|
+
/** Re-scan sections and recalculate positions. Call after DOM changes. */
|
|
9
|
+
refresh: () => void;
|
|
10
|
+
/** Full teardown: remove clones, observers, listeners, restore original DOM. */
|
|
11
|
+
destroy: () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface SectionData {
|
|
14
|
+
el: Element;
|
|
15
|
+
variant: string;
|
|
16
|
+
top: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
export interface VariantState {
|
|
20
|
+
wrapper: HTMLElement;
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MiddayOptions {
|
|
24
|
+
/** Instance name for multi-instance scoping. Sections with a matching data-midday-target will be claimed by this instance. Defaults to the header's data-midday attribute value. */
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Called when the set of visible variants changes */
|
|
27
|
+
onChange?: ((variants: ActiveVariant[]) => void) | null;
|
|
28
|
+
}
|
|
29
|
+
export interface MiddayHeadlessOptions {
|
|
30
|
+
/** The fixed/sticky header element (used for position calculations) */
|
|
31
|
+
header: HTMLElement;
|
|
32
|
+
/** Map of variant name to wrapper element. The plugin manages clip-paths on these. */
|
|
33
|
+
variants: Record<string, HTMLElement>;
|
|
34
|
+
/** Which key in `variants` is the default (shown where no section overlaps). Defaults to 'default'. */
|
|
35
|
+
defaultVariant?: string;
|
|
36
|
+
/** Instance name for multi-instance scoping. Sections with a matching data-midday-target will be claimed by this instance. */
|
|
37
|
+
name?: string;
|
|
38
|
+
/** Called when the set of visible variants changes */
|
|
39
|
+
onChange?: ((variants: ActiveVariant[]) => void) | null;
|
|
40
|
+
}
|
|
41
|
+
export interface EngineConfig {
|
|
42
|
+
header: HTMLElement;
|
|
43
|
+
variants: VariantState[];
|
|
44
|
+
defaultName: string;
|
|
45
|
+
sections: SectionData[];
|
|
46
|
+
onChange?: ((variants: ActiveVariant[]) => void) | null;
|
|
47
|
+
}
|
|
48
|
+
export interface Engine {
|
|
49
|
+
recalculate: () => void;
|
|
50
|
+
update: (variants: VariantState[], sections: SectionData[]) => void;
|
|
51
|
+
destroy: () => void;
|
|
52
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SectionData } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Get a section's position relative to the document.
|
|
4
|
+
* Uses getBoundingClientRect + scrollY for accuracy.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getSectionBounds(el: Element): {
|
|
7
|
+
top: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Get the header's current viewport-relative bounding rect.
|
|
12
|
+
* This accounts for CSS transforms, sticky offsets, etc.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getHeaderBounds(el: HTMLElement): {
|
|
15
|
+
top: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Calculate cached bounds for all tracked sections.
|
|
20
|
+
*/
|
|
21
|
+
export declare function cacheSectionBounds(sections: SectionData[]): void;
|
|
22
|
+
/**
|
|
23
|
+
* Scan the document for sections, optionally filtered by instance name.
|
|
24
|
+
* - Sections without data-midday-target apply to ALL instances.
|
|
25
|
+
* - Sections with data-midday-target apply only to the listed instance(s) (space-separated).
|
|
26
|
+
*/
|
|
27
|
+
export declare function scanSections(instanceName?: string): SectionData[];
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Ref, type ObjectDirective } from 'vue';
|
|
2
|
+
import type { MiddayOptions, MiddayInstance } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Vue composable for midday.js (auto mode).
|
|
5
|
+
* Initializes on mount, destroys on unmount.
|
|
6
|
+
* Cloning happens client-side — safe for SSR.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useMidday(headerRef: Ref<HTMLElement | null>, options?: MiddayOptions): Ref<MiddayInstance | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Vue custom directive for midday.js (auto mode).
|
|
11
|
+
* Usage: <header v-midday> or <header v-midday="{ onChange }">
|
|
12
|
+
* In <script setup>, import as `vMidday` for auto-registration.
|
|
13
|
+
*/
|
|
14
|
+
export declare const vMidday: ObjectDirective<HTMLElement, MiddayOptions | undefined>;
|
package/dist/vue.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { shallowRef as o, onMounted as u, onUnmounted as s } from "vue";
|
|
2
|
+
import { a as d } from "./core.mjs";
|
|
3
|
+
function r(n, e) {
|
|
4
|
+
const t = o(null);
|
|
5
|
+
return u(() => {
|
|
6
|
+
n.value && (t.value = d(n.value, e));
|
|
7
|
+
}), s(() => {
|
|
8
|
+
var a;
|
|
9
|
+
(a = t.value) == null || a.destroy(), t.value = null;
|
|
10
|
+
}), t;
|
|
11
|
+
}
|
|
12
|
+
const c = {
|
|
13
|
+
mounted(n, e) {
|
|
14
|
+
const t = d(n, e.value);
|
|
15
|
+
n.__middayInstance = t;
|
|
16
|
+
},
|
|
17
|
+
unmounted(n) {
|
|
18
|
+
var e;
|
|
19
|
+
(e = n.__middayInstance) == null || e.destroy(), delete n.__middayInstance;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export {
|
|
23
|
+
r as useMidday,
|
|
24
|
+
c as vMidday
|
|
25
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marcwiest/midday.js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A modern vanilla JS plugin for fixed headers that change style as you scroll through sections. Zero dependencies. The spiritual successor to midnight.js.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/marcwiest/midday.js.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/marcwiest/midday.js#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/marcwiest/midday.js/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/midday.umd.js",
|
|
18
|
+
"module": "./dist/midday.mjs",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/midday.mjs",
|
|
24
|
+
"require": "./dist/midday.umd.js"
|
|
25
|
+
},
|
|
26
|
+
"./react": {
|
|
27
|
+
"types": "./dist/react.d.ts",
|
|
28
|
+
"import": "./dist/react.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./vue": {
|
|
31
|
+
"types": "./dist/vue.d.ts",
|
|
32
|
+
"import": "./dist/vue.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./svelte": {
|
|
35
|
+
"types": "./dist/svelte.d.ts",
|
|
36
|
+
"import": "./dist/svelte.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./solid": {
|
|
39
|
+
"types": "./dist/solid.d.ts",
|
|
40
|
+
"import": "./dist/solid.mjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev": "vite",
|
|
48
|
+
"build": "vite build && vite build --config vite.config.umd.ts && tsc --emitDeclarationOnly",
|
|
49
|
+
"preview": "vite preview",
|
|
50
|
+
"build:demo": "vite build --config vite.config.demo.ts",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"prepublishOnly": "pnpm build && pnpm test"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"header",
|
|
57
|
+
"fixed-header",
|
|
58
|
+
"scroll",
|
|
59
|
+
"clip-path",
|
|
60
|
+
"navigation",
|
|
61
|
+
"midnight.js",
|
|
62
|
+
"vanilla-js",
|
|
63
|
+
"no-dependencies",
|
|
64
|
+
"react",
|
|
65
|
+
"vue",
|
|
66
|
+
"svelte",
|
|
67
|
+
"solid"
|
|
68
|
+
],
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"react": ">=18",
|
|
72
|
+
"solid-js": ">=1",
|
|
73
|
+
"vue": ">=3"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"react": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"vue": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"solid-js": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@types/react": "^19.2.14",
|
|
88
|
+
"happy-dom": "^20.6.3",
|
|
89
|
+
"solid-js": "^1.9.11",
|
|
90
|
+
"typescript": "^5.7.0",
|
|
91
|
+
"vite": "^6.1.0",
|
|
92
|
+
"vitest": "^4.0.18",
|
|
93
|
+
"vue": "^3.5.28"
|
|
94
|
+
}
|
|
95
|
+
}
|