docyard 0.7.0 → 0.9.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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +43 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +85 -12
  8. data/lib/docyard/builder.rb +6 -6
  9. data/lib/docyard/components/aliases.rb +12 -0
  10. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  11. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  12. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  13. data/lib/docyard/components/processors/callout_processor.rb +8 -2
  14. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
  16. data/lib/docyard/components/processors/code_block_processor.rb +6 -0
  17. data/lib/docyard/components/processors/code_group_processor.rb +198 -0
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
  19. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  20. data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
  21. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  22. data/lib/docyard/components/processors/include_processor.rb +86 -0
  23. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  24. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  25. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  26. data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
  27. data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
  28. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  29. data/lib/docyard/config/branding_resolver.rb +121 -17
  30. data/lib/docyard/config/constants.rb +6 -4
  31. data/lib/docyard/config/logo_detector.rb +39 -0
  32. data/lib/docyard/config/validator.rb +122 -99
  33. data/lib/docyard/config.rb +40 -42
  34. data/lib/docyard/initializer.rb +15 -76
  35. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  36. data/lib/docyard/navigation/prev_next_builder.rb +4 -1
  37. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  38. data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
  39. data/lib/docyard/navigation/sidebar/file_resolver.rb +90 -0
  40. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
  41. data/lib/docyard/navigation/sidebar/item.rb +50 -7
  42. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  43. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +71 -0
  44. data/lib/docyard/navigation/sidebar/metadata_reader.rb +51 -0
  45. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  46. data/lib/docyard/navigation/sidebar/renderer.rb +60 -38
  47. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  48. data/lib/docyard/navigation/sidebar/tree_builder.rb +100 -26
  49. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  50. data/lib/docyard/navigation/sidebar_builder.rb +105 -36
  51. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  52. data/lib/docyard/rendering/icons/phosphor.rb +26 -1
  53. data/lib/docyard/rendering/markdown.rb +29 -1
  54. data/lib/docyard/rendering/renderer.rb +75 -34
  55. data/lib/docyard/rendering/template_resolver.rb +172 -0
  56. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  57. data/lib/docyard/search/build_indexer.rb +1 -1
  58. data/lib/docyard/search/dev_indexer.rb +51 -6
  59. data/lib/docyard/search/pagefind_support.rb +2 -0
  60. data/lib/docyard/server/asset_handler.rb +25 -19
  61. data/lib/docyard/server/pagefind_handler.rb +63 -0
  62. data/lib/docyard/server/preview_server.rb +1 -1
  63. data/lib/docyard/server/rack_application.rb +81 -64
  64. data/lib/docyard/templates/assets/css/code.css +18 -51
  65. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  66. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  67. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  68. data/lib/docyard/templates/assets/css/components/banner.css +202 -0
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  72. data/lib/docyard/templates/assets/css/components/code-block.css +190 -282
  73. data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
  74. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  75. data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
  76. data/lib/docyard/templates/assets/css/components/heading-anchor.css +36 -15
  77. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  78. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  79. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  80. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  81. data/lib/docyard/templates/assets/css/components/navigation.css +193 -167
  82. data/lib/docyard/templates/assets/css/components/prev-next.css +68 -48
  83. data/lib/docyard/templates/assets/css/components/search.css +186 -174
  84. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  85. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  86. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  87. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  88. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  89. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  90. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  91. data/lib/docyard/templates/assets/css/landing.css +815 -0
  92. data/lib/docyard/templates/assets/css/layout.css +489 -87
  93. data/lib/docyard/templates/assets/css/main.css +1 -3
  94. data/lib/docyard/templates/assets/css/markdown.css +113 -93
  95. data/lib/docyard/templates/assets/css/reset.css +0 -3
  96. data/lib/docyard/templates/assets/css/typography.css +43 -41
  97. data/lib/docyard/templates/assets/css/variables.css +268 -208
  98. data/lib/docyard/templates/assets/favicon.svg +7 -8
  99. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  100. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  101. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  102. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  103. data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
  104. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  105. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  106. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  107. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  108. data/lib/docyard/templates/assets/js/components/search.js +0 -75
  109. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  110. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  111. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  112. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  113. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  114. data/lib/docyard/templates/assets/js/theme.js +0 -3
  115. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  116. data/lib/docyard/templates/assets/logo.svg +7 -4
  117. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  118. data/lib/docyard/templates/errors/404.html.erb +1 -1
  119. data/lib/docyard/templates/errors/500.html.erb +1 -1
  120. data/lib/docyard/templates/layouts/default.html.erb +19 -67
  121. data/lib/docyard/templates/layouts/splash.html.erb +177 -0
  122. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  123. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  124. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  125. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  126. data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
  127. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  129. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  130. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  131. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  132. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  133. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  134. data/lib/docyard/templates/partials/_nav_group.html.erb +31 -11
  135. data/lib/docyard/templates/partials/_nav_leaf.html.erb +4 -1
  136. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  137. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  138. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  139. data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
  140. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  141. data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
  142. data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
  143. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  144. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  145. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  146. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  147. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  148. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  149. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  150. data/lib/docyard/version.rb +1 -1
  151. metadata +70 -5
  152. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  153. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  154. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  155. data/lib/docyard/templates/markdown/index.md.erb +0 -82
