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
@@ -30,36 +30,37 @@
30
30
  setTheme(getPreferredTheme())
31
31
 
32
32
  const showActiveTheme = (theme, focus = false) => {
33
- const themeSwitcher = document.querySelector('#bd-theme')
34
-
35
- if (!themeSwitcher) {
36
- return
37
- }
38
-
39
- const themeSwitcherText = document.querySelector('#bd-theme-text')
40
- const activeThemeIcon = document.querySelector('.theme-icon-active use')
41
- const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
42
- const themeIconUse = btnToActive?.querySelector('svg use')
43
- if (!btnToActive || !themeIconUse || !activeThemeIcon) {
33
+ const activeButtons = document.querySelectorAll(`[data-bs-theme-value="${theme}"]`)
34
+ if (!activeButtons.length) {
44
35
  return
45
36
  }
46
37
 
47
- const svgOfActiveBtn = themeIconUse.getAttribute('href')
48
-
38
+ // The control can render more than once per page (settings offcanvas +
39
+ // theme-controls-bar), so sync active state across every instance.
49
40
  document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
50
41
  element.classList.remove('active')
51
42
  element.setAttribute('aria-pressed', 'false')
52
43
  })
44
+ activeButtons.forEach(element => {
45
+ element.classList.add('active')
46
+ element.setAttribute('aria-pressed', 'true')
47
+ })
53
48
 
54
- btnToActive.classList.add('active')
55
- btnToActive.setAttribute('aria-pressed', 'true')
56
- activeThemeIcon.setAttribute('href', svgOfActiveBtn)
57
- const baseLabel = themeSwitcherText?.textContent?.trim() || 'Toggle theme'
58
- const themeSwitcherLabel = `${baseLabel} (${btnToActive.dataset.bsThemeValue})`
59
- themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
60
-
61
- if (focus) {
62
- themeSwitcher.focus()
49
+ // Legacy dropdown affordances (#bd-theme toggle with a mirrored icon)
50
+ // updated only when that markup is present.
51
+ const themeSwitcher = document.querySelector('#bd-theme')
52
+ const activeThemeIcon = document.querySelector('.theme-icon-active use')
53
+ const themeIconUse = activeButtons[0].querySelector('svg use')
54
+ if (activeThemeIcon && themeIconUse) {
55
+ activeThemeIcon.setAttribute('href', themeIconUse.getAttribute('href'))
56
+ }
57
+ if (themeSwitcher) {
58
+ const themeSwitcherText = document.querySelector('#bd-theme-text')
59
+ const baseLabel = themeSwitcherText?.textContent?.trim() || 'Toggle theme'
60
+ themeSwitcher.setAttribute('aria-label', `${baseLabel} (${theme})`)
61
+ if (focus) {
62
+ themeSwitcher.focus()
63
+ }
63
64
  }
64
65
  }
