jekyll-theme-zer0 1.24.0 → 1.25.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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -0
  3. data/README.md +3 -3
  4. data/_data/authors.yml +1 -0
  5. data/_data/features.yml +37 -2
  6. data/_data/navigation/README.md +15 -5
  7. data/_data/navigation/main.yml +0 -8
  8. data/_data/roadmap.yml +1 -0
  9. data/_includes/README.md +10 -7
  10. data/_includes/analytics/google-analytics.html +1 -0
  11. data/_includes/analytics/google-tag-manager-body.html +1 -0
  12. data/_includes/analytics/google-tag-manager-head.html +1 -0
  13. data/_includes/analytics/posthog.html +1 -0
  14. data/_includes/components/README.md +2 -1
  15. data/_includes/components/admin-links.html +63 -0
  16. data/_includes/components/admin-tabs.html +1 -0
  17. data/_includes/components/ai-chat.html +27 -5
  18. data/_includes/components/author-eeat.html +1 -0
  19. data/_includes/components/background-customizer.html +1 -0
  20. data/_includes/components/background-settings.html +36 -37
  21. data/_includes/components/component-showcase.html +1 -0
  22. data/_includes/components/cookie-consent.html +1 -0
  23. data/_includes/components/dev-shortcuts.html +1 -8
  24. data/_includes/components/env-switcher.html +90 -138
  25. data/_includes/components/feature-card.html +1 -1
  26. data/_includes/components/halfmoon.html +22 -41
  27. data/_includes/components/info-section.html +127 -130
  28. data/_includes/components/mermaid.html +1 -0
  29. data/_includes/components/nanobar.html +1 -0
  30. data/_includes/components/post-type-badge.html +1 -0
  31. data/_includes/components/preview-image.html +1 -0
  32. data/_includes/components/search-modal.html +1 -0
  33. data/_includes/components/searchbar.html +1 -0
  34. data/_includes/components/shortcuts-modal.html +1 -0
  35. data/_includes/components/theme-info.html +21 -30
  36. data/_includes/components/theme-preview-gallery.html +1 -0
  37. data/_includes/content/backlinks.html +1 -0
  38. data/_includes/content/giscus.html +1 -0
  39. data/_includes/content/jsonld-faq.html +1 -0
  40. data/_includes/content/seo.html +1 -0
  41. data/_includes/content/sitemap.html +1 -0
  42. data/_includes/content/toc.html +1 -0
  43. data/_includes/core/color-mode-init.html +1 -0
  44. data/_includes/core/footer.html +1 -0
  45. data/_includes/core/head.html +1 -0
  46. data/_includes/core/header.html +5 -15
  47. data/_includes/navigation/admin-nav.html +1 -0
  48. data/_includes/navigation/breadcrumbs.html +1 -0
  49. data/_includes/navigation/local-graph-fab.html +3 -9
  50. data/_includes/navigation/local-graph.html +4 -9
  51. data/_includes/navigation/navbar.html +1 -0
  52. data/_includes/navigation/sidebar-categories.html +56 -14
  53. data/_includes/navigation/sidebar-config.html +115 -0
  54. data/_includes/navigation/sidebar-folders.html +168 -62
  55. data/_includes/navigation/sidebar-left.html +29 -52
  56. data/_includes/navigation/sidebar-nav.html +30 -0
  57. data/_includes/navigation/sidebar-right.html +1 -0
  58. data/_includes/navigation/toc-fab.html +3 -9
  59. data/_includes/navigation/unified-drawer.html +7 -14
  60. data/_includes/obsidian/full-graph.html +1 -0
  61. data/_includes/setup/wizard.html +1 -0
  62. data/_includes/stats/stats-categories.html +1 -0
  63. data/_includes/stats/stats-header.html +1 -0
  64. data/_includes/stats/stats-metrics.html +1 -0
  65. data/_includes/stats/stats-overview.html +1 -0
  66. data/_includes/stats/stats-tags.html +1 -0
  67. data/_layouts/admin.html +1 -0
  68. data/_layouts/article.html +1 -0
  69. data/_layouts/author.html +1 -0
  70. data/_layouts/authors.html +1 -0
  71. data/_layouts/collection.html +1 -0
  72. data/_layouts/default.html +14 -37
  73. data/_layouts/home.html +1 -0
  74. data/_layouts/index.html +1 -0
  75. data/_layouts/landing.html +1 -0
  76. data/_layouts/news.html +1 -0
  77. data/_layouts/note.html +1 -0
  78. data/_layouts/notebook.html +1 -0
  79. data/_layouts/root.html +3 -2
  80. data/_layouts/search.html +1 -0
  81. data/_layouts/section.html +1 -0
  82. data/_layouts/setup.html +1 -0
  83. data/_layouts/sitemap-collection.html +1 -0
  84. data/_layouts/stats.html +1 -0
  85. data/_layouts/tag.html +1 -0
  86. data/_layouts/welcome.html +1 -0
  87. data/_plugins/author_pages_generator.rb +1 -0
  88. data/_plugins/obsidian_links.rb +1 -0
  89. data/_plugins/preview_image_generator.rb +1 -0
  90. data/_plugins/search_and_sitemap_generator.rb +1 -0
  91. data/_plugins/theme_version.rb +1 -0
  92. data/_sass/components/_author.scss +12 -1
  93. data/_sass/components/_footer.scss +22 -0
  94. data/_sass/components/_ui-enhancements.scss +9 -0
  95. data/_sass/core/_docs-layout.scss +1 -0
  96. data/_sass/core/_navbar.scss +10 -27
  97. data/_sass/core/_obsidian.scss +2 -2
  98. data/_sass/core/code-copy.scss +10 -0
  99. data/_sass/custom.scss +1 -0
  100. data/_sass/notebooks.scss +1 -0
  101. data/_sass/theme/_backgrounds.scss +7 -2
  102. data/_sass/utilities/_layout.scss +11 -0
  103. data/assets/css/main.scss +2 -1
  104. data/assets/js/ai-chat.js +1 -0
  105. data/assets/js/author-profile.js +1 -0
  106. data/assets/js/auto-hide-nav.js +1 -0
  107. data/assets/js/back-to-top.js +1 -0
  108. data/assets/js/background-customizer.js +1 -0
  109. data/assets/js/code-copy.js +1 -0
  110. data/assets/js/halfmoon.js +23 -22
  111. data/assets/js/modules/navigation/gestures.js +1 -0
  112. data/assets/js/modules/navigation/keyboard.js +1 -0
  113. data/assets/js/modules/navigation/scroll-spy.js +1 -0
  114. data/assets/js/modules/navigation/sidebar-state.js +1 -0
  115. data/assets/js/modules/navigation/sidebar-visibility.js +1 -0
  116. data/assets/js/modules/navigation/smooth-scroll.js +1 -0
  117. data/assets/js/modules/theme/appearance.js +52 -16
  118. data/assets/js/nanobar-init.js +1 -0
  119. data/assets/js/navigation.js +1 -0
  120. data/assets/js/obsidian-graph.js +1 -0
  121. data/assets/js/obsidian-wiki-links.js +1 -0
  122. data/assets/js/palette-generator.js +1 -0
  123. data/assets/js/particles-source.js +1 -0
  124. data/assets/js/search-modal.js +1 -0
  125. data/assets/js/setup-wizard.js +1 -0
  126. data/assets/js/share-actions.js +1 -0
  127. data/assets/js/skin-editor.js +1 -0
  128. data/assets/js/table-copy.js +1 -0
  129. data/assets/js/theme-customizer.js +1 -0
  130. data/scripts/analyze-commits.sh +1 -0
  131. data/scripts/bin/giscus-discussions +1 -0
  132. data/scripts/bin/install +1 -0
  133. data/scripts/bin/validate +1 -0
  134. data/scripts/build +1 -0
  135. data/scripts/content-review.rb +1 -0
  136. data/scripts/convert-notebooks.sh +4 -3
  137. data/scripts/fix-markdown-format.sh +1 -0
  138. data/scripts/generate-preview-images.sh +1 -0
  139. data/scripts/generate-roadmap.sh +1 -0
  140. data/scripts/install-preview-generator.sh +1 -0
  141. data/scripts/release +1 -0
  142. data/scripts/requirements.txt +11 -0
  143. data/scripts/setup.sh +1 -0
  144. data/scripts/tag-features +136 -0
  145. data/scripts/validate-features.rb +7 -0
  146. data/scripts/vendor-install.sh +1 -0
  147. metadata +8 -3
  148. data/_plugins/admin_page_urls.rb +0 -16
