docyard 0.3.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/README.md +55 -33
  4. data/lib/docyard/build/asset_bundler.rb +139 -0
  5. data/lib/docyard/build/file_copier.rb +105 -0
  6. data/lib/docyard/build/sitemap_generator.rb +57 -0
  7. data/lib/docyard/build/static_generator.rb +141 -0
  8. data/lib/docyard/builder.rb +104 -0
  9. data/lib/docyard/cli.rb +19 -0
  10. data/lib/docyard/components/table_wrapper_processor.rb +18 -0
  11. data/lib/docyard/config.rb +4 -2
  12. data/lib/docyard/icons/phosphor.rb +1 -0
  13. data/lib/docyard/initializer.rb +80 -14
  14. data/lib/docyard/markdown.rb +13 -0
  15. data/lib/docyard/preview_server.rb +72 -0
  16. data/lib/docyard/rack_application.rb +1 -1
  17. data/lib/docyard/renderer.rb +17 -3
  18. data/lib/docyard/sidebar/config_parser.rb +180 -0
  19. data/lib/docyard/sidebar/item.rb +58 -0
  20. data/lib/docyard/sidebar/renderer.rb +33 -6
  21. data/lib/docyard/sidebar_builder.rb +45 -1
  22. data/lib/docyard/templates/assets/css/components/callout.css +1 -1
  23. data/lib/docyard/templates/assets/css/components/code-block.css +2 -2
  24. data/lib/docyard/templates/assets/css/components/navigation.css +65 -7
  25. data/lib/docyard/templates/assets/css/components/tabs.css +3 -2
  26. data/lib/docyard/templates/assets/css/components/theme-toggle.css +8 -0
  27. data/lib/docyard/templates/assets/css/markdown.css +20 -11
  28. data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
  29. data/lib/docyard/templates/assets/js/theme.js +2 -185
  30. data/lib/docyard/templates/config/docyard.yml.erb +32 -10
  31. data/lib/docyard/templates/layouts/default.html.erb +1 -1
  32. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
  33. data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
  34. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
  35. data/lib/docyard/templates/markdown/index.md.erb +55 -59
  36. data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
  37. data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
  38. data/lib/docyard/version.rb +1 -1
  39. data/lib/docyard.rb +8 -0
  40. metadata +55 -10
  41. data/lib/docyard/templates/markdown/components/callouts.md.erb +0 -204
  42. data/lib/docyard/templates/markdown/components/icons.md.erb +0 -125
  43. data/lib/docyard/templates/markdown/components/tabs.md.erb +0 -686
  44. data/lib/docyard/templates/markdown/configuration.md.erb +0 -202
  45. data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
  46. data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
  47. data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
  48. data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
  49. data/lib/docyard/templates/partials/_icons.html.erb +0 -11
@@ -7,10 +7,11 @@ module Docyard
7
7
  class Renderer
8
8
  PARTIALS_PATH = File.join(__dir__, "../templates/partials")
9
9
 
10
- attr_reader :site_title
10
+ attr_reader :site_title, :base_url
11
11
 
12
- def initialize(site_title: "Documentation")
12
+ def initialize(site_title: "Documentation", base_url: "/")
13
13
  @site_title = site_title
14
+ @base_url = normalize_base_url(base_url)
14
15
  end
15
16
 
16
17
  def render(tree)
@@ -34,8 +35,21 @@ module Docyard
34
35
  ERB.new(template).result(erb_binding)
35
36
  end
36
37
 
37
- def icon(name)
38
- render_partial(:icons, icon_name: name)
38
+ def icon(name, weight = "regular")
39
+ Icons.render(name.to_s.tr("_", "-"), weight) || ""
40
+ end
41
+
42
+ def link_path(path)
43
+ return path if path.nil? || path.start_with?("http://", "https://")
44
+
45
+ "#{base_url.chomp('/')}#{path}"
46
+ end
47
+
48
+ def normalize_base_url(url)
49
+ return "/" if url.nil? || url.empty?
50
+
51
+ url = "/#{url}" unless url.start_with?("/")
52
+ url.end_with?("/") ? url : "#{url}/"
39
53
  end
