@hortonstudio/main 1.0.1 → 1.1.1

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.
@@ -0,0 +1,305 @@
1
+ const API_NAME = 'hsmain';
2
+
3
+ const config = {
4
+ global: {
5
+ animationDelay: 0
6
+ },
7
+ wordSplit: {
8
+ duration: 1.5,
9
+ stagger: 0.075,
10
+ yPercent: 110,
11
+ ease: "power4.out",
12
+ start: "top 97%"
13
+ },
14
+ lineSplit: {
15
+ duration: 1.5,
16
+ stagger: 0.1,
17
+ yPercent: 110,
18
+ ease: "power4.out",
19
+ start: "top 97%"
20
+ },
21
+ charSplit: {
22
+ duration: 1.2,
23
+ stagger: 0.03,
24
+ yPercent: 110,
25
+ ease: "power4.out",
26
+ start: "top 97%"
27
+ },
28
+ appear: {
29
+ y: 50,
30
+ duration: 1.5,
31
+ ease: "power3.out",
32
+ start: "top 97%"
33
+ }
34
+ };
35
+
36
+ function updateConfig(newConfig) {
37
+ function deepMerge(target, source) {
38
+ for (const key in source) {
39
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
40
+ target[key] = target[key] || {};
41
+ deepMerge(target[key], source[key]);
42
+ } else {
43
+ target[key] = source[key];
44
+ }
45
+ }
46
+ return target;
47
+ }
48
+
49
+ deepMerge(config, newConfig);
50
+ }
51
+
52
+ function killTextAnimations() {
53
+ activeAnimations.forEach(({ timeline, element }) => {
54
+ if (timeline) {
55
+ timeline.kill();
56
+ }
57
+ if (element?.splitTextInstance) {
58
+ element.splitTextInstance.revert();
59
+ }
60
+ });
61
+ activeAnimations.length = 0;
62
+ }
63
+
64
+ function startTextAnimations() {
65
+ setInitialStates().then(() => {
66
+ initAnimations();
67
+ });
68
+ }
69
+
70
+ let activeAnimations = [];
71
+
72
+ function waitForFonts() {
73
+ return document.fonts.ready;
74
+ }
75
+
76
+ const CharSplitAnimations = {
77
+ async initial() {
78
+ await waitForFonts();
79
+
80
+ const elements = document.querySelectorAll(".a-char-split > *:first-child");
81
+
82
+ elements.forEach((textElement, index) => {
83
+
84
+ const split = SplitText.create(textElement, {
85
+ type: "chars",
86
+ mask: "chars",
87
+ charsClass: "char",
88
+ });
89
+ textElement.splitTextInstance = split;
90
+
91
+ gsap.set(split.chars, {
92
+ yPercent: config.charSplit.yPercent
93
+ });
94
+ gsap.set(textElement, { autoAlpha: 1 });
95
+ });
96
+ },
97
+
98
+ async animate() {
99
+ await waitForFonts();
100
+
101
+ document.querySelectorAll(".a-char-split > *:first-child").forEach((textElement) => {
102
+ const chars = textElement.querySelectorAll('.char');
103
+ const tl = gsap.timeline({
104
+ scrollTrigger: {
105
+ trigger: textElement,
106
+ start: config.charSplit.start,
107
+ invalidateOnRefresh: true,
108
+ },
109
+ onComplete: () => {
110
+ textElement.splitTextInstance.revert();
111
+ }
112
+ });
113
+
114
+ tl.to(chars, {
115
+ yPercent: 0,
116
+ duration: config.charSplit.duration,
117
+ stagger: config.charSplit.stagger,
118
+ ease: config.charSplit.ease,
119
+ });
120
+
121
+ activeAnimations.push({ timeline: tl, element: textElement });
122
+ });
123
+ }
124
+ };
125
+
126
+ const WordSplitAnimations = {
127
+ async initial() {
128
+ await waitForFonts();
129
+
130
+ const elements = document.querySelectorAll(".a-word-split > *:first-child");
131
+
132
+ elements.forEach((textElement, index) => {
133
+
134
+ const split = SplitText.create(textElement, {
135
+ type: "words",
136
+ mask: "words",
137
+ wordsClass: "word",
138
+ });
139
+ textElement.splitTextInstance = split;
140
+
141
+ gsap.set(split.words, {
142
+ yPercent: config.wordSplit.yPercent
143
+ });
144
+ gsap.set(textElement, { autoAlpha: 1 });
145
+ });
146
+ },
147
+
148
+ async animate() {
149
+ await waitForFonts();
150
+
151
+ document.querySelectorAll(".a-word-split > *:first-child").forEach((textElement) => {
152
+ const words = textElement.querySelectorAll('.word');
153
+ const tl = gsap.timeline({
154
+ scrollTrigger: {
155
+ trigger: textElement,
156
+ start: config.wordSplit.start,
157
+ invalidateOnRefresh: true,
158
+ },
159
+ onComplete: () => {
160
+ textElement.splitTextInstance.revert();
161
+ }
162
+ });
163
+
164
+ tl.to(words, {
165
+ yPercent: 0,
166
+ duration: config.wordSplit.duration,
167
+ stagger: config.wordSplit.stagger,
168
+ ease: config.wordSplit.ease,
169
+ });
170
+
171
+ activeAnimations.push({ timeline: tl, element: textElement });
172
+ });
173
+ }
174
+ };
175
+
176
+ const LineSplitAnimations = {
177
+ async initial() {
178
+ await waitForFonts();
179
+
180
+ const elements = document.querySelectorAll(".a-line-split > *:first-child");
181
+
182
+ elements.forEach((textElement, index) => {
183
+
184
+ const split = SplitText.create(textElement, {
185
+ type: "lines",
186
+ mask: "lines",
187
+ linesClass: "line",
188
+ });
189
+ textElement.splitTextInstance = split;
190
+
191
+ gsap.set(split.lines, {
192
+ yPercent: config.lineSplit.yPercent
193
+ });
194
+ gsap.set(textElement, { autoAlpha: 1 });
195
+ });
196
+ },
197
+
198
+ async animate() {
199
+ await waitForFonts();
200
+
201
+ document.querySelectorAll(".a-line-split > *:first-child").forEach((textElement) => {
202
+ const lines = textElement.querySelectorAll('.line');
203
+ const tl = gsap.timeline({
204
+ scrollTrigger: {
205
+ trigger: textElement,
206
+ start: config.lineSplit.start,
207
+ invalidateOnRefresh: true,
208
+ },
209
+ onComplete: () => {
210
+ textElement.splitTextInstance.revert();
211
+ }
212
+ });
213
+
214
+ tl.to(lines, {
215
+ yPercent: 0,
216
+ duration: config.lineSplit.duration,
217
+ stagger: config.lineSplit.stagger,
218
+ ease: config.lineSplit.ease,
219
+ });
220
+
221
+ activeAnimations.push({ timeline: tl, element: textElement });
222
+ });
223
+ }
224
+ };
225
+
226
+ const AppearAnimations = {
227
+ async initial() {
228
+ await waitForFonts();
229
+
230
+ const elements = document.querySelectorAll('.a-appear');
231
+ elements.forEach(element => {
232
+ gsap.set(element, {
233
+ y: config.appear.y,
234
+ opacity: 0
235
+ });
236
+ });
237
+ },
238
+
239
+ async animate() {
240
+ await waitForFonts();
241
+
242
+ document.querySelectorAll('.a-appear').forEach(element => {
243
+ const tl = gsap.timeline({
244
+ scrollTrigger: {
245
+ trigger: element,
246
+ start: config.appear.start,
247
+ invalidateOnRefresh: true,
248
+ }
249
+ });
250
+
251
+ tl.to(element, {
252
+ y: 0,
253
+ opacity: 1,
254
+ duration: config.appear.duration,
255
+ ease: config.appear.ease
256
+ });
257
+
258
+ activeAnimations.push({ timeline: tl, element: element });
259
+ });
260
+
261
+ }
262
+ };
263
+
264
+ async function setInitialStates() {
265
+ await Promise.all([
266
+ CharSplitAnimations.initial(),
267
+ WordSplitAnimations.initial(),
268
+ LineSplitAnimations.initial(),
269
+ AppearAnimations.initial()
270
+ ]);
271
+ }
272
+
273
+ async function initAnimations() {
274
+ if (config.global.animationDelay > 0) {
275
+ await new Promise(resolve => setTimeout(resolve, config.global.animationDelay * 1000));
276
+ }
277
+
278
+ await Promise.all([
279
+ CharSplitAnimations.animate(),
280
+ WordSplitAnimations.animate(),
281
+ LineSplitAnimations.animate(),
282
+ AppearAnimations.animate()
283
+ ]);
284
+ }
285
+
286
+ export async function init() {
287
+ await setInitialStates();
288
+ initAnimations();
289
+
290
+ window.addEventListener('resize', ScrollTrigger.refresh());
291
+
292
+ const api = window[API_NAME] || {};
293
+ api.textAnimations = {
294
+ config: config,
295
+ updateConfig: updateConfig,
296
+ start: startTextAnimations,
297
+ kill: killTextAnimations,
298
+ restart: () => {
299
+ killTextAnimations();
300
+ startTextAnimations();
301
+ }
302
+ };
303
+
304
+ return { result: 'anim-text initialized' };
305
+ }
@@ -1,4 +1,5 @@
1
1
  // Page Transition Module