@@ -1,3 +1,4 @@
1
+ {% comment %} Feature: ZER0-045 {% endcomment %}
1
2
  {%- comment -%}
2
3
  ===================================================================
3
4
  LOCAL GRAPH SIDEBAR — collapsible page-scoped graph
@@ -24,15 +25,9 @@
24
25
  Configurable depth: `local_graph_depth: 2` in page front matter (default 1).
25
26
  ===================================================================
26
27
  {%- endcomment -%}
27
- {%- assign _effective_sidebar = page.sidebar -%}
28
- {%- if _effective_sidebar == nil -%}
29
- {%- if page.post_type == "featured" or page.post_type == "breaking" -%}
30
- {%- assign _effective_sidebar = false -%}
31
- {%- else -%}
32
- {%- assign _effective_sidebar = true -%}
33
- {%- endif -%}
34
- {%- endif -%}
35
- {%- if page.local_graph != false and _effective_sidebar != false -%}
28
+ {%- comment -%} Shared visibility resolver — sets sidebar_enabled {%- endcomment -%}
29
+ {%- include navigation/sidebar-config.html -%}
30
+ {%- if page.local_graph != false and sidebar_enabled -%}
36
31
  {%- assign lg_depth = page.local_graph_depth | default: 1 -%}
37
32
  <aside class="offcanvas offcanvas-start obsidian-local-graph-panel"
38
33
  tabindex="-1"
@@ -1,3 +1,4 @@
1
+ {% comment %} Features: ZER0-049, ZER0-055 {% endcomment %}
1
2
  <!--
2
3
  file: navbar.html
3
4
  path: /includes/navbar.html