40
54
 
41
55
  def render_tree_with_sections(items)
@@ -98,12 +112,25 @@ module Docyard
98
112
  end
99
113
 
100
114
  def render_leaf_item(item)
101
- render_partial(:nav_leaf, path: item[:path], title: item[:title], active: item[:active])
115
+ render_partial(
116
+ :nav_leaf,
117
+ path: item[:path],
118
+ title: item[:title],
119
+ active: item[:active],
120
+ icon: item[:icon],
121
+ target: item[:target]
122
+ )
102
123
  end
103
124
 
104
125
  def render_group_item(item)
105
126
  children_html = render_tree(item[:children])
106
- render_partial(:nav_group, title: item[:title], children_html: children_html)
127
+ render_partial(
128
+ :nav_group,
129
+ title: item[:title],
130
+ children_html: children_html,
131
+ icon: item[:icon],
132
+ collapsed: item[:collapsed]
133
+ )
107
134
  end
108
135
  end
109
136
  end
@@ -4,6 +4,7 @@ require_relative "sidebar/file_system_scanner"
4
4
  require_relative "sidebar/title_extractor"
5
5
  require_relative "sidebar/tree_builder"
6
6
  require_relative "sidebar/renderer"
7
+ require_relative "sidebar/config_parser"
7
8
 
8
9
  module Docyard
9
10
  class SidebarBuilder
@@ -26,10 +27,44 @@ module Docyard
26
27
  private
27
28
 
28
29
  def build_tree
30
+ if config_sidebar_items?
31
+ build_tree_from_config
32
+ else
33
+ build_tree_from_filesystem
34
+ end
35
+ end
36
+
37
+ def build_tree_from_config
38
+ config_parser.parse.map(&:to_h)
39
+ end
40
+
41
+ def build_tree_from_filesystem
29
42
  file_items = scanner.scan
30
43
  tree_builder.build(file_items)
31
44
  end
32
45
 
46
+ def config_sidebar_items?
47
+ config_sidebar_items&.any?
48
+ end
49
+
50
+ def config_sidebar_items
51
+ return [] unless config
52
+
53
+ if config.is_a?(Hash)
54
+ config.dig("sidebar", "items") || config.dig(:sidebar, :items) || []
55
+ else
56
+ config.sidebar&.items || []
57
+ end
58
+ end
59
+
60
+ def config_parser
61
+ @config_parser ||= Sidebar::ConfigParser.new(
62
+ config_sidebar_items,
63
+ docs_path: docs_path,
64
+ current_path: current_path
65
+ )
66
+ end
67
+
33
68
  def scanner
34
69
  @scanner ||= Sidebar::FileSystemScanner.new(docs_path)
35
70
  end
@@ -43,10 +78,19 @@ module Docyard
43
78
 
44
79
  def renderer
45
80
  @renderer ||= Sidebar::Renderer.new(
46
- site_title: extract_site_title
81
+ site_title: extract_site_title,
82
+ base_url: extract_base_url
47
83
  )
48
84
  end
49
85
 
86
+ def extract_base_url
87
+ if config.is_a?(Hash)
88
+ config.dig(:build, :base_url) || "/"
89
+ else
90
+ config&.build&.base_url || "/"
91
+ end
92
+ end
93
+
50
94
  def extract_site_title
51
95
  if config.is_a?(Hash)
52
96
  config[:site_title] || "Documentation"
@@ -6,7 +6,7 @@
6
6
  margin: var(--space-6) 0;
7
7
  border: 1px solid;
8
8
  border-radius: var(--radius-lg);
9
- transition: all var(--transition-base);
9
+ transition: border-color var(--transition-base), background-color var(--transition-base);
10
10
  }
11
11
 
