utopia-project 0.36.0 → 0.37.1
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/utopia/project.rb +1 -1
- data/lib/utopia/project/guide.rb +8 -0
- data/lib/utopia/project/releases_document.rb +14 -0
- data/lib/utopia/project/sidebar.rb +114 -0
- data/lib/utopia/project/version.rb +1 -1
- data/pages/_page.xnode +1 -3
- data/pages/guides/show.xnode +23 -15
- data/pages/releases/index.xnode +16 -11
- data/public/_static/sidebar.js +175 -0
- data/public/_static/site.css +291 -6
- data.tar.gz.sig +0 -0
- metadata +3 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6467795f4ec1c68ecc0606fbd6ca86367532a9f4ba85ecd7a45d9246037c289
|
4
|
+
data.tar.gz: 143589e65aa521ef0ccab3886a02ccd86cd6e83519b91935ad3a1bf14086e1cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67297da20eb02586984c38b3586cf844600b67179b50d00a13f15e264399cd83fe79420d64348139eabf368fc0d56fd73fe730b7f42c19c4247032e3423cf261
|
7
|
+
data.tar.gz: c384960157c37c7a3934a40c2b557fbdf9b054a889104be3599beba0d8d9ec9143c0523c72ebd14f1d853c9394f82b318078f57c9394de4ae039eb148169e782
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/bake/utopia/project.rb
CHANGED
data/lib/utopia/project/guide.rb
CHANGED
@@ -7,6 +7,8 @@ require "utopia/path"
|
|
7
7
|
require "xrb/reference"
|
8
8
|
require "decode"
|
9
9
|
|
10
|
+
require_relative "sidebar"
|
11
|
+
|
10
12
|
module Utopia
|
11
13
|
module Project
|
12
14
|
# Provides structured access to a directory which contains documentation and source code to explain a specific process.
|
@@ -127,6 +129,12 @@ module Utopia
|
|
127
129
|
@documentation ||= self.best_documentation
|
128
130
|
end
|
129
131
|
|
132
|
+
# Generate a navigation from the guide's document.
|
133
|
+
# @returns [Sidebar]
|
134
|
+
def navigation
|
135
|
+
@navigation ||= Sidebar.build(self.document)
|
136
|
+
end
|
137
|
+
|
130
138
|
# All files associated with this guide.
|
131
139
|
# @returns [Array(String)] The file-system paths.
|
132
140
|
def files
|
@@ -4,6 +4,7 @@
|
|
4
4
|
# Copyright, 2024-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "document"
|
7
|
+
require_relative "sidebar"
|
7
8
|
|
8
9
|
module Utopia
|
9
10
|
module Project
|
@@ -107,6 +108,19 @@ module Utopia
|
|
107
108
|
yield release(name)
|
108
109
|
end
|
109
110
|
end
|
111
|
+
|
112
|
+
# Generate a navigation from the releases.
|
113
|
+
# @returns [Sidebar]
|
114
|
+
def navigation
|
115
|
+
@navigation ||= begin
|
116
|
+
entries = release_names.map do |name|
|
117
|
+
anchor = name.downcase.gsub(/\s+/, "-")
|
118
|
+
Sidebar::Entry.new(name, 2, anchor)
|
119
|
+
end
|
120
|
+
|
121
|
+
Sidebar.new(entries)
|
122
|
+
end
|
123
|
+
end
|
110
124
|
end
|
111
125
|
end
|
112
126
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "renderer"
|
7
|
+
|
8
|
+
module Utopia
|
9
|
+
module Project
|
10
|
+
# Generates a sidebar navigation from markdown document headings.
|
11
|
+
class Sidebar
|
12
|
+
|
13
|
+
# Represents a sidebar navigation entry with title, level, and anchor.
|
14
|
+
class Entry
|
15
|
+
def initialize(title_html, level, anchor)
|
16
|
+
@title_html = title_html
|
17
|
+
@level = level
|
18
|
+
@anchor = anchor
|
19
|
+
end
|
20
|
+
|
21
|
+
# The HTML content of the heading.
|
22
|
+
# @attribute [XRB::Markup]
|
23
|
+
attr :title_html
|
24
|
+
|
25
|
+
# The heading level (1-6).
|
26
|
+
# @attribute [Integer]
|
27
|
+
attr :level
|
28
|
+
|
29
|
+
# The anchor ID for linking to this heading.
|
30
|
+
# @attribute [String]
|
31
|
+
attr :anchor
|
32
|
+
end
|
33
|
+
# Build a sidebar from a markdown document by extracting headings.
|
34
|
+
# @parameter document [Document] The document to extract headings from
|
35
|
+
# @returns [Sidebar]
|
36
|
+
def self.build(document)
|
37
|
+
entries = extract_headings_from_document(document)
|
38
|
+
new(entries)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Initialize with an array of entries.
|
42
|
+
# @parameter entries [Array(Entry)] The navigation entries
|
43
|
+
def initialize(entries)
|
44
|
+
@entries = entries
|
45
|
+
end
|
46
|
+
|
47
|
+
# The navigation entries.
|
48
|
+
# @attribute [Array(Entry)]
|
49
|
+
attr :entries
|
50
|
+
|
51
|
+
# Check if there are any navigation entries.
|
52
|
+
# @returns [Boolean]
|
53
|
+
def any?
|
54
|
+
!entries.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Generate HTML markup for the sidebar navigation.
|
58
|
+
# @parameter sidebar [Boolean] Whether this is rendered in a sidebar layout (unused, kept for compatibility)
|
59
|
+
# @parameter title [String] The title for the navigation section
|
60
|
+
# @returns [XRB::MarkupString]
|
61
|
+
def to_html(sidebar: false, title: "Table of Contents")
|
62
|
+
return XRB::Markup.raw("") unless any?
|
63
|
+
|
64
|
+
XRB::Builder.fragment do |builder|
|
65
|
+
builder.tag :nav do
|
66
|
+
builder.tag :heading do
|
67
|
+
builder.text title
|
68
|
+
end
|
69
|
+
builder.tag :ul do
|
70
|
+
entries.each do |entry|
|
71
|
+
if entry.level > 2
|
72
|
+
builder.tag :li, {class: "level-#{entry.level}"} do
|
73
|
+
builder.tag :a, {href: "##{entry.anchor}"} do
|
74
|
+
builder << entry.title_html
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
builder.tag :li do
|
79
|
+
builder.tag :a, {href: "##{entry.anchor}"} do
|
80
|
+
builder << entry.title_html
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def self.extract_headings_from_document(document)
|
93
|
+
headings = []
|
94
|
+
return headings unless document&.root
|
95
|
+
|
96
|
+
document.root.walk do |node|
|
97
|
+
if node.type == :header
|
98
|
+
next if node.header_level < 2 or node.header_level > 3
|
99
|
+
|
100
|
+
fragment = node.dup.extract_children
|
101
|
+
|
102
|
+
title = XRB::Markup.raw(fragment.to_html)
|
103
|
+
level = node.header_level
|
104
|
+
anchor = Markly::Renderer::HTML.anchor_for(fragment)
|
105
|
+
|
106
|
+
headings << Entry.new(title, level, anchor)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
headings
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/pages/_page.xnode
CHANGED
@@ -39,10 +39,8 @@
|
|
39
39
|
<body>
|
40
40
|
<content:header />
|
41
41
|
|
42
|
-
<main>
|
42
|
+
<main class="#{attributes[:class]}">
|
43
43
|
<utopia:content/>
|
44
|
-
|
45
|
-
<footer>Documentation generated by <a href="https://github.com/socketry/utopia-project">Utopia::Project</a>.</footer>
|
46
44
|
</main>
|
47
45
|
</body>
|
48
46
|
</html>
|
data/pages/guides/show.xnode
CHANGED
@@ -1,24 +1,32 @@
|
|
1
|
-
<content:page>
|
1
|
+
<content:page class="with-sidebar">
|
2
2
|
<?r
|
3
3
|
base = self[:base]
|
4
4
|
guide = self[:guide]
|
5
|
+
navigation = guide.navigation
|
5
6
|
?>
|
6
|
-
<content:heading>#{guide.title}</content:heading>
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
<aside class="sidebar">
|
9
|
+
#{navigation.to_html}
|
10
|
+
</aside>
|
11
|
+
<div class="content">
|
12
|
+
<content:heading>#{guide.title}</content:heading>
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
<?r
|
15
|
+
if document = guide.document
|
16
|
+
?>#{document.to_html}<?r
|
17
|
+
end
|
18
|
+
|
19
|
+
guide.sources.each do |source|
|
20
|
+
?><h2><code>#{File.basename source.path}</code></h2><?r
|
21
|
+
source.segments.each do |segment|
|
22
|
+
if documentation = segment.documentation
|
23
|
+
?>#{base.format(documentation.text, language: source.language)}<?r
|
24
|
+
end
|
25
|
+
|
26
|
+
?><pre><code class="language-#{source.language.name}">#{segment.code}</code></pre><?r
|
18
27
|
end
|
19
|
-
|
20
|
-
?><pre><code class="language-#{source.language.name}">#{segment.code}</code></pre><?r
|
21
28
|
end
|
22
|
-
|
23
|
-
|
29
|
+
?>
|
30
|
+
</div>
|
31
|
+
<script src="/_static/sidebar.js"></script>
|
24
32
|
</content:page>
|
data/pages/releases/index.xnode
CHANGED
@@ -1,13 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
<content
|
1
|
+
<?r if document = self[:document] ?>
|
2
|
+
<content:page class="with-sidebar">
|
3
|
+
<?r navigation = document.navigation ?>
|
4
|
+
<aside class="sidebar">
|
5
|
+
#{navigation.to_html(sidebar: true, title: "Releases")}
|
6
|
+
</aside>
|
7
|
+
<div class="content">
|
8
|
+
#{Markup.raw document.to_html}
|
9
|
+
</div>
|
10
|
+
<script src="/_static/sidebar.js"></script>
|
11
|
+
</content:page>
|
12
|
+
<?r else ?>
|
13
|
+
<content:page>
|
14
|
+
<content:heading>Releases</content:heading>
|
8
15
|
|
9
16
|
<p>This project does not have a <code>releases.md</code> file.</p>
|
10
|
-
|
11
|
-
|
12
|
-
?>
|
13
|
-
</content:page>
|
17
|
+
</content:page>
|
18
|
+
<?r end ?>
|
@@ -0,0 +1,175 @@
|
|
1
|
+
/**
|
2
|
+
* Sidebar Navigation Highlighting
|
3
|
+
* Progressive enhancement for sidebar navigation with scroll tracking
|
4
|
+
*/
|
5
|
+
|
6
|
+
(function() {
|
7
|
+
'use strict';
|
8
|
+
|
9
|
+
// Only run if we have a sidebar navigation
|
10
|
+
const sidebarNav = document.querySelector('.sidebar nav');
|
11
|
+
if (!sidebarNav) return;
|
12
|
+
|
13
|
+
const navLinks = sidebarNav.querySelectorAll('a[href*="#"]');
|
14
|
+
const sections = Array.from(navLinks).map(link => {
|
15
|
+
const href = link.getAttribute('href');
|
16
|
+
// Extract fragment from both "#section" and "page.html#section" formats
|
17
|
+
const fragmentIndex = href.indexOf('#');
|
18
|
+
if (fragmentIndex === -1) return null;
|
19
|
+
const fragment = href.substring(fragmentIndex + 1);
|
20
|
+
// Try to find the section element using the fragment
|
21
|
+
let sectionElement = document.getElementById(fragment);
|
22
|
+
|
23
|
+
// If not found, try with CSS.escape for special characters
|
24
|
+
if (!sectionElement && fragment !== CSS.escape(fragment)) {
|
25
|
+
sectionElement = document.querySelector(`#${CSS.escape(fragment)}`);
|
26
|
+
}
|
27
|
+
|
28
|
+
// The element we found should be the section container, not just the heading
|
29
|
+
// If it's a heading, find its parent section
|
30
|
+
if (sectionElement && sectionElement.tagName.match(/^H[1-6]$/)) {
|
31
|
+
sectionElement = sectionElement.closest('section') || sectionElement.parentElement;
|
32
|
+
}
|
33
|
+
|
34
|
+
return { link, sectionElement, id: fragment };
|
35
|
+
}).filter(item => item && item.sectionElement);
|
36
|
+
|
37
|
+
if (sections.length === 0) return;
|
38
|
+
|
39
|
+
let currentActive = null;
|
40
|
+
|
41
|
+
function updateActiveLink(updatePageState = true) {
|
42
|
+
let activeSectionData = null;
|
43
|
+
let smallestValidBottom = Infinity;
|
44
|
+
|
45
|
+
// Find the section with the smallest bottom position that is still >= 0
|
46
|
+
// This gives us the section that we're currently reading through
|
47
|
+
sections.forEach((sectionData) => {
|
48
|
+
const { sectionElement } = sectionData;
|
49
|
+
const rect = sectionElement.getBoundingClientRect();
|
50
|
+
const sectionBottom = rect.bottom;
|
51
|
+
|
52
|
+
// Get the actual bottom padding for this section
|
53
|
+
const bottomPadding = parseFloat(getComputedStyle(sectionElement).paddingBottom);
|
54
|
+
|
55
|
+
// We want the section whose bottom is closest to the top but still visible
|
56
|
+
if (sectionBottom > bottomPadding && sectionBottom < smallestValidBottom) {
|
57
|
+
smallestValidBottom = sectionBottom;
|
58
|
+
activeSectionData = sectionData;
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
// If no section has bottom >= 0, fall back to the last section
|
63
|
+
// (meaning we've scrolled past all content)
|
64
|
+
if (!activeSectionData && sections.length > 0) {
|
65
|
+
activeSectionData = sections[sections.length - 1];
|
66
|
+
}
|
67
|
+
|
68
|
+
// Update active link
|
69
|
+
if (activeSectionData !== currentActive) {
|
70
|
+
// Remove previous active
|
71
|
+
if (currentActive) {
|
72
|
+
currentActive.link.classList.remove('active');
|
73
|
+
}
|
74
|
+
|
75
|
+
// Set new active
|
76
|
+
if (activeSectionData) {
|
77
|
+
activeSectionData.link.classList.add('active');
|
78
|
+
currentActive = activeSectionData;
|
79
|
+
|
80
|
+
if (updatePageState) {
|
81
|
+
// Update URL fragment to reflect current section
|
82
|
+
const newFragment = '#' + activeSectionData.id;
|
83
|
+
if (window.location.hash !== newFragment) {
|
84
|
+
history.replaceState(null, null, newFragment);
|
85
|
+
}
|
86
|
+
|
87
|
+
// Auto-scroll sidebar to keep active item visible
|
88
|
+
scrollToActiveItem(activeSectionData.link);
|
89
|
+
}
|
90
|
+
} else {
|
91
|
+
currentActive = null;
|
92
|
+
|
93
|
+
if (updatePageState) {
|
94
|
+
// Clear fragment if no active section
|
95
|
+
if (window.location.hash) {
|
96
|
+
history.replaceState(null, null, window.location.pathname);
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
function scrollToActiveItem(activeLink) {
|
104
|
+
const sidebar = activeLink.closest('.sidebar');
|
105
|
+
if (!sidebar) return;
|
106
|
+
|
107
|
+
const sidebarRect = sidebar.getBoundingClientRect();
|
108
|
+
const linkRect = activeLink.getBoundingClientRect();
|
109
|
+
|
110
|
+
// Check if the active link is outside the visible area
|
111
|
+
const isAbove = linkRect.top < sidebarRect.top;
|
112
|
+
const isBelow = linkRect.bottom > sidebarRect.bottom;
|
113
|
+
|
114
|
+
if (isAbove || isBelow) {
|
115
|
+
// Calculate the ideal scroll position to center the active item
|
116
|
+
const sidebarScrollTop = sidebar.scrollTop;
|
117
|
+
const linkOffsetTop = linkRect.top - sidebarRect.top + sidebarScrollTop;
|
118
|
+
const sidebarHeight = sidebarRect.height;
|
119
|
+
const targetScrollTop = linkOffsetTop - (sidebarHeight / 2) + (linkRect.height / 2);
|
120
|
+
|
121
|
+
// Smooth scroll the sidebar
|
122
|
+
sidebar.scrollTo({
|
123
|
+
top: Math.max(0, targetScrollTop),
|
124
|
+
behavior: 'smooth'
|
125
|
+
});
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
// Throttled scroll handler
|
130
|
+
let scrollTimer = null;
|
131
|
+
|
132
|
+
function onScroll() {
|
133
|
+
if (scrollTimer) return;
|
134
|
+
|
135
|
+
scrollTimer = setTimeout(() => {
|
136
|
+
updateActiveLink();
|
137
|
+
scrollTimer = null;
|
138
|
+
}, 100);
|
139
|
+
}
|
140
|
+
|
141
|
+
// Initialize and set up event listeners
|
142
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
143
|
+
window.addEventListener('resize', updateActiveLink, { passive: true });
|
144
|
+
|
145
|
+
// Initial update without changing page state (preserves URL fragments):
|
146
|
+
updateActiveLink(false);
|
147
|
+
|
148
|
+
// Smooth scroll enhancement for sidebar navigation links
|
149
|
+
navLinks.forEach(link => {
|
150
|
+
link.addEventListener('click', (event) => {
|
151
|
+
const href = link.getAttribute('href');
|
152
|
+
// Extract fragment from both "#section" and "page.html#section" formats
|
153
|
+
const fragmentIndex = href.indexOf('#');
|
154
|
+
if (fragmentIndex === -1) return;
|
155
|
+
const fragment = href.substring(fragmentIndex + 1);
|
156
|
+
|
157
|
+
// Try to find the target using the fragment
|
158
|
+
let target = document.getElementById(fragment);
|
159
|
+
|
160
|
+
// If not found, try with CSS.escape for special characters
|
161
|
+
if (!target && fragment !== CSS.escape(fragment)) {
|
162
|
+
target = document.querySelector(`#${CSS.escape(fragment)}`);
|
163
|
+
}
|
164
|
+
|
165
|
+
if (target) {
|
166
|
+
event.preventDefault();
|
167
|
+
|
168
|
+
// Update URL to trigger :target and let browser handle scrolling
|
169
|
+
window.location.hash = fragment;
|
170
|
+
}
|
171
|
+
|
172
|
+
link.focus();
|
173
|
+
});
|
174
|
+
});
|
175
|
+
})();
|
data/public/_static/site.css
CHANGED
@@ -103,21 +103,118 @@ main {
|
|
103
103
|
max-width: 48rem;
|
104
104
|
}
|
105
105
|
|
106
|
+
@media (min-width: 64em) {
|
107
|
+
main.with-sidebar {
|
108
|
+
display: grid;
|
109
|
+
grid-template-columns: 16rem 1fr;
|
110
|
+
gap: 1rem;
|
111
|
+
align-items: start;
|
112
|
+
|
113
|
+
/* 48rem content + 16rem sidebar + 1rem gap */
|
114
|
+
max-width: 65rem;
|
115
|
+
}
|
116
|
+
|
117
|
+
main.with-sidebar .content {
|
118
|
+
max-width: 48rem;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
.sidebar {
|
123
|
+
display: none; /* Hidden on small screens */
|
124
|
+
}
|
125
|
+
|
126
|
+
@media (min-width: 64em) {
|
127
|
+
.sidebar {
|
128
|
+
display: block;
|
129
|
+
position: sticky;
|
130
|
+
top: 2rem;
|
131
|
+
max-height: calc(100vh - 4rem);
|
132
|
+
overflow-y: auto;
|
133
|
+
|
134
|
+
/* Custom scrollbar styling */
|
135
|
+
scrollbar-width: thin;
|
136
|
+
scrollbar-color: var(--underlay-color) transparent;
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
/* Sidebar navigation styles */
|
141
|
+
.sidebar nav {
|
142
|
+
background-color: transparent;
|
143
|
+
border: none;
|
144
|
+
border-radius: 0;
|
145
|
+
padding: 0;
|
146
|
+
margin: 1rem;
|
147
|
+
position: sticky;
|
148
|
+
top: 0;
|
149
|
+
}
|
150
|
+
|
151
|
+
.sidebar nav h2 {
|
152
|
+
font-size: 0.9rem;
|
153
|
+
margin-bottom: 1rem;
|
154
|
+
padding-bottom: 0.5rem;
|
155
|
+
border-bottom: 1px solid var(--underlay-color);
|
156
|
+
}
|
157
|
+
|
158
|
+
.sidebar nav ul {
|
159
|
+
margin: 0;
|
160
|
+
padding: 0;
|
161
|
+
list-style: none;
|
162
|
+
}
|
163
|
+
|
164
|
+
.sidebar nav li {
|
165
|
+
margin: 0.1rem 0;
|
166
|
+
}
|
167
|
+
|
168
|
+
.sidebar nav li.level-2 {
|
169
|
+
padding-left: 0;
|
170
|
+
}
|
171
|
+
|
172
|
+
.sidebar nav li.level-3 {
|
173
|
+
padding-left: 1rem;
|
174
|
+
}
|
175
|
+
|
176
|
+
.sidebar nav li.level-4 {
|
177
|
+
padding-left: 2rem;
|
178
|
+
}
|
179
|
+
|
180
|
+
.sidebar nav li.level-5 {
|
181
|
+
padding-left: 3rem;
|
182
|
+
}
|
183
|
+
|
184
|
+
.sidebar nav li.level-6 {
|
185
|
+
padding-left: 4rem;
|
186
|
+
}
|
187
|
+
|
188
|
+
.sidebar nav a {
|
189
|
+
padding: 0.4rem 0.75rem;
|
190
|
+
font-size: 0.9rem;
|
191
|
+
border-left: 3px solid transparent;
|
192
|
+
color: var(--main-color);
|
193
|
+
text-decoration: none;
|
194
|
+
display: block;
|
195
|
+
border-radius: 0.25rem;
|
196
|
+
transition: all 0.2s ease;
|
197
|
+
}
|
198
|
+
|
199
|
+
.sidebar nav a:hover {
|
200
|
+
border-left-color: var(--accent-color);
|
201
|
+
background-color: var(--header-color);
|
202
|
+
}
|
203
|
+
|
106
204
|
main img {
|
107
205
|
max-width: 100%;
|
108
206
|
}
|
109
207
|
|
110
|
-
|
111
|
-
|
208
|
+
/* Ensure last section has enough height to be scrollable */
|
209
|
+
.content section:last-of-type {
|
210
|
+
min-height: 100vh;
|
112
211
|
}
|
113
212
|
|
114
213
|
section {
|
115
|
-
|
116
|
-
padding: 0.1rem 0;
|
214
|
+
padding: 1rem 0;
|
117
215
|
}
|
118
216
|
|
119
217
|
main > section {
|
120
|
-
border-radius: 1rem;
|
121
218
|
margin: 4rem 0;
|
122
219
|
}
|
123
220
|
|
@@ -223,7 +320,7 @@ summary {
|
|
223
320
|
}
|
224
321
|
|
225
322
|
details[open] {
|
226
|
-
|
323
|
+
padding: .5rem;
|
227
324
|
}
|
228
325
|
|
229
326
|
details[open] summary {
|
@@ -313,3 +410,191 @@ ul.pragmas li.asynchronous {
|
|
313
410
|
box-shadow: 0 0 0.3rem #8C00FF;
|
314
411
|
color: #8C00FF;
|
315
412
|
}
|
413
|
+
|
414
|
+
/* Navigation - Inline styles (boxed display for small screens) */
|
415
|
+
.inline-nav nav {
|
416
|
+
background-color: var(--header-color);
|
417
|
+
border-radius: 0.5rem;
|
418
|
+
padding: 1rem;
|
419
|
+
margin: 1rem;
|
420
|
+
border-left: 0.25rem solid var(--accent-color);
|
421
|
+
}
|
422
|
+
|
423
|
+
.inline-nav nav heading {
|
424
|
+
margin-top: 0;
|
425
|
+
margin-bottom: 0.5rem;
|
426
|
+
color: var(--accent-color);
|
427
|
+
font-size: 1rem;
|
428
|
+
display: block;
|
429
|
+
font-weight: 500;
|
430
|
+
}
|
431
|
+
|
432
|
+
.inline-nav nav ul {
|
433
|
+
margin: 0;
|
434
|
+
padding: 0;
|
435
|
+
list-style: none;
|
436
|
+
}
|
437
|
+
|
438
|
+
.inline-nav nav li {
|
439
|
+
margin: 0.25rem 0;
|
440
|
+
}
|
441
|
+
|
442
|
+
.inline-nav nav li.level-2 {
|
443
|
+
padding-left: 0;
|
444
|
+
}
|
445
|
+
|
446
|
+
.inline-nav nav li.level-3 {
|
447
|
+
padding-left: 1rem;
|
448
|
+
}
|
449
|
+
|
450
|
+
.inline-nav nav li.level-4 {
|
451
|
+
padding-left: 2rem;
|
452
|
+
}
|
453
|
+
|
454
|
+
.inline-nav nav li.level-5 {
|
455
|
+
padding-left: 3rem;
|
456
|
+
}
|
457
|
+
|
458
|
+
.inline-nav nav li.level-6 {
|
459
|
+
padding-left: 4rem;
|
460
|
+
}
|
461
|
+
|
462
|
+
.inline-nav nav a {
|
463
|
+
color: var(--main-color);
|
464
|
+
text-decoration: none;
|
465
|
+
display: block;
|
466
|
+
padding: 0.25rem 0.5rem;
|
467
|
+
border-radius: 0.25rem;
|
468
|
+
transition: all 0.2s ease;
|
469
|
+
}
|
470
|
+
|
471
|
+
.inline-nav nav a:hover {
|
472
|
+
color: var(--accent-hover-color);
|
473
|
+
background-color: var(--overlay-color);
|
474
|
+
text-decoration: none;
|
475
|
+
}
|
476
|
+
|
477
|
+
/* Sidebar navigation styles */
|
478
|
+
.sidebar nav,
|
479
|
+
.sidebar details nav {
|
480
|
+
background-color: transparent;
|
481
|
+
border: none;
|
482
|
+
border-radius: 0;
|
483
|
+
padding: 0;
|
484
|
+
margin: 1rem;
|
485
|
+
position: sticky;
|
486
|
+
top: 0;
|
487
|
+
}
|
488
|
+
|
489
|
+
.sidebar nav heading {
|
490
|
+
font-size: 0.9rem;
|
491
|
+
margin-bottom: 1rem;
|
492
|
+
padding-bottom: 0.5rem;
|
493
|
+
border-bottom: 1px solid var(--underlay-color);
|
494
|
+
display: block;
|
495
|
+
font-weight: 500;
|
496
|
+
color: var(--accent-color);
|
497
|
+
}
|
498
|
+
|
499
|
+
.sidebar nav li,
|
500
|
+
.sidebar details nav li {
|
501
|
+
margin: 0.1rem 0;
|
502
|
+
}
|
503
|
+
|
504
|
+
.sidebar nav li.level-2 {
|
505
|
+
padding-left: 0;
|
506
|
+
}
|
507
|
+
|
508
|
+
.sidebar nav li.level-3 {
|
509
|
+
padding-left: 1rem;
|
510
|
+
}
|
511
|
+
|
512
|
+
.sidebar nav li.level-4 {
|
513
|
+
padding-left: 2rem;
|
514
|
+
}
|
515
|
+
|
516
|
+
.sidebar nav li.level-5 {
|
517
|
+
padding-left: 3rem;
|
518
|
+
}
|
519
|
+
|
520
|
+
.sidebar nav li.level-6 {
|
521
|
+
padding-left: 4rem;
|
522
|
+
}
|
523
|
+
|
524
|
+
.sidebar nav a,
|
525
|
+
.sidebar details nav a {
|
526
|
+
padding: 0.4rem 0.75rem;
|
527
|
+
font-size: 0.9rem;
|
528
|
+
border-left: 3px solid transparent;
|
529
|
+
color: var(--main-color);
|
530
|
+
text-decoration: none;
|
531
|
+
display: block;
|
532
|
+
border-radius: 0.25rem;
|
533
|
+
transition: all 0.2s ease;
|
534
|
+
}
|
535
|
+
|
536
|
+
/* Compact mode for long navigation lists */
|
537
|
+
.sidebar nav.compact li {
|
538
|
+
margin: 0.05rem 0;
|
539
|
+
}
|
540
|
+
|
541
|
+
.sidebar nav.compact a {
|
542
|
+
padding: 0.25rem 0.5rem;
|
543
|
+
font-size: 0.85rem;
|
544
|
+
line-height: 1.3;
|
545
|
+
}
|
546
|
+
|
547
|
+
.sidebar nav.compact heading {
|
548
|
+
font-size: 0.8rem;
|
549
|
+
margin-bottom: 0.75rem;
|
550
|
+
}
|
551
|
+
|
552
|
+
.sidebar nav.compact li.level-3 {
|
553
|
+
padding-left: 0.75rem;
|
554
|
+
}
|
555
|
+
|
556
|
+
.sidebar nav.compact li.level-4 {
|
557
|
+
padding-left: 1.5rem;
|
558
|
+
}
|
559
|
+
|
560
|
+
.sidebar nav.compact li.level-5 {
|
561
|
+
padding-left: 2.25rem;
|
562
|
+
}
|
563
|
+
|
564
|
+
.sidebar nav.compact li.level-6 {
|
565
|
+
padding-left: 3rem;
|
566
|
+
}
|
567
|
+
|
568
|
+
.sidebar nav ul {
|
569
|
+
margin: 0;
|
570
|
+
padding: 0;
|
571
|
+
list-style: none;
|
572
|
+
}
|
573
|
+
|
574
|
+
.sidebar nav a:hover {
|
575
|
+
border-left-color: var(--accent-color);
|
576
|
+
background-color: var(--header-color);
|
577
|
+
}
|
578
|
+
|
579
|
+
/* Subtle focus indicator for keyboard navigation */
|
580
|
+
.sidebar nav a:focus {
|
581
|
+
outline: 2px solid var(--accent-color);
|
582
|
+
outline-offset: 2px;
|
583
|
+
border-radius: 0.25rem;
|
584
|
+
}
|
585
|
+
|
586
|
+
/* Brief click feedback */
|
587
|
+
.sidebar nav a:active {
|
588
|
+
background-color: var(--accent-hover-color);
|
589
|
+
color: white;
|
590
|
+
transform: scale(0.98);
|
591
|
+
transition: transform 0.1s ease;
|
592
|
+
}
|
593
|
+
|
594
|
+
/* Active section highlighting (set by JavaScript) */
|
595
|
+
.sidebar nav a.active {
|
596
|
+
background-color: var(--accent-color);
|
597
|
+
color: white;
|
598
|
+
border-left-color: var(--accent-color);
|
599
|
+
font-weight: 500;
|
600
|
+
}
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: utopia-project
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.37.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -144,6 +144,7 @@ files:
|
|
144
144
|
- lib/utopia/project/linkify.rb
|
145
145
|
- lib/utopia/project/releases_document.rb
|
146
146
|
- lib/utopia/project/renderer.rb
|
147
|
+
- lib/utopia/project/sidebar.rb
|
147
148
|
- lib/utopia/project/version.rb
|
148
149
|
- license.md
|
149
150
|
- pages/_header.xnode
|
@@ -255,6 +256,7 @@ files:
|
|
255
256
|
- public/_components/mermaid/mermaid.min.js.map
|
256
257
|
- public/_static/icon.png
|
257
258
|
- public/_static/links.js
|
259
|
+
- public/_static/sidebar.js
|
258
260
|
- public/_static/site.css
|
259
261
|
- public/robots.txt
|
260
262
|
- readme.md
|
metadata.gz.sig
CHANGED
Binary file
|