atharv-mandlavdiya 1.2.1 → 1.2.3

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.
@@ -19,10 +19,8 @@ const progressCardStyles = `
19
19
 
20
20
  .progress-wrapper {
21
21
  position: relative;
22
- min-height: 1024px;
23
22
  width: 100%;
24
23
  box-sizing: border-box;
25
- overflow: visible;
26
24
  }
27
25
 
28
26
  progress-card {
@@ -31,18 +29,20 @@ progress-card {
31
29
  top: 30vh;
32
30
  height: 40vh;
33
31
  width: 100%;
34
- max-width: 100%;
35
32
  margin-bottom: 30vh;
33
+ overflow: hidden;
34
+ --progress-card-radius-start: 2.441rem;
35
+ --progress-card-radius-end: 1rem;
36
36
  scale: calc(0.5 + var(--progress) * 0.5);
37
37
  clip-path: rect(
38
- calc(20% - var(--progress) * 20%)
39
- calc(90% + var(--progress) * 10%)
40
- calc(80% + var(--progress) * 20%)
38
+ calc(20% - var(--progress) * 20%)
39
+ calc(90% + var(--progress) * 10%)
40
+ calc(80% + var(--progress) * 20%)
41
41
  calc(10% - var(--progress) * 10%)
42
- round calc(2.441rem - var(--progress) * 2.441rem)
42
+ round calc(var(--progress-card-radius-start) - var(--progress) * (var(--progress-card-radius-start) - var(--progress-card-radius-end)))
43
43
  );
44
44
  transform: translateZ(0);
45
- overflow: hidden;
45
+ will-change: clip-path, scale;
46
46
  }
47
47
 
48
48
  @media (min-width: 1024px) {
@@ -54,11 +54,11 @@ progress-card {
54
54
  margin-right: auto;
55
55
  margin-bottom: 15vh;
56
56
  clip-path: rect(
57
- calc(20% - var(--progress) * 20%)
58
- calc(70% + var(--progress) * 30%)
57
+ calc(20% - var(--progress) * 20%)
58
+ calc(70% + var(--progress) * 30%)
59
59
  calc(80% + var(--progress) * 20%)
60
- calc(30% - var(--progress) * 30%)
61
- round calc(2.441rem - var(--progress) * 2.441rem)
60
+ calc(30% - var(--progress) * 30%)
61
+ round calc(var(--progress-card-radius-start) - var(--progress) * (var(--progress-card-radius-start) - var(--progress-card-radius-end)))
62
62
  );
63
63
  }
64
64
  }
@@ -74,7 +74,8 @@ progress-card > div {
74
74
  `;
75
75
 