12
12
  .docyard-callout__icon {
@@ -47,7 +47,7 @@
47
47
  background-color: var(--color-bg);
48
48
  color: var(--color-text-secondary);
49
49
  cursor: pointer;
50
- transition: all var(--transition-base);
50
+ transition: background-color var(--transition-base), border-color var(--transition-base), color var(--transition-base), opacity var(--transition-base), visibility var(--transition-base);
51
51
  opacity: 0;
52
52
  visibility: hidden;
53
53
  }
@@ -78,7 +78,7 @@
78
78
  .docyard-code-block__copy svg {
79
79
  width: 16px;
80
80
  height: 16px;
81
- transition: all 0.2s ease;
81
+ transition: transform 0.2s ease, opacity 0.2s ease;
82
82
  }
83
83
 
84
84
  /* Success state */
@@ -53,13 +53,15 @@
53
53
  }
54
54
 
55
55
  .sidebar nav a {
56
- display: block;
56
+ display: flex;
57
+ align-items: center;
58
+ gap: var(--space-2);
57
59
  padding: 0.5rem var(--space-3);
58
60
  font-size: 0.875rem;
59
61
  color: var(--color-text-secondary);
60
62
  text-decoration: none;
61
63
  border-radius: var(--radius-md);
62
- transition: all var(--transition-fast);
64
+ transition: color var(--transition-fast);
63
65
  line-height: 1.4;
64
66
  font-weight: var(--font-weight-medium);
65
67
  }
@@ -148,7 +150,7 @@
148
150
  color: var(--color-text-secondary);
149
151
  text-decoration: none;
150
152
  border-radius: var(--radius-md);
151
- transition: all var(--transition-fast);
153
+ transition: color var(--transition-fast);
152
154
  line-height: 1.4;
153
155
  font-weight: var(--font-weight-medium);
154
156
  background: none;
@@ -157,6 +159,13 @@
157
159
  cursor: pointer;
158
160
  }
159
161
 
162
+ .nav-group-title {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: var(--space-2);
166
+ flex: 1;
167
+ }
168
+
160
169
  .nav-group-toggle:hover {
161
170
  color: var(--color-text);
162
171
  }
@@ -167,14 +176,18 @@
167
176
  }
168
177
 
169
178
  .nav-group-icon {
179
+ display: inline-flex;
180
+ color: var(--color-text-tertiary);
181
+ flex-shrink: 0;
182
+ }
183
+
184
+ .nav-group-icon .docyard-icon {
170
185
  width: 1rem;
171
186
  height: 1rem;
172
- color: var(--color-text-tertiary);
173
187
  transition: transform var(--transition-fast);
174
- flex-shrink: 0;
175
188
  }
176
189
 
177
- .nav-group-toggle[aria-expanded="true"] .nav-group-icon {
190
+ .nav-group-toggle[aria-expanded="true"] .nav-group-icon .docyard-icon {
178
191
  transform: rotate(90deg);
179
192
  }
180
193
 
@@ -225,7 +238,7 @@
225
238
  text-decoration: none;
226
239
  padding: var(--space-2);
227
240
  border-radius: var(--radius-md);
228
- transition: all var(--transition-fast);
241
+ transition: background-color var(--transition-fast);
229
242
  }
230
243
 
231
244
  .sidebar-footer-link:hover {
@@ -256,3 +269,48 @@
256
269
  transition: opacity var(--transition-fast);
257
270
  flex-shrink: 0;
258
271
  }
