@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,203 @@
1
+ export const init = () => {
2
+ // Find all dropdown wrappers
3
+ const dropdownWrappers = document.querySelectorAll('[data-hs-nav-dropdown="wrapper"]');
4
+
5
+ // Global array to track all dropdown instances
6
+ const allDropdowns = [];
7
+
8
+ // Function to close all dropdowns except the specified one
9
+ const closeAllDropdowns = (exceptWrapper = null) => {
10
+ allDropdowns.forEach(dropdown => {
11
+ if (dropdown.wrapper !== exceptWrapper && dropdown.isOpen) {
12
+ dropdown.closeDropdown();
13
+ }
14
+ });
15
+ };
16
+
17
+ dropdownWrappers.forEach(wrapper => {
18
+ const animationDuration = 0.3;
19
+
20
+ // Find elements within this wrapper
21
+ const toggle = wrapper.querySelector('a'); // the toggle link
22
+ const list = wrapper.querySelector('[data-hs-nav-dropdown="list"]');
23
+ const contain = list.querySelector('[data-hs-nav-dropdown="container"]');
24
+ const arrow = toggle.querySelector('[data-hs-nav-dropdown="arrow"]');
25
+ const text = toggle.querySelector('[data-hs-nav-dropdown="text"]'); // find the text element
26
+
27
+ // Set initial states with GSAP
28
+ gsap.set(contain, { yPercent: -110 });
29
+ gsap.set(list, { display: 'none' });
30
+ gsap.set(arrow, { rotation: 0, scale: 1, x: 0, color: '' });
31
+ gsap.set(text, { scale: 1, color: '' }); // empty string = default color
32
+
33
+ // Track if dropdown is open
34
+ let isOpen = false;
35
+ let currentTimeline = null;
36
+
37
+ // Open animation
38
+ function openDropdown() {
39
+ if (isOpen) return;
40
+
41
+ // Kill any existing timeline
42
+ if (currentTimeline) {
43
+ currentTimeline.kill();
44
+ }
45
+
46
+ // Close all other dropdowns first
47
+ closeAllDropdowns(wrapper);
48
+
49
+ isOpen = true;
50
+
51
+ // Update ARIA states
52
+ toggle.setAttribute('aria-expanded', 'true');
53
+ list.setAttribute('aria-hidden', 'false');
54
+
55
+ // GSAP animation
56
+ currentTimeline = gsap.timeline();
57
+ currentTimeline.set(list, { display: 'flex' })
58
+ .to(contain, {
59
+ yPercent: 0,
60
+ duration: animationDuration,
61
+ ease: 'ease'
62
+ }, 0)
63
+ .to(arrow, {
64
+ rotation: 90,
65
+ scale: 1.2,
66
+ x: 4,
67
+ color: 'var(--swatch--brand)',
68
+ duration: animationDuration,
69
+ ease: 'ease'
70
+ }, 0)
71
+ .to(text, {
72
+ scale: 1.1,
73
+ color: 'var(--swatch--brand)',
74
+ duration: animationDuration,
75
+ ease: 'ease'
76
+ }, 0);
77
+ }
78
+
79
+ // Close animation
80
+ function closeDropdown() {
81
+ if (!isOpen) return;
82
+
83
+ // Kill any existing timeline
84
+ if (currentTimeline) {
85
+ currentTimeline.kill();
86
+ }
87
+
88
+ // Check if focus should be restored to toggle
89
+ const shouldRestoreFocus = list.contains(document.activeElement);
90
+
91
+ isOpen = false;
92
+ currentMenuItemIndex = -1;
93
+
94
+ // Update ARIA states
95
+ toggle.setAttribute('aria-expanded', 'false');
96
+ list.setAttribute('aria-hidden', 'true');
97
+
98
+ // GSAP animation
99
+ currentTimeline = gsap.timeline();
100
+ currentTimeline.to(contain, {
101
+ yPercent: -110,
102
+ duration: animationDuration,
103
+ ease: 'ease'
104
+ }, 0)
105
+ .to(arrow, {
106
+ rotation: 0,
107
+ scale: 1,
108
+ x: 0,
109
+ color: '', // back to default color
110
+ duration: animationDuration,
111
+ ease: 'ease'
112
+ }, 0)
113
+ .to(text, {
114
+ scale: 1,
115
+ color: '', // back to default color
116
+ duration: animationDuration,
117
+ ease: 'ease'
118
+ }, 0)
119
+ .set(list, { display: 'none' });
120
+
121
+ // Restore focus to toggle only if focus was inside dropdown
122
+ if (shouldRestoreFocus) {
123
+ toggle.focus();
124
+ }
125
+ }
126
+
127
+ // Get all menu items for navigation
128
+ const menuItems = list.querySelectorAll('a, button, [role="menuitem"]');
129
+ let currentMenuItemIndex = -1;
130
+
131
+ // Hover events
132
+ toggle.addEventListener('mouseenter', openDropdown);
133
+ wrapper.addEventListener('mouseleave', closeDropdown);
134
+
135
+ // Arrow key navigation within dropdown
136
+ list.addEventListener('keydown', function(e) {
137
+ if (!isOpen) return;
138
+
139
+ if (e.key === 'ArrowDown') {
140
+ e.preventDefault();
141
+ currentMenuItemIndex = (currentMenuItemIndex + 1) % menuItems.length;
142
+ menuItems[currentMenuItemIndex].focus();
143
+ } else if (e.key === 'ArrowUp') {
144
+ e.preventDefault();
145
+ currentMenuItemIndex = currentMenuItemIndex <= 0 ? menuItems.length - 1 : currentMenuItemIndex - 1;
146
+ menuItems[currentMenuItemIndex].focus();
147
+ } else if (e.key === 'Escape') {
148
+ e.preventDefault();
149
+ closeDropdown();
150
+ toggle.focus();
151
+ }
152
+ });
153
+
154
+ // Keyboard events for toggle
155
+ toggle.addEventListener('keydown', function(e) {
156
+ if (e.key === 'ArrowDown') {
157
+ e.preventDefault();
158
+ openDropdown();
159
+ // Focus first menu item after opening
160
+ if (menuItems.length > 0) {
161
+ currentMenuItemIndex = 0;
162
+ setTimeout(() => menuItems[0].focus(), 50);
163
+ }
164
+ } else if (e.key === ' ') {
165
+ e.preventDefault();
166
+ // Simple toggle: if closed open, if open close
167
+ if (isOpen) {
168
+ closeDropdown();
169
+ } else {
170
+ openDropdown();
171
+ }
172
+ } else if (e.key === 'ArrowUp' || e.key === 'Escape') {
173
+ e.preventDefault();
174
+ closeDropdown();
175
+ }
176
+ });
177
+
178
+ // Close dropdown when clicking outside
179
+ document.addEventListener('click', function(e) {
180
+ if (!wrapper.contains(e.target) && isOpen) {
181
+ closeDropdown();
182
+ }
183
+ });
184
+
185
+ // Add this dropdown instance to the global array
186
+ allDropdowns.push({
187
+ wrapper,
188
+ isOpen: () => isOpen,
189
+ closeDropdown
190
+ });
191
+ });
192
+
193
+ // Global focus management - close dropdown when tab focus moves outside
194
+ document.addEventListener('focusin', function(e) {
195
+ allDropdowns.forEach(dropdown => {
196
+ if (dropdown.isOpen() && !dropdown.wrapper.contains(e.target)) {
197
+ dropdown.closeDropdown();
198
+ }
199
+ });
200
+ });
201
+
202
+ return { result: 'navbar initialized' };
203
+ };
@@ -0,0 +1,29 @@
1
+ export async function init() {
2
+ const progressBar = document.querySelector('[data-hs-progress="bar"]');
3
+ const progressContent = document.querySelector('[data-hs-progress="wrapper"]');
4
+
5
+ // Check if elements exist before using them
6
+ if (!progressBar || !progressContent) {
7
+ return {
8
+ result: 'util-scroll-progress initialized'
9
+ };
10
+ }
11
+
12
+ gsap.set(progressBar, { width: "0%" });
13
+
14
+ // Create the scroll progress animation
15
+ gsap.to(progressBar, {
16
+ width: "100%",
17
+ ease: "none",
18
+ scrollTrigger: {
19
+ trigger: progressContent,
20
+ start: "top bottom",
21
+ end: "bottom bottom",
22
+ scrub: true
23
+ }
24
+ });
25
+
26
+ return {
27
+ result: 'util-scroll-progress initialized'
28
+ };
29
+ }
package/utils/toc.js ADDED
@@ -0,0 +1,54 @@
1
+ export async function init() {
2
+ const contentArea = document.querySelector('[data-hs-toc="content"]');
3
+ const tocList = document.querySelector('[data-hs-toc="list"]');
4
+
5
+ // Check main elements
6
+ if (!contentArea) { return; }
7
+ if (!tocList) { return; }
8
+ if (tocList.children.length === 0) { return; }
9
+
10
+ const template = tocList.children[0];
11
+ tocList.innerHTML = '';
12
+ const h2Headings = contentArea.querySelectorAll('h2');
13
+
14
+ // Create sections and wrap content
15
+ h2Headings.forEach((heading, index) => {
16
+ const sectionId = heading.textContent.toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/(^-|-$)/g, '');
19
+ const section = document.createElement('div');
20
+ section.id = sectionId;
21
+ heading.parentNode.insertBefore(section, heading);
22
+ section.appendChild(heading);
23
+ let nextElement = section.nextElementSibling;
24
+ while (nextElement && nextElement.tagName !== 'H2') {
25
+ const elementToMove = nextElement;
26
+ nextElement = nextElement.nextElementSibling;
27
+ section.appendChild(elementToMove);
28
+ }
29
+ });
30
+
31
+ // Create TOC entries
32
+ h2Headings.forEach((heading, index) => {
33
+ const tocItem = template.cloneNode(true);
34
+ const link = tocItem.querySelector('a');
35
+ const sectionId = heading.parentElement.id;
36
+ link.href = '#' + sectionId;
37
+
38
+ // Bold numbered text
39
+ const number = document.createElement('strong');
40
+ number.textContent = (index + 1) + '. ';
41
+
42
+ // Clear the link and add the number + text
43
+ link.innerHTML = '';
44
+ link.appendChild(number);
45
+ link.appendChild(document.createTextNode(heading.textContent));
46
+
47
+ // Add item to the TOC list
48
+ tocList.appendChild(tocItem);
49
+ });
50
+
51
+ return {
52
+ result: 'util-toc initialized'
53
+ };
54
+ }
package/animations/READ DELETED
File without changes
@@ -1,154 +0,0 @@
1
- // Hero Animations Module
2
- let heroTimeline = null;
3
- let headingSplits = [];
4
-
5
- export async function init() {
6
-
7
- // Check for GSAP
8
- if (typeof window.gsap === "undefined") {
9
- console.error('GSAP not found - hero animations disabled');
10
- return;
11
- }
12
-
13
- // Register plugins
14
- gsap.registerPlugin(ScrollTrigger, SplitText);
15
-
16
- // Variable to control when animation starts (in seconds)
17
- const heroAnimationDelay = 0.2;
18
-
19
- // SET INITIAL STATES IMMEDIATELY
20
- gsap.set('[hero-anim="announce"]', { opacity: 0, yPercent: -100 });
21
- gsap.set('[hero-anim="nav-container"]', { opacity: 0, yPercent: -100 });
22
- gsap.set('[hero-anim="subheading"]', { y: 50, opacity: 0 });
23
- gsap.set('[hero-anim="button"]', { y: 50, opacity: 0 });
24
- gsap.set('[hero-anim="image"]', { opacity: 0 });
25
- gsap.set('[hero-anim="appear1"]', { y: 50, opacity: 0 });
26
- gsap.set('[hero-anim="appear2"]', { y: 50, opacity: 0 });
27
- gsap.set('[hero-anim="appear3"]', { y: 50, opacity: 0 });
28
- gsap.set('[hero-anim="appear4"]', { y: 50, opacity: 0 });
29
-
30
- // SET UP SPLIT TEXT WITH MASKS
31
- const headingElements = document.querySelectorAll('[hero-anim="heading"]');
32
- headingSplits = []; // Reset array
33
-
34
- headingElements.forEach(heading => {
35
- const split = new SplitText(heading, {
36
- type: "words",
37
- mask: "words",
38
- wordsClass: "word"
39
- });
40
- headingSplits.push(split);
41
-
42
- // Set initial state for split text
43
- gsap.set(split.words, { yPercent: 110 });
44
- gsap.set(heading, { autoAlpha: 1 });
45
- });
46
-
47
- // Wait for fonts to load, then start animation
48
- document.fonts.ready.then(() => {
49
-
50
- setTimeout(() => {
51
- // Create main timeline
52
- heroTimeline = gsap.timeline();
53
-
54
- // 0s - Announce
55
- heroTimeline.to('[hero-anim="announce"]',
56
- { opacity: 1, yPercent: 0, duration: 1, ease: 'power3.out' },
57
- 0
58
- );
59
-
60
- // 0.05s - Nav Container
61
- heroTimeline.to('[hero-anim="nav-container"]',
62
- { opacity: 1, yPercent: 0, duration: 1, ease: 'power3.out' },
63
- 0.05
64
- );
65
-
66
- // 0.15s - Heading (Split Text with masks)
67
- headingSplits.forEach(split => {
68
- heroTimeline.to(split.words,
69
- { yPercent: 0, duration: 1.25, stagger: 0.1, ease: 'power4.out' },
70
- 0.15
71
- );
72
- });
73
-
74
- // 0.25s - Subheading
75
- heroTimeline.to('[hero-anim="subheading"]',
76
- { y: 0, opacity: 1, duration: 1.5, ease: 'power3.out' },
77
- 0.25
78
- );
79
-
80
- // 0.35s - Button
81
- heroTimeline.to('[hero-anim="button"]',
82
- { y: 0, opacity: 1, duration: 1.5, ease: 'power3.out' },
83
- 0.35
84
- );
85
-
86
- // 0.35s - Image
87
- heroTimeline.to('[hero-anim="image"]',
88
- { opacity: 1, duration: 1.25, ease: 'power3.out' },
89
- 0.35
90
- );
91
-
92
- // 0.45s - Appear1
93
- heroTimeline.to('[hero-anim="appear1"]',
94
- { y: 0, opacity: 1, duration: 1.5, ease: 'power3.out' },
95
- 0.45
96
- );
97
-
98
- // 0.5s - Appear2
99
- heroTimeline.to('[hero-anim="appear2"]',
100
- { y: 0, opacity: 1, duration: 1.5, ease: 'power3.out' },
101
- 0.5
102
- );
103
-
104
- // 0.55s - Appear3
105
- heroTimeline.to('[hero-anim="appear3"]',
106
- { y: 0, opacity: 1, duration: 1.5, ease: 'power3.out' },
107
- 0.55
108
- );
109
-
110
- // 0.55s - Appear4
111
- heroTimeline.to('[hero-anim="appear4"]',
112
- { y: 0, opacity: 1, duration: 1.5, ease: 'power3.out' },
113
- 0.55
114
- );
115
-
116
- }, heroAnimationDelay * 1000);
117
-
118
- });
119
-
120
- return { result: 'hero-main initialized' };
121
- }
122
-
123
- export function destroy() {
124
- console.log('🧹 Hero animations destroyed');
125
-
126
- // Kill the main timeline
127
- if (heroTimeline) {
128
- heroTimeline.kill();
129
- heroTimeline = null;
130
- }
131
-
132
- // Revert all SplitText instances
133
- headingSplits.forEach(split => {
134
- if (split && split.revert) {
135
- split.revert();
136
- }
137
- });
138
- headingSplits = [];
139
-
140
- // Kill any ScrollTriggers (in case there are any)
141
- ScrollTrigger.getAll().forEach(trigger => trigger.kill());
142
-
143
- // Reset elements to normal state
144
- gsap.set('[hero-anim="announce"]', { clearProps: "all" });
145
- gsap.set('[hero-anim="nav-container"]', { clearProps: "all" });
146
- gsap.set('[hero-anim="subheading"]', { clearProps: "all" });
147
- gsap.set('[hero-anim="button"]', { clearProps: "all" });
148
- gsap.set('[hero-anim="image"]', { clearProps: "all" });
149
- gsap.set('[hero-anim="appear1"]', { clearProps: "all" });
150
- gsap.set('[hero-anim="appear2"]', { clearProps: "all" });
151
- gsap.set('[hero-anim="appear3"]', { clearProps: "all" });
152
- gsap.set('[hero-anim="appear4"]', { clearProps: "all" });
153
- gsap.set('[hero-anim="heading"]', { clearProps: "all" });
154
- }
@@ -1,165 +0,0 @@
1
- let activeAnimations = [];
2
- let resizeTimeout;
3
- let lastWidth = window.innerWidth;
4
-
5
- function waitForFonts() {
6
- return document.fonts.ready;
7
- }
8
-
9
- function checkGSAP() {
10
- if (typeof window.gsap === "undefined") {
11
- document.documentElement.classList.add("gsap-not-found");
12
- return false;
13
- }
14
- gsap.registerPlugin(ScrollTrigger, SplitText);
15
- return true;
16
- }
17
-
18
- const WordSplitAnimations = {
19
- async initial() {
20
- await waitForFonts();
21
-
22
- document.querySelectorAll(".a-word-split").forEach((text) => {
23
- if (text.splitTextInstance) {
24
- text.splitTextInstance.revert();
25
- }
26
- });
27
-
28
- document.querySelectorAll(".a-word-split").forEach((text) => {
29
- const split = SplitText.create(text, {
30
- type: "words",
31
- mask: "words",
32
- wordsClass: "word",
33
- });
34
- text.splitTextInstance = split;
35
- gsap.set(split.words, {
36
- yPercent: 110
37
- });
38
- gsap.set(text, { autoAlpha: 1 });
39
- });
40
- },
41
-
42
- async animate() {
43
- await waitForFonts();
44
-
45
- document.querySelectorAll(".a-word-split").forEach((text) => {
46
- const words = text.querySelectorAll('.word');
47
- const tl = gsap.timeline({
48
- scrollTrigger: {
49
- trigger: text,
50
- start: "top 95%",
51
- invalidateOnRefresh: true,
52
- },
53
- });
54
-
55
- tl.to(words, {
56
- yPercent: 0,
57
- duration: 1.25,
58
- stagger: .1,
59
- ease: "power4.out",
60
- });
61
-
62
- activeAnimations.push({ timeline: tl, element: text });
63
- });
64
-
65
- ScrollTrigger.refresh();
66
- }
67
- };
68
-
69
- const AppearAnimations = {
70
- async initial() {
71
- await waitForFonts();
72
-
73
- const elements = document.querySelectorAll('.a-appear');
74
- elements.forEach(element => {
75
- gsap.set(element, {
76
- y: 50,
77
- opacity: 0
78
- });
79
- });
80
- },
81
-
82
- async animate() {
83
- await waitForFonts();
84
-
85
- document.querySelectorAll('.a-appear').forEach(element => {
86
- const tl = gsap.timeline({
87
- scrollTrigger: {
88
- trigger: element,
89
- start: 'top 95%',
90
- invalidateOnRefresh: true,
91
- }
92
- });
93
-
94
- tl.to(element, {
95
- y: 0,
96
- opacity: 1,
97
- duration: 1.5,
98
- ease: 'power3.out'
99
- });
100
-
101
- activeAnimations.push({ timeline: tl, element: element });
102
- });
103
-
104
- ScrollTrigger.refresh();
105
- }
106
- };
107
-
108
- async function setInitialStates() {
109
- if (!checkGSAP()) return;
110
-
111
- await Promise.all([
112
- WordSplitAnimations.initial(),
113
- AppearAnimations.initial()
114
- ]);
115
- }
116
-
117
- async function initAnimations() {
118
- if (!checkGSAP()) return;
119
-
120
- await Promise.all([
121
- WordSplitAnimations.animate(),
122
- AppearAnimations.animate()
123
- ]);
124
- }
125
-
126
- function handleResize() {
127
- clearTimeout(resizeTimeout);
128
- resizeTimeout = setTimeout(() => {
129
- const currentWidth = window.innerWidth;
130
-
131
- if (Math.abs(currentWidth - lastWidth) > 10) {
132
- lastWidth = currentWidth;
133
-
134
- setTimeout(() => {
135
- setInitialStates();
136
- setTimeout(() => {
137
- initAnimations();
138
- }, 100);
139
- }, 50);
140
- }
141
- }, 150);
142
- }
143
-
144
- export async function init() {
145
- if (!checkGSAP()) return;
146
-
147
- await setInitialStates();
148
-
149
- setTimeout(() => {
150
- initAnimations();
151
- }, 750);
152
-
153
- window.addEventListener('resize', handleResize);
154
- window.hsMain.textResizeHandler = handleResize;
155
-
156
- return {
157
- result: 'text-main initialized',
158
- reinit: () => {
159
- setInitialStates();
160
- setTimeout(() => {
161
- initAnimations();
162
- }, 100);
163
- }
164
- };
165
- }
package/utils/READ DELETED
File without changes
File without changes