65
66
 
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-009
1
2
  /**
2
3
  * ===================================================================
3
4
  * SWIPE GESTURES - Mobile Offcanvas Control
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-009
1
2
  /**
2
3
  * ===================================================================
3
4
  * KEYBOARD SHORTCUTS - Navigation Enhancements
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-008
1
2
  /**
2
3
  * ===================================================================
3
4
  * SCROLL SPY - Intersection Observer Implementation
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-008
1
2
  /**
2
3
  * ===================================================================
3
4
  * SIDEBAR STATE - Persistence and State Management
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-008
1
2
  /**
2
3
  * ===================================================================
3
4
  * SIDEBAR VISIBILITY - Hide/show left docs sidebar with persistence
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-008
1
2
  /**
2
3
  * ===================================================================
3
4
  * SMOOTH SCROLL - Enhanced Anchor Navigation
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-031
1
2
  /**
2
3
  * ===================================================================
3
4
  * APPEARANCE — Runtime theme controls (color mode + primary color)
@@ -11,13 +12,20 @@
11
12
  *
12
13
  * - Color mode buttons (light / dark / auto) — writes data-bs-theme
13
14
  * to <html> and persists to localStorage["theme"] (compatible with
14
- * halfmoon.js so both stay in sync).
15
+ * halfmoon.js so both stay in sync). Skipped in the settings
16
+ * offcanvas (compact mode), where the halfmoon segmented control
17
+ * directly above already owns color mode.
15
18
  *
16
19
  * - Primary color picker — writes to localStorage["zer0-appearance"]
17
20
  * and updates --zer0-color-primary live so every component that uses
18
21
  * the token (buttons, callouts, FABs, focus rings) reflects the
19
22
  * change without a reload. Cleared by the Reset button.
20
23
  *
24
+ * Host resolution: an explicit [data-appearance-panel-host] (e.g. the
25
+ * theme-preview sidebar) gets the full panel; otherwise the panel
26
+ * mounts compactly into #zer0AppearanceSlot inside the Settings
27
+ * offcanvas Appearance tab (legacy fallback: the offcanvas body).
28
+ *
21
29
  * Initial paint is already handled by the script emitted in
22
30
  * _includes/core/tokens-inline.html so there is no flash of default
23
31
  * color when the page loads.
@@ -33,6 +41,7 @@
33
41
  const STORAGE_KEY = 'zer0-appearance';
34
42
  const THEME_KEY = 'theme';
35
43
  const PANEL_HOST_SELECTOR = '[data-appearance-panel-host]';
44
+ const SLOT_HOST_SELECTOR = '#zer0AppearanceSlot';
36
45
  const FALLBACK_HOST_SELECTOR = '#info-section .offcanvas-body';
37
46
 
38
47
  function readPrefs() {
@@ -75,18 +84,15 @@
75
84
  try { localStorage.setItem(THEME_KEY, mode); } catch (e) { /* ignore */ }
76
85
  }
77
86
 
