diff options
| author | Danilo M. <danix@danix.xyz> | 2026-04-21 22:56:01 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-04-21 22:56:01 +0200 |
| commit | 88d46796cb84e81e1c502ccdf02ea22890e511d5 (patch) | |
| tree | 251a9b99f9d0a8cd8361e1216566954981cb7b08 /themes | |
| parent | 2236d07a5247cab38e0e6b524bdfb26c12c143a8 (diff) | |
| download | danixxyz-88d46796cb84e81e1c502ccdf02ea22890e511d5.tar.gz danixxyz-88d46796cb84e81e1c502ccdf02ea22890e511d5.zip | |
feat: Add reusable tag cloud partial with A11y and dark/light mode support
- Create tag-cloud.html partial with flexible dict interface:
* showCount (bool): Toggle count badges
* wrapInWidget (bool): Sidebar widget wrapper with .sidebar-widget class
* maxTags (int): Limit shown tags (used for sidebar: 15 max)
* headingLevel (h2|h3): Configurable heading element
- Implement visual tier scaling by frequency (3 tiers):
* low: 0.75rem, 0.75 opacity — uncommon tags
* medium: 0.875rem, 0.88 opacity — moderate frequency
* high: 1rem, 1 opacity, accent border — popular tags
- Add .tag-cloud and .tag-tier-* CSS classes (main.css):
* Uses CSS variables (--accent, --border, --text-dim) for dark/light compatibility
* Focus ring matches site standard (outline-offset: 2px)
* Hover state: accent border + subtle bg tint
* prefers-reduced-motion: transitions disabled
- Integrate in 3 locations:
* Homepage (layouts/index.html): Full cloud with counts
* Article sidebar (layouts/partials/sidebar.html): Compact widget, 15 max, no counts
* 404 pages (404.en.html, 404.it.html): Full cloud between recent articles and nav
- A11y implementation:
* <section aria-labelledby> landmark (non-sidebar mode)
* <nav aria-label="Browse by topic"> named navigation
* Each link aria-label includes count text even when visual badge hidden
* <span aria-hidden="true"> on count badge to avoid duplication
* Proper heading hierarchy (h2 homepage, h3 on 404)
- Add i18n keys (en.yaml, it.yaml):
* tagCloud: "Explore Topics" / "Esplora gli argomenti"
* exploreTopics: "Browse by topic" / "Sfoglia per argomento"
- URL handling: Use .Page.RelPermalink from OrderedTaxonomyEntry — no manual /tags/ construction, language-aware paths work automatically
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Diffstat (limited to 'themes')
| -rw-r--r-- | themes/danix-xyz-hacker/assets/css/main.css | 76 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/assets/css/main.min.css | 59 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/layouts/404.en.html | 5 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/layouts/404.it.html | 5 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/layouts/index.html | 5 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/layouts/partials/sidebar.html | 5 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/layouts/partials/tag-cloud.html | 85 |
7 files changed, 240 insertions, 0 deletions
diff --git a/themes/danix-xyz-hacker/assets/css/main.css b/themes/danix-xyz-hacker/assets/css/main.css index 1588b61..31a977d 100644 --- a/themes/danix-xyz-hacker/assets/css/main.css +++ b/themes/danix-xyz-hacker/assets/css/main.css @@ -350,6 +350,82 @@ html.theme-light picture img[src="/images/default_thumbnail_dark.png"] { margin-bottom: 1.5rem; } + /* ===================== + Tag Cloud Component + ===================== */ + + .tag-cloud { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: baseline; + } + + .tag-cloud-link { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.625rem; + border: 1px solid var(--border); + border-radius: 0.25rem; + font-family: var(--font-mono, 'JetBrains Mono', monospace); + font-size: 0.75rem; + color: var(--text-dim); + text-decoration: none; + transition: border-color 150ms ease-out, color 150ms ease-out, background-color 150ms ease-out; + white-space: nowrap; + line-height: 1.4; + } + + .tag-cloud-link:hover { + border-color: rgba(var(--accent-rgb), 0.5); + color: var(--accent); + background-color: rgba(var(--accent-rgb), 0.06); + } + + .tag-cloud-link:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 0.25rem; + } + + .tag-tier-low { + font-size: 0.75rem; + opacity: 0.75; + } + + .tag-tier-medium { + font-size: 0.875rem; + opacity: 0.88; + } + + .tag-tier-high { + font-size: 1rem; + opacity: 1; + border-color: rgba(var(--accent-rgb), 0.3); + color: var(--text); + } + + .tag-cloud-count { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 0.375rem; + border-radius: 9999px; + font-size: 0.65em; + font-weight: 600; + background-color: rgba(var(--accent-rgb), 0.12); + color: var(--accent); + line-height: 1.6; + min-width: 1.2em; + } + + @media (prefers-reduced-motion: reduce) { + .tag-cloud-link { + transition: none; + } + } + .share-grid { display: grid; grid-template-columns: repeat(3, 50px); diff --git a/themes/danix-xyz-hacker/assets/css/main.min.css b/themes/danix-xyz-hacker/assets/css/main.min.css index 8604392..236b11c 100644 --- a/themes/danix-xyz-hacker/assets/css/main.min.css +++ b/themes/danix-xyz-hacker/assets/css/main.min.css @@ -1448,6 +1448,65 @@ button, margin-bottom: 1.5rem; } +/* ===================== + Tag Cloud Component + ===================== */ + +.tag-cloud { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: baseline; +} + +.tag-cloud-link { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.625rem; + border: 1px solid var(--border); + border-radius: 0.25rem; + font-family: var(--font-mono, 'JetBrains Mono', monospace); + font-size: 0.75rem; + color: var(--text-dim); + text-decoration: none; + transition: border-color 150ms ease-out, color 150ms ease-out, background-color 150ms ease-out; + white-space: nowrap; + line-height: 1.4; +} + +.tag-cloud-link:hover { + border-color: rgba(var(--accent-rgb), 0.5); + color: var(--accent); + background-color: rgba(var(--accent-rgb), 0.06); +} + +.tag-cloud-link:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 0.25rem; +} + +.tag-cloud-count { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 0.375rem; + border-radius: 9999px; + font-size: 0.65em; + font-weight: 600; + background-color: rgba(var(--accent-rgb), 0.12); + color: var(--accent); + line-height: 1.6; + min-width: 1.2em; +} + +@media (prefers-reduced-motion: reduce) { + .tag-cloud-link { + transition: none; + } +} + .share-grid { display: grid; grid-template-columns: repeat(3, 50px); diff --git a/themes/danix-xyz-hacker/layouts/404.en.html b/themes/danix-xyz-hacker/layouts/404.en.html index 8d8641d..2829a20 100644 --- a/themes/danix-xyz-hacker/layouts/404.en.html +++ b/themes/danix-xyz-hacker/layouts/404.en.html @@ -61,6 +61,11 @@ </div> </div> + <!-- Explore Topics --> + <div class="mb-12 text-left"> + {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h3") }} + </div> + <!-- Navigation Links --> <div class="space-y-4 flex flex-col items-center mb-12"> <a href="/" class="btn btn-primary"> diff --git a/themes/danix-xyz-hacker/layouts/404.it.html b/themes/danix-xyz-hacker/layouts/404.it.html index 1a8fbbe..d876371 100644 --- a/themes/danix-xyz-hacker/layouts/404.it.html +++ b/themes/danix-xyz-hacker/layouts/404.it.html @@ -61,6 +61,11 @@ </div> </div> + <!-- Explore Topics --> + <div class="mb-12 text-left"> + {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h3") }} + </div> + <!-- Navigation Links --> <div class="space-y-4 flex flex-col items-center mb-12"> <a href="/it/" class="btn btn-primary"> diff --git a/themes/danix-xyz-hacker/layouts/index.html b/themes/danix-xyz-hacker/layouts/index.html index 7247c3f..a290d6a 100644 --- a/themes/danix-xyz-hacker/layouts/index.html +++ b/themes/danix-xyz-hacker/layouts/index.html @@ -49,6 +49,11 @@ {{ i18n "contact" }} </a> </div> + + <!-- Tag Cloud Section --> + <div class="mt-16 pt-8 border-t border-border"> + {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h2") }} + </div> </div> </section> {{ end }} diff --git a/themes/danix-xyz-hacker/layouts/partials/sidebar.html b/themes/danix-xyz-hacker/layouts/partials/sidebar.html index dc263e6..a2225f1 100644 --- a/themes/danix-xyz-hacker/layouts/partials/sidebar.html +++ b/themes/danix-xyz-hacker/layouts/partials/sidebar.html @@ -46,4 +46,9 @@ {{ end }} </div> {{ end }} + + <hr class="sidebar-hr"> + + <!-- Tag Cloud Widget --> + {{ partial "tag-cloud.html" (dict "page" . "showCount" false "wrapInWidget" true "maxTags" 15) }} </aside> diff --git a/themes/danix-xyz-hacker/layouts/partials/tag-cloud.html b/themes/danix-xyz-hacker/layouts/partials/tag-cloud.html new file mode 100644 index 0000000..0d59e3c --- /dev/null +++ b/themes/danix-xyz-hacker/layouts/partials/tag-cloud.html @@ -0,0 +1,85 @@ +{{/* tag-cloud.html + Reusable tag cloud partial for homepage, sidebar, and 404 pages. + + Params (dict): + page Page required — calling page context (provides .Site.Taxonomies.tags, .Lang) + showCount bool optional — show post count per tag (default true) + heading string optional — heading text override (default: i18n "tagCloud") + headingLevel string optional — h2|h3|p for non-widget mode (default "h2") + wrapInWidget bool optional — wrap in .sidebar-widget for sidebar placement (default false) + maxTags int optional — max tags to show, 0 = all (default 0) +*/}} + +{{- $page := .page -}} +{{- $showCount := .showCount | default true -}} +{{- $heading := .heading | default (i18n "tagCloud") -}} +{{- $headingLevel := .headingLevel | default "h2" -}} +{{- $wrapInWidget := .wrapInWidget | default false -}} +{{- $maxTags := .maxTags | default 0 -}} + +{{- $tags := $page.Site.Taxonomies.tags -}} + +{{/* Early exit if no tags */}} +{{- if $tags -}} + +{{/* Compute max count for tier thresholds */}} +{{- $maxCount := 0 -}} +{{- range $tags -}} + {{- if gt .Count $maxCount -}}{{- $maxCount = .Count -}}{{- end -}} +{{- end -}} + +{{/* Tier thresholds (integer division) */}} +{{- $tierMedThreshold := div $maxCount 3 -}} +{{- $tierHighThreshold := mul (div $maxCount 3) 2 -}} + +{{/* Ordered tag list (descending by count) */}} +{{- $orderedTags := $tags.ByCount -}} +{{- if gt $maxTags 0 -}} + {{- $orderedTags = first $maxTags $orderedTags -}} +{{- end -}} + +{{/* Render based on placement mode */}} +{{- if $wrapInWidget -}} +<div class="sidebar-widget"> + <p class="sidebar-widget-label"># {{ i18n "tags" }}</p> + <nav aria-label="{{ i18n "exploreTopics" }}"> + <div class="tag-cloud"> +{{- else -}} +<section aria-labelledby="tag-cloud-heading"> + <{{ $headingLevel }} id="tag-cloud-heading" class="text-lg font-semibold text-accent mb-4"> + {{ $heading }} + </{{ $headingLevel }}> + <nav aria-label="{{ i18n "exploreTopics" }}"> + <div class="tag-cloud"> +{{- end -}} + + {{- range $orderedTags -}} + {{- $count := .Count -}} + {{- $tier := "low" -}} + {{- if gt $count $tierHighThreshold -}} + {{- $tier = "high" -}} + {{- else if gt $count $tierMedThreshold -}} + {{- $tier = "medium" -}} + {{- end -}} + <a + href="{{ .Page.RelPermalink }}" + class="tag-cloud-link tag-tier-{{ $tier }}" + aria-label="{{ .Name }}{{- if $showCount }} ({{ i18n "postCount" $count }}){{- end -}}" + > + {{- .Name -}} + {{- if $showCount -}} + <span class="tag-cloud-count" aria-hidden="true">{{ $count }}</span> + {{- end -}} + </a> + {{- end -}} + + </div> + </nav> + +{{- if $wrapInWidget -}} +</div> +{{- else -}} +</section> +{{- end -}} + +{{- end -}}{{/* end if $tags */}} |
