docyard 0.2.0 → 0.4.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/LICENSE.vscode-icons +42 -0
  4. data/README.md +86 -23
  5. data/lib/docyard/asset_handler.rb +33 -0
  6. data/lib/docyard/build/asset_bundler.rb +139 -0
  7. data/lib/docyard/build/file_copier.rb +105 -0
  8. data/lib/docyard/build/sitemap_generator.rb +57 -0
  9. data/lib/docyard/build/static_generator.rb +141 -0
  10. data/lib/docyard/builder.rb +104 -0
  11. data/lib/docyard/cli.rb +19 -0
  12. data/lib/docyard/components/base_processor.rb +24 -0
  13. data/lib/docyard/components/callout_processor.rb +121 -0
  14. data/lib/docyard/components/code_block_processor.rb +55 -0
  15. data/lib/docyard/components/code_detector.rb +59 -0
  16. data/lib/docyard/components/icon_detector.rb +57 -0
  17. data/lib/docyard/components/icon_processor.rb +51 -0
  18. data/lib/docyard/components/registry.rb +34 -0
  19. data/lib/docyard/components/table_wrapper_processor.rb +18 -0
  20. data/lib/docyard/components/tabs_parser.rb +60 -0
  21. data/lib/docyard/components/tabs_processor.rb +44 -0
  22. data/lib/docyard/config/validator.rb +171 -0
  23. data/lib/docyard/config.rb +135 -0
  24. data/lib/docyard/constants.rb +5 -0
  25. data/lib/docyard/icons/LICENSE.phosphor +21 -0
  26. data/lib/docyard/icons/file_types.rb +92 -0
  27. data/lib/docyard/icons/phosphor.rb +64 -0
  28. data/lib/docyard/icons.rb +40 -0
  29. data/lib/docyard/initializer.rb +93 -9
  30. data/lib/docyard/language_mapping.rb +52 -0
  31. data/lib/docyard/markdown.rb +27 -3
  32. data/lib/docyard/preview_server.rb +72 -0
  33. data/lib/docyard/rack_application.rb +77 -8
  34. data/lib/docyard/renderer.rb +56 -9
  35. data/lib/docyard/server.rb +5 -2
  36. data/lib/docyard/sidebar/config_parser.rb +180 -0
  37. data/lib/docyard/sidebar/item.rb +58 -0
  38. data/lib/docyard/sidebar/renderer.rb +33 -6
  39. data/lib/docyard/sidebar_builder.rb +54 -2
  40. data/lib/docyard/templates/assets/css/code.css +150 -2
  41. data/lib/docyard/templates/assets/css/components/callout.css +169 -0
  42. data/lib/docyard/templates/assets/css/components/code-block.css +196 -0
  43. data/lib/docyard/templates/assets/css/components/icon.css +16 -0
  44. data/lib/docyard/templates/assets/css/components/logo.css +44 -0
  45. data/lib/docyard/templates/assets/css/{components.css → components/navigation.css} +111 -53
  46. data/lib/docyard/templates/assets/css/components/tabs.css +299 -0
  47. data/lib/docyard/templates/assets/css/components/theme-toggle.css +69 -0
  48. data/lib/docyard/templates/assets/css/layout.css +14 -4
  49. data/lib/docyard/templates/assets/css/markdown.css +27 -17
  50. data/lib/docyard/templates/assets/css/reset.css +4 -0
  51. data/lib/docyard/templates/assets/css/variables.css +94 -3
  52. data/lib/docyard/templates/assets/favicon.svg +16 -0
  53. data/lib/docyard/templates/assets/js/components/code-block.js +162 -0
  54. data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
  55. data/lib/docyard/templates/assets/js/components/tabs.js +338 -0
  56. data/lib/docyard/templates/assets/js/theme.js +12 -179
  57. data/lib/docyard/templates/assets/logo-dark.svg +4 -0
  58. data/lib/docyard/templates/assets/logo.svg +12 -0
  59. data/lib/docyard/templates/config/docyard.yml.erb +42 -0
  60. data/lib/docyard/templates/layouts/default.html.erb +32 -4
  61. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
  62. data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
  63. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
  64. data/lib/docyard/templates/markdown/index.md.erb +55 -59
  65. data/lib/docyard/templates/partials/_callout.html.erb +11 -0
  66. data/lib/docyard/templates/partials/_code_block.html.erb +6 -0
  67. data/lib/docyard/templates/partials/_icon.html.erb +1 -0
  68. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +1 -0
  69. data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
  70. data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
  71. data/lib/docyard/templates/partials/_tabs.html.erb +40 -0
  72. data/lib/docyard/templates/partials/_theme_toggle.html.erb +13 -0
  73. data/lib/docyard/version.rb +1 -1
  74. data/lib/docyard.rb +8 -0
  75. metadata +91 -7
  76. data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
  77. data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
  78. data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
  79. data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
  80. data/lib/docyard/templates/partials/_icons.html.erb +0 -11
