docyard 0.4.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db5ddabfe4a3b7bf25c24876b915188bd4768ca543772ad0a8cfc971ee791fee
4
- data.tar.gz: d906b1c2ab33102c9ade6e2cb23f6d067fdbf240d6a4a8e2aa27a52a9ba62b42
3
+ metadata.gz: 52a27eaf396879abae3d0b091e5a488629022bb777838fafc17c0bb07e15d65b
4
+ data.tar.gz: ec6a567e2e411800f67f96351a80f7e60823ac399df8cac7e82b7c3a9a1370b5
5
5
  SHA512:
6
- metadata.gz: 2abc1d991cade7059cfaa0b54c2b6f88b33625937e8073cee9d8f61948149567a209a378efd225e9fe85b45b7579ef4e1d81599be5a8cbee970e9cb6e5218a3c
7
- data.tar.gz: dcd2baa65fcdd4c00825c6f895c035ffd53603ec22503233b600925084b6512b651c9a009687682f1f5cab3a0c8d8e3d0a5c7586634d6da174e4e4c5a95f9a53
6
+ metadata.gz: 6e0d9995254e35e291250db40c9572d3bde1a76d86433d0fbcfb6e2a8602d6e68ad6be910d7a03036814dcdc0fba64289d68c760d9d6f1336376e2f709ec35b8
7
+ data.tar.gz: 127131f44ea7140f227a8e95468b96e09256b5fced9ef557e37e5a1caefc888adb902a8b2d164474cb1f31ae0ddd4b76b1c4684b39e5125ddce17c84bbd754e7
data/CHANGELOG.md CHANGED
@@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.4.0] - 2025-01-16
10
+ ## [0.5.0] - 2025-11-18
11
+
12
+ ### Added
13
+ - **Table of Contents** - Auto-generated TOC from h2-h4 headings with clickable anchor links and smooth scrolling (#30)
14
+ - **Previous/Next Navigation** - Auto-detection from sidebar order with frontmatter override support and configurable labels (#31)
15
+
16
+ ## [0.4.0] - 2025-11-16
11
17
 
12
18
  ### Added
13
19
  - **Static site generation** - Build system with `docyard build` command (#27)
@@ -29,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
29
35
  - Component CSS accessibility and performance improvements (#24)
30
36
  - Table responsive styling with proper wrapper element (#23)
31
37
 
32
- ## [0.3.0] - 2025-01-09
38
+ ## [0.3.0] - 2025-11-09
33
39
 
34
40
  ### Added
35
41
  - Configuration system with optional `docyard.yml` file (#20)
@@ -85,7 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
85
91
  - Initial gem structure
86
92
  - Project scaffolding
87
93
 
88
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.4.0...HEAD
94
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.5.0...HEAD
95
+ [0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
89
96
  [0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
90
97
  [0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
91
98
  [0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -20,6 +20,8 @@ Build beautiful documentation sites with hot reload, dark mode, and powerful mar
20
20
  ### Navigation
21
21
  - **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
22
22
  - **Sidebar customization** - Custom ordering, icons, and external links via config
23
+ - **Table of Contents** - Auto-generated TOC with heading anchors and smooth scrolling
24
+ - **Previous/Next navigation** - Auto-detection from sidebar with frontmatter override support
23
25
  - **Active page highlighting** - Always know where you are
24
26
 
25
27
  ### Markdown
@@ -139,6 +141,32 @@ description: Page description
139
141
 
140
142
  Currently supported:
141
143
  - `title` - Page title (shown in `<title>` tag)
144
+ - `prev` - Customize or disable previous link
145
+ - `next` - Customize or disable next link
146
+
147
+ ### Customizing Navigation
148
+
149
+ Control previous/next links per page via frontmatter:
150
+
151
+ ```yaml
152
+ ---
153
+ title: My Page
154
+ prev: false # Disable previous link
155
+ next:
156
+ text: Custom Next Page
157
+ link: /custom-path
158
+ ---
159
+ ```
160
+
161
+ Configure labels globally in `docyard.yml`:
162
+
163
+ ```yaml
164
+ navigation:
165
+ footer:
166
+ enabled: true
167
+ prev_text: "← Back"
168
+ next_text: "Forward →"
169
+ ```
142
170
 
143
171
  ### Linking Between Pages
144
172
 
@@ -227,18 +255,15 @@ bundle exec rubocop
227
255
 
228
256
  ## Roadmap
229
257
 
230
- **v0.4.0 - Just shipped:**
231
- - Static site generation with `docyard build`
232
- - Asset bundling with minification and cache busting
233
- - SEO files (sitemap.xml, robots.txt)
234
- - Preview server for testing builds
235
- - Sidebar customization via config
236
- - Improved init templates
237
-
238
- **Next up:**
239
- - Search functionality
240
- - Table of contents
241
- - More markdown components
258
+ **v0.5.0 - Just shipped:**
259
+ - Table of Contents with heading anchors
260
+ - Previous/Next page navigation with auto-detection
261
+
262
+ **Next up (v0.6.0+):**
263
+ - Code block enhancements (line numbers, highlighting, diffs)
264
+ - Search functionality (client-side with Cmd/K)
265
+ - Details/collapsible blocks
266
+ - More markdown extensions
242
267
 
243
268
  ## Contributing
244
269
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ class HeadingAnchorProcessor < BaseProcessor
6
+ self.priority = 30
7
+
8
+ def postprocess(html)
9
+ add_anchor_links(html)
10
+ end
11
+
12
+ private
13
+
14
+ def add_anchor_links(html)
15
+ html.gsub(%r{<(h[2-6])\s+id="([^"]+)">(.*?)</\1>}m) do |_match|
16
+ tag = Regexp.last_match(1)
17
+ id = Regexp.last_match(2)
18
+ content = Regexp.last_match(3)
19
+
20
+ anchor_html = render_anchor_link(id)
21
+
22
+ "<#{tag} id=\"#{id}\">#{content}#{anchor_html}</#{tag}>"
23
+ end
24
+ end
25
+
26
+ def render_anchor_link(id)
27
+ renderer = Renderer.new
28
+ renderer.render_partial("_heading_anchor", {
29
+ id: id
30
+ })
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ class TableOfContentsProcessor < BaseProcessor
6
+ self.priority = 35
7
+
8
+ def postprocess(html)
9
+ headings = extract_headings(html)
10
+ Thread.current[:docyard_toc] = headings
11
+ html
12
+ end
13
+
14
+ private
15
+
16
+ def extract_headings(html)
17
+ headings = []
18
+
19
+ html.scan(%r{<(h[2-4])\s+id="([^"]+)">(.*?)</\1>}m) do
20
+ level = Regexp.last_match(1)[1].to_i
21
+ id = Regexp.last_match(2)
22
+ text = strip_html(Regexp.last_match(3))
23
+
24
+ headings << {
25
+ level: level,
26
+ id: id,
27
+ text: text
28
+ }
29
+ end
30
+
31
+ build_hierarchy(headings)
32
+ end
33
+
34
+ def build_hierarchy(headings)
35
+ return [] if headings.empty?
36
+
37
+ root = []
38
+ stack = []
39
+
40
+ headings.each do |heading|
41
+ heading[:children] = []
42
+
43
+ stack.pop while stack.any? && stack.last[:level] >= heading[:level]
44
+
45
+ if stack.empty?
46
+ root << heading
47
+ else
48
+ stack.last[:children] << heading
49
+ end
50
+
51
+ stack << heading
52
+ end
53
+
54
+ root
55
+ end
56
+
57
+ def strip_html(text)
58
+ text.gsub(%r{<a[^>]*class="heading-anchor"[^>]*>.*?</a>}, "")
59
+ .gsub(/<[^>]+>/, "")
60
+ .strip
61
+ end
62
+ end
63
+ end
64
+ end
@@ -27,6 +27,13 @@ module Docyard
27
27
  },
28
28
  "sidebar" => {
29
29
  "items" => []
30
+ },
31
+ "navigation" => {
32
+ "footer" => {
33
+ "enabled" => true,
34
+ "prev_text" => "Previous",
35
+ "next_text" => "Next"
36
+ }
30
37
  }
31
38
  }.freeze
32
39
 
@@ -63,6 +70,10 @@ module Docyard
63
70
  @sidebar ||= ConfigSection.new(data["sidebar"])
64
71
  end
65
72
 
73
+ def navigation
74
+ @navigation ||= ConfigSection.new(data["navigation"])
75
+ end
76
+
66
77
  private
67
78
 
68
79
  def load_config_data
@@ -35,7 +35,8 @@ module Docyard
35
35
  "warning-octagon" => '<path d="M120,136V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0ZM232,91.55v72.9a15.86,15.86,0,0,1-4.69,11.31l-51.55,51.55A15.86,15.86,0,0,1,164.45,232H91.55a15.86,15.86,0,0,1-11.31-4.69L28.69,175.76A15.86,15.86,0,0,1,24,164.45V91.55a15.86,15.86,0,0,1,4.69-11.31L80.24,28.69A15.86,15.86,0,0,1,91.55,24h72.9a15.86,15.86,0,0,1,11.31,4.69l51.55,51.55A15.86,15.86,0,0,1,232,91.55Zm-16,0L164.45,40H91.55L40,91.55v72.9L91.55,216h72.9L216,164.45ZM128,160a12,12,0,1,0,12,12A12,12,0,0,0,128,160Z"/>',
36
36
  "siren" => '<path d="M120,16V8a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0Zm80,32a8,8,0,0,0,5.66-2.34l8-8a8,8,0,0,0-11.32-11.32l-8,8A8,8,0,0,0,200,48ZM50.34,45.66A8,8,0,0,0,61.66,34.34l-8-8A8,8,0,0,0,42.34,37.66Zm87,26.45a8,8,0,1,0-2.64,15.78C153.67,91.08,168,108.32,168,128a8,8,0,0,0,16,0C184,100.6,163.93,76.57,137.32,72.11ZM232,176v24a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V176a16,16,0,0,1,16-16V128a88,88,0,0,1,88.67-88c48.15.36,87.33,40.29,87.33,89v31A16,16,0,0,1,232,176ZM56,160H200V129c0-40-32.05-72.71-71.45-73H128a72,72,0,0,0-72,72Zm160,40V176H40v24H216Z"/>',
37
37
  "file" => '<path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"/>',
38
- "terminal-window" => '<path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"/>'
38
+ "terminal-window" => '<path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"/>',
39
+ "list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>'
39
40
  },
40
41
  "bold" => {
41
42
  "heart" => '<path d="M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z"/>'
@@ -10,6 +10,8 @@ require_relative "components/tabs_processor"
10
10
  require_relative "components/icon_processor"
11
11
  require_relative "components/code_block_processor"
12
12
  require_relative "components/table_wrapper_processor"
13
+ require_relative "components/heading_anchor_processor"
14
+ require_relative "components/table_of_contents_processor"
13
15
 
14
16
  module Docyard
15
17
  class Markdown
@@ -53,6 +55,10 @@ module Docyard
53
55
  frontmatter.dig("sidebar", "collapsed")
54
56
  end
55
57
 
58
+ def toc
59
+ @toc ||= Thread.current[:docyard_toc] || []
60
+ end
61
+
56
62
  private
57
63
 
58
64
  def parse_frontmatter
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "renderer"
4
+ require_relative "utils/path_resolver"
5
+
6
+ module Docyard
7
+ class PrevNextBuilder
8
+ attr_reader :sidebar_tree, :current_path, :frontmatter, :config
9
+
10
+ def initialize(sidebar_tree:, current_path:, frontmatter: {}, config: {})
11
+ @sidebar_tree = sidebar_tree
12
+ @current_path = Utils::PathResolver.normalize(current_path)
13
+ @frontmatter = frontmatter
14
+ @config = config
15
+ end
16
+
17
+ def prev_next_links
18
+ return nil unless enabled?
19
+
20
+ {
21
+ prev: build_prev_link,
22
+ next: build_next_link
23
+ }
24
+ end
25
+
26
+ def to_html
27
+ links = prev_next_links
28
+ return "" if links.nil? || (links[:prev].nil? && links[:next].nil?)
29
+
30
+ Renderer.new.render_partial(
31
+ "_prev_next", {
32
+ prev: links[:prev],
33
+ next: links[:next],
34
+ prev_text: config_prev_text,
35
+ next_text: config_next_text
36
+ }
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def enabled?
43
+ return false if config_disabled?
44
+ return false if frontmatter_disabled?
45
+
46
+ true
47
+ end
48
+
49
+ def config_disabled?
50
+ return false if config.nil? || config.empty?
51
+
52
+ config == false || config["enabled"] == false || config[:enabled] == false
53
+ end
54
+
55
+ def frontmatter_disabled?
56
+ frontmatter["prev"] == false && frontmatter["next"] == false
57
+ end
58
+
59
+ def build_prev_link
60
+ return nil if frontmatter["prev"] == false
61
+
62
+ return build_frontmatter_link(frontmatter["prev"]) if frontmatter["prev"]
63
+
64
+ auto_prev_link
65
+ end
66
+
67
+ def build_next_link
68
+ return nil if frontmatter["next"] == false
69
+
70
+ return build_frontmatter_link(frontmatter["next"]) if frontmatter["next"]
71
+
72
+ auto_next_link
73
+ end
74
+
75
+ def build_frontmatter_link(value)
76
+ case value
77
+ when String
78
+ find_link_by_text(value)
79
+ when Hash
80
+ {
81
+ title: value["text"] || value[:text],
82
+ path: value["link"] || value[:link]
83
+ }
84
+ end
85
+ end
86
+
87
+ def find_link_by_text(text)
88
+ flat_links.find { |link| link[:title].downcase == text.downcase }
89
+ end
90
+
91
+ def auto_prev_link
92
+ index = current_page_index
93
+ return nil unless index&.positive?
94
+
95
+ flat_links[index - 1]
96
+ end
97
+
98
+ def auto_next_link
99
+ index = current_page_index
100
+ return nil unless index && index < flat_links.length - 1
101
+
102
+ flat_links[index + 1]
103
+ end
104
+
105
+ def current_page_index
106
+ @current_page_index ||= flat_links.find_index do |link|
107
+ normalized_path(link[:path]) == normalized_path(current_path)
108
+ end
109
+ end
110
+
111
+ def flat_links
112
+ @flat_links ||= begin
113
+ links = []
114
+ flatten_tree(sidebar_tree, links)
115
+ links.uniq { |link| normalized_path(link[:path]) }
116
+ end
117
+ end
118
+
119
+ def flatten_tree(items, links)
120
+ items.each do |item|
121
+ links << build_link(item) if valid_navigation_item?(item)
122
+ flatten_tree(item[:children], links) if item[:children]&.any?
123
+ end
124
+ end
125
+
126
+ def valid_navigation_item?(item)
127
+ item[:type] == :file && item[:path] && !external_link?(item[:path])
128
+ end
129
+
130
+ def build_link(item)
131
+ {
132
+ title: item[:footer_text] || item[:title],
133
+ path: item[:path]
134
+ }
135
+ end
136
+
137
+ def external_link?(path)
138
+ path.start_with?("http://", "https://")
139
+ end
140
+
141
+ def normalized_path(path)
142
+ return "" if path.nil?
143
+
144
+ path.gsub(/[?#].*$/, "")
145
+ end
146
+
147
+ def config_prev_text
148
+ return "Previous" if config.nil? || config.empty?
149
+
150
+ config["prev_text"] || config[:prev_text] || "Previous"
151
+ end
152
+
153
+ def config_next_text
154
+ return "Next" if config.nil? || config.empty?
155
+
156
+ config["next_text"] || config[:next_text] || "Next"
157
+ end
158
+ end
159
+ end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "rack"
5
5
  require_relative "sidebar_builder"
6
+ require_relative "prev_next_builder"
6
7
  require_relative "constants"
7
8
 
8
9
  module Docyard
@@ -46,9 +47,12 @@ module Docyard
46
47
  end
47
48
 
48
49
  def render_documentation_page(file_path, current_path)
50
+ sidebar_builder = build_sidebar_instance(current_path)
51
+
49
52
  html = renderer.render_file(
50
53
  file_path,
51
- sidebar_html: build_sidebar(current_path),
54
+ sidebar_html: sidebar_builder.to_html,
55
+ prev_next_html: build_prev_next(sidebar_builder, current_path, file_path),
52
56
  branding: branding_options
53
57
  )
54
58
 
@@ -60,14 +64,32 @@ module Docyard
60
64
  [Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
61
65
  end
62
66
 
63
- def build_sidebar(current_path)
67
+ def build_sidebar_instance(current_path)
64
68
  SidebarBuilder.new(
65
69
  docs_path: docs_path,
66
70
  current_path: current_path,
67
71
  config: config
72
+ )
73
+ end
74
+
75
+ def build_prev_next(sidebar_builder, current_path, file_path)
76
+ markdown_content = File.read(file_path)
77
+ markdown = Markdown.new(markdown_content)
78
+
79
+ PrevNextBuilder.new(
80
+ sidebar_tree: sidebar_builder.tree,
81
+ current_path: current_path,
82
+ frontmatter: markdown.frontmatter,
83
+ config: navigation_config
68
84
  ).to_html
69
85
  end
70
86
 
87
+ def navigation_config
88
+ return {} unless config
89
+
90
+ config.navigation&.footer || {}
91
+ end
92
+
71
93
  def branding_options
72
94
  return default_branding unless config
73
95
 
@@ -16,24 +16,33 @@ module Docyard
16
16
  @base_url = normalize_base_url(base_url)
17
17
  end
18
18
 
19
- def render_file(file_path, sidebar_html: "", branding: {})
19
+ def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
20
20
  markdown_content = File.read(file_path)
21
21
  markdown = Markdown.new(markdown_content)
22
22
 
23
23
  html_content = strip_md_from_links(markdown.html)
24
+ toc = markdown.toc
24
25
 
25
26
  render(
26
27
  content: html_content,
27
28
  page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
28
- sidebar_html: sidebar_html,
29
+ navigation: {
30
+ sidebar_html: sidebar_html,
31
+ prev_next_html: prev_next_html,
32
+ toc: toc
33
+ },
29
34
  branding: branding
30
35
  )
31
36
  end
32
37
 
33
- def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, sidebar_html: "", branding: {})
38
+ def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {})
34
39
  template = File.read(layout_path)
35
40
 
36
- assign_content_variables(content, page_title, sidebar_html)
41
+ sidebar_html = navigation[:sidebar_html] || ""
42
+ prev_next_html = navigation[:prev_next_html] || ""
43
+ toc = navigation[:toc] || []
44
+
45
+ assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
37
46
  assign_branding_variables(branding)
38
47
 
39
48
  ERB.new(template).result(binding)
@@ -85,10 +94,12 @@ module Docyard
85
94
  url.end_with?("/") ? url : "#{url}/"
86
95
  end
87
96
 
88
- def assign_content_variables(content, page_title, sidebar_html)
97
+ def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
89
98
  @content = content
90
99
  @page_title = page_title
91
100
  @sidebar_html = sidebar_html
101
+ @prev_next_html = prev_next_html
102
+ @toc = toc
92
103
  end
93
104
 
94
105
  def assign_branding_variables(branding)
@@ -0,0 +1,77 @@
1
+ .content h2[id],
2
+ .content h3[id],
3
+ .content h4[id],
4
+ .content h5[id],
5
+ .content h6[id] {
6
+ position: relative;
7
+ scroll-margin-top: 100px;
8
+ }
9
+
10
+ /* Tablet: Account for both primary + secondary header */
11
+ @media (max-width: 1280px) and (min-width: 1025px) {
12
+ .content h2[id],
13
+ .content h3[id],
14
+ .content h4[id],
15
+ .content h5[id],
16
+ .content h6[id] {
17
+ scroll-margin-top: calc(var(--header-height) + 3rem + var(--space-4));
18
+ }
19
+ }
20
+
21
+ .heading-anchor {
22
+ float: left;
23
+ margin-left: -0.75em;
24
+ padding-right: 0.25em;
25
+ font-weight: var(--font-weight-normal);
26
+ color: var(--color-primary);
27
+ text-decoration: none !important;
28
+ opacity: 0;
29
+ transition: opacity var(--transition-fast);
30
+ cursor: pointer;
31
+ user-select: none;
32
+ }
33
+
34
+ .heading-anchor:hover,
35
+ .heading-anchor:focus {
36
+ opacity: 1;
37
+ text-decoration: none !important;
38
+ }
39
+
40
+ .heading-anchor:active,
41
+ .heading-anchor:visited {
42
+ text-decoration: none !important;
43
+ }
44
+
45
+ .heading-anchor:focus {
46
+ outline: 2px solid var(--color-primary);
47
+ outline-offset: 2px;
48
+ border-radius: 4px;
49
+ }
50
+
51
+ .content h2[id]:hover .heading-anchor,
52
+ .content h3[id]:hover .heading-anchor,
53
+ .content h4[id]:hover .heading-anchor,
54
+ .content h5[id]:hover .heading-anchor,
55
+ .content h6[id]:hover .heading-anchor {
56
+ opacity: 1;
57
+ }
58
+
59
+ @media (max-width: 1024px) {
60
+ .heading-anchor {
61
+ position: static;
62
+ margin-left: var(--space-2);
63
+ float: none;
64
+ opacity: 0.6;
65
+ }
66
+
67
+ .heading-anchor:hover,
68
+ .heading-anchor:focus {
69
+ opacity: 1;
70
+ }
71
+ }
72
+
73
+ @media (prefers-reduced-motion: reduce) {
74
+ .heading-anchor {
75
+ transition: none;
76
+ }
77
+ }