@hortonstudio/main 1.9.6 → 1.9.8

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.
Files changed (32) hide show
  1. package/autoInit/accessibility/README.md +94 -0
  2. package/autoInit/accessibility/accessibility.js +52 -0
  3. package/autoInit/accessibility/functions/blog-remover/README.md +61 -0
  4. package/autoInit/accessibility/functions/blog-remover/blog-remover.js +31 -0
  5. package/autoInit/accessibility/functions/click-forwarding/README.md +60 -0
  6. package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +82 -0
  7. package/autoInit/accessibility/functions/dropdown/README.md +212 -0
  8. package/autoInit/accessibility/functions/dropdown/dropdown.js +167 -0
  9. package/autoInit/accessibility/functions/list-accessibility/README.md +56 -0
  10. package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +23 -0
  11. package/autoInit/accessibility/functions/text-synchronization/README.md +62 -0
  12. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +101 -0
  13. package/autoInit/accessibility/functions/toc/README.md +79 -0
  14. package/autoInit/accessibility/functions/toc/toc.js +191 -0
  15. package/autoInit/accessibility/functions/year-replacement/README.md +54 -0
  16. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +43 -0
  17. package/autoInit/button/README.md +122 -0
  18. package/autoInit/counter/README.md +274 -0
  19. package/autoInit/{counter.js → counter/counter.js} +20 -5
  20. package/autoInit/form/README.md +338 -0
  21. package/autoInit/{form.js → form/form.js} +44 -29
  22. package/autoInit/navbar/README.md +366 -0
  23. package/autoInit/site-settings/README.md +218 -0
  24. package/autoInit/smooth-scroll/README.md +386 -0
  25. package/autoInit/transition/README.md +301 -0
  26. package/autoInit/{transition.js → transition/transition.js} +13 -2
  27. package/index.js +7 -7
  28. package/package.json +1 -1
  29. package/autoInit/accessibility.js +0 -786
  30. /package/autoInit/{button.js → button/button.js} +0 -0
  31. /package/autoInit/{site-settings.js → site-settings/site-settings.js} +0 -0
  32. /package/autoInit/{smooth-scroll.js → smooth-scroll/smooth-scroll.js} +0 -0
