@hortonstudio/main 1.9.4 → 1.9.7

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 (41) hide show
  1. package/autoInit/accessibility/README.md +126 -0
  2. package/autoInit/accessibility/accessibility.js +56 -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/convert-to-span/README.md +59 -0
  8. package/autoInit/accessibility/functions/convert-to-span/convert-to-span.js +70 -0
  9. package/autoInit/accessibility/functions/custom-values-replacement/README.md +71 -0
  10. package/autoInit/accessibility/functions/custom-values-replacement/custom-values-replacement.js +102 -0
  11. package/autoInit/accessibility/functions/dropdown/README.md +212 -0
  12. package/autoInit/accessibility/functions/dropdown/dropdown.js +167 -0
  13. package/autoInit/accessibility/functions/list-accessibility/README.md +56 -0
  14. package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +23 -0
  15. package/autoInit/accessibility/functions/prevent-default/README.md +58 -0
  16. package/autoInit/accessibility/functions/prevent-default/prevent-default.js +58 -0
  17. package/autoInit/accessibility/functions/remove-list-accessibility/README.md +57 -0
  18. package/autoInit/accessibility/functions/remove-list-accessibility/remove-list-accessibility.js +68 -0
  19. package/autoInit/accessibility/functions/text-synchronization/README.md +62 -0
  20. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +101 -0
  21. package/autoInit/accessibility/functions/toc/README.md +79 -0
  22. package/autoInit/accessibility/functions/toc/toc.js +191 -0
  23. package/autoInit/accessibility/functions/year-replacement/README.md +54 -0
  24. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +43 -0
  25. package/autoInit/button/README.md +122 -0
  26. package/autoInit/counter/README.md +274 -0
  27. package/autoInit/{counter.js → counter/counter.js} +20 -5
  28. package/autoInit/form/README.md +338 -0
  29. package/autoInit/{form.js → form/form.js} +44 -29
  30. package/autoInit/navbar/README.md +366 -0
  31. package/autoInit/{navbar.js → navbar/navbar.js} +128 -118
  32. package/autoInit/site-settings/README.md +218 -0
  33. package/autoInit/smooth-scroll/README.md +386 -0
  34. package/autoInit/transition/README.md +301 -0
  35. package/autoInit/{transition.js → transition/transition.js} +13 -2
  36. package/index.js +8 -8
  37. package/package.json +1 -1
  38. package/autoInit/accessibility.js +0 -786
  39. /package/autoInit/{button.js → button/button.js} +0 -0
  40. /package/autoInit/{site-settings.js → site-settings/site-settings.js} +0 -0
  41. /package/autoInit/{smooth-scroll.js → smooth-scroll/smooth-scroll.js} +0 -0
