Skip to content

Commit 0986ab9

Browse files
committed
changes for collapsible left sidebar
1 parent 69da87d commit 0986ab9

7 files changed

Lines changed: 413 additions & 16 deletions

File tree

.claude/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
"Bash(node capture.mjs --jaspr-only)",
1414
"Bash(node inspect-dark.mjs)",
1515
"Bash(node inspect-spr.mjs)",
16-
"Bash(node inspect-width.mjs)"
16+
"Bash(node inspect-width.mjs)",
17+
"Bash(dart run jaspr_cli:jaspr build)",
18+
"Bash(pkill -f build_daemon)",
19+
"Bash(pkill -f dart.*build_runner)",
20+
"Bash(curl -s http://localhost:4000/docs/overview/)",
21+
"mcp__plugin_wingspan_dart__dart_fix"
1722
]
1823
}
1924
}

site_jaspr/content/docs/workflows.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: "Workflows"
33
description: "Learn about all of the workflows that Very Good Workflows supports."
44
---
55

6-
# Workflows
6+
# Workflows 🦄
77

88
Learn about all of the workflows that Very Good Workflows supports.
99

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import 'package:jaspr/dom.dart';
2+
import 'package:jaspr/jaspr.dart';
3+
import 'package:jaspr_content/components/sidebar.dart';
4+
import 'package:jaspr_content/jaspr_content.dart';
5+
import 'package:jaspr_content/theme.dart';
6+
7+
/// A sidebar entry that can optionally have collapsible children.
8+
class SidebarEntry {
9+
const SidebarEntry({
10+
required this.text,
11+
required this.href,
12+
this.children = const [],
13+
});
14+
15+
final String text;
16+
final String href;
17+
final List<SidebarLink> children;
18+
19+
bool get hasChildren => children.isNotEmpty;
20+
}
21+
22+
/// A sidebar with support for collapsible nested items.
23+
///
24+
/// Items with [SidebarEntry.children] render as a toggle row with a chevron
25+
/// on the right. Clicking the row expands/collapses the children with a
26+
/// 0.5s max-height animation.
27+
///
28+
/// The initial expanded/collapsed state is determined at build time from the
29+
/// current page URL.
30+
class CollapsibleSidebar extends StatelessComponent {
31+
const CollapsibleSidebar({required this.items, super.key});
32+
33+
final List<SidebarEntry> items;
34+
35+
@override
36+
Component build(BuildContext context) {
37+
final currentRoute = context.page.url;
38+
39+
return Component.fragment([
40+
Document.head(children: [
41+
Style(styles: styles),
42+
script(defer: true, content: _toggleScript),
43+
]),
44+
nav(classes: 'sidebar', [
45+
// Close button for mobile overlay
46+
button(classes: 'sidebar-close', [
47+
svg(
48+
viewBox: '0 0 24 24',
49+
attributes: {
50+
'width': '20',
51+
'height': '20',
52+
'fill': 'none',
53+
'stroke': 'currentColor',
54+
'stroke-width': '2',
55+
},
56+
[
57+
line(
58+
attributes: {
59+
'x1': '18',
60+
'y1': '6',
61+
'x2': '6',
62+
'y2': '18',
63+
},
64+
[],
65+
),
66+
line(
67+
attributes: {
68+
'x1': '6',
69+
'y1': '6',
70+
'x2': '18',
71+
'y2': '18',
72+
},
73+
[],
74+
),
75+
],
76+
),
77+
]),
78+
div(classes: 'sidebar-group', [
79+
ul([
80+
for (final item in items) _buildItem(item, currentRoute),
81+
]),
82+
]),
83+
]),
84+
]);
85+
}
86+
87+
Component _buildItem(SidebarEntry item, String currentRoute) {
88+
final isActive = currentRoute == item.href;
89+
90+
if (!item.hasChildren) {
91+
// Simple link item
92+
return li([
93+
div(
94+
classes: isActive ? 'active' : null,
95+
[a(href: item.href, [Component.text(item.text)])],
96+
),
97+
]);
98+
}
99+
100+
// Collapsible item: expanded if current route matches any child
101+
final isExpanded = item.children.any(
102+
(child) => currentRoute == child.href,
103+
);
104+
105+
return li(
106+
classes: [
107+
'sidebar-collapsible',
108+
if (isExpanded) 'expanded',
109+
].join(' '),
110+
[
111+
// Toggle row: text + chevron (clicking toggles, no navigation)
112+
div(classes: isActive ? 'sidebar-toggle active' : 'sidebar-toggle', [
113+
span([Component.text(item.text)]),
114+
span(classes: 'sidebar-chevron', [
115+
svg(
116+
viewBox: '0 0 24 24',
117+
attributes: {
118+
'width': '16',
119+
'height': '16',
120+
'fill': 'none',
121+
'stroke': 'currentColor',
122+
'stroke-width': '2.5',
123+
'stroke-linecap': 'round',
124+
'stroke-linejoin': 'round',
125+
},
126+
[
127+
// Right-pointing chevron (>) — rotated 90° when expanded
128+
polyline(points: '9 6 15 12 9 18', []),
129+
],
130+
),
131+
]),
132+
]),
133+
// Collapsible children
134+
ul(classes: 'sidebar-children', [
135+
for (final child in item.children)
136+
li([
137+
div(
138+
classes: currentRoute == child.href ? 'active' : null,
139+
[a(href: child.href, [Component.text(child.text)])],
140+
),
141+
]),
142+
]),
143+
],
144+
);
145+
}
146+
147+
/// Inline script that toggles the expanded class on collapsible items.
148+
static const _toggleScript = '''
149+
(function(){
150+
document.addEventListener('click', function(e){
151+
var toggle = e.target.closest('.sidebar-toggle');
152+
if (!toggle) return;
153+
e.preventDefault();
154+
toggle.closest('.sidebar-collapsible').classList.toggle('expanded');
155+
});
156+
})();
157+
''';
158+
159+
@css
160+
static List<StyleRule> get styles => [
161+
// Base sidebar styles (matching jaspr_content Sidebar)
162+
css('.sidebar', [
163+
css('&').styles(
164+
fontSize: 0.875.rem,
165+
lineHeight: 1.25.rem,
166+
padding: Padding.only(left: 0.5.rem, bottom: 1.25.rem, top: 0.75.rem),
167+
position: Position.relative(),
168+
),
169+
css.media(MediaQuery.all(minWidth: 1024.px), [
170+
css('&').styles(padding: Padding.only(top: Unit.zero)),
171+
]),
172+
css('.sidebar-close', [
173+
css('&').styles(
174+
position: Position.absolute(top: 0.75.rem, right: 0.75.rem),
175+
),
176+
css.media(MediaQuery.all(minWidth: 1024.px), [
177+
css('&').styles(display: Display.none),
178+
]),
179+
]),
180+
css('.sidebar-group', [
181+
css('&').styles(
182+
padding: Padding.only(top: 1.5.rem, right: 0.75.rem),
183+
),
184+
css('ul').styles(
185+
listStyle: ListStyle.none,
186+
margin: Margin.zero,
187+
padding: Padding.zero,
188+
),
189+
css('li', [
190+
css('div', [
191+
css('&').styles(
192+
display: Display.flex,
193+
margin: Margin.only(bottom: 1.px),
194+
opacity: 0.75,
195+
overflow: Overflow.hidden,
196+
radius: BorderRadius.circular(.375.rem),
197+
textOverflow: TextOverflow.ellipsis,
198+
transition:
199+
Transition('all', duration: 150.ms, curve: Curve.easeInOut),
200+
whiteSpace: WhiteSpace.noWrap,
201+
),
202+
css('&:hover').styles(
203+
opacity: 1,
204+
backgroundColor: Color('#0000000d'),
205+
),
206+
css('&.active').styles(
207+
opacity: 1,
208+
color: ContentColors.primary,
209+
fontWeight: FontWeight.w700,
210+
backgroundColor:
211+
Color('color-mix(in srgb, currentColor 15%, transparent)'),
212+
),
213+
]),
214+
css('a').styles(
215+
display: Display.inlineFlex,
216+
flex: Flex(grow: 1),
217+
padding: Padding.only(left: 12.px, top: .5.rem, bottom: .5.rem),
218+
),
219+
]),
220+
]),
221+
]),
222+
223+
// Toggle row: flex with space-between so chevron goes to the right
224+
css('.sidebar-toggle', [
225+
css('&').styles(
226+
alignItems: AlignItems.center,
227+
cursor: Cursor.pointer,
228+
raw: {'justify-content': 'space-between', 'user-select': 'none'},
229+
),
230+
css('span').styles(
231+
display: Display.inlineFlex,
232+
flex: Flex(grow: 1),
233+
padding: Padding.only(left: 12.px, top: .5.rem, bottom: .5.rem),
234+
),
235+
]),
236+
237+
// Chevron: sits on the right, rotates when expanded
238+
css('.sidebar-chevron', [
239+
css('&').styles(
240+
alignItems: AlignItems.center,
241+
display: Display.flex,
242+
opacity: 0.4,
243+
padding: Padding.all(0.25.rem),
244+
transition: Transition('transform', duration: 300.ms, curve: Curve.ease),
245+
),
246+
]),
247+
css('.sidebar-toggle:hover .sidebar-chevron').styles(opacity: 0.8),
248+
css('.sidebar-collapsible.expanded .sidebar-chevron').styles(
249+
raw: {'transform': 'rotate(90deg)'},
250+
),
251+
252+
// Collapsible children: animated via max-height
253+
css('.sidebar-children').styles(
254+
overflow: Overflow.hidden,
255+
raw: {
256+
'max-height': '0',
257+
'transition': 'max-height 0.5s ease',
258+
},
259+
),
260+
css('.sidebar-collapsible.expanded .sidebar-children').styles(
261+
raw: {'max-height': '500px'},
262+
),
263+
];
264+
}

