jekyll-theme-zer0 0.15.2 → 0.17.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -10
  3. data/README.md +7 -7
  4. data/_data/authors.yml +2 -2
  5. data/_data/features.yml +676 -0
  6. data/_data/navigation/README.md +54 -0
  7. data/_data/navigation/about.yml +7 -5
  8. data/_data/navigation/docs.yml +77 -31
  9. data/_data/navigation/home.yml +4 -3
  10. data/_data/navigation/main.yml +16 -7
  11. data/_data/navigation/quickstart.yml +4 -2
  12. data/_includes/components/js-cdn.html +2 -2
  13. data/_includes/components/preview-image.html +20 -3
  14. data/_includes/content/intro.html +11 -2
  15. data/_includes/content/seo.html +12 -1
  16. data/_includes/landing/landing-install-cards.html +8 -8
  17. data/_includes/landing/landing-quick-links.html +4 -4
  18. data/_includes/navigation/breadcrumbs.html +29 -6
  19. data/_includes/navigation/nav-tree.html +181 -0
  20. data/_includes/navigation/navbar.html +262 -9
  21. data/_includes/navigation/sidebar-left.html +22 -23
  22. data/_layouts/default.html +1 -1
  23. data/_layouts/landing.html +1 -1
  24. data/_layouts/notebook.html +4 -4
  25. data/_layouts/root.html +2 -2
  26. data/_plugins/preview_image_generator.rb +29 -8
  27. data/_sass/core/_nav-tree.scss +145 -0
  28. data/_sass/custom.scss +3 -0
  29. data/assets/images/previews/site-personalization-configuration.png +0 -0
  30. data/assets/js/modules/navigation/config.js +149 -0
  31. data/assets/js/modules/navigation/focus.js +189 -0
  32. data/assets/js/modules/navigation/gestures.js +179 -0
  33. data/assets/js/modules/navigation/index.js +227 -0
  34. data/assets/js/modules/navigation/keyboard.js +237 -0
  35. data/assets/js/modules/navigation/scroll-spy.js +219 -0
  36. data/assets/js/modules/navigation/sidebar-state.js +267 -0
  37. data/assets/js/modules/navigation/smooth-scroll.js +153 -0
  38. data/scripts/README.md +8 -1
  39. data/scripts/lib/preview_generator.py +164 -8
  40. data/scripts/migrate-nav-modes.sh +146 -0
  41. data/scripts/update-preview-paths.sh +145 -0
  42. metadata +17 -3
  43. data/assets/js/sidebar.js +0 -511
@@ -33,6 +33,8 @@ module Jekyll
33
33
  'style' => 'retro pixel art, 8-bit video game aesthetic, vibrant colors, nostalgic, clean pixel graphics',
34
34
  'style_modifiers' => 'pixelated, retro gaming style, CRT screen glow effect, limited color palette',
35
35
  'output_dir' => 'assets/images/previews',
36
+ 'assets_prefix' => '/assets',
37
+ 'auto_prefix' => true,
36
38
  'auto_generate' => false,
37
39
  'collections' => ['posts', 'docs', 'quickstart']
38
40
  }.freeze
@@ -52,18 +54,37 @@ module Jekyll
52
54
  site = doc.site
53
55
  config = self.config(site)
54
56
 
57
+ # Normalize the preview path using assets_prefix
58
+ normalized_preview = normalize_preview_path(preview, config)
59
+
55
60
  # Build the full path