@@ -0,0 +1,94 @@
1
+ # **Accessibility System Documentation**
2
+
3
+ ## **Overview**
4
+
5
+ The accessibility system provides 7 modular functions to enhance website accessibility and functionality. Each function operates independently and can be customized through data attributes.
6
+
7
+ **Note:** This module auto-initializes and loads all functions on page load.
8
+
9
+ ---
10
+
11
+ ## **Functions**
12
+
13
+ ### **1. Blog Remover**
14
+ Automatically removes blog wrapper elements that have no blog list content.
15
+
16
+ **Use case:** Clean up empty blog sections when using Webflow CMS conditional visibility.
17
+
18
+ ---
19
+
20
+ ### **2. List Accessibility**
21
+ Adds proper ARIA `role="list"` and `role="listitem"` to custom-styled lists.
22
+
23
+ **Use case:** Making non-semantic list layouts accessible to screen readers.
24
+
25
+ ---
26
+
27
+ ### **3. Year Replacement**
28
+ Replaces `{{year}}` and `{{month}}` placeholders with current year and month.
29
+
30
+ **Use case:** Auto-updating copyright years and date-based content.
31
+
32
+ ---
33
+
34
+ ### **4. Click Forwarding**
35
+ Forwards clicks from decorative wrapper elements to actual interactive trigger elements.
36
+
37
+ **Use case:** Making entire card areas clickable while maintaining semantic structure.
38
+
39
+ ---
40
+
41
+ ### **5. Text Synchronization**
42
+ Synchronizes text content and aria-labels from original element to multiple match elements in real-time.
43
+
44
+ **Use case:** Keeping duplicate content in sync across multiple locations.
45
+
46
+ ---
47
+
48
+ ### **6. Table of Contents (TOC)**
49
+ Automatically generates a table of contents from H2 headings with smooth scrolling, focus management, and active state tracking.
50
+
51
+ **Use case:** Auto-generated TOC navigation for blog posts and documentation pages.
52
+
53
+ ---
54
+
55
+ ### **7. Dropdown**
56
+ Universal dropdown system for FAQ, summary/read-more, and general toggle components. Syncs ARIA with Webflow interactions and optionally updates text content.
57
+
58
+ **Use case:** All dropdown/accordion/toggle patterns with unified ARIA management and optional text swapping.
59
+
60
+ **Note:** This function replaces the legacy `faq-accessibility` and `summary` functions with a single unified system.
61
+
62
+ ---
63
+
64
+ ## **Documentation**
65
+
66
+ Each function has detailed documentation in its respective folder:
67
+
68
+ - `functions/blog-remover/README.md`
69
+ - `functions/list-accessibility/README.md`
70
+ - `functions/year-replacement/README.md`
71
+ - `functions/click-forwarding/README.md`
72
+ - `functions/text-synchronization/README.md`
73
+ - `functions/toc/README.md`
74
+ - `functions/dropdown/README.md`
75
+
76
+ ---
77
+
78
+ ## **How It Works**
79
+
80
+ The accessibility system uses a dynamic loader pattern:
81
+
82
+ 1. Main `accessibility.js` imports all function modules
83
+ 2. Each function is loaded in parallel via `Promise.all()`
84
+ 3. All destroy functions are collected for cleanup
85
+ 4. On destroy, all functions are cleaned up properly
86
+
87
+ ---
88
+
89
+ ## **Notes**
90
+
91
+ - All functions auto-initialize on page load
92
+ - Each function operates independently
93
+ - Barba.js compatible with proper cleanup
94
+ - No configuration required - works with data attributes
@@ -0,0 +1,52 @@
1
+ export async function init() {
2
+ // Centralized cleanup tracking
3
+ const cleanup = {
4
+ modules: {},
5
+ destroyFunctions: []
6
+ };
7
+
8
+ const functionMap = {
9
+ "blog-remover": () => import("./functions/blog-remover/blog-remover.js"),
10
+ "list-accessibility": () => import("./functions/list-accessibility/list-accessibility.js"),
11
+ "year-replacement": () => import("./functions/year-replacement/year-replacement.js"),
12
+ "click-forwarding": () => import("./functions/click-forwarding/click-forwarding.js"),
13
+ "text-synchronization": () => import("./functions/text-synchronization/text-synchronization.js"),
14
+ "toc": () => import("./functions/toc/toc.js"),
15
+ "dropdown": () => import("./functions/dropdown/dropdown.js")
16
+ };
17
+
18
+ const loadFunction = async (functionName) => {
19
+ try {
20
+ const { init } = await functionMap[functionName]();
21
+ const result = await init();
22
+ cleanup.modules[functionName] = result;
23
+ if (result && result.destroy) {
24
+ cleanup.destroyFunctions.push(result.destroy);
25
+ }
26
+ return result;
27
+ } catch (error) {
28
+ console.error(`Failed to load accessibility function: ${functionName}`, error);
29
+ throw error;
30
+ }
31
+ };
32
+
33
+ // Load all functions
34
+ const functionPromises = Object.keys(functionMap).map(name => loadFunction(name));
35
+ await Promise.all(functionPromises);
36
+
37
+ return {
38
+ result: "accessibility initialized",
39
+ destroy: () => {
40
+ // Call all destroy functions
41
+ cleanup.destroyFunctions.forEach(destroyFn => {
42
+ try {
43
+ destroyFn();
44
+ } catch (error) {
45
+ console.error('Error during accessibility cleanup:', error);
46
+ }
47
+ });
48
+ cleanup.destroyFunctions.length = 0;
49
+ cleanup.modules = {};
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,61 @@
1
+ # **Blog Remover**
2
+
3
+ ## **Overview**
4
+
5
+ Automatically removes blog wrapper elements that have no blog list content. Useful for cleaning up empty blog sections when using Webflow CMS conditional visibility.
6
+
7
+ ---
8
+
9
+ ## **Required Elements**
10
+
11
+ **Blog Wrapper**
12
+ * data-site-blog="wrapper"
13
+ * data-site-blog-config="delete-if-no-list" (triggers deletion check)
14
+
15
+ **Blog List** *(descendant of wrapper)*
16
+ * data-site-blog="list"
17
+
18
+ ---
19
+
20
+ ## **What It Does**
21
+
22
+ 1. Finds all `[data-site-blog="wrapper"]` elements
23
+ 2. Checks if wrapper has `data-site-blog-config="delete-if-no-list"`
24
+ 3. If yes, checks for descendant with `[data-site-blog="list"]`
25
+ 4. If no list found, deletes the entire wrapper
26
+
27
+ ---
28
+
29
+ ## **Usage Example**
30
+
31
+ ```html
32
+ <!-- Will be deleted if no blog list inside -->
33
+ <div data-site-blog="wrapper" data-site-blog-config="delete-if-no-list">
34
+ <h2>Recent Posts</h2>
35
+ <!-- If Webflow CMS hides the list, wrapper gets removed -->
36
+ </div>
37
+
38
+ <!-- Will remain (no delete config) -->
39
+ <div data-site-blog="wrapper">
40
+ <h2>Recent Posts</h2>
41
+ <p>No posts yet.</p>
42
+ </div>
43
+ ```
44
+
45
+ ---
46
+
47
+ ## **Key Attributes**
48
+
49
+ | Attribute | Purpose |
50
+ | ----- | ----- |
51
+ | `data-site-blog="wrapper"` | Blog container |
52
+ | `data-site-blog-config="delete-if-no-list"` | Enable deletion if empty |
53
+ | `data-site-blog="list"` | Blog list (prevents deletion) |
54
+
55
+ ---
56
+
57
+ ## **Notes**
58
+
59
+ * One-time operation on page load
60
+ * No cleanup needed
61
+ * Barba.js compatible
@@ -0,0 +1,31 @@
1
+ export function init() {
2
+ function setupBlogListCleanup() {
3
+ const wrappers = document.querySelectorAll('[data-site-blog="wrapper"]');
4
+
5
+ wrappers.forEach(wrapper => {
6
+ // Check if wrapper has the delete-if-no-list config
7
+ const shouldDelete = wrapper.getAttribute('data-site-blog-config') === 'delete-if-no-list';
8
+
9
+ if (!shouldDelete) {
10
+ return;
11
+ }
12
+
13
+ // Check if there's a descendant with data-site-blog="list"
14
+ const hasList = wrapper.querySelector('[data-site-blog="list"]') !== null;
15
+
16
+ // Delete wrapper if it doesn't have a list
17
+ if (!hasList) {
18
+ wrapper.remove();
19
+ }
20
+ });
21
+ }
22
+
23
+ setupBlogListCleanup();
24
+
25
+ return {
26
+ result: "blog-remover initialized",
27
+ destroy: () => {
28
+ // No cleanup needed - this is a one-time DOM operation
29
+ }
30
+ };
31
+ }
@@ -0,0 +1,60 @@
1
+ # **Click Forwarding**
2
+
3
+ ## **Overview**
4
+
5
+ Forwards clicks from decorative/wrapper elements to actual interactive trigger elements. Useful for making entire card areas clickable while maintaining semantic button/link.
6
+
7
+ ---
8
+
9
+ ## **Required Elements**
10
+
11
+ **Clickable Element** *(wrapper users click)*
12
+ * data-hs-a11y="click-trigger-[identifier], clickable"
13
+
14
+ **Trigger Element** *(actual button/link)*
15
+ * data-hs-a11y="click-trigger-[identifier], trigger"
16
+
17
+ **Note:** `[identifier]` must match between clickable and trigger.
18
+
19
+ ---
20
+
21
+ ## **What It Does**
22
+
23
+ 1. Finds all clickable elements
24
+ 2. Matches them to trigger elements by identifier
25
+ 3. Forwards click events from clickable → trigger
26
+ 4. Adds keyboard support (Enter/Space)
27
+ 5. Sets `tabindex="0"` and `role="button"` on clickable
28
+
29
+ ---
30
+
31
+ ## **Usage Example**
32
+
33
+ ```html
34
+ <div data-hs-a11y="click-trigger-card1, clickable">
35
+ <h3>Card Title</h3>
36
+ <p>Card description...</p>
37
+ <button data-hs-a11y="click-trigger-card1, trigger">
38
+ Learn More
39
+ </button>
40
+ </div>
41
+ ```
42
+
43
+ **Result:** Clicking anywhere in the div triggers the button.
44
+
45
+ ---
46
+
47
+ ## **Key Attributes**
48
+
49
+ | Attribute | Purpose |
50
+ | ----- | ----- |
51
+ | `data-hs-a11y="click-trigger-[id], clickable"` | Wrapper to make clickable |
52
+ | `data-hs-a11y="click-trigger-[id], trigger"` | Actual button/link |
53
+
54
+ ---
55
+
56
+ ## **Notes**
57
+
58
+ * Identifier must match exactly
59
+ * Event listeners cleaned up on destroy
60
+ * Supports keyboard activation
@@ -0,0 +1,82 @@
1
+ export function init() {
2
+ const cleanup = {
3
+ handlers: []
4
+ };
5
+
6
+ const addHandler = (element, event, handler, options) => {
7
+ element.addEventListener(event, handler, options);
8
+ cleanup.handlers.push({ element, event, handler, options });
9
+ };
10
+
11
+ function setupClickForwarding(addHandler) {
12
+ // Find all clickable elements (custom styled elements users click)
13
+ const clickableElements = document.querySelectorAll('[data-hs-a11y*="clickable"]');
14
+
15
+ clickableElements.forEach(clickableElement => {
16
+ const attribute = clickableElement.getAttribute('data-hs-a11y');
17
+
18
+ // Parse the attribute: "click-trigger-[identifier], clickable"
19
+ const parts = attribute.split(',').map(part => part.trim());
20
+
21
+ // Find the part with click-trigger and the part with clickable
22
+ const triggerPart = parts.find(part => part.startsWith('click-trigger-'));
23
+ const rolePart = parts.find(part => part === 'clickable');
24
+
25
+ if (!triggerPart || !rolePart) {
26
+ return;
27
+ }
28
+
29
+ // Extract identifier from "click-trigger-[identifier]"
30
+ const identifier = triggerPart.replace('click-trigger-', '').trim();
31
+
32
+ // Find the corresponding trigger element
33
+ const triggerSelector = `[data-hs-a11y*="click-trigger-${identifier}"][data-hs-a11y*="trigger"]`;
34
+ const triggerElement = document.querySelector(triggerSelector);
35
+
36
+ if (!triggerElement) {
37
+ return;
38
+ }
39
+
40
+ // Add click event listener to forward clicks
41
+ const clickHandler = (event) => {
42
+ // Prevent default behavior on the clickable element
43
+ event.preventDefault();
44
+ event.stopPropagation();
45
+
46
+ // Trigger click on the target element
47
+ triggerElement.click();
48
+ };
49
+ addHandler(clickableElement, 'click', clickHandler);
50
+
51
+ // Also handle keyboard events for accessibility
52
+ const keydownHandler = (event) => {
53
+ if (event.key === 'Enter' || event.key === ' ') {
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ triggerElement.click();
57
+ }
58
+ };
59
+ addHandler(clickableElement, 'keydown', keydownHandler);
60
+
61
+ // Ensure clickable element is keyboard accessible
62
+ if (!clickableElement.hasAttribute('tabindex')) {
63
+ clickableElement.setAttribute('tabindex', '0');
64
+ }
65
+ if (!clickableElement.hasAttribute('role')) {
66
+ clickableElement.setAttribute('role', 'button');
67
+ }
68
+ });
69
+ }
70
+
71
+ setupClickForwarding(addHandler);
72
+
73
+ return {
74
+ result: "click-forwarding initialized",
75
+ destroy: () => {
76
+ cleanup.handlers.forEach(({ element, event, handler, options }) => {
77
+ element.removeEventListener(event, handler, options);
78
+ });
79
+ cleanup.handlers.length = 0;
80
+ }
81
+ };
82
+ }
@@ -0,0 +1,212 @@
1
+ # **Dropdown Accessibility**
2
+
3
+ ## **Overview**
4
+
5
+ Universal dropdown system for FAQ, summary/read-more, and general toggle components. Automatically syncs ARIA attributes with Webflow interactions and optionally updates text content.
6
+
7
+ **This function handles all dropdown/accordion/toggle patterns with a single unified system.**
8
+
9
+ ---
10
+
11
+ ## **Required Elements**
12
+
13
+ **Dropdown Wrapper**
14
+ * data-hs-dropdown="wrapper"
15
+ * data-hs-dropdown-text-swap="Close" (text for open state, defaults to "Close")
16
+
17
+ **Dropdown Toggle** *(gets is-active class from Webflow IX)*
18
+ * data-hs-dropdown="toggle"
19
+ * Contains clickable element
20
+
21
+ **Clickable Element**
22
+ * data-site-clickable="element"
23
+ * First child receives ARIA attributes
24
+
25
+ **Text Wrapper** *(optional - controls text update scope)*
26
+ * data-hs-dropdown="text"
27
+ * If present: all text inside swaps
28
+ * If absent: only aria-label updates (visual text stays static)
29
+
30
+ **Dropdown Content** *(expandable area)*
31
+ * data-hs-dropdown="content"
32
+
33
+ ---
34
+
35
+ ## **What It Does**
36
+
37
+ 1. **Monitors Toggle State**: MutationObserver watches for `is-active` class changes
38
+ 2. **Updates ARIA**: Sets `aria-expanded`, `aria-controls`, `aria-hidden`, `role="region"`
39
+ 3. **Text Updates**:
40
+ - **With text wrapper**: Swaps all text nodes + aria-label
41
+ - **Without text wrapper**: Only updates aria-label (visual stays static)
42
+ 4. **Focus Management**: Returns focus to toggle button when closing if focus is inside content
43
+ 5. **Supports Nesting**: Each dropdown manages its own state independently
44
+
45
+ ---
46
+
47
+ ## **Usage Examples**
48
+
49
+ ### **FAQ (No Visual Text Change)**
50
+
51
+ ```html
52
+ <div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Close FAQ">
53
+ <div data-hs-dropdown="toggle">
54
+ <div data-site-clickable="element">
55
+ <button>
56
+ <span class="u-sr-only">Open FAQ</span>
57
+ </button>
58
+ </div>
59
+ <h3>What is this product?</h3>
60
+ </div>
61
+ <div data-hs-dropdown="content">
62
+ <p>This is a detailed answer...</p>
63
+ </div>
64
+ </div>
65
+ ```
66
+
67
+ **Result:**
68
+ - Question text stays visible ✅
69
+ - Only aria-label swaps: "Open FAQ" ↔ "Close FAQ"
70
+ - Perfect for FAQ lists
71
+
72
+ ---
73
+
74
+ ### **Summary (Full Text Swap)**
75
+
76
+ ```html
77
+ <div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Close">
78
+ <div data-hs-dropdown="toggle">
79
+ <p>View Quick Summary</p>
80
+ <div data-hs-dropdown="text">
81
+ <div data-site-clickable="element">
82
+ <button>View</button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ <div data-hs-dropdown="content">
87
+ <p>Full summary content...</p>
88
+ </div>
89
+ </div>
90
+ ```
91
+
92
+ **Result:**
93
+ - All text in "text" wrapper swaps: "View" ↔ "Close"
94
+ - aria-label also swaps to match
95
+ - Perfect for read-more toggles
96
+
97
+ ---
98
+
99
+ ### **Product Details (No Text Wrapper)**
100
+
101
+ ```html
102
+ <div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Hide Details">
103
+ <div data-hs-dropdown="toggle">
104
+ <div data-site-clickable="element">
105
+ <button>Show Details</button>
106
+ </div>
107
+ </div>
108
+ <div data-hs-dropdown="content">
109
+ <p>Product specifications...</p>
110
+ </div>
111
+ </div>
112
+ ```
113
+
114
+ **Result:**
115
+ - Only aria-label swaps: "Show Details" ↔ "Hide Details"
116
+ - Visual button text stays static
117
+
118
+ ---
119
+
120
+ ### **Nested Dropdowns**
121
+
122
+ ```html
123
+ <div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Hide">
124
+ <div data-hs-dropdown="toggle">
125
+ <button>Product Details</button>
126
+ </div>
127
+ <div data-hs-dropdown="content">
128
+ <p>Product info...</p>
129
+
130
+ <!-- Nested FAQ -->
131
+ <div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Close FAQ">
132
+ <div data-hs-dropdown="toggle">
133
+ <button>Shipping Questions</button>
134
+ </div>
135
+ <div data-hs-dropdown="content">
136
+ <p>Shipping details...</p>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ ```
142
+
143
+ **Result:**
144
+ - Each dropdown manages its own state
145
+ - Focus returns to parent when parent closes
146
+ - Fully accessible keyboard navigation
147
+
148
+ ---
149
+
150
+ ## **Key Attributes**
151
+
152
+ | Attribute | Purpose |
153
+ | ----- | ----- |
154
+ | `data-hs-dropdown="wrapper"` | Container for entire dropdown |
155
+ | `data-hs-dropdown-text-swap` | Text for open state (default: "Close") |
156
+ | `data-hs-dropdown="toggle"` | Gets is-active from Webflow IX |
157
+ | `data-hs-dropdown="text"` | Optional text update scope |
158
+ | `data-site-clickable="element"` | Contains actual button/link |
159
+ | `data-hs-dropdown="content"` | Expandable content area |
160
+
161
+ ---
162
+
163
+ ## **How It Works**
164
+
165
+ 1. **Initial Setup**: Scans for all `[data-hs-dropdown="wrapper"]` elements
166
+ 2. **ID Generation**: Creates unique IDs for ARIA relationships
167
+ 3. **Text Capture**: Saves original text from text wrapper or clickable
168
+ 4. **State Detection**: Checks for existing `is-active` class on toggle
169
+ 5. **Observer Setup**: MutationObserver watches toggle for class changes
170
+ 6. **State Sync**: When `is-active` changes, updates ARIA and text
171
+ 7. **Focus Management**: Returns focus to button when closing if needed
172
+
173
+ ---
174
+
175
+ ## **Focus Management**
176
+
177
+ When any dropdown closes:
178
+ - If focus is anywhere inside content area
179
+ - Focus automatically returns to toggle button
180
+ - Then ARIA updates apply
181
+ - Prevents keyboard users from losing focus
182
+
183
+ Works for:
184
+ - Buttons inside content ✅
185
+ - Nested dropdowns ✅
186
+ - Form fields inside content ✅
187
+
188
+ ---
189
+
190
+ ## **Notes**
191
+
192
+ * Pattern matches navbar structure
193
+ * Works with Webflow IX for visual state (`is-active`)
194
+ * Text wrapper is optional - use for visual text changes
195
+ * Without text wrapper, only aria-label changes
196
+ * Supports infinite nesting depth
197
+ * Warns in console if multiple clickables found in toggle
198
+ * All observers cleaned up on destroy for Barba.js compatibility
199
+
200
+ ---
201
+
202
+ ## **Common Patterns**
203
+
204
+ ### **When to use text wrapper:**
205
+ - Summary/Read More toggles
206
+ - Buttons that should visually change
207
+ - Any toggle where text swap is visible
208
+
209
+ ### **When NOT to use text wrapper:**
210
+ - FAQ items (question should stay visible)
211
+ - Toggles with static visible labels
212
+ - Dropdowns where only accessible text should change