site_jaspr/lib/components/site_footer.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class SiteFooter extends StatelessComponent {
2323
href: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap',
2424
),
2525
Style(styles: siteStyles),
26+
script(defer: true, content: _tocScrollspy),
2627
]),
2728
footer(classes: 'site-footer', [
2829
p([
@@ -36,6 +37,31 @@ class SiteFooter extends StatelessComponent {
3637
]);
3738
}
3839

40+
/// Inline script that highlights the active TOC link based on scroll position.
41+
static const _tocScrollspy = '''
42+
(function(){
43+
var links = document.querySelectorAll('.toc a');
44+
if (!links.length) return;
45+
var ids = [];
46+
links.forEach(function(a) {
47+
var h = a.getAttribute('href');
48+
if (h) { var id = h.split('#')[1]; if (id) ids.push({id:id, el:a}); }
49+
});
50+
if (!ids.length) return;
51+
function update() {
52+
var active = null;
53+
for (var i = 0; i < ids.length; i++) {
54+
var t = document.getElementById(ids[i].id);
55+
if (t && t.getBoundingClientRect().top <= 100) active = i;
56+
}
57+
links.forEach(function(a) { a.classList.remove('toc-active'); });
58+
if (active !== null) ids[active].el.classList.add('toc-active');
59+
}
60+
window.addEventListener('scroll', update, {passive:true});
61+
update();
62+
})();
63+
''';
64+
3965
@css
4066
static List<StyleRule> get styles => [
4167
css('.site-footer', [
@@ -52,7 +78,11 @@ class SiteFooter extends StatelessComponent {
5278
),
5379
]),
5480
css('[data-theme="dark"] .site-footer').styles(
81+
color: Color('#e3e3e3'),
5582
backgroundColor: Color('#020f30'),
5683
),
84+
css('[data-theme="dark"] .site-footer a').styles(
85+
color: Color('#44fac7'),
86+
),
5787
];
5888
}

site_jaspr/lib/main.server.dart

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import 'package:jaspr_content/components/callout.dart';
1414
import 'package:jaspr_content/components/header.dart';
1515
import 'package:jaspr_content/components/image.dart';
1616
import 'package:jaspr_content/components/sidebar.dart';
17+
18+
import 'components/collapsible_sidebar.dart';
1719
import 'package:jaspr_content/components/theme_toggle.dart';
1820
import 'package:jaspr_content/jaspr_content.dart';
1921
import 'package:jaspr_content/theme.dart';
@@ -69,17 +71,13 @@ void main() {
6971
ThemeToggle(),
7072
],
7173
),
72-
sidebar: Sidebar(
73-
groups: [
74-
SidebarGroup(
75-
links: [
76-
SidebarLink(text: 'Overview', href: '/docs/overview'),
77-
SidebarLink(text: 'Workflows', href: '/docs/workflows'),
78-
],
79-
),
80-
SidebarGroup(
81-
title: 'Workflows',
82-
links: [
74+
sidebar: CollapsibleSidebar(
75+
items: [
76+
SidebarEntry(text: 'Overview', href: '/docs/overview'),
77+
SidebarEntry(
78+
text: 'Workflows',
79+
href: '/docs/workflows',
80+
children: [
8381
SidebarLink(text: 'Dart Package', href: '/docs/workflows/dart_package'),
8482
SidebarLink(text: 'Dart Pub Publish', href: '/docs/workflows/dart_pub_publish'),
8583
SidebarLink(text: 'Flutter Package', href: '/docs/workflows/flutter_package'),

site_jaspr/lib/main.server.options.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'package:jaspr_content/components/sidebar_toggle_button.dart'
1616
as _sidebar_toggle_button;
1717
import 'package:jaspr_content/components/theme_toggle.dart' as _theme_toggle;
1818
import 'package:site_jaspr/components/breadcrumb.dart' as _breadcrumb;
19+
import 'package:site_jaspr/components/collapsible_sidebar.dart'
20+
as _collapsible_sidebar;
1921
import 'package:site_jaspr/components/icon_link.dart' as _icon_link;
2022
import 'package:site_jaspr/components/nav_link.dart' as _nav_link;
2123
import 'package:site_jaspr/components/page_navigation.dart' as _page_navigation;
@@ -63,6 +65,7 @@ ServerOptions get defaultServerOptions => ServerOptions(
6365
..._image.Image.styles,
6466
..._theme_toggle.ThemeToggleState.styles,
6567
..._breadcrumb.Breadcrumb.styles,
68+
..._collapsible_sidebar.CollapsibleSidebar.styles,
6669
..._icon_link.IconLink.styles,
6770
..._nav_link.NavLink.styles,
6871
..._page_navigation.PageNavigation.styles,

0 commit comments

Comments
 (0)