Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions assets/scss/components/_sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,38 @@ $sidebar-item-icon-column: calc(1.25em + #{$spacer * 0.25});
}
}

// Hold the icon column open on rows that have no icon.
//
// A menu is free to mix the two - a "Home" entry among icon-bearing sections is
// the common case - and the row without an icon then starts its label at the
// padding edge while every neighbour starts one icon-width further right. The
// labels no longer form a column, and the odd row reads as misplaced rather
// than as merely unadorned.
//
// So such a row reserves the same column instead. `assets/sidebar.html` sets
// the class on a row that has no icon in a list where something else does, so
// the reservation follows the list it has to line up with: a list that uses no
// icons at all is untouched, and neither is a nested list of its own.
//
// This is the width alone - no `text-indent` to go with it, unlike the hanging
// indent above. There is no icon to pull the first line back past, and the
// wrapped lines of a row whose text starts at the padding edge already line up
// underneath it. Applies in both the collapsible and the plain sidebar, since
// what it corrects is the resting layout the two share; in icon-only mode the
// row has nothing left to show and the reserved column costs nothing.
//
// `!important` for the same reason the block above needs it: the base
// `.sidebar-item` padding carries one.
.sidebar-item.sidebar-item-icon-indent {
padding-left: calc(0.5rem + #{$sidebar-item-icon-column}) !important;
}

// Group headers again sit on the wrapper's padding, so they reserve the column
// alone. They are not `.sidebar-item`, so this needs its own selector.
.sidebar-item-group > div > a.sidebar-item-icon-indent {
padding-left: $sidebar-item-icon-column;
}

.btn-toggle-group {
padding: 0.25rem 0.5rem;
font-weight: 600;
Expand Down
15 changes: 14 additions & 1 deletion layouts/_partials/assets/link.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,20 @@
{{- else -}}
{{- $destination = (strings.TrimPrefix (strings.TrimSuffix "/" site.BaseURL) $destination) -}}
{{- end -}}
{{- if not $destination }}{{ $destination = "/" }}{{ end -}}
{{/* A destination that normalizes away is the site root.

Stripping the base URL's path leaves nothing behind for a link to the site's own
home - "/" on a site rooted at "/", "/docs/" on one rooted at "/docs/" - and
`path.Clean` renders that empty string as ".". Both mean the same place, but only
the empty one is caught by an emptiness test: "." survives as a *relative* page
reference meaning "the page being rendered", so the lookup below resolves it to
the caller and the link quietly points at whatever page happened to render it.

The sidebar is where that surfaces: its markup is cached per section against the
section root, so a menu entry linking to "/" comes out as a link to the section
root, then matches the current page in the client-side active pass and takes the
highlight from the entry that owns it. */}}
{{- if or (not $destination) (eq $destination ".") }}{{ $destination = "/" }}{{ end -}}

{{/* cue and tab carry no schema default, so an absent value is nil and an
explicit false is a real choice. "default" treats false as empty and would
Expand Down
137 changes: 94 additions & 43 deletions layouts/_partials/assets/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,34 @@
{{- $args = merge $args (dict "menu" $menu) -}}
{{- end -}}

{{/*
Does any entry in this list carry a leading icon?

Icons are per entry, but alignment is a property of the list: as soon as one row leads
with one, every other row's label in that list starts an icon-width further left and the
column of labels breaks. The rows that need to reserve that column are therefore the ones
without an icon in a list that has them, which only the list knows - hence a single
lookup per sibling list, passed down to the rows as `iconColumn`.

Scoped to the list rather than the whole sidebar on purpose: a nested list whose entries
are all icon-less is aligned already, and reserving a column there would indent it away
from the label it hangs under, changing every existing site that renders icons only on
its top level.
*/ -}}
{{- define "_partials/inline/sidebar/has-icons.html" -}}
{{- $found := false -}}
{{- range . -}}
{{- if .pre }}{{ $found = true }}{{ end -}}
{{- end -}}
{{- return $found -}}
{{- end -}}

{{- define "_partials/inline/sidebar/group-link.html" -}}
{{- $page := .page -}}
{{- $group := .group -}}
{{- $baseURL := .baseURL -}}
{{- $pre := $group.pre -}}
{{- $iconColumn := .iconColumn | default false -}}
{{- $collapsible := .collapsible | default false -}}
{{- $href := $group.link | default "" -}}
{{- if and $href (not (hasPrefix $href "/")) -}}
Expand All @@ -92,8 +115,10 @@
{{- if and $href (not (hasSuffix $href "/")) -}}{{- $href = printf "%s/" $href -}}{{- end -}}
{{- $groupTitle := $group.title -}}
{{- $groupTitle = partial "utilities/TitleCase.html" (dict "page" $page "text" $groupTitle) -}}
{{- $class := "sidebar-item text-decoration-none rounded w-100" -}}
{{- if and $iconColumn (not $pre) -}}{{- $class = printf "%s sidebar-item-icon-indent" $class -}}{{- end -}}
<li class="mb-1">
<a class="sidebar-item text-decoration-none rounded w-100"
<a class="{{ $class }}"
{{- if $collapsible }} data-sidebar-label="{{ $groupTitle }}"{{ end }}
{{ with $href }}href="{{ . }}"{{ end }}>
{{- with $pre -}}
Expand All @@ -120,8 +145,12 @@
{{- $group := .group -}}
{{- $data := .menu -}}
{{- $pre := $group.pre -}}
{{- $iconColumn := .iconColumn | default false -}}
{{- $filename := .filename -}}

{{- /* Resolved for this group's own children, who are a list of their own */ -}}
{{- $childIconColumn := partial "inline/sidebar/has-icons.html" $group.pages -}}

{{- $doc_slug := partial "utilities/URLJoin.html" (dict "base" $baseURL "path" ($group.title | urlize)) -}}
{{- $href := or $group.link $doc_slug -}}
{{- /* Handle absolute paths from hierarchical menu (starting with /) */ -}}
Expand All @@ -146,7 +175,12 @@
{{ if not $ref }}
{{ $dest = "" }}
{{ end }}
<a {{ if gt $level 1 }}class="small" {{ end }}{{ with $dest }} href="{{ . }}"{{ else }}
{{- $headClass := "" -}}
{{- if gt $level 1 }}{{ $headClass = "small" }}{{ end -}}
{{- if and $iconColumn (not $pre) -}}
{{- $headClass = trim (printf "%s sidebar-item-icon-indent" $headClass) " " -}}
{{- end -}}
<a {{ with $headClass }}class="{{ . }}" {{ end }}{{ with $dest }} href="{{ . }}"{{ else }}
data-bs-toggle="collapse"
data-bs-target="#sidebar-collapse-{{ $index }}-{{ $level }}"
{{ end }}>
Expand Down Expand Up @@ -183,14 +217,15 @@
nested collapse element IDs stay unique across
sibling branches at the same depth. */}}
{{ partial "inline/sidebar/group.html" (dict
"page" $page
"index" (printf "%v-%v" $index $childIndex)
"level" (add $level 1)
"baseURL" $href
"group" $item
"menu" $data
"pre" $item.pre
"filename" $filename
"page" $page
"index" (printf "%v-%v" $index $childIndex)
"level" (add $level 1)
"baseURL" $href
"group" $item
"menu" $data
"pre" $item.pre
"iconColumn" $childIconColumn
"filename" $filename
)
}}
{{- else -}}
Expand All @@ -202,6 +237,7 @@
"href" $item.link
"menu" $data
"pre" $item.pre
"iconColumn" $childIconColumn
"filename" $filename
)
}}
Expand All @@ -219,8 +255,10 @@
{{- $title := .title -}}
{{- $data := .menu -}}
{{- $pre := .pre -}}
{{- $iconColumn := .iconColumn | default false -}}
{{- $collapsible := .collapsible | default false -}}
{{- $filename := .filename -}}
{{- $iconIndent := cond (and $iconColumn (not $pre)) " sidebar-item-icon-indent" "" -}}
{{- $title = partial "utilities/TitleCase.html" (dict "page" $page "text" $title) -}}
{{- $titleText := $title -}}
{{- if $collapsible -}}
Expand Down Expand Up @@ -253,7 +291,7 @@
<li>
<ul class="btn-toggle-nav list-unstyled pb-1">
<li>
{{ $class := "sidebar-item text-decoration-none rounded w-100" }}
{{ $class := printf "sidebar-item text-decoration-none rounded w-100%s" $iconIndent }}
{{ $link := partial "assets/link.html" (dict "href" $href "text" $itemText "class" $class "page" $page "exact" true "icon-mode" "svg") }}{{/* icon-mode "svg": same reason as the icon call sites - see the header comment. */}}
{{- if and $collapsible $title $link -}}
{{- $link = $link | replaceRE `(<a)\s` (printf `${1} data-sidebar-label="%s" ` ($title | htmlEscape)) -}}
Expand All @@ -268,7 +306,7 @@
</li>
{{ else }}
<li>
{{ $class := "sidebar-item text-decoration-none rounded small w-100" }}
{{ $class := printf "sidebar-item text-decoration-none rounded small w-100%s" $iconIndent }}
{{ $link := partial "assets/link.html" (dict "href" $href "text" $itemText "class" $class "page" $page "exact" true "icon-mode" "svg") }}{{/* icon-mode "svg": same reason as the icon call sites - see the header comment. */}}
{{ if $link }}
{{ print $link | safeHTML }}
Expand All @@ -289,6 +327,7 @@
{{- $iconSecondary := partial "utilities/GetThemeIcon.html" (dict "id" "sidebarSecondary" "default" "fas angle-up") -}}
{{- $hasSpacerItem := gt (len (where $args.menu "spacer" true)) 0 -}}
{{- $sidebarId := $section | urlize | default "sidebar" -}}
{{- $iconColumn := partial "inline/sidebar/has-icons.html" $args.menu -}}
<nav class="sidebar flex-shrink-0 ps-1{{ if ge $levelMin 2 }} sidebar-sub{{ end }}{{ if $collapsible }} sidebar-collapsible{{ end }}{{ if $hasSpacerItem }} sidebar-has-spacer{{ end }}"
data-sidebar-nav
{{- if $collapsible }} data-storage-key="sidebar-collapsed"{{ end }}
Expand Down Expand Up @@ -354,6 +393,7 @@
"page" $page
"group" $item
"baseURL" $baseURL
"iconColumn" $iconColumn
"collapsible" $collapsible
) }}
{{- else -}}
Expand All @@ -365,6 +405,7 @@
"href" $item.link
"menu" $args.menu
"pre" $item.pre
"iconColumn" $iconColumn
"collapsible" $collapsible
"filename" $args.filename
) }}
Expand All @@ -383,6 +424,7 @@
"href" .link
"menu" $args.menu
"pre" .pre
"iconColumn" $iconColumn
"collapsible" $collapsible
"filename" $args.filename
) }}
Expand All @@ -403,8 +445,10 @@
{{- if and $avHref (not (hasSuffix $avHref "/")) -}}{{- $avHref = printf "%s/" $avHref -}}{{- end -}}
{{- $avTitle := .title -}}
{{- $avTitle = partial "utilities/TitleCase.html" (dict "page" $page "text" $avTitle) -}}
{{- $avClass := "sidebar-item text-decoration-none rounded flex-grow-1" -}}
{{- if and $iconColumn (not .pre) -}}{{- $avClass = printf "%s sidebar-item-icon-indent" $avClass -}}{{- end -}}
<li class="d-flex align-items-center sidebar-secondary-row">
<a class="sidebar-item text-decoration-none rounded flex-grow-1"
<a class="{{ $avClass }}"
{{- if $collapsible }} data-sidebar-label="{{ $avTitle }}"{{ end }}
href="{{ $avHref }}">
{{- with .pre -}}
Expand Down Expand Up @@ -444,28 +488,33 @@
{{- end -}}
{{- end -}}
{{- with $activeGroup.pages -}}
{{- /* This mode renders the active group's children, not the L1 menu, so the
icon column is resolved against that list */ -}}
{{- $subIconColumn := partial "inline/sidebar/has-icons.html" . -}}
<ul class="list-unstyled ps-0">
{{- range $index, $item := . -}}
{{- if $item.pages -}}
{{ partial "inline/sidebar/group.html" (dict
"page" $page
"index" $index
"level" 1
"baseURL" $baseURL
"group" $item
"menu" $activeGroup.pages
"filename" $args.filename
"page" $page
"index" $index
"level" 1
"baseURL" $baseURL
"group" $item
"menu" $activeGroup.pages
"iconColumn" $subIconColumn
"filename" $args.filename
) }}
{{- else -}}
{{ partial "inline/sidebar/item.html" (dict
"page" $page
"level" $level
"baseURL" $baseURL
"title" $item.title
"href" $item.link
"menu" $activeGroup.pages
"pre" $item.pre
"filename" $args.filename
"page" $page
"level" $level
"baseURL" $baseURL
"title" $item.title
"href" $item.link
"menu" $activeGroup.pages
"pre" $item.pre
"iconColumn" $subIconColumn
"filename" $args.filename
) }}
{{- end -}}
{{- end -}}
Expand All @@ -477,25 +526,27 @@
{{- range $index, $item := $args.menu -}}
{{- if $item.pages }}
{{ partial "inline/sidebar/group.html" (dict
"page" $page
"index" $index
"level" (add $level 1)
"baseURL" $baseURL
"group" $item
"menu" $args.menu
"filename" $args.filename
"page" $page
"index" $index
"level" (add $level 1)
"baseURL" $baseURL
"group" $item
"menu" $args.menu
"iconColumn" $iconColumn
"filename" $args.filename
)
}}
{{- else }}
{{ partial "inline/sidebar/item.html" (dict
"page" $page
"level" $level
"baseURL" $baseURL
"title" $item.title
"href" $item.link
"menu" $args.menu
"pre" $item.pre
"filename" $args.filename
"page" $page
"level" $level
"baseURL" $baseURL
"title" $item.title
"href" $item.link
"menu" $args.menu
"pre" $item.pre
"iconColumn" $iconColumn
"filename" $args.filename
)
}}
{{- end }}
Expand Down
34 changes: 34 additions & 0 deletions tests/templates/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,37 @@ disableKinds = ['taxonomy', 'term', 'RSS', 'sitemap', 'robotsTXT', '404']
[[module.mounts]]
source = '../../_vendor/github.com/gethinode/mod-utils/v6/data/structures'
target = 'data/structures'