2
+ const API_NAME = 'hsmain';
2
3
  export async function init() {
3
4
 
4
5
  // Your original code with minimal changes
@@ -36,5 +37,5 @@ export async function init() {
36
37
  setTimeout(() => {$(".transition").css("display", "none");}, 50);});
37
38
  }, introDurationMS);
38
39
 
39
- return { result: 'transition-main initialized' };
40
+ return { result: 'anim-transition initialized' };
40
41
  }
@@ -0,0 +1,89 @@
1
+ const API_NAME = 'hsmain';
2
+
3
+ export async function init() {
4
+ const api = window[API_NAME];
5
+ api.afterWebflowReady(() => {
6
+ if (typeof $ !== 'undefined') {
7
+ $(document).off('click.wf-scroll');
8
+ }
9
+ });
10
+
11
+ // Disable CSS smooth scrolling
12
+ document.documentElement.style.scrollBehavior = 'auto';
13
+ document.body.style.scrollBehavior = 'auto';
14
+
15
+ // Check if user prefers reduced motion
16
+ function prefersReducedMotion() {
17
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
18
+ }
19
+
20
+ function getScrollOffset() {
21
+ const offsetValue = getComputedStyle(document.documentElement)
22
+ .getPropertyValue('--misc--scroll-offset').trim();
23
+ return parseInt(offsetValue) || 0;
24
+ }
25
+
26
+ // Smooth scroll to element with offset
27
+ function scrollToElement(target, offset = 0) {
28
+ if (!target) return;
29
+
30
+ // Skip animation if user prefers reduced motion
31
+ if (prefersReducedMotion()) {
32
+ const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
33
+ window.scrollTo(0, targetPosition);
34
+ target.setAttribute('tabindex', '-1');
35
+ target.focus({ preventScroll: true });
36
+ return;
37
+ }
38
+
39
+ gsap.to(window, {
40
+ duration: 1,
41
+ scrollTo: {
42
+ y: target,
43
+ offsetY: offset
44
+ },
45
+ ease: "power2.out",
46
+ onComplete: function() {
47
+ target.setAttribute('tabindex', '-1');
48
+ target.focus({ preventScroll: true });
49
+ }
50
+ });
51
+ }
52
+
53
+ // Handle anchor link clicks and keyboard activation
54
+ function handleAnchorClicks() {
55
+ document.addEventListener('click', handleAnchorActivation);
56
+ document.addEventListener('keydown', function(e) {
57
+ if (e.key === 'Enter' || e.key === ' ') {
58
+ handleAnchorActivation(e);
59
+ }
60
+ });
61
+ }
62
+
63
+ function handleAnchorActivation(e) {
64
+ const link = e.target.closest('a[href^="#"]');
65
+ if (!link) return;
66
+
67
+ const href = link.getAttribute('href');
68
+ if (!href || href === '#') return;
69
+
70
+ const targetId = href.substring(1);
71
+ const targetElement = document.getElementById(targetId);
72
+
73
+ if (targetElement) {
74
+ e.preventDefault();
75
+ if (history.replaceState) {
76
+ history.replaceState(null, null, `#${targetElement.id}`);
77
+ }
78
+ const offset = getScrollOffset();
79
+ scrollToElement(targetElement, offset);
80
+ }
81
+ }
82
+
83
+ // Initialize anchor link handling
84
+ handleAnchorClicks();
85
+
86
+ return {
87
+ result: 'autoInit-smooth-scroll initialized'
88
+ };
89
+ }
package/index.js CHANGED
@@ -1,100 +1,184 @@
1
- window.hsMain = window.hsMain || {};
1
+ // Configuration: Change this variable to rename the API
2
+ const API_NAME = 'hsmain';
2
3
 