@@ -1,32 +1,74 @@
1
- <!-- sidebar-categories.html -->
2
- {%- assign categories = include.categories | default: site.categories -%}
1
+ {% comment %} Feature: ZER0-008 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: sidebar-categories
4
+ Path: _includes/navigation/sidebar-categories.html
5
+ Purpose: "categories" / "tags" sidebar modes — collapsible taxonomy
6
+ groups with the posts filed under each term.
7
+ Params: taxonomy (string, optional) — "categories" (default) | "tags"
8
+ categories (map, optional) — explicit term→posts map to render
9
+ (back-compat; overrides the taxonomy lookup)
10
+ title (string, optional) — heading override
11
+ icon (string, optional) — Bootstrap Icons class override
12
+ Front matter (page.sidebar hash, all optional):
13
+ taxonomy — "categories" | "tags" (same as the param)
14
+ limit — max posts listed per term (default: all)
15
+ Depends on: site.categories / site.tags (posts only — Jekyll does not
16
+ index collection docs here), Bootstrap 5 collapse,
17
+ Bootstrap Icons, _sass/core/_sidebar-categories.scss
18
+ Notes: Groups containing the current page start expanded. Each term
19
+ shows a post-count badge.
20
+ {%- endcomment -%}
21
+ {%- assign _type = include.taxonomy | default: page.sidebar.taxonomy | default: "categories" -%}
22
+ {%- if _type == "tags" -%}
23
+ {%- assign _terms = site.tags -%}
24
+ {%- assign _heading = include.title | default: "Tags" -%}
25
+ {%- assign _heading_icon = include.icon | default: "bi-tags" -%}
26
+ {%- assign _id_prefix = "tag-" -%}
27
+ {%- else -%}
28
+ {%- assign _terms = site.categories -%}
29
+ {%- assign _heading = include.title | default: "Categories" -%}
30
+ {%- assign _heading_icon = include.icon | default: "bi-folder2-open" -%}
31
+ {%- assign _id_prefix = "cat-" -%}
32
+ {%- endif -%}
33
+ {%- assign _terms = include.categories | default: _terms -%}
34
+ {%- assign _terms = _terms | sort -%}
35
+ {%- assign _limit = page.sidebar.limit -%}
3
36
  <div class="sidebar-categories nav-tree nav-tree--categories w-100">
4
37
  <h2 class="sidebar-categories-heading h6 text-uppercase text-body-secondary fw-semibold mb-3 d-flex align-items-center gap-2">
5
- <i class="bi bi-folder2-open" aria-hidden="true"></i>
6
- <span>Categories</span>
38
+ <i class="bi {{ _heading_icon }}" aria-hidden="true"></i>
39
+ <span>{{ _heading }}</span>
7
40
  </h2>
8
41
 
9
42
  <ul class="list-unstyled nav-tree-root mb-0">
10
- {%- for category in categories -%}
11
- {%- assign category_id = category[0] | slugify | prepend: "cat-" -%}
43
+ {%- for term in _terms -%}
44
+ {%- assign term_id = term[0] | slugify | prepend: _id_prefix -%}
45
+ {%- assign term_posts = term[1] -%}
46
+ {%- assign _active_here = term_posts | where: "url", page.url | size -%}
47
+ {%- if _active_here > 0 -%}
48
+ {%- assign _expanded = true -%}
49
+ {%- else -%}
50
+ {%- assign _expanded = false -%}
51
+ {%- endif -%}
12
52
  <li class="nav-tree-item sidebar-categories-group" data-depth="0">
13
- <button class="btn btn-link nav-tree-toggle sidebar-categories-toggle w-100 d-flex align-items-center text-start collapsed"
53
+ <button class="btn btn-link nav-tree-toggle sidebar-categories-toggle w-100 d-flex align-items-center text-start{% unless _expanded %} collapsed{% endunless %}"
14
54
  type="button"
15
55
  data-bs-toggle="collapse"
16
- data-bs-target="#{{ category_id }}-list"
17
- aria-expanded="false"
18
- aria-controls="{{ category_id }}-list">
56
+ data-bs-target="#{{ term_id }}-list"
57
+ aria-expanded="{{ _expanded }}"
58
+ aria-controls="{{ term_id }}-list">
19
59
  <i class="bi bi-folder sidebar-categories-toggle__icon" aria-hidden="true"></i>
20
- <span class="sidebar-categories-toggle__label flex-grow-1">{{ category[0] }}</span>
60
+ <span class="sidebar-categories-toggle__label flex-grow-1">{{ term[0] }}</span>
61
+ <span class="badge text-bg-secondary bg-opacity-25 me-1">{{ term[1] | size }}</span>
21
62
  <i class="bi bi-chevron-down nav-tree-chevron sidebar-categories-toggle__chevron" aria-hidden="true"></i>
22
63
  </button>
23
64
 
24
- <div class="collapse" id="{{ category_id }}-list">
65
+ <div class="collapse{% if _expanded %} show{% endif %}" id="{{ term_id }}-list">
25
66
  <ul class="list-unstyled nav-tree-children sidebar-categories-posts mb-0">
26
- {%- for post in category[1] -%}
67
+ {%- for post in term_posts -%}
68
+ {%- if _limit and forloop.index0 >= _limit -%}{%- break -%}{%- endif -%}
27
69
  <li class="nav-tree-item" data-depth="1">
28
70
  <a href="{{ post.url | relative_url }}"
29
- class="nav-tree-link sidebar-categories-link d-flex align-items-start rounded text-decoration-none{% if page.url == post.url %} active{% endif %}">
71
+ class="nav-tree-link sidebar-categories-link d-flex align-items-start rounded text-decoration-none{% if page.url == post.url %} active{% endif %}"{% if page.url == post.url %} aria-current="page"{% endif %}>
30
72
  <i class="bi bi-file-earmark-text sidebar-categories-link__icon" aria-hidden="true"></i>
31
73
  <span class="sidebar-categories-link__text">{{ post.title }}</span>
32
74
  </a>