# assets/link.html resolves its destination against the site and the calling page, so the
# assertions need the real partial and its argument structure. Its icon call site is only
# reached for external links with a cue, which none of the cases exercise, so no icon
# partial is mounted.
[[module.mounts]]
source = '../../layouts/_partials/assets/link.html'
target = 'layouts/_partials/assets/link.html'
[[module.mounts]]
source = '../../data/structures/link.yml'
target = 'data/structures/link.yml'

# assets/sidebar.html renders real rows through assets/link.html and real icons through
# mod-fontawesome, and both are what the assertions read, so neither is stubbed. The icon
# module brings its own data (the structure it validates against plus the icon table), which
# merges with the `data/structures` mounts above.
[[module.mounts]]
source = '../../layouts/_partials/assets/sidebar.html'
target = 'layouts/_partials/assets/sidebar.html'
[[module.mounts]]
source = '../../data/structures/sidebar.yml'
target = 'data/structures/sidebar.yml'
[[module.mounts]]
source = '../../_vendor/github.com/gethinode/mod-fontawesome/v6/layouts/_partials/assets/icon.html'
target = 'layouts/_partials/assets/icon.html'
[[module.mounts]]
source = '../../_vendor/github.com/gethinode/mod-fontawesome/v6/data'
target = 'data'
# Where mod-fontawesome's own config mounts the icon set it resolves by name; without it
# every icon renders as an error and the assertions could no longer tell an icon-bearing row
# from an icon-less one.
[[module.mounts]]
source = '../../_vendor/github.com/FortAwesome/Font-Awesome/svgs'
target = 'assets/svgs/fa'
Loading