3
- const supportedModules = new Set([
4
- "hs-a-text", "hs-a-hero", "hs-u-toc", "hs-a-transition"
5
- ]);
4
+ window[API_NAME] = window[API_NAME] || {};
5
+ window[API_NAME].loaded = false
6
6
 
7
- const moduleMap = {
8
- "hs-a-text": "./animations/modules/text.js",
9
- "hs-a-hero": "./animations/modules/hero.js",
10
- "hs-u-toc": "./utils/modules/toc.js",
11
- "hs-a-transition": "./animations/modules/transition.js"
7
+ const animationModules = {
8
+ "data-hs-anim-text": "./animations/text.js",
9
+ "data-hs-anim-hero": "./animations/hero.js",
10
+ "data-hs-anim-transition": "./animations/transition.js"
11
+ };
12
+
13
+ const utilityModules = {
14
+ "data-hs-util-toc": "./utils/toc.js",
15
+ "data-hs-util-progress": "./utils/scroll-progress.js",
16
+ "data-hs-util-navbar": "./utils/navbar.js"
17
+ };
18
+
19
+ // Modules that auto-initialize
20
+ const autoInitModules = {
21
+ "smooth-scroll": "./autoInit/smooth-scroll.js"
22
+ };
23
+
24
+ // Store callbacks to run after Webflow.ready()
25
+ const postWebflowCallbacks = [];
26
+
27
+ // Get the base URL from the current script's location
28
+ const getBaseUrl = () => {
29
+ const currentScript = document.querySelector('script[data-hs-main]');
30
+ if (currentScript && currentScript.src) {
31
+ // Extract directory path from script URL
32
+ return currentScript.src.substring(0, currentScript.src.lastIndexOf('/') + 1);
33
+ }
34
+ // Fallback to current origin + path for relative imports
35
+ return new URL('./', import.meta.url).href;
12
36
  };
13
37
 
14
38
  const loadModule = (moduleName) => {
15
- const modulePath = moduleMap[moduleName];
39
+ // Check manual modules first
40
+ let modulePath = animationModules[moduleName] || utilityModules[moduleName];
41
+
42
+ // Then check auto-init modules
43
+ if (!modulePath) {
44
+ modulePath = autoInitModules[moduleName];
45
+ }
46
+
16
47
  if (!modulePath) {
17
48
  throw new Error(`HortonStudio module "${moduleName}" is not supported.`);
18
49
  }
19
- return import(modulePath);
50
+
51
+ // Convert relative path to absolute URL
52
+ const baseUrl = getBaseUrl();
53
+ const absoluteUrl = new URL(modulePath, baseUrl).href;
54
+
55
+ return import(absoluteUrl);
20
56
  };
21
57
 
22
58
  const findCurrentScriptTag = () => {
23
- const scriptTag = document.querySelector('script[hs-main]');
59
+ const scriptTag = document.querySelector('script[data-hs-main]');
24
60
  return scriptTag || null;
25
61
  };
26
62
 
27
- const processModules = (scriptTag) => {
28
- if (!scriptTag) {
29
- return;
30
- }
63
+ const processModules = async (scriptTag) => {
64
+ const modulePromises = [];
31
65
 
32
- for (const moduleName of supportedModules) {
33
- if (scriptTag.hasAttribute(moduleName)) {
34
- loadHsModule(moduleName);
66
+ // Load manual modules based on attributes
67
+ for (const moduleName of Object.keys({ ...animationModules, ...utilityModules })) {
68
+ if (scriptTag && scriptTag.hasAttribute(moduleName)) {
69
+ modulePromises.push(loadHsModule(moduleName));
35
70
  }
36
71
  }
37
- };
38
-
39
- const loadHsModule = async (moduleName) => {
40
- const { hsMain } = window;
41
72
 
42
- if (hsMain.process.has(moduleName)) {
43
- return hsMain.modules[moduleName]?.loading;
73
+ // Load auto-init modules
74
+ for (const moduleName of Object.keys(autoInitModules)) {
75
+ modulePromises.push(loadHsModule(moduleName));
44
76
  }
45
77
 
46
- hsMain.process.add(moduleName);
78
+ // Wait for ALL modules to finish loading
79
+ await Promise.all(modulePromises);
47
80
 
48
- const moduleObj = hsMain.modules[moduleName] || {};
49
- hsMain.modules[moduleName] = moduleObj;
81
+ // Always refresh Webflow after all modules are loaded
82
+ refreshWebflow();
83
+ };
84
+
85
+ const refreshWebflow = () => {
50
86
 
87
+ setTimeout(() => {
88
+ if (window.Webflow && window.Webflow.ready) {
89
+ window.Webflow.ready();
90
+
91
+ // Run all registered post-Webflow callbacks
92
+ setTimeout(() => {
93
+ postWebflowCallbacks.forEach(callback => {
94
+ try {
95
+ callback();
96
+ } catch (error) {
97
+ }
98
+ });
99
+ window[API_NAME].loaded = true;
100
+ },);
101
+ }
102
+ },);
103
+ };
104
+
105
+ const loadHsModule = async (moduleName) => {
106
+ const apiInstance = window[API_NAME];
107
+
108
+ if (apiInstance.process.has(moduleName)) {
109
+ return apiInstance.modules[moduleName]?.loading;
110
+ }
111
+
112
+ apiInstance.process.add(moduleName);
113
+
114
+ const moduleObj = apiInstance.modules[moduleName] || {};
115
+ apiInstance.modules[moduleName] = moduleObj;
116
+
51
117
  moduleObj.loading = new Promise((resolve, reject) => {
52
118
  moduleObj.resolve = resolve;
53
119
  moduleObj.reject = reject;
54
120
  });
55
-
121
+
56
122
  try {
57
123
  const { init, version } = await loadModule(moduleName);
58
124
  const initResult = await init();
59
125
  const { result } = initResult || {};
60
-
126
+
61
127
  moduleObj.version = version;
62
128
  moduleObj.restart = () => {
63
- hsMain.process.delete(moduleName);
129
+ apiInstance.process.delete(moduleName);
64
130
  return loadHsModule(moduleName);
65
131
  };
66
-
132
+
67
133
  moduleObj.resolve?.(result);
68
134
  delete moduleObj.resolve;
69
135
  delete moduleObj.reject;
70
-
136
+
71
137
  return result;
72
-
138
+
73
139
  } catch (error) {
74
140
  moduleObj.reject?.(error);
75
- hsMain.process.delete(moduleName);
141
+ apiInstance.process.delete(moduleName);
76
142
  throw error;
77
143
  }
78
144
  };
79
145
 
80
- const initializeHsMain = () => {
81
- const { hsMain } = window;
82
-
83
- const existingRequests = Array.isArray(hsMain) ? hsMain : [];
146
+ const initializeAPI = () => {
147
+ const apiInstance = window[API_NAME];
148
+
149
+ const existingRequests = Array.isArray(apiInstance) ? apiInstance : [];
84
150
  const scriptTag = findCurrentScriptTag();
85
-
86
- window.hsMain = {
151
+ const richTextBlocks = document.querySelectorAll('.w-richtext');
152
+ richTextBlocks.forEach(block => {
153
+ const images = block.querySelectorAll('img');
154
+ images.forEach(img => {
155
+ img.loading = 'eager';
156
+ });
157
+ });
158
+
159
+ window[API_NAME] = {
87
160
  scriptTag,
88
161
  modules: {},
89
162
  process: new Set(),
90
-
163
+
91
164
  load: loadHsModule,
92
-
165
+
166
+ // API function for scripts to register post-Webflow callbacks
167
+ afterWebflowReady: (callback) => {
168
+ if (typeof callback === 'function') {
169
+ postWebflowCallbacks.push(callback);
170
+ } else {
171
+ }
172
+ },
173
+
93
174
  status(moduleName) {
94
175
  if (!moduleName) {
95
176
  return {
96
177
  loaded: Object.keys(this.modules),
97
- loading: [...this.process]
178
+ loading: [...this.process],
179
+ animations: Object.keys(animationModules),
180
+ utilities: Object.keys(utilityModules),
181
+ autoInit: Object.keys(autoInitModules)
98
182
  };
99
183
  }
100
184
  return {
@@ -103,20 +187,20 @@ const initializeHsMain = () => {
103
187
  };
104
188
  }
105
189
  };
106
-
190
+
107
191
  processModules(scriptTag);
108
-
192
+
109
193
  if (existingRequests.length > 0) {
110
194
  existingRequests.forEach(request => {
111
195
  if (typeof request === 'string') {
112
- window.hsMain.load(request);
196
+ window[API_NAME].load(request);
113
197
  }
114
198
  });
115
199
  }
116
200
  };
117
201
 
118
202
  if (document.readyState === 'loading') {
119
- document.addEventListener('DOMContentLoaded', initializeHsMain);
203
+ document.addEventListener('DOMContentLoaded', initializeAPI);
120
204
  } else {
121
- initializeHsMain();
205
+ initializeAPI();
122
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
package/styles.css CHANGED
@@ -4,7 +4,14 @@ body .transition {display: block}
4
4
  .no-scroll-transition {overflow: hidden; position: relative;}
5
5
 
6
6
  /* splittext */
7
- .line-mask, .word-mask, .char-mask {
8
- padding: .1em .1em;
9
- margin: -.1em -.1em;
7
+ .line-mask, .word-mask, .char-mask {
8
+ padding-bottom: .1em;
9
+ margin-bottom: -.1em;
10
+ padding-inline: .1em;
11
+ margin-inline: -.1em;
12
+ }
13
+ /* scroll cleanliness */
14
+ html, body {
15
+ overscroll-behavior: none;
16
+ scrollbar-gutter: stable;
10
17
  }