@@ -0,0 +1,126 @@
1
+ # **Accessibility System Documentation**
2
+
3
+ ## **Overview**
4
+
5
+ The accessibility system provides 11 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. Remove List Accessibility**
28
+ Removes list semantics by stripping ARIA roles and converting `<ul>/<ol>/<li>` to divs.
29
+
30
+ **Use case:** Decorative list layouts that shouldn't be announced as lists.
31
+
32
+ ---
33
+
34
+ ### **4. Convert to Span**
35
+ Converts most HTML elements to span elements while preserving attributes and content.
36
+
37
+ **Use case:** Removing semantic meaning from purely decorative wrapper elements.
38
+
39
+ ---
40
+
41
+ ### **5. Year Replacement**
42
+ Replaces `{{year}}` and `{{month}}` placeholders with current year and month.
43
+
44
+ **Use case:** Auto-updating copyright years and date-based content.
45
+
46
+ ---
47
+
48
+ ### **6. Prevent Default**
49
+ Prevents default behavior on elements including clicks and keyboard activation.
50
+
51
+ **Use case:** Decorative buttons or preventing anchor link scrolling.
52
+
53
+ ---
54
+
55
+ ### **7. Custom Values Replacement**
56
+ Collects custom name-value pairs and replaces `{{name}}` placeholders throughout the page.
57
+
58
+ **Use case:** Dynamic content replacement for user-defined values.
59
+
60
+ ---
61
+
62
+ ### **8. Click Forwarding**
63
+ Forwards clicks from decorative wrapper elements to actual interactive trigger elements.
64
+
65
+ **Use case:** Making entire card areas clickable while maintaining semantic structure.
66
+
67
+ ---
68
+
69
+ ### **9. Text Synchronization**
70
+ Synchronizes text content and aria-labels from original element to multiple match elements in real-time.
71
+
72
+ **Use case:** Keeping duplicate content in sync across multiple locations.
73
+
74
+ ---
75
+
76
+ ### **10. Table of Contents (TOC)**
77
+ Automatically generates a table of contents from H2 headings with smooth scrolling, focus management, and active state tracking.
78
+
79
+ **Use case:** Auto-generated TOC navigation for blog posts and documentation pages.
80
+
81
+ ---
82
+
83
+ ### **11. Dropdown**
84
+ Universal dropdown system for FAQ, summary/read-more, and general toggle components. Syncs ARIA with Webflow interactions and optionally updates text content.
85
+
86
+ **Use case:** All dropdown/accordion/toggle patterns with unified ARIA management and optional text swapping.
87
+
88
+ **Note:** This function replaces the legacy `faq-accessibility` and `summary` functions with a single unified system.
89
+
90
+ ---
91
+
92
+ ## **Documentation**
93
+
94
+ Each function has detailed documentation in its respective folder:
95
+
96
+ - `functions/blog-remover/README.md`
97
+ - `functions/list-accessibility/README.md`
98
+ - `functions/remove-list-accessibility/README.md`
99
+ - `functions/convert-to-span/README.md`
100
+ - `functions/year-replacement/README.md`
101
+ - `functions/prevent-default/README.md`
102
+ - `functions/custom-values-replacement/README.md`
103
+ - `functions/click-forwarding/README.md`
104
+ - `functions/text-synchronization/README.md`
105
+ - `functions/toc/README.md`
106
+ - `functions/dropdown/README.md`
107
+
108
+ ---
109
+
110
+ ## **How It Works**
111
+
112
+ The accessibility system uses a dynamic loader pattern:
113
+
114
+ 1. Main `accessibility.js` imports all function modules
115
+ 2. Each function is loaded in parallel via `Promise.all()`
116
+ 3. All destroy functions are collected for cleanup
117
+ 4. On destroy, all functions are cleaned up properly
118
+
119
+ ---
120
+
121
+ ## **Notes**
122
+
123
+ - All functions auto-initialize on page load
124
+ - Each function operates independently
125
+ - Barba.js compatible with proper cleanup
126
+ - No configuration required - works with data attributes
@@ -0,0 +1,56 @@
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
+ "remove-list-accessibility": () => import("./functions/remove-list-accessibility/remove-list-accessibility.js"),
12
+ "convert-to-span": () => import("./functions/convert-to-span/convert-to-span.js"),
13
+ "year-replacement": () => import("./functions/year-replacement/year-replacement.js"),
14
+ "prevent-default": () => import("./functions/prevent-default/prevent-default.js"),
15
+ "custom-values-replacement": () => import("./functions/custom-values-replacement/custom-values-replacement.js"),
16
+ "click-forwarding": () => import("./functions/click-forwarding/click-forwarding.js"),
17
+ "text-synchronization": () => import("./functions/text-synchronization/text-synchronization.js"),
18
+ "toc": () => import("./functions/toc/toc.js"),
19
+ "dropdown": () => import("./functions/dropdown/dropdown.js")
20
+ };
21
+
22
+ const loadFunction = async (functionName) => {
23
+ try {
24
+ const { init } = await functionMap[functionName]();
25
+ const result = await init();
26
+ cleanup.modules[functionName] = result;
27
+ if (result && result.destroy) {
28
+ cleanup.destroyFunctions.push(result.destroy);
29
+ }
30
+ return result;
31
+ } catch (error) {
32
+ console.error(`Failed to load accessibility function: ${functionName}`, error);
33
+ throw error;
34
+ }
35
+ };
36
+
37
+ // Load all functions
38
+ const functionPromises = Object.keys(functionMap).map(name => loadFunction(name));
39
+ await Promise.all(functionPromises);
40
+
41
+ return {
42
+ result: "accessibility initialized",
43
+ destroy: () => {
44
+ // Call all destroy functions
45
+ cleanup.destroyFunctions.forEach(destroyFn => {
46
+ try {
47
+ destroyFn();
48
+ } catch (error) {
49
+ console.error('Error during accessibility cleanup:', error);
50
+ }
51
+ });
52
+ cleanup.destroyFunctions.length = 0;
53
+ cleanup.modules = {};
54
+ }
55
+ };
56
+ }
@@ -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,59 @@
1
+ # **Convert to Span**
2
+
3
+ ## **Overview**
4
+
5
+ Converts most HTML elements to span elements while preserving attributes and content. Useful for removing semantic meaning from decorative elements.
6
+
7
+ ---
8
+
9
+ ## **Required Elements**
10
+
11
+ **Container**
12
+ * data-hs-a11y="convert-span"
13
+
14
+ ---
15
+
16
+ ## **What It Does**
17
+
18
+ 1. Finds all `[data-hs-a11y="convert-span"]` containers
19
+ 2. Converts all descendant elements to `<span>` (except skip list)
20
+ 3. Converts container itself to `<span>`
21
+ 4. Preserves all attributes except `data-hs-a11y`
22
+
23
+ **Skip List:** `span`, `a`, `button`, `input`, `textarea`, `select`, `img`, `video`, `audio`, `iframe`, `object`, `embed`, `canvas`, `svg`, `form`, `table`, semantic tags, headings, `script`, `style`
24
+
25
+ ---
26
+
27
+ ## **Usage Example**
28
+
29
+ ```html
30
+ <!-- Before -->
31
+ <div data-hs-a11y="convert-span">
32
+ <p>Text here</p>
33
+ <section>Content</section>
34
+ </div>
35
+
36
+ <!-- After -->
37
+ <span>
38
+ <span>Text here</span>
39
+ <span>Content</span>
40
+ </span>
41
+ ```
42
+
43
+ **Result:** All elements become spans, removing semantic meaning.
44
+
45
+ ---
46
+
47
+ ## **Key Attributes**
48
+
49
+ | Attribute | Purpose |
50
+ | ----- | ----- |
51
+ | `data-hs-a11y="convert-span"` | Container to convert |
52
+
53
+ ---
54
+
55
+ ## **Notes**
56
+
57
+ * One-time DOM transformation
58
+ * Skips interactive and media elements
59
+ * Use for purely decorative wrappers
@@ -0,0 +1,70 @@
1
+ export function init() {
2
+ function setupConvertToSpan() {
3
+ const containers = document.querySelectorAll('[data-hs-a11y="convert-span"]');
4
+
5
+ containers.forEach(container => {
6
+ const skipTags = [
7
+ 'span', 'a', 'button', 'input', 'textarea', 'select', 'img', 'video', 'audio',
8
+ 'iframe', 'object', 'embed', 'canvas', 'svg', 'form', 'table', 'thead', 'tbody',
9
+ 'tr', 'td', 'th', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4',
10
+ 'h5', 'h6', 'script', 'style', 'link', 'meta', 'title', 'head', 'html', 'body'
11
+ ];
12
+
13
+ // Convert all child elements first
14
+ const elementsToConvert = container.querySelectorAll('*');
15
+
16
+ elementsToConvert.forEach(element => {
17
+ const tagName = element.tagName.toLowerCase();
18
+
19
+ if (!skipTags.includes(tagName)) {
20
+ const newSpan = document.createElement('span');
21
+
22
+ // Copy all attributes except data-hs-a11y
23
+ Array.from(element.attributes).forEach(attr => {
24
+ if (attr.name !== 'data-hs-a11y') {
25
+ newSpan.setAttribute(attr.name, attr.value);
26
+ }
27
+ });
28
+
29
+ // Move all child nodes
30
+ while (element.firstChild) {
31
+ newSpan.appendChild(element.firstChild);
32
+ }
33
+
34
+ // Replace the element
35
+ element.parentNode.replaceChild(newSpan, element);
36
+ }
37
+ });
38
+
39
+ // Convert the container itself to span
40
+ const containerTagName = container.tagName.toLowerCase();
41
+ if (!skipTags.includes(containerTagName)) {
42
+ const newSpan = document.createElement('span');
43
+
44
+ // Copy all attributes except data-hs-a11y
45
+ Array.from(container.attributes).forEach(attr => {
46
+ if (attr.name !== 'data-hs-a11y') {
47
+ newSpan.setAttribute(attr.name, attr.value);
48
+ }
49
+ });
50
+
51
+ // Move all child nodes
52
+ while (container.firstChild) {
53
+ newSpan.appendChild(container.firstChild);
54
+ }
55
+
56
+ // Replace the container
57
+ container.parentNode.replaceChild(newSpan, container);
58
+ }
59
+ });
60
+ }
61
+
62
+ setupConvertToSpan();
63
+
64
+ return {
65
+ result: "convert-to-span initialized",
66
+ destroy: () => {
67
+ // No cleanup needed - this is a one-time DOM operation
68
+ }
69
+ };
70
+ }
@@ -0,0 +1,71 @@
1
+ # **Custom Values Replacement**
2
+
3
+ ## **Overview**
4
+
5
+ Collects custom name-value pairs from a list and replaces `{{name}}` placeholders throughout the page. Similar to site-settings but for user-defined values.
6
+
7
+ ---
8
+
9
+ ## **Required Elements**
10
+
11
+ **Custom Values List**
12
+ * data-hs-a11y="custom-values-list"
13
+
14
+ **Name Element** *(within list descendants)*
15
+ * data-hs-a11y="custom-values-name"
16
+
17
+ **Value Element** *(within list descendants)*
18
+ * data-hs-a11y="custom-values-value"
19
+
20
+ ---
21
+
22
+ ## **What It Does**
23
+
24
+ 1. Finds custom values list
25
+ 2. Collects name-value pairs from descendants
26
+ 3. Replaces `{{name}}` placeholders in all text nodes
27
+ 4. Replaces `{{name}}` placeholders in link hrefs
28
+
29
+ ---
30
+
31
+ ## **Usage Example**
32
+
33
+ ```html
34
+ <div data-hs-a11y="custom-values-list" style="display: none;">
35
+ <div>
36
+ <span data-hs-a11y="custom-values-name">support-email</span>
37
+ <span data-hs-a11y="custom-values-value">help@example.com</span>
38
+ </div>
39
+ <div>
40
+ <span data-hs-a11y="custom-values-name">phone</span>
41
+ <span data-hs-a11y="custom-values-value">555-1234</span>
42
+ </div>
43
+ </div>
44
+
45
+ <p>Contact us at {{support-email}} or call {{phone}}</p>
46
+ <a href="mailto:{{support-email}}">Email Us</a>
47
+ ```
48
+
49
+ **Result:**
50
+ ```html
51
+ <p>Contact us at help@example.com or call 555-1234</p>
52
+ <a href="mailto:help@example.com">Email Us</a>
53
+ ```
54
+
55
+ ---
56
+
57
+ ## **Key Attributes**
58
+
59
+ | Attribute | Purpose |
60
+ | ----- | ----- |
61
+ | `data-hs-a11y="custom-values-list"` | Container for values |
62
+ | `data-hs-a11y="custom-values-name"` | Placeholder name |
63
+ | `data-hs-a11y="custom-values-value"` | Replacement value |
64
+
65
+ ---
66
+
67
+ ## **Notes**
68
+
69
+ * One-time text replacement
70
+ * List should be hidden with CSS
71
+ * No cleanup needed