76
76
  function injectProgressCardStyles(root) {
77
- if (!root.document || root.document.getElementById('atharv-progress-card-styles')) return;
77
+ if (!root.document) return;
78
+ if (root.document.getElementById('atharv-progress-card-styles')) return;
78
79
 
79
80
  const style = root.document.createElement('style');
80
81
  style.id = 'atharv-progress-card-styles';
@@ -82,11 +83,14 @@ function injectProgressCardStyles(root) {
82
83
  (root.document.head || root.document.documentElement).appendChild(style);
83
84
  }
84
85
 
86
+ function clamp(value, min, max) {
87
+ return Math.min(max, Math.max(min, value));
88
+ }
89
+
85
90
  function isScrollable(element, root) {
86
91
  const style = root.getComputedStyle(element);
87
92
  const overflow = `${style.overflow} ${style.overflowY}`;
88
- return /(auto|scroll|overlay)/.test(overflow) &&
89
- (element.scrollHeight > element.clientHeight || /(auto|scroll|overlay)/.test(overflow));
93
+ return /(auto|scroll|overlay)/.test(overflow);
90
94
  }
91
95
 
92
96
  function findScrollContainer(element, root) {
@@ -99,84 +103,138 @@ function findScrollContainer(element, root) {
99
103
  }
100
104
 
101
105
  function getScrollPosition(container, root) {
102
- return container === root ? (root.pageYOffset || root.scrollY || 0) : container.scrollTop;
106
+ if (container !== root) return container.scrollTop;
107
+ return root.scrollY || root.pageYOffset || 0;
103
108
  }
104
109
 
105
110
  function getViewportHeight(container, root) {
106
- return container === root ? root.innerHeight : container.clientHeight;
111
+ if (container !== root) return container.clientHeight;
112
+ return root.innerHeight;
107
113
  }
108
114
 
109
- function getWrapperPosition(wrapper, container, root) {
110
- const wrapperRect = wrapper.getBoundingClientRect();
111
- if (container === root) return wrapperRect.top + getScrollPosition(container, root);
115
+ const cards = new Set();
116
+ let frameQueued = false;
112
117
 
113
- const containerRect = container.getBoundingClientRect();
114
- return wrapperRect.top - containerRect.top + getScrollPosition(container, root);
118
+ function queueFrame() {
119
+ if (frameQueued) return;
120
+ frameQueued = true;
121
+ requestAnimationFrame(runFrame);
115
122
  }
116
123
 
117
- function clamp(value, min, max) {
118
- return Math.min(max, Math.max(min, value));
124
+ function runFrame() {
125
+ frameQueued = false;
126
+ for (const card of cards) {
127
+ if (card.dirty) card.measure();
128
+ }
129
+ for (const card of cards) card.render();
130
+ }
131
+
132
+ function markAllDirty() {
133
+ for (const card of cards) card.dirty = true;
134
+ queueFrame();
135
+ }
136
+
137
+ function releaseContainer(container) {
138
+ for (const card of cards) {
139
+ if (card.container === container) return;
140
+ }
141
+ container.removeEventListener('scroll', queueFrame);
119
142
  }
120
143
 
121
144
  function defineProgressCard(root) {
122
- if (!root.document || !root.customElements || root.customElements.get('progress-card')) return;
145
+ if (!root.document || !root.customElements) return;
146
+ if (root.customElements.get('progress-card')) return;
147
+
148
+ root.addEventListener('resize', markAllDirty, { passive: true });
123
149
 
124
150
  root.customElements.define('progress-card', class extends root.HTMLElement {
125
151
  constructor() {
126
152
  super();
127
- this._wrapper = null;
128
- this._scrollContainer = null;
129
- this._onScroll = () => this._updateProgress();
130
- this._onResize = () => this._updateProgress();
153
+ this.wrapper = null;
154
+ this.container = null;
155
+ this.observer = null;
156
+ this.mounting = false;
157
+ this.dirty = true;
158
+ this.start = 0;
159
+ this.range = 1;
160
+ this.last = -1;
131
161
  }
132
162
 
133
163
  connectedCallback() {
134
- if (!this._wrapper) {
135
- const scrollLength = Number.parseFloat(this.dataset.scrollLength) || 300;
136
- const wrapper = root.document.createElement('div');
137
- wrapper.className = 'progress-wrapper';
138
- wrapper.style.minHeight = `${scrollLength}vh`;
139
- this._wrapper = wrapper;
140
- this.parentNode.insertBefore(wrapper, this);
141
- wrapper.appendChild(this);
164
+ if (this.mounting) return;
165
+ if (!this.wrapper) this.mount();
166
+
167
+ this.container = findScrollContainer(this, root);
168
+ this.container.addEventListener('scroll', queueFrame, { passive: true });
169
+
170
+ if (root.ResizeObserver && !this.observer) {
171
+ this.observer = new root.ResizeObserver(markAllDirty);
172
+ this.observer.observe(this.wrapper);
142
173
  }
143
174
 
144
- this._scrollContainer = findScrollContainer(this, root);
145
- this._scrollContainer.addEventListener('scroll', this._onScroll, { passive: true });
146
- root.addEventListener('resize', this._onResize, { passive: true });
147
- this._updateProgress();
175
+ cards.add(this);
176
+ this.dirty = true;
177
+ queueFrame();
148
178
  }
149
179
 
150
180
  disconnectedCallback() {
151
- if (this._scrollContainer) {
152
- this._scrollContainer.removeEventListener('scroll', this._onScroll);
153
- this._scrollContainer = null;
154
- }
155
- root.removeEventListener('resize', this._onResize);
156
- if (this._wrapper && this._wrapper.parentNode && !this._wrapper.contains(this)) {
157
- this._wrapper.parentNode.removeChild(this._wrapper);
181
+ if (this.mounting) return;
182
+ cards.delete(this);
183
+
184
+ if (this.observer) this.observer.disconnect();
185
+ this.observer = null;
186
+
187
+ if (this.container) releaseContainer(this.container);
188
+ this.container = null;
189
+
190
+ const wrapper = this.wrapper;
191
+ this.wrapper = null;
192
+ if (!wrapper || !wrapper.parentNode) return;
193
+ if (wrapper.contains(this)) return;
194
+ wrapper.parentNode.removeChild(wrapper);
195
+ }
196
+
197
+ mount() {
198
+ const length = Number.parseFloat(this.dataset.scrollLength) || 300;
199
+ const wrapper = root.document.createElement('div');
200
+ wrapper.className = 'progress-wrapper';
201
+ wrapper.style.minHeight = `${length}vh`;
202
+
203
+ this.wrapper = wrapper;
204
+ this.mounting = true;
205
+ this.parentNode.insertBefore(wrapper, this);
206
+ wrapper.appendChild(this);
207
+ this.mounting = false;
208
+ }
209
+
210
+ measure() {
211
+ const container = this.container;
212
+ if (!container || !this.wrapper) return;
213
+
214
+ const viewport = getViewportHeight(container, root);
215
+ const rect = this.wrapper.getBoundingClientRect();
216
+ const scroll = getScrollPosition(container, root);
217
+
218
+ let top = rect.top + scroll;
219
+ if (container !== root) {
220
+ top = rect.top - container.getBoundingClientRect().top + scroll;
158
221
  }
159
- this._wrapper = null;
222
+
223
+ this.start = top;
224
+ this.range = Math.max(1, rect.height - viewport);
225
+ this.dirty = false;
160
226
  }
161
227
 
162
- _updateProgress() {
163
- if (!this._wrapper || !this._scrollContainer) return;
164
- const viewportHeight = getViewportHeight(this._scrollContainer, root);
165
- const wrapperStart = getWrapperPosition(this._wrapper, this._scrollContainer, root);
166
- const wrapperHeight = this._wrapper.offsetHeight ||
167
- this._wrapper.getBoundingClientRect().height;
168
- const sectionStart = wrapperStart - viewportHeight;
169
- const sectionEnd = wrapperStart + wrapperHeight - viewportHeight;
170
- const scrollPosition = getScrollPosition(this._scrollContainer, root);
171
- const progress = sectionEnd <= sectionStart
172
- ? (scrollPosition >= sectionEnd ? 1 : 0)
173
- : clamp(
174
- (scrollPosition - sectionStart) / (sectionEnd - sectionStart),
175
- 0,
176
- 1
177
- );
178
-
179
- this.style.setProperty('--progress', String(progress));
228
+ render() {
229
+ if (!this.container) return;
230
+
231
+ const scroll = getScrollPosition(this.container, root);
232
+ const raw = (scroll - this.start) / this.range;
233
+ const value = Math.round(clamp(raw, 0, 1) * 1e4) / 1e4;
234
+
235
+ if (value === this.last) return;
236
+ this.last = value;
237
+ this.style.setProperty('--progress', String(value));
180
238
  }
181
239
  });
182
240
  }
@@ -190,6 +248,63 @@ export function progressCard() {
190
248
  defineProgressCard(globalThis);
191
249
  }
192
250
 
251
+ export function smoothScroll(options = {}) {
252
+ const root = globalThis;
253
+ if (!root.document || !root.matchMedia) return;
254
+ if (root.matchMedia('(pointer: coarse)').matches) return;
255
+
256
+ const speed = options.speed ?? 0.09;
257
+ const accel = options.accel ?? 0.42;
258
+ const multiplier = options.multiplier ?? 1.1;
259
+ const maxDelta = options.maxDelta ?? 2400;
260
+ const stop = options.stop ?? 0.08;
261
+
262
+ let delta = 0;
263
+ let target = 0;
264
+ let current = 0;
265
+ let running = false;
266
+
267
+ function limit() {
268
+ const doc = root.document.documentElement;
269
+ return Math.max(0, doc.scrollHeight - root.innerHeight);
270
+ }
271
+
272
+ function step() {
273
+ if (delta === 0) {
274
+ running = false;
275
+ return;
276
+ }
277
+
278
+ delta = clamp(delta, -maxDelta, maxDelta);
279
+ target = clamp(target + delta * accel, 0, limit());
280
+
281
+ const lerped = (target - current) * speed;
282
+ current += lerped;
283
+ delta *= 1 - accel;
284
+
285
+ if (Math.abs(lerped) < stop) {
286
+ current = Math.round(target);
287
+ delta = 0;
288
+ }
289
+
290
+ root.scrollTo(0, current);
291
+ requestAnimationFrame(step);
292
+ }
293
+
294
+ root.addEventListener('wheel', (event) => {
295
+ event.preventDefault();
296
+
297
+ if (!running) {
298
+ target = getScrollPosition(root, root);
299
+ current = target;
300
+ running = true;
301
+ requestAnimationFrame(step);
302
+ }
303
+
304
+ delta += event.deltaY * multiplier;
305
+ }, { passive: false });
306
+ }
307
+
193
308
  const _svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 200" width="650" height="200">
194
309
  <defs>
195
310
  <linearGradient id="pathGradient" x1="0%" y1="0%" x2="100%" y2="0%">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atharv-mandlavdiya",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Made With Love By Atharv Mandlavdiya",
5
5
  "type": "module",
6
6
  "main": "atharvmandlavdiya.js",
@@ -1,213 +0,0 @@
1
- import test, { afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- class FakeStyle {
5
- setProperty(name, value) {
6
- this[name] = value;
7
- }
8
-
9
- getPropertyValue(name) {
10
- return this[name] || '';
11
- }
12
- }
13
-
14
- class FakeElement {
15
- constructor() {
16
- this.children = [];
17
- this.attributes = new Map();
18
- this.style = new FakeStyle();
19
- this.listeners = new Map();
20
- this.parentElement = null;
21
- this._rect = { top: 0, height: 0 };
22
- this._height = 0;
23
- this.clientHeight = 0;
24
- this.scrollHeight = 0;
25
- this.scrollTop = 0;
26
- }
27
-
28
- get parentNode() {
29
- return this.parentElement;
30
- }
31
-
32
- get dataset() {
33
- return Object.fromEntries([...this.attributes]
34
- .filter(([name]) => name.startsWith('data-'))
35
- .map(([name, value]) => [
36
- name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()),
37
- value
38
- ]));
39
- }
40
-
41
- get offsetHeight() {
42
- return this._height;
43
- }
44
-
45
- set className(value) {
46
- this.attributes.set('class', value);
47
- }
48
-
49
- get className() {
50
- return this.attributes.get('class') || '';
51
- }
52
-
53
- setAttribute(name, value) {
54
- this.attributes.set(name, String(value));
55
- }
56
-
57
- hasAttribute(name) {
58
- return this.attributes.has(name);
59
- }
60
-
61
- appendChild(child) {
62
- child.parentElement = this;
63
- this.children.push(child);
64
- return child;
65
- }
66
-
67
- insertBefore(child, reference) {
68
- child.parentElement = this;
69
- const index = this.children.indexOf(reference);
70
- this.children.splice(index < 0 ? this.children.length : index, 0, child);
71
- return child;
72
- }
73
-
74
- removeChild(child) {
75
- this.children.splice(this.children.indexOf(child), 1);
76
- child.parentElement = null;
77
- return child;
78
- }
79
-
80
- contains(child) {
81
- return this.children.includes(child) || this.children.some((item) => item.contains(child));
82
- }
83
-
84
- getBoundingClientRect() {
85
- return this._rect;
86
- }
87
-
88
- addEventListener(type, listener) {
89
- if (!this.listeners.has(type)) this.listeners.set(type, new Set());
90
- this.listeners.get(type).add(listener);
91
- }
92
-
93
- removeEventListener(type, listener) {
94
- this.listeners.get(type)?.delete(listener);
95
- }
96
- }
97
-
98
- class FakeHTMLElement extends FakeElement {}
99
-
100
- const body = new FakeElement();
101
- const head = new FakeElement();
102
- const documentElement = new FakeElement();
103
- globalThis.HTMLElement = FakeHTMLElement;
104
- globalThis.document = {
105
- body,
106
- head,
107
- documentElement,
108
- createElement: () => new FakeElement(),
109
- getElementById(id) {
110
- return [head, ...head.children].find((element) => element.attributes.get('id') === id) || null;
111
- }
112
- };
113
- globalThis.customElements = {
114
- registry: new Map(),
115
- define(name, constructor) {
116
- this.registry.set(name, constructor);
117
- },
118
- get(name) {
119
- return this.registry.get(name);
120
- }
121
- };
122
- globalThis.innerHeight = 1000;
123
- globalThis.pageYOffset = 0;
124
- globalThis.scrollY = 0;
125
- globalThis.listeners = new Map();
126
- globalThis.addEventListener = (type, listener) => {
127
- if (!globalThis.listeners.has(type)) globalThis.listeners.set(type, new Set());
128
- globalThis.listeners.get(type).add(listener);
129
- };
130
- globalThis.removeEventListener = (type, listener) => globalThis.listeners.get(type)?.delete(listener);
131
- globalThis.getComputedStyle = (element) => ({
132
- overflow: element._overflow || 'visible',
133
- overflowY: element._overflowY || 'visible'
134
- });
135
-
136
- await import('../atharvmandlavdiya.js');
137
-
138
- const ProgressCard = customElements.get('progress-card');
139
- const createdCards = [];
140
-
141
- afterEach(() => {
142
- for (const card of createdCards.splice(0)) {
143
- card.parentElement?.removeChild(card);
144
- card.disconnectedCallback();
145
- }
146
- globalThis.pageYOffset = 0;
147
- });
148
-
149
- function createCard(parent = body, scrollLength = '300') {
150
- const card = new ProgressCard();
151
- card.setAttribute('data-scroll-length', scrollLength);
152
- parent.appendChild(card);
153
- card.connectedCallback();
154
- createdCards.push(card);
155
- const wrapper = card.parentElement;
156
- wrapper._height = 3000;
157
- wrapper._rect = { top: 0, height: 3000 };
158
- card._onScroll();
159
- return { card, wrapper };
160
- }
161
-
162
- test('registers automatically, injects CSS, and needs no StringTune', () => {
163
- assert.equal(typeof ProgressCard, 'function');
164
- assert.match(head.children[0].textContent || head.children[0].style?.textContent || '', /progress-wrapper/);
165
- assert.equal(globalThis.StringTune, undefined);
166
- });
167
-
168
- test('uses the window scroll range and clamps at both ends', () => {
169
- const { card } = createCard();
170
- assert.equal(card.style.getPropertyValue('--progress'), '0');
171
- globalThis.pageYOffset = 1000;
172
- card.parentElement._rect = { top: -1000, height: 3000 };
173
- card._onScroll();
174
- assert.equal(card.style.getPropertyValue('--progress'), '0.5');
175
- globalThis.pageYOffset = 2000;
176
- card.parentElement._rect = { top: -2000, height: 3000 };
177
- card._onScroll();
178
- assert.equal(card.style.getPropertyValue('--progress'), '1');
179
- globalThis.pageYOffset = 2500;
180
- card.parentElement._rect = { top: -2500, height: 3000 };
181
- card._onScroll();
182
- assert.equal(card.style.getPropertyValue('--progress'), '1');
183
- });
184
-
185
- test('uses the nearest nested scroll container', () => {
186
- globalThis.pageYOffset = 0;
187
- const brand = new FakeElement();
188
- brand._overflowY = 'auto';
189
- brand.clientHeight = 500;
190
- brand.scrollHeight = 2000;
191
- brand._rect = { top: 10, height: 500 };
192
- body.appendChild(brand);
193
- const { card, wrapper } = createCard(brand);
194
- wrapper._height = 1500;
195
- wrapper._rect = { top: 10, height: 1500 };
196
- brand.scrollTop = 500;
197
- wrapper._rect = { top: -490, height: 1500 };
198
- card._onScroll();
199
- assert.equal(card.style.getPropertyValue('--progress'), '0.5');
200
- });
201
-
202
- test('keeps cards independent and removes listeners on disconnect', () => {
203
- const first = createCard();
204
- const second = createCard();
205
- assert.notEqual(first.card, second.card);
206
- assert.equal(globalThis.listeners.get('resize').size >= 2, true);
207
- first.card.parentElement.removeChild(first.card);
208
- first.card.disconnectedCallback();
209
- assert.equal(globalThis.listeners.get('resize').size >= 1, true);
210
- second.card.parentElement.removeChild(second.card);
211
- second.card.disconnectedCallback();
212
- assert.equal(globalThis.listeners.get('resize').size, 0);
213
- });