@@ -0,0 +1,115 @@
1
+ {%- comment -%}
2
+ Component: sidebar-config
3
+ Path: _includes/navigation/sidebar-config.html
4
+ Purpose: Resolve the effective left-sidebar configuration for the
5
+ current page. Jekyll `assign`s inside an include propagate to
6
+ the including template, so this is the single source of truth
7
+ shared by _layouts/default.html, _includes/core/header.html,
8
+ navigation/sidebar-left.html and navigation/unified-drawer.html
9
+ (previously each duplicated this logic and drifted).
10
+ Params: none — reads page / collection / site state.
11
+ Sets (caller scope):
12
+ sidebar_enabled — false when the page opts out entirely
13
+ (`sidebar: false`, or featured/breaking
14
+ post_type with no explicit setting)
15
+ sidebar_nav — resolved concrete mode: "collection",
16
+ "categories", "tags", or the name of a
17
+ _data/navigation/*.yml file (nil = none)
18
+ sidebar_has_content — true only when sidebar_nav will actually
19
+ render items (gates the left column and
20
+ the mobile toggle)
21
+ sidebar_title — heading text for the sidebar panel
22
+ sidebar_icon — Bootstrap Icons class for the heading
23
+ Config precedence (most specific wins):
24
+ 1. page.sidebar (false | true | {nav, title, icon, ...})
25
+ 2. `sidebar:` metadata on the page's collection (_config.yml
26
+ `collections.<label>.sidebar.{nav,title,icon}`)
27
+ 3. site.sidebar ({nav, title, icon} in _config.yml)
28
+ Notes: `nav: auto` picks the best mode for the page's collection —
29
+ a curated _data/navigation/<collection>.yml wins, then the
30
+ live "collection" folder tree, then post "categories".
31
+ {%- endcomment -%}
32
+
33
+ {%- comment -%} 1. Visibility: page.sidebar wins; featured/breaking posts default off {%- endcomment -%}
34
+ {%- assign sidebar_enabled = true -%}
35
+ {%- if page.sidebar == false -%}
36
+ {%- assign sidebar_enabled = false -%}
37
+ {%- elsif page.sidebar == nil -%}
38
+ {%- if page.post_type == "featured" or page.post_type == "breaking" -%}
39
+ {%- assign sidebar_enabled = false -%}
40
+ {%- endif -%}
41
+ {%- endif -%}
42
+
43
+ {%- comment -%} 2. Collection-level sidebar metadata (optional) {%- endcomment -%}
44
+ {%- assign sidebar_collection_cfg = nil -%}
45
+ {%- if page.collection -%}
46
+ {%- assign _sidebar_coll = site.collections | where: "label", page.collection | first -%}
47
+ {%- if _sidebar_coll -%}
48
+ {%- assign sidebar_collection_cfg = _sidebar_coll.sidebar -%}
49
+ {%- endif -%}
50
+ {%- endif -%}
51
+
52
+ {%- comment -%} 3. Nav mode: page → collection metadata → site default {%- endcomment -%}
53
+ {%- assign sidebar_nav = nil -%}
54
+ {%- if sidebar_enabled -%}
55
+ {%- if page.sidebar.nav -%}
56
+ {%- assign sidebar_nav = page.sidebar.nav -%}
57
+ {%- elsif sidebar_collection_cfg.nav -%}
58
+ {%- assign sidebar_nav = sidebar_collection_cfg.nav -%}
59
+ {%- elsif site.sidebar.nav -%}
60
+ {%- assign sidebar_nav = site.sidebar.nav -%}
61
+ {%- endif -%}
62
+ {%- endif -%}
63
+
64
+ {%- comment -%} 4. "auto" → concrete mode based on the page's collection {%- endcomment -%}
65
+ {%- if sidebar_nav == "auto" -%}
66
+ {%- assign sidebar_nav = nil -%}
67
+ {%- if page.collection -%}
68
+ {%- if site.data.navigation[page.collection] -%}
69
+ {%- assign sidebar_nav = page.collection -%}
70
+ {%- else -%}
71
+ {%- assign sidebar_nav = "collection" -%}
72
+ {%- endif -%}
73
+ {%- elsif site.categories.size > 0 -%}
74
+ {%- assign sidebar_nav = "categories" -%}
75
+ {%- endif -%}
76
+ {%- endif -%}
77
+
78
+ {%- comment -%} 5. Content gate — never reserve an empty left column {%- endcomment -%}
79
+ {%- assign sidebar_has_content = false -%}
80
+ {%- if sidebar_nav -%}
81
+ {%- case sidebar_nav -%}
82
+ {%- when "collection" -%}
83
+ {%- if page.sidebar.collection or page.collection -%}
84
+ {%- assign sidebar_has_content = true -%}
85
+ {%- endif -%}
86
+ {%- when "categories" -%}
87
+ {%- if site.categories.size > 0 -%}
88
+ {%- assign sidebar_has_content = true -%}
89
+ {%- endif -%}
90
+ {%- when "tags" -%}
91
+ {%- if site.tags.size > 0 -%}
92
+ {%- assign sidebar_has_content = true -%}
93
+ {%- endif -%}
94
+ {%- else -%}
95
+ {%- if site.data.navigation[sidebar_nav] -%}
96
+ {%- assign sidebar_has_content = true -%}
97
+ {%- endif -%}
98
+ {%- endcase -%}
99
+ {%- endif -%}
100
+
101
+ {%- comment -%} 6. Panel heading text + icon (page → collection → site → theme default) {%- endcomment -%}
102
+ {%- assign sidebar_title = site.sidebar.title | default: "Browse docs" -%}
103
+ {%- assign sidebar_icon = site.sidebar.icon | default: "bi-journal-bookmark" -%}
104
+ {%- if sidebar_collection_cfg.title -%}
105
+ {%- assign sidebar_title = sidebar_collection_cfg.title -%}
106
+ {%- endif -%}
107
+ {%- if sidebar_collection_cfg.icon -%}
108
+ {%- assign sidebar_icon = sidebar_collection_cfg.icon -%}
109
+ {%- endif -%}
110
+ {%- if page.sidebar.title -%}
111
+ {%- assign sidebar_title = page.sidebar.title -%}
112
+ {%- endif -%}
113
+ {%- if page.sidebar.icon -%}
114
+ {%- assign sidebar_icon = page.sidebar.icon -%}
115
+ {%- endif -%}
@@ -1,68 +1,174 @@
1
- <!--
2
- ===================================================================
3
- SIDEBAR FOLDERS - Collection Document Folder Structure
4
- ===================================================================
5
-
6
- File: sidebar-folders.html
7
- Path: _includes/navigation/sidebar-folders.html
8
- Purpose: Display collection documents organized by folder structure
9
-
10
- Template Logic:
11
- - Gets the current page's collection
12
- - Sorts documents by path to maintain folder hierarchy
13
- - Displays folder names and document links
14
- - Uses Bootstrap list-group for styling with Bootstrap Icons
15
-
16
- Dependencies:
17
- - page.collection: Current collection name from page front matter
18
- - site.collections: Jekyll collections configuration
19
- - Bootstrap 5 list-group component
20
- - Bootstrap Icons for folder/file indicators
21
-
22
- Usage:
23
- - Include in sidebar when page.sidebar.nav == "dynamic"
24
- - Requires page to be part of a Jekyll collection
25
- ===================================================================
26
- -->
1
+ {% comment %} Feature: ZER0-008 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: sidebar-folders
4
+ Path: _includes/navigation/sidebar-folders.html
5
+ Purpose: "collection" sidebar mode — live folder tree of a collection's
6
+ documents, grouped by sub-folder into collapsible sections.
7
+ Zero-config: any collection gets a usable sidebar without a
8
+ curated _data/navigation/*.yml file.
9
+ Params: collection (string, optional) — collection label to list
10
+ (default: page.sidebar.collection, then page.collection)
11
+ sort (string, optional) "path" | "title" | "date"
12
+ (default: page.sidebar.sort, then "path")
13
+ Front matter (page.sidebar hash, all optional):
14
+ collection list a different collection than the page's own
15
+ sort — see above; "date" needs date/lastmod front matter
16
+ reverse — true to reverse the sort (e.g. newest-first)
17
+ expand — true to expand every folder group
18
+ (default: only the group containing the page)
19
+ Per-document opt-out: set `sidebar_exclude: true` in a doc's front matter
20
+ to hide it from this tree.
21
+ Collection metadata (optional, in _config.yml `collections.<label>:`):
22
+ title — heading text (default: humanized label)
23
+ icon Bootstrap Icons class (default: bi-folder2-open)
24
+ Depends on: site.collections, Bootstrap 5 collapse, Bootstrap Icons,
25
+ nav-tree styles (_sass/core/_nav-tree.scss)
26
+ Notes: A folder's index.* doc becomes the folder link itself (link +
27
+ separate collapse toggle, mirroring nav-tree.html). Groups
28
+ containing the current page start expanded so the active link
29
+ is always visible. Folder names are humanized (hyphens/
30
+ underscores → spaces). Nested sub-folders render as their own
31
+ "parent / child" groups.
32
+ {%- endcomment -%}
27
33
 
28
- {% assign root_folder = site.collections | where: "label", page.collection | first %}
34
+ {%- assign _coll_label = include.collection | default: page.sidebar.collection | default: page.collection -%}
35
+ {%- assign _coll = site.collections | where: "label", _coll_label | first -%}
29
36
 
30
- {% if root_folder %}
31
- <h2 class="h5 mb-3 d-flex align-items-center">
32
- <i class="bi bi-folder2-open me-2"></i>
33
- {{ root_folder.label | capitalize }}
37
+ {%- if _coll -%}
38
+ {%- assign _sort = include.sort | default: page.sidebar.sort | default: "path" -%}
39
+ {%- assign _docs = _coll.docs | where_exp: "d", "d.sidebar_exclude != true" -%}
40
+ {%- case _sort -%}
41
+ {%- when "title" -%}
42
+ {%- assign _docs = _docs | sort_natural: "title" -%}
43
+ {%- when "date" -%}
44
+ {%- assign _docs = _docs | sort: "date", "first" -%}
45
+ {%- else -%}
46
+ {%- assign _docs = _docs | sort: "path" -%}
47
+ {%- endcase -%}
48
+ {%- if page.sidebar.reverse == true -%}
49
+ {%- assign _docs = _docs | reverse -%}
50
+ {%- endif -%}
51
+
52
+ {%- comment -%}
53
+ Group documents by their directory relative to the collection folder.
54
+ Splitting on "_<label>/" works with and without a collections_dir prefix
55
+ in doc.path. Root-level docs land in the "" group.
56
+ {%- endcomment -%}
57
+ {%- capture _coll_prefix -%}_{{ _coll_label }}/{%- endcapture -%}
58
+ {%- assign _groups = _docs | group_by_exp: "d", "d.path | split: _coll_prefix | last | split: '/' | pop | join: '/'" | sort: "name" -%}
59
+
60
+ {%- assign _coll_title = _coll.title -%}
61
+ {%- unless _coll_title -%}
62
+ {%- assign _coll_title = _coll_label | replace: "-", " " | replace: "_", " " | capitalize -%}
63
+ {%- endunless -%}
64
+ {%- assign _coll_icon = _coll.icon | default: "bi-folder2-open" -%}
65
+ {%- assign _expand_all = page.sidebar.expand | default: false -%}
66
+
67
+ <div class="sidebar-collection w-100">
68
+ <h2 class="sidebar-collection-heading h6 text-uppercase text-body-secondary fw-semibold mb-3 d-flex align-items-center gap-2">
69
+ <i class="bi {{ _coll_icon }}" aria-hidden="true"></i>
70
+ <span>{{ _coll_title }}</span>
34
71
  </h2>
35
-
36
- {% assign docs = root_folder.docs | sort: 'path' %}
37
- {% assign prev_path = "" %}
38
-
39
- <ul class="list-group list-group-flush">
40
- {% for doc in docs %}
41
- {% assign current_path = doc.path | split: '/' | pop %}
42
-
43
- {% if current_path != prev_path %}
44
- {% for folder in current_path %}
45
- {% if forloop.index != 1 %}
46
- <li class="folder list-group-item bg-body-tertiary fw-semibold d-flex align-items-center">
47
- <i class="bi bi-folder2 me-2"></i>
48
- {{ folder }}
49
- </li>
50
- {% endif %}
51
- {% endfor %}
52
- {% assign prev_path = current_path %}
53
- {% endif %}
54
-
55
- <li class="file list-group-item list-group-item-action{% if page.url == doc.url %} active{% endif %}">
56
- <a href="{{ doc.url | relative_url }}" class="text-decoration-none d-flex align-items-center">
57
- <i class="bi bi-file-earmark-text me-2"></i>
58
- {{ doc.title }}
59
- </a>
60
- </li>
61
- {% endfor %}
72
+
73
+ <ul class="list-unstyled nav-tree-root mb-0">
74
+ {%- for group in _groups -%}
75
+ {%- if group.name == "" or group.name == nil -%}
76
+ {%- comment -%} Collection-root documents: flat depth-0 links {%- endcomment -%}
77
+ {%- for doc in group.items -%}
78
+ {%- assign _doc_title = doc.title -%}
79
+ {%- unless _doc_title -%}
80
+ {%- assign _doc_title = doc.path | split: "/" | last | split: "." | first | replace: "-", " " | replace: "_", " " | capitalize -%}
81
+ {%- endunless -%}
82
+ <li class="nav-tree-item" data-depth="0">
83
+ <a href="{{ doc.url | relative_url }}"
84
+ class="nav-tree-link d-flex align-items-start rounded py-1 px-2 text-decoration-none{% if page.url == doc.url %} active{% endif %}"{% if page.url == doc.url %} aria-current="page"{% endif %}>
85
+ <i class="bi bi-file-earmark-text me-2 flex-shrink-0" aria-hidden="true"></i>
86
+ <span>{{ _doc_title }}</span>
87
+ </a>
88
+ </li>
89
+ {%- endfor -%}
90
+ {%- else -%}
91
+ {%- assign _group_id = group.name | slugify | prepend: "sidebar-coll-" -%}
92
+ {%- assign _group_title = group.name | replace: "-", " " | replace: "_", " " | replace: "/", " / " | capitalize -%}
93
+ {%- assign _group_index = group.items | where_exp: "d", "d.path contains '/index.'" | first -%}
94
+ {%- assign _active_here = group.items | where: "url", page.url | size -%}
95
+ {%- if _expand_all or _active_here > 0 -%}
96
+ {%- assign _expanded = true -%}
97
+ {%- else -%}
98
+ {%- assign _expanded = false -%}
99
+ {%- endif -%}
100
+
101
+ {%- assign _group_size = group.items | size -%}
102
+ {%- if _group_index and _group_size == 1 -%}
103
+ {%- comment -%} Folder holds only its index doc — plain leaf, no toggle {%- endcomment -%}
104
+ <li class="nav-tree-item sidebar-collection-group" data-depth="0">
105
+ <a href="{{ _group_index.url | relative_url }}"
106
+ class="nav-tree-link d-flex align-items-start rounded py-1 px-2 text-decoration-none{% if page.url == _group_index.url %} active{% endif %}"{% if page.url == _group_index.url %} aria-current="page"{% endif %}>
107
+ <i class="bi bi-folder2 me-2 flex-shrink-0" aria-hidden="true"></i>
108
+ <span>{{ _group_title }}</span>
109
+ </a>
110
+ </li>
111
+ {%- else -%}
112
+ <li class="nav-tree-item sidebar-collection-group" data-depth="0">
113
+ {%- if _group_index -%}
114
+ {%- comment -%} Folder index doc → folder name links to it, chevron toggles {%- endcomment -%}
115
+ <div class="d-flex align-items-center">
116
+ <a href="{{ _group_index.url | relative_url }}"
117
+ class="nav-tree-link flex-grow-1 d-inline-flex align-items-center rounded py-1 px-2 text-decoration-none{% if page.url == _group_index.url %} active{% endif %}"{% if page.url == _group_index.url %} aria-current="page"{% endif %}>
118
+ <i class="bi bi-folder2 me-2" aria-hidden="true"></i>
119
+ <span>{{ _group_title }}</span>
120
+ </a>
121
+ <button class="btn btn-sm btn-link nav-tree-toggle p-1{% unless _expanded %} collapsed{% endunless %}"
122
+ type="button"
123
+ data-bs-toggle="collapse"
124
+ data-bs-target="#{{ _group_id }}"
125
+ aria-expanded="{{ _expanded }}"
126
+ aria-controls="{{ _group_id }}"
127
+ aria-label="Toggle {{ _group_title }} section">
128
+ <i class="bi bi-chevron-down nav-tree-chevron" aria-hidden="true"></i>
129
+ </button>
130
+ </div>
131
+ {%- else -%}
132
+ <button class="btn btn-link nav-tree-toggle w-100 d-flex align-items-center text-start py-1 px-2{% unless _expanded %} collapsed{% endunless %}"
133
+ type="button"
134
+ data-bs-toggle="collapse"
135
+ data-bs-target="#{{ _group_id }}"
136
+ aria-expanded="{{ _expanded }}"
137
+ aria-controls="{{ _group_id }}">
138
+ <i class="bi bi-folder2 me-2" aria-hidden="true"></i>
139
+ <span class="flex-grow-1">{{ _group_title }}</span>
140
+ <i class="bi bi-chevron-down nav-tree-chevron" aria-hidden="true"></i>
141
+ </button>
142
+ {%- endif -%}
143
+
144
+ <div class="collapse{% if _expanded %} show{% endif %}" id="{{ _group_id }}">
145
+ <ul class="list-unstyled nav-tree-children mb-0">
146
+ {%- for doc in group.items -%}
147
+ {%- unless _group_index and doc.path == _group_index.path -%}
148
+ {%- assign _doc_title = doc.title -%}
149
+ {%- unless _doc_title -%}
150
+ {%- assign _doc_title = doc.path | split: "/" | last | split: "." | first | replace: "-", " " | replace: "_", " " | capitalize -%}
151
+ {%- endunless -%}
152
+ <li class="nav-tree-item" data-depth="1">
153
+ <a href="{{ doc.url | relative_url }}"
154
+ class="nav-tree-link d-flex align-items-start rounded py-1 px-2 text-decoration-none{% if page.url == doc.url %} active{% endif %}"{% if page.url == doc.url %} aria-current="page"{% endif %}>
155
+ <i class="bi bi-file-earmark-text me-2 flex-shrink-0" aria-hidden="true"></i>
156
+ <span>{{ _doc_title }}</span>
157
+ </a>
158
+ </li>
159
+ {%- endunless -%}
160
+ {%- endfor -%}
161
+ </ul>
162
+ </div>
163
+ </li>
164
+ {%- endif -%}
165
+ {%- endif -%}
166
+ {%- endfor -%}
62
167
  </ul>
63
- {% else %}
64
- <p class="text-muted small">
65
- <i class="bi bi-info-circle me-1"></i>
168
+ </div>
169
+ {%- else -%}
170
+ <p class="text-body-secondary small mb-0">
171
+ <i class="bi bi-info-circle me-1" aria-hidden="true"></i>
66
172
  No collection found for this page.
67
173
  </p>
68
- {% endif %}
174
+ {%- endif -%}
@@ -1,32 +1,36 @@
1
- <!--
1
+ {% comment %} Feature: ZER0-008 {% endcomment %}
2
+ <!--
2
3
  ===================================================================
3
4
  SIDEBAR LEFT - Dynamic Navigation Sidebar
4
5
  ===================================================================
5
-
6
- File: sidebar-left.html
6
+
7
+ File: sidebar-left.html
7
8
  Path: _includes/navigation/sidebar-left.html
8
- Purpose: Left sidebar navigation with three standardized content modes
9
-
9
+ Purpose: Left sidebar panel (responsive offcanvas) around the resolved
10
+ sidebar navigation content
11
+
10
12
  Template Logic:
11
13
  - Responsive offcanvas sidebar for mobile devices
12
- - Three navigation modes based on page.sidebar.nav:
13
- * "auto": Auto-generated folder structure from collection documents
14
- * "tree": YAML-defined hierarchical navigation from _data/navigation/
15
- * "categories": Category-based navigation from site categories
16
-
17
- Dependencies:
18
- - sidebar-folders.html: Dynamic folder structure generation (auto mode)
19
- - sidebar-categories.html: Category-based navigation (categories mode)
20
- - nav-tree.html: YAML-based hierarchical tree (tree mode)
21
- - Bootstrap 5 offcanvas component
22
-
23
- Navigation Modes:
24
- 1. auto: Scans collection documents to build folder structure
25
- 2. tree: Uses predefined YAML navigation from _data/navigation/*.yml
26
- 3. categories: Groups content by Jekyll categories
14
+ - Mode resolution + rendering are delegated to shared includes:
15
+ * navigation/sidebar-config.html resolves sidebar_nav / title / icon
16
+ from page.sidebar collection `sidebar:` metadata → site.sidebar
17
+ * navigation/sidebar-nav.html renders the resolved mode
18
+
19
+ Navigation modes (page.sidebar.nav — see sidebar-config.html):
20
+ - "auto": best mode for the page's collection (curated data file →
21
+ collection folder tree post categories)
22
+ - "collection": live folder tree of the page's (or a named) collection
23
+ - "categories" / "tags": posts grouped by taxonomy term
24
+ - any other value: _data/navigation/<value>.yml rendered by nav-tree.html
25
+
26
+ Configuration:
27
+ - Heading text/icon: page.sidebar.title/icon collection metadata
28
+ site.sidebar.title/icon "Browse docs"
27
29
  ===================================================================
28
30
  -->
29
31
 
32
+ {% include navigation/sidebar-config.html %}
33
+
30
34
  <!-- ================================ -->
31
35
  <!-- SIDEBAR NAVIGATION CONTAINER -->
32
36
  <!-- ================================ -->
@@ -45,7 +49,7 @@
45
49
  <!-- ========================== -->
46
50
  <!-- Mobile-only header with close button -->
47
51
  <div class="offcanvas-header">
48
- <h2 class="offcanvas-title h5 mb-0" id="bdSidebarOffcanvasLabel">Browse docs</h2>
52
+ <h2 class="offcanvas-title h5 mb-0" id="bdSidebarOffcanvasLabel">{{ sidebar_title }}</h2>
49
53
  <div class="d-flex align-items-center gap-1">
50
54
  <button type="button"
51
55
  class="btn btn-sm btn-link text-body-secondary bd-sidebar-visibility-toggle d-lg-none px-2"
@@ -67,8 +71,8 @@
67
71
 
68
72
  <div class="bd-sidebar-desktop-header mb-3 d-none d-lg-flex align-items-center justify-content-between gap-2 w-100">
69
73
  <strong class="d-flex align-items-center mb-0">
70
- <i class="bi bi-journal-bookmark me-2" aria-hidden="true"></i>
71
- Browse docs
74
+ <i class="bi {{ sidebar_icon }} me-2" aria-hidden="true"></i>
75
+ {{ sidebar_title }}
72
76
  </strong>
73
77
  <button type="button"
74
78
  class="btn btn-sm btn-link text-body-secondary bd-sidebar-visibility-toggle d-none d-lg-inline-flex flex-shrink-0 px-1"
@@ -81,35 +85,8 @@
81
85
  </button>
82
86
  </div>
83
87
 
84
- <!-- ========================== -->
85
- <!-- AUTO MODE: Collection Docs -->
86
- <!-- ========================== -->
87
- <!-- Auto-generates folder structure from collection documents -->
88
- {% if page.sidebar.nav == "auto" %}
89
- <div class="list-group nav-tree" id="sidebar-content" data-nav-tree>
90
- {% assign folders = site.pages | where_exp: "item", "item.path contains 'index.md'" | sort: 'path' %}
91
- {% include navigation/sidebar-folders.html folders=folders %}
92
- </div>
88
+ <!-- Resolved navigation content (see navigation/sidebar-nav.html) -->
89
+ {% include navigation/sidebar-nav.html id="sidebar-content" %}
93
90
 
94
- <!-- ========================== -->
95
- <!-- CATEGORIES MODE -->
96
- <!-- ========================== -->
97
- <!-- Creates navigation from Jekyll post/page categories -->
98
- {% elsif page.sidebar.nav == "categories" %}
99
- <div class="nav-tree nav-tree--categories" id="sidebar-content" data-nav-tree>
100
- {% assign categories = site.categories | sort %}
101
- {% include navigation/sidebar-categories.html categories=categories %}
102
- </div>
103
-
104
- <!-- ========================== -->
105
- <!-- TREE MODE: YAML Navigation -->
106
- <!-- ========================== -->
107
- <!-- Uses predefined YAML navigation structure from _data files -->
108
- {% elsif page.sidebar.nav %}
109
- <nav class="w-100 nav-tree" data-nav-tree>
110
- {% include navigation/nav-tree.html nav=page.sidebar.nav %}
111
- </nav>
112
- {% endif %}
113
-
114
91
  </div>
115
92
  </div>
@@ -0,0 +1,30 @@
1
+ {%- comment -%}
2
+ Component: sidebar-nav
3
+ Path: _includes/navigation/sidebar-nav.html
4
+ Purpose: Render the resolved sidebar navigation content for the current
5
+ page. Expects navigation/sidebar-config.html to have been
6
+ included first in the same render context (it sets
7
+ `sidebar_nav`). Shared by navigation/sidebar-left.html and
8
+ navigation/unified-drawer.html so desktop sidebar and mobile
9
+ drawer can never drift apart.
10
+ Params: id (string, optional) — id attribute for the content wrapper
11
+ Depends on: navigation/sidebar-config.html (resolver),
12
+ navigation/sidebar-folders.html ("collection" mode),
13
+ navigation/sidebar-categories.html ("categories"/"tags" modes),
14
+ navigation/nav-tree.html (curated _data/navigation/*.yml modes)
15
+ Notes: Renders nothing when sidebar_nav is nil or its data is empty —
16
+ callers gate the surrounding column on `sidebar_has_content`.
17
+ {%- endcomment -%}
18
+ {%- if sidebar_nav == "collection" -%}
19
+ <div class="nav-tree"{% if include.id %} id="{{ include.id }}"{% endif %} data-nav-tree>
20
+ {% include navigation/sidebar-folders.html %}
21
+ </div>
22
+ {%- elsif sidebar_nav == "categories" or sidebar_nav == "tags" -%}
23
+ <div class="nav-tree nav-tree--categories"{% if include.id %} id="{{ include.id }}"{% endif %} data-nav-tree>
24
+ {% include navigation/sidebar-categories.html taxonomy=sidebar_nav %}
25
+ </div>
26
+ {%- elsif sidebar_nav -%}
27
+ <nav class="w-100 nav-tree"{% if include.id %} id="{{ include.id }}"{% endif %} data-nav-tree aria-label="Section navigation">
28
+ {% include navigation/nav-tree.html nav=sidebar_nav %}
29
+ </nav>
30
+ {%- endif -%}
@@ -1,3 +1,4 @@
1
+ {% comment %} Features: ZER0-008, ZER0-010 {% endcomment %}
1
2
  <!--
2
3
  file: sidebar-right.html
3
4
  path: _includes/sidebar-right.html