272
+
273
+ /* Icon styling */
274
+ .nav-item-icon {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ flex-shrink: 0;
278
+ }
279
+
280
+ .nav-item-icon .docyard-icon {
281
+ width: 1rem;
282
+ height: 1rem;
283
+ }
284
+
285
+ .nav-item-text {
286
+ flex: 1;
287
+ }
288
+
289
+ .nav-item-external {
290
+ display: inline-flex;
291
+ align-items: center;
292
+ opacity: 0.5;
293
+ flex-shrink: 0;
294
+ }
295
+
296
+ .nav-item-external .docyard-icon {
297
+ width: 0.75rem;
298
+ height: 0.75rem;
299
+ }
300
+
301
+ /* Reduced motion support */
302
+ @media (prefers-reduced-motion: reduce) {
303
+ .sidebar nav a,
304
+ .nav-group-toggle,
305
+ .nav-group-icon .docyard-icon,
306
+ .nav-group-children,
307
+ .mobile-menu-toggle,
308
+ .sidebar-footer-link,
309
+ .external-icon {
310
+ transition: none;
311
+ }
312
+
313
+ .nav-group-toggle[aria-expanded="true"] .nav-group-icon .docyard-icon {
314
+ transform: none;
315
+ }
316
+ }
@@ -62,7 +62,7 @@
62
62
  font-family: var(--font-sans);
63
63
  color: var(--color-text-secondary);
64
64
  cursor: pointer;
65
- transition: all 0.2s ease;
65
+ transition: color 0.2s ease, background-color 0.2s ease;
66
66
  position: relative;
67
67
  white-space: nowrap;
68
68
  flex-shrink: 0;
@@ -280,7 +280,8 @@
280
280
 
281
281
  /* Reduced motion support */
282
282
  @media (prefers-reduced-motion: reduce) {
283
- .docyard-tabs__tab {
283
+ .docyard-tabs__tab,
284
+ .docyard-tabs__indicator {
284
285
  transition: none;
285
286
  }
286
287
  }
@@ -59,3 +59,11 @@
59
59
  .dark .theme-toggle-thumb {
60
60
  transform: translateX(1.5rem);
61
61
  }
62
+
63
+ /* Reduced motion support */
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .theme-toggle-track,
66
+ .theme-toggle-thumb {
67
+ transition: none;
68
+ }
69
+ }
@@ -72,43 +72,52 @@
72
72
  }
73
73
 
74
74
  /* Tables */
75
+ .content .table-wrapper {
76
+ margin: var(--space-6) 0;
77
+ overflow-x: auto;
78
+ -webkit-overflow-scrolling: touch;
79
+ border: 1px solid var(--color-border);
80
+ border-radius: var(--radius-lg);
81
+ }
82
+
75
83
  .content table {
76
84
  width: 100%;
77
- margin: var(--space-6) 0;
78
85
  border-collapse: collapse;
79
86
  font-size: var(--font-size-sm);
80
- display: block;
81
- overflow-x: auto;
82
- border-radius: var(--radius-lg);
83
- border: 1px solid var(--color-border);
84
- box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.03);
87
+ margin: 0;
85
88
  }
86
89
 
87
90
  .content thead {
88
- background-color: var(--color-bg-secondary);
91
+ background-color: var(--color-bg-tertiary);
89
92
  }
90
93
 
91
94
  .content th {
92
95
  padding: var(--space-3) var(--space-4);
93
96
  text-align: left;
94
97
  font-weight: var(--font-weight-semibold);
95
- border-bottom: 1px solid var(--color-border);
96
98
  color: var(--color-text);
99
+ border-bottom: 2px solid var(--color-border);
100
+ white-space: nowrap;
97
101
  }
98
102
 
99
103
  .content td {
100
104
  padding: var(--space-3) var(--space-4);
101
- border-bottom: 1px solid var(--color-border-secondary);
105
+ border-bottom: 1px solid var(--color-border);
106
+ white-space: nowrap;
102
107
  }
103
108
 
104
- .content tr:last-child td {
105
- border-bottom: none;
109
+ .content tbody tr {
110
+ transition: background-color var(--transition-fast);
106
111
  }
107
112
 
108
113
  .content tbody tr:hover {
109
114
  background-color: var(--color-bg-secondary);
110
115
  }
111
116
 
117
+ .content tbody tr:last-child td {
118
+ border-bottom: none;
119
+ }
120
+
112
121
  /* Images */
113
122
  .content img {
114
123
  max-width: 100%;
@@ -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
+ })();