@@ -0,0 +1,162 @@
1
+ /**
2
+ * CodeBlockManager - Manages code block copy functionality
3
+ *
4
+ * @class CodeBlockManager
5
+ */
6
+ class CodeBlockManager {
7
+ /**
8
+ * Create a CodeBlockManager instance
9
+ * @param {HTMLElement} container - The .docyard-code-block container element
10
+ */
11
+ constructor(container) {
12
+ if (!container) return;
13
+
14
+ this.container = container;
15
+ this.copyButton = container.querySelector('.docyard-code-block__copy');
16
+ this.codeText = this.copyButton?.getAttribute('data-code') || '';
17
+
18
+ this.originalIcon = this.copyButton?.innerHTML || '';
19
+
20
+ this.checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"/></svg>';
21
+
22
+ this.handleCopy = this.handleCopy.bind(this);
23
+
24
+ this.init();
25
+ }
26
+
27
+ /**
28
+ * Initialize the code block component
29
+ */
30
+ init() {
31
+ if (!this.copyButton) return;
32
+
33
+ this.copyButton.addEventListener('click', this.handleCopy);
34
+ }
35
+
36
+ /**
37
+ * Handle copy button click
38
+ */
39
+ async handleCopy() {
40
+ try {
41
+ await this.copyToClipboard(this.codeText);
42
+ this.showSuccess();
43
+ } catch (error) {
44
+ console.warn('Failed to copy code:', error);
45
+ this.showError();
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Copy text to clipboard
51
+ * @param {string} text - Text to copy
52
+ * @returns {Promise<void>}
53
+ */
54
+ async copyToClipboard(text) {
55
+ if (navigator.clipboard && window.isSecureContext) {
56
+ await navigator.clipboard.writeText(text);
57
+ } else {
58
+ this.fallbackCopy(text);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Fallback copy method for older browsers
64
+ * @param {string} text - Text to copy
65
+ */
66
+ fallbackCopy(text) {
67
+ const textArea = document.createElement('textarea');
68
+ textArea.value = text;
69
+ textArea.style.position = 'fixed';
70
+ textArea.style.left = '-999999px';
71
+ textArea.style.top = '-999999px';
72
+ document.body.appendChild(textArea);
73
+ textArea.focus();
74
+ textArea.select();
75
+
76
+ try {
77
+ document.execCommand('copy');
78
+ textArea.remove();
79
+ } catch (error) {
80
+ textArea.remove();
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Show success state
87
+ */
88
+ showSuccess() {
89
+ this.copyButton.classList.add('is-success');
90
+ this.copyButton.setAttribute('aria-label', 'Copied to clipboard!');
91
+
92
+ this.copyButton.innerHTML = this.checkIcon;
93
+
94
+ if (this.resetTimeout) {
95
+ clearTimeout(this.resetTimeout);
96
+ }
97
+
98
+ this.resetTimeout = setTimeout(() => {
99
+ this.resetState();
100
+ }, 2000);
101
+ }
102
+
103
+ /**
104
+ * Show error state
105
+ */
106
+ showError() {
107
+ this.copyButton.classList.add('is-error');
108
+ this.copyButton.setAttribute('aria-label', 'Failed to copy');
109
+
110
+ if (this.resetTimeout) {
111
+ clearTimeout(this.resetTimeout);
112
+ }
113
+
114
+ this.resetTimeout = setTimeout(() => {
115
+ this.resetState();
116
+ }, 2000);
117
+ }
118
+
119
+ /**
120
+ * Reset button to default state
121
+ */
122
+ resetState() {
123
+ this.copyButton.classList.remove('is-success', 'is-error');
124
+ this.copyButton.setAttribute('aria-label', 'Copy code to clipboard');
125
+
126
+ this.copyButton.innerHTML = this.originalIcon;
127
+ }
128
+
129
+ /**
130
+ * Cleanup - remove event listeners
131
+ */
132
+ destroy() {
133
+ if (this.copyButton) {
134
+ this.copyButton.removeEventListener('click', this.handleCopy);
135
+ }
136
+
137
+ if (this.resetTimeout) {
138
+ clearTimeout(this.resetTimeout);
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Auto-initialize all code blocks on page load
145
+ */
146
+ function initializeCodeBlocks() {
147
+ const codeBlocks = document.querySelectorAll('.docyard-code-block');
148
+
149
+ codeBlocks.forEach(container => {
150
+ new CodeBlockManager(container);
151
+ });
152
+ }
153
+
154
+ if (document.readyState === 'loading') {
155
+ document.addEventListener('DOMContentLoaded', initializeCodeBlocks);
156
+ } else {
157
+ initializeCodeBlocks();
158
+ }
159
+
160
+ if (typeof module !== 'undefined' && module.exports) {
161
+ module.exports = { CodeBlockManager };
162
+ }
@@ -0,0 +1,221 @@
1
+ // Docyard Navigation JavaScript
2
+ // Handles sidebar navigation, mobile menu, accordion groups, and scroll behavior
3
+
4
+ (function() {
5
+ 'use strict';
6
+
7
+ function initMobileMenu() {
8
+ const toggle = document.querySelector('.secondary-header-menu');
9
+ const sidebar = document.querySelector('.sidebar');
10
+ const overlay = document.querySelector('.mobile-overlay');
11
+
12
+ if (!toggle || !sidebar || !overlay) return;
13
+
14
+ function openMenu() {
15
+ sidebar.classList.add('is-open');
16
+ overlay.classList.add('is-visible');
17
+ toggle.setAttribute('aria-expanded', 'true');
18
+ document.body.style.overflow = 'hidden';
19
+ }
20
+
21
+ function closeMenu() {
22
+ sidebar.classList.remove('is-open');
23
+ overlay.classList.remove('is-visible');
24
+ toggle.setAttribute('aria-expanded', 'false');
25
+ document.body.style.overflow = '';
26
+ }
27
+
28
+ function toggleMenu() {
29
+ if (sidebar.classList.contains('is-open')) {
30
+ closeMenu();
31
+ } else {
32
+ openMenu();
33
+ }
34
+ }
35
+
36
+ toggle.addEventListener('click', toggleMenu);
37
+ overlay.addEventListener('click', closeMenu);
38
+
39
+ document.addEventListener('keydown', function(e) {
40
+ if (e.key === 'Escape' && sidebar.classList.contains('is-open')) {
41
+ closeMenu();
42
+ }
43
+ });
44
+
45
+ sidebar.querySelectorAll('a').forEach(function(link) {
46
+ link.addEventListener('click', closeMenu);
47
+ });
48
+ }
49
+
50
+ function initAccordion() {
51
+ const toggles = document.querySelectorAll('.nav-group-toggle');
52
+
53
+ toggles.forEach(function(toggle) {
54
+ toggle.addEventListener('click', function() {
55
+ const expanded = toggle.getAttribute('aria-expanded') === 'true';
56
+ const children = toggle.nextElementSibling;
57
+
58
+ if (!children || !children.classList.contains('nav-group-children')) {
59
+ return;
60
+ }
61
+
62
+ toggle.setAttribute('aria-expanded', !expanded);
63
+ children.classList.toggle('collapsed');
64
+
65
+ if (expanded) {
66
+ children.style.maxHeight = '0';
67
+ } else {
68
+ children.style.maxHeight = children.scrollHeight + 'px';
69
+ }
70
+ });
71
+ });
72
+
73
+ document.querySelectorAll('.nav-group-children.collapsed').forEach(function(el) {
74
+ el.style.maxHeight = '0';
75
+ });
76
+ }
77
+
78
+ function expandActiveGroups() {
79
+ const activeLinks = document.querySelectorAll('a.active');
80
+
81
+ activeLinks.forEach(function(activeLink) {
82
+ let parent = activeLink.closest('.nav-group-children');
83
+
84
+ while (parent) {
85
+ if (parent.classList.contains('nav-group-children')) {
86
+ parent.classList.remove('collapsed');
87
+
88
+ const toggle = parent.previousElementSibling;
89
+ if (toggle && toggle.classList.contains('nav-group-toggle')) {
90
+ toggle.setAttribute('aria-expanded', 'true');
91
+
92
+ parent.style.maxHeight = 'none';
93
+ const height = parent.scrollHeight;
94
+ parent.style.maxHeight = height + 'px';
95
+ }
96
+ }
97
+
98
+ parent = parent.parentElement?.closest('.nav-group-children');
99
+ }
100
+ });
101
+ }
102
+
103
+ function initSidebarScroll() {
104
+ const scrollContainer = document.querySelector('.sidebar nav');
105
+ if (!scrollContainer) return;
106
+
107
+ const STORAGE_KEY = 'docyard_sidebar_scroll';
108
+ const savedPosition = sessionStorage.getItem(STORAGE_KEY);
109
+
110
+ if (savedPosition) {
111
+ const position = parseInt(savedPosition, 10);
112
+ scrollContainer.scrollTop = position;
113
+
114
+ setTimeout(function() {
115
+ scrollContainer.scrollTop = position;
116
+ }, 100);
117
+ } else {
118
+ const activeLink = scrollContainer.querySelector('a.active');
119
+ if (activeLink) {
120
+ setTimeout(function() {
121
+ activeLink.scrollIntoView({
122
+ behavior: 'instant',
123
+ block: 'center'
124
+ });
125
+ }, 50);
126
+ }
127
+ }
128
+
129
+ scrollContainer.querySelectorAll('a').forEach(function(link) {
130
+ link.addEventListener('click', function() {
131
+ sessionStorage.setItem(STORAGE_KEY, scrollContainer.scrollTop);
132
+ });
133
+ });
134
+
135
+ let scrollTimeout;
136
+ scrollContainer.addEventListener('scroll', function() {
137
+ clearTimeout(scrollTimeout);
138
+ scrollTimeout = setTimeout(function() {
139
+ sessionStorage.setItem(STORAGE_KEY, scrollContainer.scrollTop);
140
+ }, 150);
141
+ });
142
+
143
+ const logo = document.querySelector('.header-logo');
144
+ if (logo) {
145
+ logo.addEventListener('click', function() {
146
+ sessionStorage.removeItem(STORAGE_KEY);
147
+ scrollContainer.scrollTop = 0;
148
+ });
149
+ }
150
+ }
151
+
152
+ function initScrollBehavior() {
153
+ const header = document.querySelector('.header');
154
+ const secondaryHeader = document.querySelector('.secondary-header');
155
+
156
+ if (!header || !secondaryHeader) return;
157
+
158
+ let lastScrollTop = 0;
159
+ let ticking = false;
160
+
161
+ function isMobile() {
162
+ return window.innerWidth <= 1024;
163
+ }
164
+
165
+ function updateHeaders() {
166
+ if (!isMobile()) {
167
+ header.classList.remove('hide-on-scroll');
168
+ secondaryHeader.classList.remove('shift-up');
169
+ ticking = false;
170
+ return;
171
+ }
172
+
173
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
174
+
175
+ if (scrollTop > lastScrollTop && scrollTop > 100) {
176
+ header.classList.add('hide-on-scroll');
177
+ secondaryHeader.classList.add('shift-up');
178
+ } else if (scrollTop < lastScrollTop) {
179
+ header.classList.remove('hide-on-scroll');
180
+ secondaryHeader.classList.remove('shift-up');
181
+ }
182
+
183
+ lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
184
+ ticking = false;
185
+ }
186
+
187
+ window.addEventListener('scroll', function() {
188
+ if (!ticking) {
189
+ window.requestAnimationFrame(updateHeaders);
190
+ ticking = true;
191
+ }
192
+ });
193
+
194
+ window.addEventListener('resize', function() {
195
+ if (!isMobile()) {
196
+ header.classList.remove('hide-on-scroll');
197
+ secondaryHeader.classList.remove('shift-up');
198
+ }
199
+ });
200
+ }
201
+
202
+ if ('scrollRestoration' in history) {
203
+ history.scrollRestoration = 'manual';
204
+ }
205
+
206
+ if (document.readyState === 'loading') {
207
+ document.addEventListener('DOMContentLoaded', function() {
208
+ initMobileMenu();
209
+ initAccordion();
210
+ expandActiveGroups();
211
+ initSidebarScroll();
212
+ initScrollBehavior();
213
+ });
214
+ } else {
215
+ initMobileMenu();
216
+ initAccordion();
217
+ expandActiveGroups();
218
+ initSidebarScroll();
219
+ initScrollBehavior();
220
+ }
221
+ })();
@@ -0,0 +1,338 @@
1
+ /**
2
+ * TabsManager - Manages tab component interactions
3
+ *
4
+ * @class TabsManager
5
+ */
6
+ class TabsManager {
7
+ /**
8
+ * Create a TabsManager instance
9
+ * @param {HTMLElement} container - The .docyard-tabs container element
10
+ */
11
+ constructor(container) {
12
+ if (!container) return;
13
+
14
+ this.container = container;
15
+ this.tabListWrapper = container.querySelector('.docyard-tabs__list-wrapper');
16
+ this.tabList = container.querySelector('[role="tablist"]');
17
+ this.tabs = Array.from(container.querySelectorAll('[role="tab"]'));
18
+ this.panels = Array.from(container.querySelectorAll('[role="tabpanel"]'));
19
+ this.indicator = container.querySelector('.docyard-tabs__indicator');
20
+ this.activeIndex = 0;
21
+ this.groupId = container.getAttribute('data-tabs');
22
+
23
+ this.handleTabClick = this.handleTabClick.bind(this);
24
+ this.handleKeyDown = this.handleKeyDown.bind(this);
25
+ this.handleResize = this.handleResize.bind(this);
26
+ this.handleScroll = this.handleScroll.bind(this);
27
+
28
+ this.init();
29
+ }
30
+
31
+ /**
32
+ * Initialize the tabs component
33
+ */
34
+ init() {
35
+ if (!this.tabList || this.tabs.length === 0 || this.panels.length === 0) {
36
+ return;
37
+ }
38
+
39
+ this.createScrollIndicators();
40
+ this.loadPreference();
41
+ this.attachEventListeners();
42
+ this.activateTab(this.activeIndex, false);
43
+ this.updateIndicator();
44
+ this.updateScrollIndicators();
45
+ }
46
+
47
+ /**
48
+ * Create scroll indicator elements
49
+ */
50
+ createScrollIndicators() {
51
+ if (!this.tabListWrapper || !this.tabList) return;
52
+
53
+ this.leftIndicator = document.createElement('div');
54
+ this.leftIndicator.className = 'docyard-tabs__scroll-indicator docyard-tabs__scroll-indicator--left';
55
+ this.tabListWrapper.insertBefore(this.leftIndicator, this.tabList);
56
+
57
+ this.rightIndicator = document.createElement('div');
58
+ this.rightIndicator.className = 'docyard-tabs__scroll-indicator docyard-tabs__scroll-indicator--right';
59
+ this.tabListWrapper.appendChild(this.rightIndicator);
60
+ }
61
+
62
+ /**
63
+ * Attach all event listeners
64
+ */
65
+ attachEventListeners() {
66
+ this.tabs.forEach((tab, index) => {
67
+ tab.addEventListener('click', () => this.handleTabClick(index));
68
+ });
69
+
70
+ this.tabList.addEventListener('keydown', this.handleKeyDown);
71
+
72
+ this.tabList.addEventListener('scroll', this.handleScroll);
73
+
74
+ window.addEventListener('resize', this.handleResize);
75
+ }
76
+
77
+ /**
78
+ * Handle tab click
79
+ * @param {number} index - Index of clicked tab
80
+ */
81
+ handleTabClick(index) {
82
+ if (index === this.activeIndex) return;
83
+
84
+ this.activateTab(index, true);
85
+ this.savePreference(index);
86
+ }
87
+
88
+ /**
89
+ * Handle keyboard navigation
90
+ * @param {KeyboardEvent} event - Keyboard event
91
+ */
92
+ handleKeyDown(event) {
93
+ const { key } = event;
94
+
95
+ if (key === 'ArrowLeft' || key === 'ArrowRight') {
96
+ event.preventDefault();
97
+
98
+ if (key === 'ArrowLeft') {
99
+ this.activatePreviousTab();
100
+ } else {
101
+ this.activateNextTab();
102
+ }
103
+
104
+ this.tabs[this.activeIndex].focus();
105
+ }
106
+
107
+ if (key === 'Home') {
108
+ event.preventDefault();
109
+ this.activateTab(0, true);
110
+ this.tabs[0].focus();
111
+ }
112
+
113
+ if (key === 'End') {
114
+ event.preventDefault();
115
+ const lastIndex = this.tabs.length - 1;
116
+ this.activateTab(lastIndex, true);
117
+ this.tabs[lastIndex].focus();
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Handle scroll event - update scroll indicators
123
+ */
124
+ handleScroll() {
125
+ if (this.scrollTimeout) {
126
+ cancelAnimationFrame(this.scrollTimeout);
127
+ }
128
+
129
+ this.scrollTimeout = requestAnimationFrame(() => {
130
+ this.updateScrollIndicators();
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Handle window resize - update indicator and scroll indicators
136
+ */
137
+ handleResize() {
138
+ if (this.resizeTimeout) {
139
+ cancelAnimationFrame(this.resizeTimeout);
140
+ }
141
+
142
+ this.resizeTimeout = requestAnimationFrame(() => {
143
+ this.updateIndicator(false);
144
+ this.updateScrollIndicators();
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Activate a specific tab
150
+ * @param {number} index - Index of tab to activate
151
+ * @param {boolean} animate - Whether to animate the transition
152
+ */
153
+ activateTab(index, animate = true) {
154
+ if (index < 0 || index >= this.tabs.length) return;
155
+
156
+ const previousIndex = this.activeIndex;
157
+ this.activeIndex = index;
158
+
159
+ this.tabs.forEach((tab, i) => {
160
+ const isActive = i === index;
161
+ tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
162
+ tab.setAttribute('tabindex', isActive ? '0' : '-1');
163
+ });
164
+
165
+ this.panels.forEach((panel, i) => {
166
+ const isActive = i === index;
167
+ panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
168
+ });
169
+
170
+ this.updateIndicator(animate);
171
+
172
+ if (previousIndex !== index) {
173
+ this.savePreference(index);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Activate the next tab (wraps around)
179
+ */
180
+ activateNextTab() {
181
+ const nextIndex = (this.activeIndex + 1) % this.tabs.length;
182
+ this.activateTab(nextIndex, true);
183
+ }
184
+
185
+ /**
186
+ * Activate the previous tab (wraps around)
187
+ */
188
+ activatePreviousTab() {
189
+ const prevIndex = (this.activeIndex - 1 + this.tabs.length) % this.tabs.length;
190
+ this.activateTab(prevIndex, true);
191
+ }
192
+
193
+ /**
194
+ * Update the visual indicator position
195
+ * @param {boolean} animate - Whether to animate the transition
196
+ */
197
+ updateIndicator(animate = true) {
198
+ if (!this.indicator || !this.tabs[this.activeIndex]) return;
199
+
200
+ const activeTab = this.tabs[this.activeIndex];
201
+ const tabListRect = this.tabList.getBoundingClientRect();
202
+ const activeTabRect = activeTab.getBoundingClientRect();
203
+
204
+ const left = activeTabRect.left - tabListRect.left + this.tabList.scrollLeft;
205
+ const width = activeTabRect.width;
206
+
207
+ this.indicator.style.width = `${width}px`;
208
+ this.indicator.style.transform = `translateX(${left}px)`;
209
+
210
+ if (!animate) {
211
+ this.indicator.style.transition = 'none';
212
+ void this.indicator.offsetWidth;
213
+ this.indicator.style.transition = '';
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Update scroll indicators visibility based on scroll position
219
+ */
220
+ updateScrollIndicators() {
221
+ if (!this.tabList || !this.leftIndicator || !this.rightIndicator) return;
222
+
223
+ const { scrollLeft, scrollWidth, clientWidth } = this.tabList;
224
+ const hasOverflow = scrollWidth > clientWidth;
225
+
226
+ if (!hasOverflow) {
227
+ this.leftIndicator.classList.remove('is-visible');
228
+ this.rightIndicator.classList.remove('is-visible');
229
+ return;
230
+ }
231
+
232
+ const canScrollLeft = scrollLeft > 5;
233
+ if (canScrollLeft) {
234
+ this.leftIndicator.classList.add('is-visible');
235
+ } else {
236
+ this.leftIndicator.classList.remove('is-visible');
237
+ }
238
+
239
+ const canScrollRight = scrollLeft < scrollWidth - clientWidth - 5;
240
+ if (canScrollRight) {
241
+ this.rightIndicator.classList.add('is-visible');
242
+ } else {
243
+ this.rightIndicator.classList.remove('is-visible');
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Load user preference from localStorage
249
+ */
250
+ loadPreference() {
251
+ try {
252
+ const preferredTab = localStorage.getItem('docyard-preferred-pm');
253
+ if (!preferredTab) return;
254
+
255
+ const index = this.tabs.findIndex(tab =>
256
+ tab.textContent.trim().toLowerCase() === preferredTab.toLowerCase()
257
+ );
258
+
259
+ if (index !== -1) {
260
+ this.activeIndex = index;
261
+ }
262
+ } catch (error) {
263
+ console.warn('Could not load tab preference:', error);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Save user preference to localStorage
269
+ * @param {number} index - Index of active tab
270
+ */
271
+ savePreference(index) {
272
+ if (index < 0 || index >= this.tabs.length) return;
273
+
274
+ try {
275
+ const tabName = this.tabs[index].textContent.trim().toLowerCase();
276
+ localStorage.setItem('docyard-preferred-pm', tabName);
277
+ } catch (error) {
278
+ console.warn('Could not save tab preference:', error);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Activate tab by name
284
+ * @param {string} name - Name of tab to activate
285
+ */
286
+ activateTabByName(name) {
287
+ const index = this.tabs.findIndex(tab =>
288
+ tab.textContent.trim().toLowerCase() === name.toLowerCase()
289
+ );
290
+
291
+ if (index !== -1) {
292
+ this.activateTab(index, true);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Cleanup - remove event listeners
298
+ */
299
+ destroy() {
300
+ this.tabs.forEach((tab, index) => {
301
+ tab.removeEventListener('click', () => this.handleTabClick(index));
302
+ });
303
+
304
+ this.tabList.removeEventListener('keydown', this.handleKeyDown);
305
+ this.tabList.removeEventListener('scroll', this.handleScroll);
306
+ window.removeEventListener('resize', this.handleResize);
307
+
308
+ if (this.resizeTimeout) {
309
+ cancelAnimationFrame(this.resizeTimeout);
310
+ }
311
+
312
+ if (this.scrollTimeout) {
313
+ cancelAnimationFrame(this.scrollTimeout);
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Auto-initialize all tabs on page load
320
+ */
321
+ function initializeTabs() {
322
+ const tabsContainers = document.querySelectorAll('.docyard-tabs');
323
+
324
+ tabsContainers.forEach(container => {
325
+ new TabsManager(container);
326
+ });
327
+ }
328
+
329
+ // Initialize on DOM ready
330
+ if (document.readyState === 'loading') {
331
+ document.addEventListener('DOMContentLoaded', initializeTabs);
332
+ } else {
333
+ initializeTabs();
334
+ }
335
+
336
+ if (typeof module !== 'undefined' && module.exports) {
337
+ module.exports = { TabsManager };
338
+ }