Skip to content

Commit 40b4001

Browse files
flaportclaude
andcommitted
feat: Add nested navigation and external link support
This commit adds several improvements to the sidebar navigation: - Support for nested sections (3+ levels) with collapsible details elements - External link support in navigation items with visual indicator icon - New components: sidebar_section.html and bottom_sidebar_section.html - Custom CSS for hover permalink icons and GitHub-style code blocks - Mobile sidebar (bottom_sidebar) now supports nested sections and links The nested navigation allows for deeper content hierarchies while maintaining a clean, collapsible UI. External links open in new tabs with a visual indicator to distinguish them from internal pages. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 80db839 commit 40b4001

7 files changed

Lines changed: 215 additions & 41 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<details class="group/mobile-section" {% if item.active %}open{% endif %}>
2+
<summary
3+
class="hover:bg-accent text-muted-foreground flex cursor-pointer list-none items-center gap-2 rounded-md p-2 text-sm font-medium [&::-webkit-details-marker]:hidden">
4+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
5+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
6+
class="transition-transform duration-200 group-open/mobile-section:rotate-90">
7+
<path d="m9 18 6-6-6-6" />
8+
</svg>
9+
{{ item.title }}
10+
</summary>
11+
<ul class="mt-1 flex flex-col gap-0.5 pl-4">
12+
{% for subitem in item.children or [] %}
13+
{% if subitem.is_page or subitem.is_link %}
14+
<li>
15+
<a class="hover:bg-accent {% if subitem.active %}bg-accent text-accent-foreground{% endif %} flex items-center gap-2 rounded-md p-2 text-sm font-medium"
16+
href="{% if subitem.is_link %}{{ subitem.url }}{% else %}{{ subitem.url | url }}{% endif %}"
17+
{% if subitem.is_link %}target="_blank" rel="noopener noreferrer"{% endif %}>
18+
{% if subitem.meta and subitem.meta.sidebar_title %}
19+
{{ subitem.meta.sidebar_title }}
20+
{% else %}
21+
{{ subitem.title }}
22+
{% endif %}
23+
{% if subitem.is_link %}
24+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
25+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
26+
class="opacity-50">
27+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
28+
<polyline points="15 3 21 3 21 9" />
29+
<line x1="10" y1="14" x2="21" y2="3" />
30+
</svg>
31+
{% endif %}
32+
</a>
33+
</li>
34+
{% else %}
35+
{# Even deeper nesting #}
36+
<li>
37+
{% with item = subitem %}
38+
{% include "components/bottom_sidebar_section.html" %}
39+
{% endwith %}
40+
</li>
41+
{% endif %}
42+
{% endfor %}
43+
</ul>
44+
</details>

shadcn/components/sidebar_item.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<li data-slot="sidebar-menu-item" data-sidebar="menu-item" class="group/menu-item relative">
22
<a data-slot="sidebar-menu-button" data-active="{{ '%s' % sidebar_item.active|lower }}" data-sidebar="menu-button"
3-
data-size="default" href="{{ sidebar_item.abs_url }}"
3+
data-size="default"
4+
href="{% if sidebar_item.is_link %}{{ sidebar_item.url }}{% else %}{{ sidebar_item.abs_url }}{% endif %}"
5+
{% if sidebar_item.is_link %}target="_blank" rel="noopener noreferrer"{% endif %}
46
class="hpeer/menu-button flex items-center gap-2 rounded-md p-2 text-left ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md data-[active=true]:border-accent data-[active=true]:bg-accent 3xl:fixed:w-full 3xl:fixed:max-w-48">
57

68
{% if sidebar_item.meta and sidebar_item.meta.sidebar_title %}
@@ -9,6 +11,15 @@
911
{{ sidebar_item.title }}
1012
{% endif %}
1113

14+
{% if sidebar_item.is_link %}
15+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
16+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-50">
17+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
18+
<polyline points="15 3 21 3 21 9" />
19+
<line x1="10" y1="14" x2="21" y2="3" />
20+
</svg>
21+
{% endif %}
22+
1223
{% if sidebar_item.meta and sidebar_item.meta.new %}
1324
{% with tag_content = "New" %}
1425
{% include "components/new.html" %}
@@ -27,4 +38,4 @@
2738
{% endwith %}
2839
{% endif %}
2940
</a>
30-
</li>
41+
</li>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<li data-slot="sidebar-menu-item" data-sidebar="menu-item" class="group/menu-item relative">
2+
<details class="group/collapsible" {% if item.active %}open{% endif %}>
3+
<summary data-slot="sidebar-menu-button" data-sidebar="menu-button"
4+
class="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer/menu-button ring-sidebar-ring text-muted-foreground relative flex h-[30px] w-full cursor-pointer list-none items-center gap-2 overflow-visible rounded-md border border-transparent p-2 text-left text-[0.8rem] font-medium outline-hidden transition-[width,height,padding] focus-visible:ring-2 [&::-webkit-details-marker]:hidden">
5+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
6+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
7+
class="transition-transform duration-200 group-open/collapsible:rotate-90">
8+
<path d="m9 18 6-6-6-6" />
9+
</svg>
10+
{{ item.title }}
11+
</summary>
12+
<ul data-slot="sidebar-menu" data-sidebar="menu" class="mt-1 flex w-full min-w-0 flex-col gap-0.5 pl-4">
13+
{% for subitem in item.children or [] %}
14+
{% if subitem.is_page or subitem.is_link %}
15+
{% with sidebar_item = subitem %}
16+
{% include "components/sidebar_item.html" %}
17+
{% endwith %}
18+
{% else %}
19+
{# Even deeper nesting (4th level+) #}
20+
{% with item = subitem %}
21+
{% include "components/sidebar_section.html" %}
22+
{% endwith %}
23+
{% endif %}
24+
{% endfor %}
25+
</ul>
26+
</details>
27+
</li>

shadcn/css/custom.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* mkdocs-shadcn Custom Styles */
2+
3+
/* Hide permalink icons by default, show on hover */
4+
.headerlink {
5+
opacity: 0;
6+
transition: opacity 0.2s ease;
7+
margin-left: 0.5em;
8+
}
9+
10+
h1:hover .headerlink,
11+
h2:hover .headerlink,
12+
h3:hover .headerlink,
13+
h4:hover .headerlink,
14+
h5:hover .headerlink,
15+
h6:hover .headerlink {
16+
opacity: 0.5;
17+
}
18+
19+
.headerlink:hover {
20+
opacity: 1 !important;
21+
}
22+
23+
/* Code block styling - GitHub style */
24+
.codehilite {
25+
background: #f6f8fa;
26+
border: 1px solid #d0d7de;
27+
border-radius: 3px;
28+
padding: 1px;
29+
overflow-x: auto;
30+
}
31+
32+
.dark .codehilite {
33+
background: #161b22;
34+
border-color: #30363d;
35+
}

shadcn/main.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
<link href="{{ 'css/base.css' | url }}" rel="stylesheet">
2323
<link href="{{ 'css/geist.css' | url }}" rel="stylesheet">
24+
<link href="{{ 'css/custom.css' | url }}" rel="stylesheet">
2425

2526
{% if "codehilite" in config.markdown_extensions %}
2627
{% include "templates/pygments.html" %}
Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,90 @@
11
<dialog id="bottom-sidebar" onclick="onBottomSidebarDialogClick(event)" class="bg-transparent"
2-
style="position: fixed; left: 0px; top: 0px; transform: translate(0px, 58px); min-width: max-content; --radix-popper-transform-origin: 0% 0px; will-change: transform; z-index: 50; --radix-popper-available-width: 504px; --radix-popper-available-height: 857px; --radix-popper-anchor-width: 72.94999694824219px; --radix-popper-anchor-height: 32px;">
2+
style="position: fixed; left: 0px; top: 0px; transform: translate(0px, 58px); min-width: max-content; --radix-popper-transform-origin: 0% 0px; will-change: transform; z-index: 50;">
33
<div data-side="bottom" data-align="start" data-state="open" role="dialog" data-slot="popover-content"
4-
class="text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-(--radix-popover-content-transform-origin) border outline-hidden bg-background/90 no-scrollbar h-(--radix-popper-available-height) w-(--radix-popper-available-width) overflow-y-auto rounded-none border-none p-0 shadow-none backdrop-blur duration-100"
5-
style="--radix-popover-content-transform-origin: var(--radix-popper-transform-origin); --radix-popover-content-available-width: var(--radix-popper-available-width); --radix-popover-content-available-height: var(--radix-popper-available-height); --radix-popover-trigger-width: var(--radix-popper-anchor-width); --radix-popover-trigger-height: var(--radix-popper-anchor-height);"
4+
class="text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 bg-background no-scrollbar z-50 w-64 overflow-y-auto rounded-lg border shadow-lg outline-hidden duration-100"
5+
style="max-height: calc(100vh - 80px)"
66
tabindex="-1">
7-
<div class="flex flex-col gap-12 overflow-auto px-6 py-6">
7+
<div class="flex flex-col p-2">
88
{% if nav|length > 1 %}
9-
{% set top_items = nav|selectattr("is_page")|sort(attribute="meta.order") %}
10-
{% set other_items = nav|rejectattr("is_page")|list %}
9+
{% set top_items = nav|selectattr("is_page")|list %}
10+
{% set top_links = nav|selectattr("is_link")|list %}
11+
{% set other_items = nav|rejectattr("is_page")|rejectattr("is_link")|list %}
1112

13+
{# Top level pages #}
1214
{% if top_items|length > 0 %}
13-
<div class="flex flex-col gap-4">
14-
<div class="text-muted-foreground text-sm font-medium">Menu</div>
15-
<div class="flex flex-col gap-3">
16-
{% for top_item in top_items %}
17-
<a class="text-2xl font-medium" href="{{ top_item.url | url }}">
18-
{% if top_item.meta and top_item.meta.sidebar_title %}
19-
{{ top_item.meta.sidebar_title }}
20-
{% else %}
21-
{{ top_item.title }}
22-
{% endif %}
23-
</a>
24-
{% endfor %}
25-
</div>
15+
<div class="mb-2 flex flex-col gap-0.5">
16+
{% for top_item in top_items %}
17+
<a class="hover:bg-accent {% if top_item.active %}bg-accent text-accent-foreground{% endif %} flex items-center gap-2 rounded-md p-2 text-sm font-medium"
18+
href="{{ top_item.url | url }}">
19+
{% if top_item.meta and top_item.meta.sidebar_title %}
20+
{{ top_item.meta.sidebar_title }}
21+
{% else %}
22+
{{ top_item.title }}
23+
{% endif %}
24+
</a>
25+
{% endfor %}
2626
</div>
2727
{% endif %}
2828

29-
{% if other_items|length > 0 %}
30-
<div class="flex flex-col gap-8">
31-
{% for top_item in other_items %}
32-
<div class="flex flex-col gap-4">
33-
<div class="text-muted-foreground text-sm font-medium">{{ top_item.title }}</div>
34-
{% if top_item.children %}
35-
<div class="flex flex-col gap-3">
36-
{% for sidebar_item in top_item.children|selectattr("is_page")|sort(attribute="meta.order") %}
37-
<a class="text-2xl font-medium" href="{{ sidebar_item.url | url}}">
38-
{% if sidebar_item.meta and sidebar_item.meta.sidebar_title %}
39-
{{ sidebar_item.meta.sidebar_title }}
29+
{# Sections #}
30+
{% for section in other_items %}
31+
{% if section.children %}
32+
<div class="flex flex-col p-2">
33+
<div class="text-muted-foreground flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium">
34+
{{ section.title }}
35+
</div>
36+
<ul class="flex flex-col gap-0.5">
37+
{% for item in section.children %}
38+
{% if item.is_page or item.is_link %}
39+
<li>
40+
<a class="hover:bg-accent {% if item.active %}bg-accent text-accent-foreground{% endif %} flex items-center gap-2 rounded-md p-2 text-sm font-medium"
41+
href="{% if item.is_link %}{{ item.url }}{% else %}{{ item.url | url }}{% endif %}"
42+
{% if item.is_link %}target="_blank" rel="noopener noreferrer"{% endif %}>
43+
{% if item.meta and item.meta.sidebar_title %}
44+
{{ item.meta.sidebar_title }}
4045
{% else %}
41-
{{ sidebar_item.title }}
46+
{{ item.title }}
47+
{% endif %}
48+
{% if item.is_link %}
49+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
50+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
51+
stroke-linejoin="round" class="opacity-50">
52+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
53+
<polyline points="15 3 21 3 21 9" />
54+
<line x1="10" y1="14" x2="21" y2="3" />
55+
</svg>
4256
{% endif %}
4357
</a>
44-
{% endfor %}
45-
</div>
58+
</li>
59+
{% else %}
60+
{# Nested section #}
61+
<li>{% include "components/bottom_sidebar_section.html" %}</li>
4662
{% endif %}
47-
</div>
63+
{% endfor %}
64+
</ul>
65+
</div>
66+
{% endif %}
67+
{% endfor %}
68+
69+
{# Top level links #}
70+
{% if top_links|length > 0 %}
71+
<div class="mt-2 flex flex-col gap-0.5">
72+
{% for top_link in top_links %}
73+
<a class="hover:bg-accent flex items-center gap-2 rounded-md p-2 text-sm font-medium"
74+
href="{{ top_link.url }}" target="_blank" rel="noopener noreferrer">
75+
{{ top_link.title }}
76+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
77+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
78+
class="opacity-50">
79+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
80+
<polyline points="15 3 21 3 21 9" />
81+
<line x1="10" y1="14" x2="21" y2="3" />
82+
</svg>
83+
</a>
4884
{% endfor %}
4985
</div>
5086
{% endif %}
5187
{% endif %}
52-
5388
</div>
5489
</div>
55-
</dialog>
90+
</dialog>

shadcn/templates/sidebar.html

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
{% endif %}
1313

1414
{% set pages = current|selectattr("is_page")|sort(attribute="meta.order") %}
15-
{% set sections = current|rejectattr("is_page")|list %}
15+
{% set links = current|selectattr("is_link")|list %}
16+
{% set sections = current|rejectattr("is_page")|rejectattr("is_link")|list %}
1617

1718
{% if pages|length > 0 %}
1819
<div data-slot="sidebar-group" data-sidebar="group" class="relative flex w-full min-w-0 flex-col p-2 pt-6">
@@ -36,15 +37,35 @@
3637
{{ section.title }}</div>
3738
<div data-slot="sidebar-group-content" data-sidebar="group-content" class="w-full text-sm">
3839
<ul data-slot="sidebar-menu" data-sidebar="menu" class="flex w-full min-w-0 flex-col gap-0.5">
39-
{% for sidebar_item in section.children|selectattr("is_page")|sort(attribute="meta.order") %}
40+
{% for item in section.children %}
41+
{% if item.is_page or item.is_link %}
42+
{% with sidebar_item = item %}
4043
{% include "components/sidebar_item.html" %}
44+
{% endwith %}
45+
{% else %}
46+
{# Nested section (3rd level) #}
47+
{% include "components/sidebar_section.html" %}
48+
{% endif %}
4149
{% endfor %}
4250
</ul>
4351
</div>
4452
</div>
4553
{% endif %}
4654
{% endfor %}
4755

56+
{% if links|length > 0 %}
57+
<div data-slot="sidebar-group" data-sidebar="group" class="relative flex w-full min-w-0 flex-col p-2">
58+
<div data-slot="sidebar-group-content" data-sidebar="group-content" class="w-full text-sm">
59+
<ul data-slot="sidebar-menu" data-sidebar="menu" class="flex w-full min-w-0 flex-col gap-0.5">
60+
{% for link in links %}
61+
{% with sidebar_item = link %}
62+
{% include "components/sidebar_item.html" %}
63+
{% endwith %}
64+
{% endfor %}
65+
</ul>
66+
</div>
67+
</div>
68+
{% endif %}
4869

4970
{% endif %}
50-
<div class="sticky -bottom-1 z-10 h-16 shrink-0 bg-gradient-to-t from-background via-background/80 to-background/50 blur-xs"></div>
71+
<div class="sticky -bottom-1 z-10 h-16 shrink-0 bg-gradient-to-t from-background via-background/80 to-background/50 blur-xs"></div>

0 commit comments

Comments
 (0)