@hortonstudio/main 1.0.0 → 1.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.
@@ -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,18 +1,38 @@
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"
12
11
  };
13
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
+
14
27
  const loadModule = (moduleName) => {
15
- const modulePath = moduleMap[moduleName];
28
+ // Check manual modules first
29
+ let modulePath = animationModules[moduleName] || utilityModules[moduleName];
30
+
31
+ // Then check auto-init modules
32
+ if (!modulePath) {
33
+ modulePath = autoInitModules[moduleName];
34
+ }
35
+
16
36
  if (!modulePath) {
17
37
  throw new Error(`HortonStudio module "${moduleName}" is not supported.`);
18
38
  }
@@ -20,81 +40,129 @@ const loadModule = (moduleName) => {
20
40
  };
21
41
 
22
42
  const findCurrentScriptTag = () => {
23
- const scriptTag = document.querySelector('script[hs-main]');
43
+ const scriptTag = document.querySelector('script[data-hs-main]');
24
44
  return scriptTag || null;
25
45
  };
26
46
 
27
- const processModules = (scriptTag) => {
28
- if (!scriptTag) {
29
- return;
30
- }
47
+ const processModules = async (scriptTag) => {
48
+ const modulePromises = [];
31
49
 
32
- for (const moduleName of supportedModules) {
33
- if (scriptTag.hasAttribute(moduleName)) {
34
- loadHsModule(moduleName);
50
+ // Load manual modules based on attributes
51
+ for (const moduleName of Object.keys({ ...animationModules, ...utilityModules })) {
52
+ if (scriptTag && scriptTag.hasAttribute(moduleName)) {
53
+ modulePromises.push(loadHsModule(moduleName));
35
54
  }
36
55
  }
37
- };
38
-
39
- const loadHsModule = async (moduleName) => {
40
- const { hsMain } = window;
41
56
 
42
- if (hsMain.process.has(moduleName)) {
43
- return hsMain.modules[moduleName]?.loading;
57
+ // Load auto-init modules
58
+ for (const moduleName of Object.keys(autoInitModules)) {
59
+ modulePromises.push(loadHsModule(moduleName));
44
60
  }
45
61
 
46
- hsMain.process.add(moduleName);
62
+ // Wait for ALL modules to finish loading
63
+ await Promise.all(modulePromises);
47
64
 
48
- const moduleObj = hsMain.modules[moduleName] || {};
49
- hsMain.modules[moduleName] = moduleObj;
65
+ // Always refresh Webflow after all modules are loaded
66
+ refreshWebflow();
67
+ };
68
+
69
+ const refreshWebflow = () => {
50
70
 
71
+ setTimeout(() => {
72
+ if (window.Webflow && window.Webflow.ready) {
73
+ window.Webflow.ready();
74
+
75
+ // Run all registered post-Webflow callbacks
76
+ setTimeout(() => {
77
+ postWebflowCallbacks.forEach(callback => {
78
+ try {
79
+ callback();
80
+ } catch (error) {
81
+ }
82
+ });
83
+ window[API_NAME].loaded = true;
84
+ },);
85
+ }
86
+ },);
87
+ };
88
+
89
+ const loadHsModule = async (moduleName) => {
90
+ const apiInstance = window[API_NAME];
91
+
92
+ if (apiInstance.process.has(moduleName)) {
93
+ return apiInstance.modules[moduleName]?.loading;
94
+ }
95
+
96
+ apiInstance.process.add(moduleName);
97
+
98
+ const moduleObj = apiInstance.modules[moduleName] || {};
99
+ apiInstance.modules[moduleName] = moduleObj;
100
+
51
101
  moduleObj.loading = new Promise((resolve, reject) => {
52
102
  moduleObj.resolve = resolve;
53
103
  moduleObj.reject = reject;
54
104
  });
55
-
105
+
56
106
  try {
57
107
  const { init, version } = await loadModule(moduleName);
58
108
  const initResult = await init();
59
109
  const { result } = initResult || {};
60
-
110
+
61
111
  moduleObj.version = version;
62
112
  moduleObj.restart = () => {
63
- hsMain.process.delete(moduleName);
113
+ apiInstance.process.delete(moduleName);
64
114
  return loadHsModule(moduleName);
65
115
  };
66
-
116
+
67
117
  moduleObj.resolve?.(result);
68
118
  delete moduleObj.resolve;
69
119
  delete moduleObj.reject;
70
-
120
+
71
121
  return result;
72
-
122
+
73
123
  } catch (error) {
74
124
  moduleObj.reject?.(error);
75
- hsMain.process.delete(moduleName);
125
+ apiInstance.process.delete(moduleName);
76
126
  throw error;
77
127
  }
78
128
  };
79
129
 
80
- const initializeHsMain = () => {
81
- const { hsMain } = window;
82
-
83
- const existingRequests = Array.isArray(hsMain) ? hsMain : [];
130
+ const initializeAPI = () => {
131
+ const apiInstance = window[API_NAME];
132
+
133
+ const existingRequests = Array.isArray(apiInstance) ? apiInstance : [];
84
134
  const scriptTag = findCurrentScriptTag();
85
-
86
- window.hsMain = {
135
+ const richTextBlocks = document.querySelectorAll('.w-richtext');
136
+ richTextBlocks.forEach(block => {
137
+ const images = block.querySelectorAll('img');
138
+ images.forEach(img => {
139
+ img.loading = 'eager';
140
+ });
141
+ });
142
+
143
+ window[API_NAME] = {
87
144
  scriptTag,
88
145
  modules: {},
89
146
  process: new Set(),
90
-
147
+
91
148
  load: loadHsModule,
92
-
149
+
150
+ // API function for scripts to register post-Webflow callbacks
151
+ afterWebflowReady: (callback) => {
152
+ if (typeof callback === 'function') {
153
+ postWebflowCallbacks.push(callback);
154
+ } else {
155
+ }
156
+ },
157
+
93
158
  status(moduleName) {
94
159
  if (!moduleName) {
95
160
  return {
96
161
  loaded: Object.keys(this.modules),
97
- loading: [...this.process]
162
+ loading: [...this.process],
163
+ animations: Object.keys(animationModules),
164
+ utilities: Object.keys(utilityModules),
165
+ autoInit: Object.keys(autoInitModules)
98
166
  };
99
167
  }
100
168
  return {
@@ -103,20 +171,20 @@ const initializeHsMain = () => {
103
171
  };
104
172
  }
105
173
  };
106
-
174
+
107
175
  processModules(scriptTag);
108
-
176
+
109
177
  if (existingRequests.length > 0) {
110
178
  existingRequests.forEach(request => {
111
179
  if (typeof request === 'string') {
112
- window.hsMain.load(request);
180
+ window[API_NAME].load(request);
113
181
  }
114
182
  });
115
183
  }
116
184
  };
117
185
 
118
186
  if (document.readyState === 'loading') {
119
- document.addEventListener('DOMContentLoaded', initializeHsMain);
187
+ document.addEventListener('DOMContentLoaded', initializeAPI);
120
188
  } else {
121
- initializeHsMain();
189
+ initializeAPI();
122
190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
package/styles.css ADDED
@@ -0,0 +1,17 @@
1
+ /* transition */
2
+ body .transition {display: block}
3
+ .w-editor .transition {display: none;}
4
+ .no-scroll-transition {overflow: hidden; position: relative;}
5
+
6
+ /* splittext */
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;
17
+ }