78
- function buildPanel(host) {
87
+ function buildPanel(host, compact) {
79
88
  const prefs = readPrefs();
80
89
  const mode = resolveColorMode();
81
90
  const primary = prefs.primary || getInitialPrimary();
82
91
 
83
92
  const wrapper = document.createElement('section');
84
- wrapper.className = 'zer0-appearance-panel border rounded p-3 mt-3';
85
93
  wrapper.setAttribute('aria-labelledby', 'zer0-appearance-heading');
86
- wrapper.innerHTML =
87
- '<h3 id="zer0-appearance-heading" class="h6 mb-3">' +
88
- '<i class="bi bi-palette me-2" aria-hidden="true"></i>Appearance' +
89
- '</h3>' +
94
+
95
+ const modeGroupHtml =
90
96
  '<div class="mb-3" role="group" aria-label="Color mode">' +
91
97
  '<label class="form-label small text-body-secondary mb-2">Color mode</label>' +
92
98
  '<div class="btn-group w-100" role="group">' +
@@ -97,18 +103,45 @@
97
103
  '<button type="button" class="btn btn-outline-secondary btn-sm" data-mode="auto">' +
98
104
  '<i class="bi bi-circle-half me-1" aria-hidden="true"></i>Auto</button>' +
99
105
  '</div>' +
100
- '</div>' +
106
+ '</div>';
107
+
108
+ // In compact mode the section heading is the visible label, so the
109
+ // <label> stays for assistive tech but is visually hidden.
110
+ const pickerHtml = (labelClass) =>
101
111
  '<div class="mb-3">' +
102
- '<label for="zer0-appearance-primary" class="form-label small text-body-secondary">' +
112
+ '<label for="zer0-appearance-primary" class="' + labelClass + '">' +
103
113
  'Primary color</label>' +
104
114
  '<input type="color" id="zer0-appearance-primary" class="form-control form-control-color" ' +
105
115
  'aria-describedby="zer0-appearance-primary-help">' +
106
116
  '<div id="zer0-appearance-primary-help" class="form-text small">' +
107
117
  'Overrides <code>--zer0-color-primary</code> across the theme.</div>' +
108
- '</div>' +
109
- '<button type="button" class="btn btn-link btn-sm p-0" data-appearance-reset>' +
110
- '<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset to defaults' +
111
- '</button>';
118
+ '</div>';
119
+
120
+ if (compact) {
121
+ // Settings offcanvas: color mode lives in the halfmoon control just
122
+ // above, so render only the primary-color section, styled to match
123
+ // the panel's other h6 section headings.
124
+ wrapper.className = 'zer0-appearance-panel mb-4';
125
+ wrapper.innerHTML =
126
+ '<h6 id="zer0-appearance-heading" class="text-body-secondary small text-uppercase fw-semibold mb-2">' +
127
+ '<i class="bi bi-eyedropper me-1" aria-hidden="true"></i>Primary Color' +
128
+ '</h6>' +
129
+ pickerHtml('visually-hidden') +
130
+ '<button type="button" class="btn btn-link btn-sm p-0" data-appearance-reset>' +
131
+ '<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset color' +
132
+ '</button>';
133
+ } else {
134
+ wrapper.className = 'zer0-appearance-panel border rounded p-3 mt-3';
135
+ wrapper.innerHTML =
136
+ '<h3 id="zer0-appearance-heading" class="h6 mb-3">' +
137
+ '<i class="bi bi-palette me-2" aria-hidden="true"></i>Appearance' +
138
+ '</h3>' +
139
+ modeGroupHtml +
140
+ pickerHtml('form-label small text-body-secondary') +
141
+ '<button type="button" class="btn btn-link btn-sm p-0" data-appearance-reset>' +
142
+ '<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset to defaults' +
143
+ '</button>';
144
+ }
112
145
 
113
146
  host.appendChild(wrapper);
114
147
 
@@ -116,7 +149,7 @@
116
149
  const picker = wrapper.querySelector('#zer0-appearance-primary');
117
150
  picker.value = hexishOrDefault(primary, '#007bff');
118
151
 
119
- // Wire color-mode buttons
152
+ // Wire color-mode buttons (full panel only)
120
153
  wrapper.querySelectorAll('[data-mode]').forEach((btn) => {
121
154
  const m = btn.dataset.mode;
122
155
  btn.setAttribute('aria-pressed', String(m === mode));
@@ -183,10 +216,13 @@
183
216
  }
184
217
 
185
218
  function mount() {
186
- const host = document.querySelector(PANEL_HOST_SELECTOR) || document.querySelector(FALLBACK_HOST_SELECTOR);
219
+ const explicitHost = document.querySelector(PANEL_HOST_SELECTOR);
220
+ const host = explicitHost
221
+ || document.querySelector(SLOT_HOST_SELECTOR)
222
+ || document.querySelector(FALLBACK_HOST_SELECTOR);
187
223
  if (!host) return; // info-section not present on this page
188
224
  if (host.querySelector('.zer0-appearance-panel')) return; // already mounted
189
- buildPanel(host);
225
+ buildPanel(host, !explicitHost);
190
226
  }
191
227
 
192
228
  if (document.readyState === 'loading') {
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-066
1
2
  /**
2
3
  * Nanobar Initialization — config-driven page-load progress bar.
3
4
  *
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-055
1
2
  /**
2
3
  * ==============================================================================
3
4
  * NAVIGATION SCRIPTS — DEPRECATED
@@ -1,3 +1,4 @@
1
+ // Features: ZER0-044, ZER0-045
1
2
  /*
2
3
  * obsidian-graph.js
3
4
  *
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-044
1
2
  /*
2
3
  * obsidian-wiki-links.js
3
4
  *
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-065
1
2
  /**
2
3
  * palette-generator.js
3
4
  * Color palette generator + live CSS variable editor for the Theme Customizer.
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-034
1
2
  /* -----------------------------------------------
2
3
  /* Author :
3
4
  /* MIT license: http://opensource.org/licenses/MIT
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-032
1
2
  /**
2
3
  * Search Modal Controller
3
4
  * - Opens modal on navigation:searchRequest event ("/" shortcut)
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-067
1
2
  /**
2
3
  * ===================================================================
3
4
  * Setup Wizard — _config.yml Generator
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-069
1
2
  (function () {
2
3
  'use strict';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-065
1
2
  /**
2
3
  * skin-editor.js — Colorffy-inspired skin editor for zer0-mistakes theme
3
4
  *
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-068
1
2
  /**
2
3
  * Content table CSV copy — injects a toolbar button on markdown/HTML tables
3
4
  * in main reading areas. Scoped to content wrappers; skips admin and sitemap.
@@ -1,3 +1,4 @@
1
+ // Feature: ZER0-065
1
2
  /**
2
3
  * theme-customizer.js
3
4
  * Powers the Theme Customizer admin page.
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Features: ZER0-015, ZER0-028
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/utils/analyze-commits
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Features: ZER0-035, ZER0-073
2
3
 
3
4
  # giscus-discussions — read, draft, and post the GitHub Discussions that back
4
5
  # the site's Giscus comment threads. This is the engine the Claude Code
data/scripts/bin/install CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ # Feature: ZER0-003
2
3
  # scripts/bin/install — zer0-mistakes installer entrypoint (v2)
3
4
  #
4
5
  # Routes to scripts/install/cli.sh (new spec-driven architecture).
data/scripts/bin/validate CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Feature: ZER0-048
2
3
 
3
4
  # Canonical preflight validator for zer0-mistakes.
4
5
  # Usage: ./scripts/bin/validate [options]
data/scripts/build CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Feature: ZER0-028
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/bin/build
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # Feature: ZER0-072
2
3
  # frozen_string_literal: true
3
4
 
4
5
  # =============================================================================
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Features: ZER0-012, ZER0-028
2
3
  #
3
4
  # Script Name: convert-notebooks.sh
4
5
  # Description: Converts Jupyter notebooks (.ipynb) to Jekyll-compatible Markdown
@@ -339,7 +340,7 @@ clean_converted() {
339
340
  rm -f "$md_file"
340
341
  info "Removed: $md_file"
341
342
  fi
342
- ((count++))
343
+ count=$((count + 1))
343
344
  fi
344
345
  done < <(find "$OUTPUT_DIR" -name "*.md" -print0 2>/dev/null)
345
346
 
@@ -364,7 +365,7 @@ list_notebooks() {
364
365
  else
365
366
  while IFS= read -r -d '' notebook_file; do
366
367
  echo "$notebook_file"
367
- ((count++))
368
+ count=$((count + 1))
368
369
  done < <(find "$NOTEBOOKS_DIR" -name "*.ipynb" -print0 2>/dev/null)
369
370
  fi
370
371
 
@@ -400,7 +401,7 @@ main() {
400
401
  local count=0
401
402
  while IFS= read -r -d '' notebook_file; do
402
403
  convert_notebook "$notebook_file"
403
- ((count++))
404
+ count=$((count + 1))
404
405
  done < <(find "$NOTEBOOKS_DIR" -name "*.ipynb" -print0 2>/dev/null)
405
406
 
406
407
  if [[ $count -eq 0 ]]; then
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Feature: ZER0-028
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/utils/fix-markdown
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Features: ZER0-004, ZER0-028
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/features/generate-preview-images
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ # Feature: ZER0-052
2
3
  # =============================================================================
3
4
  # generate-roadmap.sh
4
5
  # =============================================================================
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Feature: ZER0-004
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/features/install-preview-generator
data/scripts/release CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Features: ZER0-015, ZER0-028
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/bin/release
@@ -0,0 +1,11 @@
1
+ # Python toolchain for repository automation scripts.
2
+ #
3
+ # Currently the notebook-conversion pipeline: scripts/convert-notebooks.sh and
4
+ # .github/workflows/convert-notebooks.yml use nbconvert to render
5
+ # pages/_notebooks/*.ipynb into Jekyll-compatible Markdown.
6
+ #
7
+ # This file also acts as the pip cache key for actions/setup-python in that
8
+ # workflow (see `cache-dependency-path`). Keep it in sync with the toolchain the
9
+ # scripts import; a missing file here makes `cache: pip` hard-fail the run.
10
+ jupyter
11
+ nbconvert
data/scripts/setup.sh CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Features: ZER0-002, ZER0-028
2
3
 
3
4
  # ============================================================================
4
5
  # WRAPPER: This script forwards to scripts/utils/setup
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # tag-features — reverse-traceability enforcement for the feature registry.
6
+ #
7
+ # Every source file a feature lists under `references:` carries a top-of-file
8
+ # `Feature: ZER0-NNN` comment, so the link runs both ways (registry -> code and
9
+ # code -> registry). This tool is the single source of truth for that rule:
10
+ #
11
+ # ruby scripts/tag-features # --check (default): exit 1 + list any
12
+ # # referenced source file missing its tag
13
+ # ruby scripts/tag-features --write # insert the missing tags (idempotent,
14
+ # # line-ending-preserving)
15
+ #
16
+ # The `features` test suite runs `--check`, so a new feature that forgets to tag
17
+ # its files fails CI. Governance: .github/instructions/features.instructions.md.
18
+ #
19
+ # Scope (mirrors what a feature's references implement, and only what can carry a
20
+ # comment): .rb .js .mjs .scss .sh .html .yml, plus Dockerfile/Makefile and the
21
+ # scripts/{bin/*,build,release} wrappers. Excluded by design: markdown docs,
22
+ # JSON (no comment syntax), vendored/minified third-party libs, directory refs,
23
+ # and _config.yml (it has an exact-copy admin mirror enforced elsewhere).
24
+ #
25
+ # Portable across Ruby 2.6 (macOS system) and 3.x (CI/Docker).
26
+ # ---------------------------------------------------------------------------
27
+
28
+ require 'date'
29
+ require 'yaml'
30
+
31
+ MIRROR = '_data/features.yml'
32
+ MODE = ARGV.include?('--write') ? :write : :check
33
+
34
+ CODE_EXT = %w[.rb .js .mjs .scss .sh .html .yml].freeze
35
+ SLASH_EXT = %w[.js .mjs .scss].freeze
36
+ NOEXT_FILES = %w[docker/Dockerfile Makefile scripts/release scripts/build].freeze
37
+ EXCLUDE_FILES = %w[_config.yml assets/js/particles.js assets/js/halfmoon.js].freeze
38
+
39
+ def load_yaml(path)
40
+ YAML.load_file(path, permitted_classes: [Date, Time], aliases: true)
41
+ rescue ArgumentError
42
+ YAML.load_file(path)
43
+ end
44
+
45
+ def ref_paths(refs)
46
+ return [] unless refs.is_a?(Hash)
47
+
48
+ refs.values.flat_map { |v| v.is_a?(Array) ? v : [v] }.select { |v| v.is_a?(String) }
49
+ end
50
+
51
+ def vendored?(path)
52
+ path.include?('/vendor/') || path.end_with?('.min.js') || EXCLUDE_FILES.include?(path)
53
+ end
54
+
55
+ # Which referenced paths must carry a tag.
56
+ def taggable?(path)
57
+ return false if path.end_with?('/') || vendored?(path)
58
+
59
+ ext = File.extname(path)
60
+ return false if %w[.md .json].include?(ext)
61
+
62
+ CODE_EXT.include?(ext) || NOEXT_FILES.include?(path) || (path.start_with?('scripts/bin/') && ext.empty?)
63
+ end
64
+
65
+ def tag_text(ids)
66
+ (ids.length == 1 ? 'Feature: ' : 'Features: ') + ids.join(', ')
67
+ end
68
+
69
+ def already_tagged?(raw, ids)
70
+ head = raw[0, 500].to_s
71
+ ids.any? { |id| head.include?(id) }
72
+ end
73
+
74
+ # Insert the tag with type-aware placement, preserving the file's line endings.
75
+ def insert_tag(path, ids)
76
+ raw = File.binread(path).force_encoding('UTF-8')
77
+ return :skipped if already_tagged?(raw, ids)
78
+
79
+ nl = raw.include?("\r\n") ? "\r\n" : "\n"
80
+ ext = File.extname(path)
81
+ tag = tag_text(ids)
82
+ lines = raw.each_line.to_a
83
+
84
+ if ext == '.html' && raw.lstrip.start_with?('---')
85
+ idx = lines.index { |l| l.strip == '---' } || 0
86
+ lines.insert(idx + 1, "# #{tag}#{nl}") # invisible YAML front-matter comment
87
+ elsif ext == '.html'
88
+ lines.unshift("{% comment %} #{tag} {% endcomment %}#{nl}") # stripped at build
89
+ else
90
+ prefix = SLASH_EXT.include?(ext) ? '//' : '#'
91
+ line = "#{prefix} #{tag}#{nl}"
92
+ first = lines[0].to_s
93
+ if first.start_with?('#!') || first.lstrip.start_with?('# syntax=') || first.lstrip.start_with?('# escape=')
94
+ lines.insert(1, line) # keep shebang / Dockerfile directive on line 1
95
+ else
96
+ lines.unshift(line)
97
+ end
98
+ end
99
+
100
+ File.binwrite(path, lines.join)
101
+ :written
102
+ end
103
+
104
+ data = load_yaml(MIRROR)
105
+ feats = data && data['features']
106
+ abort 'tag-features: could not read features registry' unless feats.is_a?(Array)
107
+
108
+ by_file = Hash.new { |h, k| h[k] = [] }
109
+ feats.each do |f|
110
+ next if f['implemented'] == false # removed features keep (absent) refs; nothing to tag
111
+
112
+ ref_paths(f['references']).each do |p|
113
+ by_file[p] << f['id'] if taggable?(p) && File.exist?(p)
114
+ end
115
+ end
116
+
117
+ if MODE == :write
118
+ written = 0
119
+ by_file.each { |path, ids| written += 1 if insert_tag(path, ids.uniq.sort) == :written }
120
+ puts "tag-features: wrote #{written} tag(s); #{by_file.size - written} already current"
121
+ exit 0
122
+ end
123
+
124
+ missing = by_file.reject do |path, ids|
125
+ already_tagged?(File.binread(path).force_encoding('UTF-8'), ids.uniq.sort)
126
+ end
127
+
128
+ if missing.empty?
129
+ puts "tag-features: all #{by_file.size} referenced source files carry their Feature tag"
130
+ exit 0
131
+ end
132
+
133
+ warn "tag-features: #{missing.length} referenced source file(s) missing a `Feature: ZER0-NNN` comment:"
134
+ missing.each { |path, ids| warn " - #{path} (needs #{ids.uniq.sort.join(', ')})" }
135
+ warn 'Fix: run `ruby scripts/tag-features --write` (or add the comment by hand).'
136
+ exit 1
@@ -81,6 +81,13 @@ feats.each_with_index do |f, i|
81
81
  die "#{id}: required field `#{key}` is missing/empty" if val.nil? || (val.respond_to?(:empty?) && val.empty?)
82
82
  end
83
83
 
84
+ # Title/description are rendered UNwrapped into HTML on /features/; a raw
85
+ # angle bracket (e.g. "/authors/<key>/") becomes a stray tag that swallows
86
+ # every following card. Keep them plain prose.
87
+ %w[title description].each do |key|
88
+ die "#{id}: `#{key}` must not contain raw `<`/`>` (breaks HTML on /features/ — use :key or &lt;)" if f[key].to_s =~ /[<>]/
89
+ end
90
+
84
91
  die "#{id}: id must match ZER0-NNN" unless f['id'] =~ /\AZER0-\d{3}\z/
85
92
  die "#{id}: duplicate id" if seen[f['id']]
86
93
  seen[f['id']] = true
@@ -1,4 +1,5 @@
1
1
  #!/bin/bash
2
+ # Feature: ZER0-053
2
3
 
3
4
  # Vendor asset installer for zer0-mistakes Jekyll theme
4
5
  # Downloads third-party JS/CSS libraries defined in vendor-manifest.json