56
- preview_path = if preview.start_with?('/')
57
- File.join(site.source, preview)
58
- elsif preview.start_with?('http')
61
+ preview_path = if normalized_preview.start_with?('/')
62
+ File.join(site.source, normalized_preview.sub(/^\//, ''))
63
+ elsif normalized_preview.start_with?('http')
59
64
  return true # External URL, assume it exists
60
65
  else
61
- File.join(site.source, config['output_dir'], preview)
66
+ File.join(site.source, config['output_dir'], normalized_preview)
62
67
  end
63
68
 
64
69
  File.exist?(preview_path)
65
70
  end
66
71
 
72
+ # Normalize a preview path by adding assets_prefix if needed
73
+ def self.normalize_preview_path(preview, config)
74
+ return preview if preview.nil? || preview.to_s.strip.empty?
75
+ return preview if preview.start_with?('http')
76
+
77
+ assets_prefix = config['assets_prefix'] || '/assets'
78
+ auto_prefix = config['auto_prefix'] != false # Default to true
79
+
80
+ # If auto_prefix is enabled and path doesn't already contain assets_prefix
81
+ if auto_prefix && !preview.include?(assets_prefix)
82
+ "#{assets_prefix}#{preview}"
83
+ else
84
+ preview
85
+ end
86
+ end
87
+
67
88
  # Get the preview image path for a document
68
89
  def self.preview_path(doc)
69
90
  preview = doc.data['preview']
@@ -72,11 +93,11 @@ module Jekyll
72
93
  site = doc.site
73
94
  config = self.config(site)
74
95
 
75
- # If it's already a full path or URL, return as-is
76
- return preview if preview.start_with?('/') || preview.start_with?('http')
96
+ # If it's an external URL, return as-is
97
+ return preview if preview.start_with?('http')
77
98
 
78
- # Build relative path from output_dir
79
- "#{config['output_dir']}/#{preview}"
99
+ # Normalize path with assets_prefix
100
+ normalize_preview_path(preview, config)
80
101
  end
81
102
 
82
103
  # Get list of documents missing preview images
@@ -0,0 +1,145 @@
1
+ // ===================================================================
2
+ // NAV TREE - Sidebar Navigation Tree Styles
3
+ // ===================================================================
4
+ //
5
+ // File: _nav-tree.scss
6
+ // Path: _sass/core/_nav-tree.scss
7
+ // Purpose: Styles for the hierarchical navigation tree component
8
+ //
9
+ // Features:
10
+ // - Collapsible tree nodes with smooth transitions
11
+ // - Active state highlighting
12
+ // - Hover effects
13
+ // - Chevron rotation on expand/collapse
14
+ // - Nested indentation
15
+ //
16
+ // ===================================================================
17
+
18
+ // Navigation Tree Container
19
+ .nav-tree {
20
+ font-size: 0.875rem;
21
+ }
22
+
23
+ .nav-tree-root {
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ // Navigation Tree Items
29
+ .nav-tree-item {
30
+ margin-bottom: 0.125rem;
31
+
32
+ // Nested items get additional indentation via ps-3 class
33
+ }
34
+
35
+ // Navigation Tree Links
36
+ .nav-tree-link {
37
+ color: var(--bs-body-color);
38
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out;
39
+
40
+ &:hover {
41
+ color: var(--bs-primary);
42
+ background-color: var(--bs-tertiary-bg);
43
+ }
44
+
45
+ &.active {
46
+ color: var(--bs-primary);
47
+ background-color: var(--bs-primary-bg-subtle);
48
+ font-weight: 500;
49
+ }
50
+ }
51
+
52
+ // Navigation Tree Text (non-link items)
53
+ .nav-tree-text {
54
+ color: var(--bs-secondary-color);
55
+ }
56
+
57
+ // Navigation Tree Toggle Buttons
58
+ .nav-tree-toggle {
59
+ color: var(--bs-body-color);
60
+ text-decoration: none;
61
+ border: none;
62
+ background: transparent;
63
+ transition: color 0.15s ease-in-out;
64
+
65
+ &:hover {
66
+ color: var(--bs-primary);
67
+ }
68
+
69
+ &:focus {
70
+ outline: none;
71
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
72
+ }
73
+
74
+ // Chevron rotation
75
+ .bi-chevron-down,
76
+ .nav-tree-chevron {
77
+ transition: transform 0.2s ease-in-out;
78
+ }
79
+
80
+ &.collapsed {
81
+ .bi-chevron-down,
82
+ .nav-tree-chevron {
83
+ transform: rotate(-90deg);
84
+ }
85
+ }
86
+ }
87
+
88
+ // Children container
89
+ .nav-tree-children {
90
+ margin-top: 0.25rem;
91
+ border-left: 1px solid var(--bs-border-color);
92
+ margin-left: 0.5rem;
93
+ padding-left: 0.75rem;
94
+ }
95
+
96
+ // Collapse animation
97
+ .nav-tree .collapse {
98
+ transition: height 0.2s ease-in-out;
99
+ }
100
+
101
+ // Depth-based styling
102
+ .nav-tree-item[data-depth="0"] > .nav-tree-link,
103
+ .nav-tree-item[data-depth="0"] > div > .nav-tree-link,
104
+ .nav-tree-item[data-depth="0"] > .nav-tree-toggle {
105
+ font-weight: 500;
106
+ }
107
+
108
+ .nav-tree-item[data-depth="1"] > .nav-tree-link,
109
+ .nav-tree-item[data-depth="1"] > div > .nav-tree-link {
110
+ font-weight: normal;
111
+ }
112
+
113
+ .nav-tree-item[data-depth="2"] > .nav-tree-link,
114
+ .nav-tree-item[data-depth="2"] > div > .nav-tree-link {
115
+ font-size: 0.8125rem;
116
+ color: var(--bs-secondary-color);
117
+
118
+ &.active {
119
+ color: var(--bs-primary);
120
+ }
121
+ }
122
+
123
+ // Keyboard navigation focus state
124
+ .keyboard-nav .nav-tree-link:focus,
125
+ .keyboard-nav .nav-tree-toggle:focus {
126
+ outline: 2px solid var(--bs-primary);
127
+ outline-offset: 2px;
128
+ }
129
+
130
+ // Dark mode adjustments
131
+ [data-bs-theme="dark"] {
132
+ .nav-tree-link {
133
+ &:hover {
134
+ background-color: rgba(255, 255, 255, 0.05);
135
+ }
136
+
137
+ &.active {
138
+ background-color: rgba(var(--bs-primary-rgb), 0.15);
139
+ }
140
+ }
141
+
142
+ .nav-tree-children {
143
+ border-left-color: var(--bs-border-color);
144
+ }
145
+ }
data/_sass/custom.scss CHANGED
@@ -5,6 +5,9 @@
5
5
  // Import notebook-specific styles
6
6
  @import "notebooks";
7
7
 
8
+ // Import navigation tree styles
9
+ @import "core/nav-tree";
10
+
8
11
  html, body {
9
12
  max-width: 100%;
10
13
  // overflow-x: hidden;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * ===================================================================
3
+ * NAVIGATION CONFIG - Shared Configuration for Navigation Modules
4
+ * ===================================================================
5
+ *
6
+ * File: config.js
7
+ * Path: assets/js/modules/navigation/config.js
8
+ * Purpose: Centralized configuration for all navigation modules
9
+ *
10
+ * Usage:
11
+ * import { config } from './config.js';
12
+ * const toc = document.querySelector(config.selectors.toc);
13
+ *
14
+ * ===================================================================
15
+ */
16
+
17
+ export const config = {
18
+ // ===================================================================
19
+ // DOM SELECTORS
20
+ // ===================================================================
21
+ selectors: {
22
+ // Table of Contents
23
+ toc: '#TableOfContents',
24
+ tocLinks: '#TableOfContents a',
25
+ tocContainer: '.bd-toc .offcanvas-body',
26
+
27
+ // Sidebars
28
+ leftSidebar: '#bdSidebar',
29
+ rightSidebar: '#tocContents',
30
+
31
+ // Content areas
32
+ mainContent: '.bd-content',
33
+
34
+ // Navigation elements
35
+ navTree: '.nav-tree',
36
+ navTreeToggle: '[data-nav-toggle]',
37
+
38
+ // Offcanvas
39
+ offcanvas: '.offcanvas'
40
+ },
41
+
42
+ // ===================================================================
43
+ // SCROLL SPY SETTINGS
44
+ // ===================================================================
45
+ scrollSpy: {
46
+ // Root margin for Intersection Observer
47
+ // Negative values account for fixed headers
48
+ rootMargin: '-80px 0px -80px 0px',
49
+
50
+ // Intersection threshold levels
51
+ threshold: [0, 0.25, 0.5, 0.75, 1]
52
+ },
53
+
54
+ // ===================================================================
55
+ // SMOOTH SCROLL SETTINGS
56
+ // ===================================================================
57
+ smoothScroll: {
58
+ // Offset for fixed headers when scrolling to anchor
59
+ offset: 80,
60
+
61
+ // Scroll behavior
62
+ behavior: 'smooth'
63
+ },
64
+
65
+ // ===================================================================
66
+ // KEYBOARD SHORTCUTS
67
+ // ===================================================================
68
+ keyboard: {
69
+ // Enable/disable keyboard shortcuts
70
+ enabled: true,
71
+
72
+ // Key mappings
73
+ keys: {
74
+ previousSection: '[',
75
+ nextSection: ']',
76
+ search: '/',
77
+ toggleSidebar: 'b',
78
+ toggleToc: 't'
79
+ }
80
+ },
81
+
82
+ // ===================================================================
83
+ // SWIPE GESTURES
84
+ // ===================================================================
85
+ gestures: {
86
+ // Enable/disable swipe gestures
87
+ enabled: true,
88
+
89
+ // Minimum distance (px) for swipe to register
90
+ threshold: 50,
91
+
92
+ // Edge detection zone (px) for sidebar swipes
93
+ edgeZone: 50
94
+ },
95
+
96
+ // ===================================================================
97
+ // SIDEBAR STATE PERSISTENCE
98
+ // ===================================================================
99
+ state: {
100
+ // localStorage key prefix
101
+ storagePrefix: 'zer0-nav-',
102
+
103
+ // Keys for different state values
104
+ keys: {
105
+ expandedNodes: 'expanded-nodes',
106
+ sidebarOpen: 'sidebar-open',
107
+ tocOpen: 'toc-open'
108
+ }
109
+ },
110
+
111
+ // ===================================================================
112
+ // DEBOUNCE/THROTTLE TIMINGS
113
+ // ===================================================================
114
+ timing: {
115
+ debounceDelay: 100,
116
+ scrollDebounce: 50
117
+ },
118
+
119
+ // ===================================================================
120
+ // BREAKPOINTS (match Bootstrap 5)
121
+ // ===================================================================
122
+ breakpoints: {
123
+ sm: 576,
124
+ md: 768,
125
+ lg: 992,
126
+ xl: 1200,
127
+ xxl: 1400
128
+ }
129
+ };
130
+
131
+ /**
132
+ * Check if viewport is below a breakpoint
133
+ * @param {string} breakpoint - Breakpoint name (sm, md, lg, xl, xxl)
134
+ * @returns {boolean}
135
+ */
136
+ export function isBelowBreakpoint(breakpoint) {
137
+ return window.innerWidth < config.breakpoints[breakpoint];
138
+ }
139
+
140
+ /**
141
+ * Check if viewport is at or above a breakpoint
142
+ * @param {string} breakpoint - Breakpoint name (sm, md, lg, xl, xxl)
143
+ * @returns {boolean}
144
+ */
145
+ export function isAtOrAboveBreakpoint(breakpoint) {
146
+ return window.innerWidth >= config.breakpoints[breakpoint];
147
+ }
148
+
149
+ export default config;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * ===================================================================
3
+ * FOCUS MANAGER - Accessibility Enhancements
4
+ * ===================================================================
5
+ *
6
+ * File: focus.js
7
+ * Path: assets/js/modules/navigation/focus.js
8
+ * Purpose: Focus management for accessibility in navigation components
9
+ *
10
+ * Features:
11
+ * - Returns focus to trigger element when offcanvas closes
12
+ * - Manages focus trap in modal/offcanvas contexts
13
+ * - Provides focus ring styling hooks
14
+ *
15
+ * Usage:
16
+ * import { FocusManager } from './focus.js';
17
+ * const focus = new FocusManager();
18
+ *
19
+ * ===================================================================
20
+ */
21
+
22
+ import { config } from './config.js';
23
+
24
+ /**
25
+ * Get all elements safely
26
+ * @param {string} selector - CSS selector
27
+ * @returns {NodeList}
28
+ */
29
+ function getElements(selector) {
30
+ try {
31
+ return document.querySelectorAll(selector);
32
+ } catch (error) {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ export class FocusManager {
38
+ constructor() {
39
+ this._boundHandleHidden = this._handleOffcanvasHidden.bind(this);
40
+ this._init();
41
+ }
42
+
43
+ /**
44
+ * Initialize focus management
45
+ * @private
46
+ */
47
+ _init() {
48
+ // Handle focus return when offcanvas closes
49
+ const offcanvasElements = getElements(config.selectors.offcanvas);
50
+
51
+ offcanvasElements.forEach(offcanvas => {
52
+ offcanvas.addEventListener('hidden.bs.offcanvas', this._boundHandleHidden);
53
+ });
54
+
55
+ // Add focus-visible polyfill behavior
56
+ this._setupFocusVisible();
57
+
58
+ console.log(`FocusManager: Initialized with ${offcanvasElements.length} offcanvas elements`);
59
+ }
60
+
61
+ /**
62
+ * Handle offcanvas hidden event - return focus to trigger
63
+ * @private
64
+ * @param {Event} event
65
+ */
66
+ _handleOffcanvasHidden(event) {
67
+ const offcanvas = event.target;
68
+ this.returnFocus(offcanvas);
69
+ }
70
+
71
+ /**
72
+ * Return focus to the trigger element that opened an offcanvas
73
+ * @param {Element} offcanvas - The offcanvas element
74
+ */
75
+ returnFocus(offcanvas) {
76
+ const triggerId = offcanvas.id;
77
+
78
+ // Find the trigger button that opened this offcanvas
79
+ const trigger = document.querySelector(
80
+ `[data-bs-target="#${triggerId}"], [href="#${triggerId}"]`
81
+ );
82
+
83
+ if (trigger) {
84
+ // Small delay to ensure offcanvas animation completes
85
+ requestAnimationFrame(() => {
86
+ trigger.focus();
87
+ });
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Setup focus-visible behavior for keyboard users
93
+ * @private
94
+ */
95
+ _setupFocusVisible() {
96
+ // Add class to body when user is navigating with keyboard
97
+ let hadKeyboardEvent = false;
98
+
99
+ document.addEventListener('keydown', (e) => {
100
+ if (e.key === 'Tab') {
101
+ hadKeyboardEvent = true;
102
+ document.body.classList.add('keyboard-nav');
103
+ }
104
+ });
105
+
106
+ document.addEventListener('mousedown', () => {
107
+ hadKeyboardEvent = false;
108
+ document.body.classList.remove('keyboard-nav');
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Focus the first focusable element within a container
114
+ * @param {Element} container
115
+ */
116
+ focusFirst(container) {
117
+ const focusable = container.querySelector(
118
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
119
+ );
120
+ if (focusable) {
121
+ focusable.focus();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Focus the last focusable element within a container
127
+ * @param {Element} container
128
+ */
129
+ focusLast(container) {
130
+ const focusables = container.querySelectorAll(
131
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
132
+ );
133
+ if (focusables.length > 0) {
134
+ focusables[focusables.length - 1].focus();
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Trap focus within a container (for modals/offcanvas)
140
+ * @param {Element} container
141
+ * @returns {Function} Cleanup function to remove trap
142
+ */
143
+ trapFocus(container) {
144
+ const focusableElements = container.querySelectorAll(
145
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
146
+ );
147
+
148
+ const firstFocusable = focusableElements[0];
149
+ const lastFocusable = focusableElements[focusableElements.length - 1];
150
+
151
+ const handleKeydown = (e) => {
152
+ if (e.key !== 'Tab') return;
153
+
154
+ if (e.shiftKey) {
155
+ // Shift + Tab
156
+ if (document.activeElement === firstFocusable) {
157
+ e.preventDefault();
158
+ lastFocusable.focus();
159
+ }
160
+ } else {
161
+ // Tab
162
+ if (document.activeElement === lastFocusable) {
163
+ e.preventDefault();
164
+ firstFocusable.focus();
165
+ }
166
+ }
167
+ };
168
+
169
+ container.addEventListener('keydown', handleKeydown);
170
+
171
+ // Return cleanup function
172
+ return () => {
173
+ container.removeEventListener('keydown', handleKeydown);
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Cleanup event listeners
179
+ */
180
+ destroy() {
181
+ const offcanvasElements = getElements(config.selectors.offcanvas);
182
+ offcanvasElements.forEach(offcanvas => {
183
+ offcanvas.removeEventListener('hidden.bs.offcanvas', this._boundHandleHidden);
184
+ });
185
+ console.log('FocusManager: Destroyed');
186
+ }
187
+ }
188
+
189
+ export default FocusManager;