@@ -1,6 +1,3 @@
1
- // Docyard Navigation JavaScript
2
- // Handles sidebar navigation, mobile menu, accordion groups, and scroll behavior
3
-
4
1
  (function() {
5
2
  'use strict';
6
3
 
@@ -13,18 +10,36 @@
13
10
  return;
14
11
  }
15
12
 
13
+ var scrollPosition = 0;
14
+
15
+ function lockBodyScroll() {
16
+ scrollPosition = window.pageYOffset;
17
+ document.body.style.overflow = 'hidden';
18
+ document.body.style.position = 'fixed';
19
+ document.body.style.top = -scrollPosition + 'px';
20
+ document.body.style.width = '100%';
21
+ }
22
+
23
+ function unlockBodyScroll() {
24
+ document.body.style.removeProperty('overflow');
25
+ document.body.style.removeProperty('position');
26
+ document.body.style.removeProperty('top');
27
+ document.body.style.removeProperty('width');
28
+ window.scrollTo(0, scrollPosition);
29
+ }
30
+
16
31
  function openMenu() {
32
+ lockBodyScroll();
17
33
  sidebar.classList.add('is-open');
18
34
  overlay.classList.add('is-visible');
19
35
  toggle.setAttribute('aria-expanded', 'true');
20
- document.body.style.overflow = 'hidden';
21
36
  }
22
37
 
23
38
  function closeMenu() {
24
39
  sidebar.classList.remove('is-open');
25
40
  overlay.classList.remove('is-visible');
26
41
  toggle.setAttribute('aria-expanded', 'false');
27
- document.body.style.overflow = '';
42
+ unlockBodyScroll();
28
43
  }
29
44
 
30
45
  function toggleMenu() {
@@ -52,24 +67,86 @@
52
67
  }
53
68
 
54
69
  function initAccordion() {
55
- const toggles = document.querySelectorAll('.nav-group-toggle');
70
+ var TOGGLE_STATE_KEY = 'docyard_toggle_states';
71
+ var toggles = document.querySelectorAll('[data-nav-toggle]');
72
+
73
+ function getDefaultCollapsed(navGroup) {
74
+ return navGroup.getAttribute('data-default-collapsed') === 'true';
75
+ }
76
+
77
+ function collapseGroup(navGroup, animate) {
78
+ var header = navGroup.querySelector('[data-nav-toggle]');
79
+ var children = navGroup.querySelector('.nav-group-children');
80
+ if (!header || !children) return;
81
+
82
+ if (animate) {
83
+ children.style.transition = 'max-height 0.2s cubic-bezier(0.4, 0, 1, 1)';
84
+ } else {
85
+ children.style.transition = 'none';
86
+ }
87
+ header.setAttribute('aria-expanded', 'false');
88
+ children.classList.add('collapsed');
89
+ children.style.maxHeight = '0';
90
+ }
91
+
92
+ function expandGroup(navGroup, animate) {
93
+ var header = navGroup.querySelector('[data-nav-toggle]');
94
+ var children = navGroup.querySelector('.nav-group-children');
95
+ if (!header || !children) return;
96
+
97
+ var fullHeight = children.scrollHeight;
98
+ if (animate) {
99
+ children.style.transition = 'max-height 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)';
100
+ } else {
101
+ children.style.transition = 'none';
102
+ }
103
+ header.setAttribute('aria-expanded', 'true');
104
+ children.classList.remove('collapsed');
105
+ children.style.maxHeight = fullHeight + 'px';
106
+ }
107
+
108
+ function revertOthersToDefault(currentNavGroup) {
109
+ document.querySelectorAll('.nav-group').forEach(function(navGroup) {
110
+ if (navGroup === currentNavGroup) return;
111
+
112
+ var defaultCollapsed = getDefaultCollapsed(navGroup);
113
+ var children = navGroup.querySelector('.nav-group-children');
114
+ if (!children) return;
115
+
116
+ var isCurrentlyCollapsed = children.classList.contains('collapsed');
117
+
118
+ if (defaultCollapsed && !isCurrentlyCollapsed) {
119
+ collapseGroup(navGroup, true);
120
+ } else if (!defaultCollapsed && isCurrentlyCollapsed) {
121
+ expandGroup(navGroup, true);
122
+ }
123
+ });
124
+ }
56
125
 
57
126
  toggles.forEach(function(toggle) {
58
- toggle.addEventListener('click', function() {
59
- const expanded = toggle.getAttribute('aria-expanded') === 'true';
60
- const children = toggle.nextElementSibling;
127
+ toggle.addEventListener('click', function(e) {
128
+ var expanded = toggle.getAttribute('aria-expanded') === 'true';
129
+ var navGroup = toggle.closest('.nav-group');
130
+ var children = navGroup ? navGroup.querySelector('.nav-group-children') : null;
61
131
 
62
- if (!children || !children.classList.contains('nav-group-children')) {
132
+ if (!children) {
63
133
  return;
64
134
  }
65
135
 
66
- toggle.setAttribute('aria-expanded', !expanded);
67
- children.classList.toggle('collapsed');
136
+ revertOthersToDefault(navGroup);
137
+
138
+ if (toggle.tagName === 'A') {
139
+ var href = toggle.getAttribute('href');
140
+ var states = {};
141
+ states[href] = true;
142
+ sessionStorage.setItem(TOGGLE_STATE_KEY, JSON.stringify(states));
143
+ return;
144
+ }
68
145
 
69
146
  if (expanded) {
70
- children.style.maxHeight = '0';
147
+ collapseGroup(navGroup, true);
71
148
  } else {
72
- children.style.maxHeight = children.scrollHeight + 'px';
149
+ expandGroup(navGroup, true);
73
150
  }
74
151
  });
75
152
  });
@@ -80,32 +157,80 @@
80
157
  }
81
158
 
82
159
  function expandActiveGroups() {
83
- const activeLinks = document.querySelectorAll('a.active');
160
+ var TOGGLE_STATE_KEY = 'docyard_toggle_states';
161
+ var currentUrl = window.location.pathname;
162
+ var lastUrl = sessionStorage.getItem('docyard_last_url') || '';
163
+ var toggleStates = JSON.parse(sessionStorage.getItem(TOGGLE_STATE_KEY) || '{}');
84
164
 
85
- activeLinks.forEach(function(activeLink) {
86
- let parent = activeLink.closest('.nav-group-children');
165
+ sessionStorage.setItem('docyard_last_url', currentUrl);
166
+ sessionStorage.removeItem(TOGGLE_STATE_KEY);
87
167
 
88
- while (parent) {
89
- if (parent.classList.contains('nav-group-children')) {
90
- parent.classList.remove('collapsed');
168
+ document.querySelectorAll('[data-nav-toggle]').forEach(function(toggle) {
169
+ if (toggle.tagName !== 'A') return;
91
170
 
92
- const toggle = parent.previousElementSibling;
93
- if (toggle && toggle.classList.contains('nav-group-toggle')) {
94
- toggle.setAttribute('aria-expanded', 'true');
171
+ var href = toggle.getAttribute('href');
172
+ var shouldOpen = toggleStates[href] === true;
95
173
 
96
- parent.style.maxHeight = 'none';
97
- const height = parent.scrollHeight;
98
- parent.style.maxHeight = height + 'px';
99
- }
100
- }
174
+ if (!shouldOpen) return;
175
+
176
+ var navGroup = toggle.closest('.nav-group');
177
+ var children = navGroup ? navGroup.querySelector('.nav-group-children') : null;
178
+
179
+ if (!children || !children.classList.contains('collapsed')) return;
101
180
 
102
- parent = parent.parentElement?.closest('.nav-group-children');
181
+ var fullHeight = children.scrollHeight;
182
+
183
+ children.style.transition = 'none';
184
+ children.style.maxHeight = '0';
185
+ children.offsetHeight;
186
+
187
+ children.style.transition = 'max-height 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)';
188
+ requestAnimationFrame(function() {
189
+ toggle.setAttribute('aria-expanded', 'true');
190
+ children.classList.remove('collapsed');
191
+ children.style.maxHeight = fullHeight + 'px';
192
+ });
193
+ });
194
+
195
+ var expandedGroups = document.querySelectorAll('.nav-group-children:not(.collapsed)');
196
+
197
+ expandedGroups.forEach(function(group) {
198
+ if (group.style.maxHeight) {
199
+ return;
200
+ }
201
+
202
+ var navGroup = group.closest('.nav-group');
203
+ var header = navGroup ? navGroup.querySelector('.nav-group-header') : null;
204
+ var headerHref = header && header.tagName === 'A' ? header.getAttribute('href') : null;
205
+
206
+ if (headerHref && toggleStates[headerHref] === true) {
207
+ group.style.maxHeight = group.scrollHeight + 'px';
208
+ return;
209
+ }
210
+
211
+ var wasInGroup = headerHref && (lastUrl === headerHref || lastUrl.startsWith(headerHref + '/'));
212
+ var shouldAnimate = header && header.classList.contains('active') && !wasInGroup;
213
+ var fullHeight = group.scrollHeight;
214
+
215
+ if (shouldAnimate) {
216
+ group.style.transition = 'none';
217
+ group.style.maxHeight = '0';
218
+ group.classList.add('collapsed');
219
+ group.offsetHeight;
220
+
221
+ group.style.transition = 'max-height 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)';
222
+ requestAnimationFrame(function() {
223
+ group.classList.remove('collapsed');
224
+ group.style.maxHeight = fullHeight + 'px';
225
+ });
226
+ } else {
227
+ group.style.maxHeight = fullHeight + 'px';
103
228
  }
104
229
  });
105
230
  }
106
231
 
107
232
  function initSidebarScroll() {
108
- const scrollContainer = document.querySelector('.sidebar nav');
233
+ const scrollContainer = document.querySelector('.sidebar-scroll');
109
234
  if (!scrollContainer) return;
110
235
 
111
236
  const STORAGE_KEY = 'docyard_sidebar_scroll';
@@ -153,54 +278,38 @@
153
278
  }
154
279
  }
155
280
 
156
- function initScrollBehavior() {
157
- const header = document.querySelector('.header');
158
- const secondaryHeader = document.querySelector('.secondary-header');
159
-
160
- if (!header || !secondaryHeader) return;
161
-
162
- let lastScrollTop = 0;
163
- let ticking = false;
281
+ function initScrollFadeIndicators() {
282
+ const sidebar = document.querySelector('.sidebar');
283
+ const scrollContainer = document.querySelector('.sidebar-scroll');
284
+ if (!sidebar || !scrollContainer) return;
164
285
 
165
- function isMobile() {
166
- return window.innerWidth <= 1024;
167
- }
286
+ function updateFadeIndicators() {
287
+ const scrollTop = scrollContainer.scrollTop;
288
+ const scrollHeight = scrollContainer.scrollHeight;
289
+ const clientHeight = scrollContainer.clientHeight;
290
+ const threshold = 10;
168
291
 
169
- function updateHeaders() {
170
- if (!isMobile()) {
171
- header.classList.remove('hide-on-scroll');
172
- secondaryHeader.classList.remove('shift-up');
173
- ticking = false;
174
- return;
292
+ if (scrollTop > threshold) {
293
+ sidebar.classList.add('can-scroll-top');
294
+ } else {
295
+ sidebar.classList.remove('can-scroll-top');
175
296
  }
176
297
 
177
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
178
-
179
- if (scrollTop > lastScrollTop && scrollTop > 100) {
180
- header.classList.add('hide-on-scroll');
181
- secondaryHeader.classList.add('shift-up');
182
- } else if (scrollTop < lastScrollTop) {
183
- header.classList.remove('hide-on-scroll');
184
- secondaryHeader.classList.remove('shift-up');
298
+ if (scrollTop + clientHeight < scrollHeight - threshold) {
299
+ sidebar.classList.add('can-scroll-bottom');
300
+ } else {
301
+ sidebar.classList.remove('can-scroll-bottom');
185
302
  }
186
-
187
- lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
188
- ticking = false;
189
303
  }
190
304
 
191
- window.addEventListener('scroll', function() {
192
- if (!ticking) {
193
- window.requestAnimationFrame(updateHeaders);
194
- ticking = true;
195
- }
196
- });
305
+ updateFadeIndicators();
197
306
 
198
- window.addEventListener('resize', function() {
199
- if (!isMobile()) {
200
- header.classList.remove('hide-on-scroll');
201
- secondaryHeader.classList.remove('shift-up');
202
- }
203
- });
307
+ scrollContainer.addEventListener('scroll', updateFadeIndicators);
308
+
309
+ window.addEventListener('resize', updateFadeIndicators);
310
+ }
311
+
312
+ function initScrollBehavior() {
204
313
  }
205
314
 
206
315
  if ('scrollRestoration' in history) {
@@ -213,6 +322,7 @@
213
322
  initAccordion();
214
323
  expandActiveGroups();
215
324
  initSidebarScroll();
325
+ initScrollFadeIndicators();
216
326
  initScrollBehavior();
217
327
  });
218
328
  } else {
@@ -220,6 +330,7 @@
220
330
  initAccordion();
221
331
  expandActiveGroups();
222
332
  initSidebarScroll();
333
+ initScrollFadeIndicators();
223
334
  initScrollBehavior();
224
335
  }
225
336
  })();
@@ -1,14 +1,3 @@
1
- /**
2
- * SearchManager - Handles search functionality with Pagefind integration
3
- *
4
- * Features:
5
- * - Cmd+K / Ctrl+K keyboard shortcut
6
- * - Lazy loading of Pagefind
7
- * - Keyboard navigation in results
8
- * - Debounced search input
9
- *
10
- * @class SearchManager
11
- */
12
1
  class SearchManager {
13
2
  constructor() {
14
3
  this.modal = document.querySelector('[data-search-modal]');
@@ -32,7 +21,6 @@ class SearchManager {
32
21
  this.DEBOUNCE_DELAY = 150;
33
22
  this.RESULTS_PER_PAGE = 6;
34
23
 
35
- // State for "load more" functionality
36
24
  this.allSearchResults = [];
37
25
  this.displayedCount = 0;
38
26
  this.currentQuery = '';
@@ -52,36 +40,29 @@ class SearchManager {
52
40
  }
53
41
 
54
42
  attachEventListeners() {
55
- // Global keyboard shortcut
56
43
  document.addEventListener('keydown', this.handleKeyDown);
57
44
 
58
- // Trigger button
59
45
  if (this.trigger) {
60
46
  this.trigger.addEventListener('click', () => this.open());
61
47
  }
62
48
 
63
- // Backdrop click to close
64
49
  if (this.backdrop) {
65
50
  this.backdrop.addEventListener('click', () => this.close());
66
51
  }
67
52
 
68
- // Close button
69
53
  if (this.closeButton) {
70
54
  this.closeButton.addEventListener('click', () => this.close());
71
55
  }
72
56
 
73
- // Clear button
74
57
  if (this.clearButton) {
75
58
  this.clearButton.addEventListener('click', () => this.clearSearch());
76
59
  }
77
60
 
78
- // Search input
79
61
  if (this.input) {
80
62
  this.input.addEventListener('input', this.handleInput);
81
63
  this.input.addEventListener('keydown', (e) => this.handleInputKeyDown(e));
82
64
  }
83
65
 
84
- // Results click delegation
85
66
  if (this.resultsContainer) {
86
67
  this.resultsContainer.addEventListener('click', this.handleResultClick);
87
68
  }
@@ -99,21 +80,18 @@ class SearchManager {
99
80
  }
100
81
 
101
82
  handleKeyDown(event) {
102
- // Cmd+K or Ctrl+K to open
103
83
  if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
104
84
  event.preventDefault();
105
85
  this.toggle();
106
86
  return;
107
87
  }
108
88
 
109
- // Escape to close
110
89
  if (event.key === 'Escape' && this.isOpen) {
111
90
  event.preventDefault();
112
91
  this.close();
113
92
  return;
114
93
  }
115
94
 
116
- // Forward slash to focus search (when not in an input)
117
95
  if (event.key === '/' && !this.isOpen && !this.isInputFocused()) {
118
96
  event.preventDefault();
119
97
  this.open();
@@ -140,12 +118,10 @@ class SearchManager {
140
118
  handleInput(event) {
141
119
  const query = event.target.value.trim();
142
120
 
143
- // Update clear button visibility
144
121
  if (this.clearButton) {
145
122
  this.clearButton.hidden = query.length === 0;
146
123
  }
147
124
 
148
- // Debounce search
149
125
  if (this.searchTimeout) {
150
126
  clearTimeout(this.searchTimeout);
151
127
  }
@@ -195,10 +171,8 @@ class SearchManager {
195
171
  this.modal.hidden = false;
196
172
  document.body.style.overflow = 'hidden';
197
173
 
198
- // Trigger animation on next frame
199
174
  requestAnimationFrame(() => {
200
175
  this.modal.classList.add('is-open');
201
- // Double rAF ensures focus works after CSS transition starts
202
176
  requestAnimationFrame(() => {
203
177
  if (this.input) {
204
178
  this.input.focus();
@@ -206,7 +180,6 @@ class SearchManager {
206
180
  });
207
181
  });
208
182
 
209
- // Initialize Pagefind if not already done
210
183
  if (!this.pagefind) {
211
184
  await this.initPagefind();
212
185
  }
@@ -220,18 +193,15 @@ class SearchManager {
220
193
  document.body.style.overflow = '';
221
194
  this.selectedIndex = -1;
222
195
 
223
- // Hide modal after animation completes
224
196
  setTimeout(() => {
225
197
  if (!this.isOpen) {
226
198
  this.modal.hidden = true;
227
- // Reset body visibility for next open
228
199
  if (this.body) {
229
200
  this.body.hidden = true;
230
201
  }
231
202
  }
232
203
  }, 200);
233
204
 
234
- // Return focus to trigger
235
205
  if (this.trigger) {
236
206
  this.trigger.focus();
237
207
  }
@@ -274,13 +244,11 @@ class SearchManager {
274
244
  return;
275
245
  }
276
246
 
277
- // Store state for "load more"
278
247
  this.allSearchResults = searchResults.results;
279
248
  this.currentQuery = query;
280
249
  this.displayedCount = 0;
281
250
  this.groupedResults = [];
282
251
 
283
- // Load initial batch
284
252
  await this.loadMoreResults();
285
253
  } catch (error) {
286
254
  console.error('Search error:', error);
@@ -294,17 +262,14 @@ class SearchManager {
294
262
 
295
263
  if (startIndex >= this.allSearchResults.length) return;
296
264
 
297
- // Load the next batch of results
298
265
  const resultsData = await Promise.all(
299
266
  this.allSearchResults.slice(startIndex, endIndex).map(r => r.data())
300
267
  );
301
268
 
302
- // Group results by page with sections nested
303
269
  const newGrouped = this.groupResults(resultsData);
304
270
  this.groupedResults = [...this.groupedResults, ...newGrouped];
305
271
  this.displayedCount = endIndex;
306
272
 
307
- // Flatten for keyboard navigation
308
273
  this.results = this.flattenForNavigation(this.groupedResults);
309
274
  this.renderGroupedResults(this.groupedResults, this.currentQuery, this.allSearchResults.length);
310
275
  }
@@ -320,14 +285,11 @@ class SearchManager {
320
285
  for (const result of resultsData) {
321
286
  const pageTitle = result.meta?.title || this.extractTitleFromUrl(result.url);
322
287
 
323
- // Get sub-results (sections) for this page
324
- // Filter out sections with same title as page (H1 heading duplicates)
325
288
  const subResults = (result.sub_results || [])
326
289
  .filter(sub => {
327
290
  if (sub.url === result.url) return false;
328
291
  if (!sub.title) return false;
329
292
  const cleanedTitle = this.cleanSectionTitle(sub.title);
330
- // Skip if section title matches page title
331
293
  if (cleanedTitle.toLowerCase() === pageTitle.toLowerCase()) return false;
332
294
  return true;
333
295
  })
@@ -352,7 +314,6 @@ class SearchManager {
352
314
  }
353
315
 
354
316
  cleanSectionTitle(title) {
355
- // Remove trailing # that Pagefind sometimes includes
356
317
  return title.replace(/#$/, '').trim();
357
318
  }
358
319
 
@@ -389,7 +350,6 @@ class SearchManager {
389
350
  return this.renderPageResult(page, pageIndex, isPageSelected, sectionsHtml, query);
390
351
  }).join('');
391
352
 
392
- // Add "View more results" link if there are more results
393
353
  const hasMore = this.displayedCount < totalResults;
394
354
  const loadMoreHtml = hasMore ? `
395
355
  <li class="search-load-more">
@@ -401,7 +361,6 @@ class SearchManager {
401
361
 
402
362
  this.resultsContainer.innerHTML = resultsHtml + loadMoreHtml;
403
363
 
404
- // Attach event listener to "View more" button
405
364
  const loadMoreBtn = this.resultsContainer.querySelector('[data-search-load-more]');
406
365
  if (loadMoreBtn) {
407
366
  loadMoreBtn.addEventListener('click', this.handleLoadMore);
@@ -409,7 +368,6 @@ class SearchManager {
409
368
  }
410
369
 
411
370
  renderPageResult(page, index, isSelected, sectionsHtml, query) {
412
- // Article icon (Phosphor)
413
371
  const pageIcon = `<svg class="search-result-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor">
414
372
  <path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM184,96a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,96Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,128Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,160Z"></path>
415
373
  </svg>`;
@@ -473,7 +431,6 @@ class SearchManager {
473
431
  const terms = query.trim().split(/\s+/).filter(t => t.length > 1);
474
432
  if (terms.length === 0) return escaped;
475
433
 
476
- // Match exact search term only (like Stripe does)
477
434
  const regex = new RegExp(`(${terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
478
435
  .join('|')})`, 'gi');
479
436
  return escaped.replace(regex, '<mark class="search-title-highlight">$1</mark>');
@@ -482,30 +439,22 @@ class SearchManager {
482
439
  highlightQuery(text, query, title = '') {
483
440
  if (!query || !text) return this.escapeHtml(text);
484
441
 
485
- // Decode HTML entities first (Pagefind may return encoded HTML)
486
442
  let cleanText = this.decodeHtmlEntities(text);
487
443
 
488
- // Strip all HTML tags
489
444
  cleanText = cleanText.replace(/<[^>]*>/g, '');
490
445
 
491
- // Remove the title if it appears at the start of the excerpt (Pagefind often includes it)
492
446
  if (title) {
493
- // Remove "Title#" or "Title:" patterns at the start
494
447
  const titlePattern = new RegExp(`^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[#:]?\\s*`, 'i');
495
448
  cleanText = cleanText.replace(titlePattern, '');
496
449
  }
497
450
 
498
- // Clean markdown and special characters
499
451
  cleanText = this.cleanMarkdown(cleanText);
500
452
 
501
- // Escape for safe HTML
502
453
  const escaped = this.escapeHtml(cleanText);
503
454
 
504
- // Highlight exact search term only (like Stripe does)
505
455
  const terms = query.trim().split(/\s+/).filter(t => t.length > 1);
506
456
  if (terms.length === 0) return escaped;
507
457
 
508
- // Match exact search term only
509
458
  const regex = new RegExp(`(${terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
510
459
  .join('|')})`, 'gi');
511
460
  return escaped.replace(regex, '<mark>$1</mark>');
@@ -519,51 +468,33 @@ class SearchManager {
519
468
 
520
469
  cleanMarkdown(text) {
521
470
  return text
522
- // Remove code block markers with optional language/title (```ruby, ```js title="foo")
523
471
  .replace(/```\w*(?:\s+[^`\n]*)?\n?/g, '')
524
472
  .replace(/```/g, '')
525
- // Remove Kramdown/Jekyll directives like {:/nomarkdown}, {::nomarkdown}, {:.class}
526
473
  .replace(/\{:?:?[^}]*\}/g, '')
527
- // Remove heading anchors like "Dark Mode#" or "Styling#" (word followed by #)
528
474
  .replace(/(\w)#(?=\s|$|[A-Z])/g, '$1')
529
- // Remove markdown bold/italic
530
475
  .replace(/\*\*([^*]+)\*\*/g, '$1')
531
476
  .replace(/\*([^*]+)\*/g, '$1')
532
477
  .replace(/__([^_]+)__/g, '$1')
533
478
  .replace(/_([^_]+)_/g, '$1')
534
- // Remove markdown links [text](url)
535
479
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
536
- // Remove inline code backticks
537
480
  .replace(/`([^`]+)`/g, '$1')
538
- // Remove standalone backticks
539
481
  .replace(/`/g, '')
540
- // Remove heading markers
541
482
  .replace(/^#+\s*/gm, '')
542
- // Remove title="..." and similar attributes
543
483
  .replace(/\s*title=["'][^"']*["']/gi, '')
544
- // Remove URLs (http, https, ftp)
545
484
  .replace(/https?:\/\/[^\s<>"{}|\\^`[\]]+/gi, '')
546
485
  .replace(/ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi, '')
547
- // Remove YAML-like patterns (key: value)
548
486
  .replace(/\b\w+:\s*["']?[^"'\s,]+["']?(?=\s|,|$)/g, '')
549
- // Remove common code syntax patterns
550
487
  .replace(/\b(const|let|var|function|interface|class|import|export|return|if|else)\b/g, '')
551
488
  .replace(/[=;{}()<>[\]]/g, ' ')
552
- // Remove common unicode symbols
553
489
  .replace(/[✓✔✗✘→←↑↓•·►▸▹▶]/g, '')
554
- // Remove YAML-like frontmatter patterns
555
490
  .replace(/^---[\s\S]*?---/m, '')
556
- // Clean up navigation/menu text patterns
557
491
  .replace(/Skip to main content/gi, '')
558
492
  .replace(/On this page/gi, '')
559
493
  .replace(/Menu/gi, '')
560
494
  .replace(/Search\.\.\./gi, '')
561
- // Remove list markers
562
495
  .replace(/^[\s]*[-*+]\s+/gm, '')
563
496
  .replace(/^[\s]*\d+\.\s+/gm, '')
564
- // Clean up excessive whitespace
565
497
  .replace(/\s+/g, ' ')
566
- // Remove leading/trailing punctuation
567
498
  .replace(/^[.\s,;:]+/, '')
568
499
  .replace(/[.\s,;:]+$/, '')
569
500
  .trim();
@@ -586,12 +517,10 @@ class SearchManager {
586
517
  updateSelection(newIndex) {
587
518
  const resultElements = this.resultsContainer.querySelectorAll('.search-result');
588
519
 
589
- // Remove previous selection
590
520
  if (this.selectedIndex >= 0 && resultElements[this.selectedIndex]) {
591
521
  resultElements[this.selectedIndex].setAttribute('aria-selected', 'false');
592
522
  }
593
523
 
594
- // Add new selection
595
524
  this.selectedIndex = newIndex;
596
525
  if (resultElements[newIndex]) {
597
526
  resultElements[newIndex].setAttribute('aria-selected', 'true');
@@ -666,14 +595,10 @@ class SearchManager {
666
595
  }
667
596
  }
668
597
 
669
- /**
670
- * Initialize search on page load
671
- */
672
598
  function initializeSearch() {
673
599
  new SearchManager();
674
600
  }
675
601
 
676
- // Initialize on DOM ready
677
602
  if (document.readyState === 'loading') {
678
603
  document.addEventListener('DOMContentLoaded', initializeSearch);
679
604
  } else {
@@ -0,0 +1,29 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ var STORAGE_KEY = 'docyard_sidebar_collapsed';
5
+
6
+ function initSidebarToggle() {
7
+ var toggle = document.querySelector('.breadcrumb-toggle');
8
+ var sidebar = document.querySelector('.sidebar');
9
+
10
+ if (!toggle || !sidebar) {
11
+ return;
12
+ }
13
+
14
+ var isCollapsed = document.documentElement.classList.contains('sidebar-collapsed');
15
+ toggle.setAttribute('aria-expanded', !isCollapsed);
16
+
17
+ toggle.addEventListener('click', function() {
18
+ var collapsed = document.documentElement.classList.toggle('sidebar-collapsed');
19
+ localStorage.setItem(STORAGE_KEY, collapsed);
20
+ toggle.setAttribute('aria-expanded', !collapsed);
21
+ });
22
+ }
23
+
24
+ if (document.readyState === 'loading') {
25
+ document.addEventListener('DOMContentLoaded', initSidebarToggle);
26
+ } else {
27
+ initSidebarToggle();
28
+ }
29
+ })();