diff --git a/modules/blox-tailwind/assets/css/components/all.css b/modules/blox-tailwind/assets/css/components/all.css
index 6961a79b..8869f149 100644
--- a/modules/blox-tailwind/assets/css/components/all.css
+++ b/modules/blox-tailwind/assets/css/components/all.css
@@ -9,3 +9,4 @@
@import "notebook.css";
@import "author-notes.css";
@import "glassmorphism.css";
+@import "cover.css";
diff --git a/modules/blox-tailwind/assets/css/components/cover.css b/modules/blox-tailwind/assets/css/components/cover.css
new file mode 100644
index 00000000..318353dc
--- /dev/null
+++ b/modules/blox-tailwind/assets/css/components/cover.css
@@ -0,0 +1,146 @@
+/**
+ * Article Cover Component Styles
+ * Notion-inspired page cover with icon overlay
+ */
+
+.article-cover {
+ @apply relative w-full;
+ /* Height is set inline via style attribute, but ensure it displays */
+ min-height: 200px;
+ /* Allow icon to overflow bottom */
+ overflow: visible;
+ /* Add margin for overlapping icon */
+ margin-bottom: 2.5rem;
+
+ /* Responsive height adjustments for mobile */
+ @media (max-width: 768px) {
+ height: 240px !important;
+ min-height: 240px;
+ }
+}
+
+/* Cover image container */
+.article-cover img {
+ @apply w-full h-full object-cover;
+}
+
+/* Cover icon container base styles */
+.article-cover-icon {
+ @apply flex items-center justify-center w-full h-full;
+ transition: transform 0.2s ease-in-out;
+}
+
+.article-cover-icon:hover {
+ @apply scale-105;
+}
+
+/* Glass morphism effect for icons */
+.article-cover-icon.glass {
+ background: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(12px) saturate(180%);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+.dark .article-cover-icon.glass {
+ background: rgba(0, 0, 0, 0.7);
+ border-color: rgba(255, 255, 255, 0.1);
+}
+
+/* Fade effect styles */
+.article-cover-fade {
+ @apply absolute left-0 right-0 pointer-events-none;
+ bottom: -1px;
+ background: linear-gradient(to bottom, transparent, white);
+}
+
+.dark .article-cover-fade {
+ background: linear-gradient(to bottom, transparent, rgb(15, 23, 42));
+}
+
+/* Style variants */
+.article-cover[data-cover-style="minimal"] {
+ @apply shadow-sm;
+}
+
+.article-cover[data-cover-style="glass"] {
+ @apply overflow-visible;
+}
+
+.article-cover[data-cover-style="gradient"] {
+ /* Applied via inline overlay divs */
+}
+
+/* Ensure cover works well with dark mode */
+.dark .article-cover {
+ @apply border-b border-gray-800;
+}
+
+/* Caption styles */
+.article-cover .cover-caption {
+ @apply absolute bottom-4 left-4 right-4 text-sm text-white/90;
+ @apply bg-black/50 backdrop-blur-sm px-3 py-2 rounded-lg;
+}
+
+/* Responsive adjustments */
+@media (max-width: 640px) {
+ .article-cover-icon {
+ @apply scale-90;
+ }
+}
+
+/* Parallax effect support (optional feature) */
+.article-cover.parallax {
+ transform-style: preserve-3d;
+}
+
+.article-cover.parallax img {
+ transform: translateZ(-1px) scale(1.5);
+}
+
+/* Link hover effect if cover is clickable */
+.article-cover a {
+ @apply block w-full h-full;
+}
+
+.article-cover a:hover img {
+ @apply scale-105 transition-transform duration-500;
+}
+
+/* Print styles - show cover but optimize */
+@media print {
+ .article-cover {
+ max-height: 200px !important;
+ page-break-after: avoid;
+ }
+
+ .article-cover-fade,
+ .article-cover-icon {
+ @apply hidden;
+ }
+}
+
+/* Accessibility: Reduced motion */
+@media (prefers-reduced-motion: reduce) {
+ .article-cover,
+ .article-cover *,
+ .article-cover-icon {
+ animation: none !important;
+ transition: none !important;
+ }
+
+ .article-cover.parallax img {
+ transform: none !important;
+ }
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .article-cover-icon {
+ @apply border-2 border-current;
+ }
+
+ .article-cover .cover-caption {
+ @apply bg-black text-white;
+ }
+}
diff --git a/modules/blox-tailwind/layouts/_partials/components/cover.html b/modules/blox-tailwind/layouts/_partials/components/cover.html
new file mode 100644
index 00000000..ca3fa6d8
--- /dev/null
+++ b/modules/blox-tailwind/layouts/_partials/components/cover.html
@@ -0,0 +1,220 @@
+{{/* Article Cover Component - Notion-inspired page cover with icon overlay */}}
+{{/* Requires: page context */}}
+
+{{ $cover := partial "functions/get_cover_image.html" . }}
+{{ if $cover }}
+
+{{/* Get cover configuration with defaults */}}
+{{ $cover_config := .Params.cover | default (dict) }}
+{{ if not (reflect.IsMap $cover_config) }}
+ {{/* Simple string format: cover: "image.jpg" */}}
+ {{ $cover_config = dict "image" $cover_config }}
+{{ end }}
+
+{{/* Default alt text to page title if not specified */}}
+{{ $alt_text := $cover_config.alt_text | default (.Title | plainify) }}
+
+{{/* Height configuration */}}
+{{ $height_map := dict
+ "small" "200px"
+ "medium" "320px"
+ "large" "480px"
+ "full" "60vh"
+}}
+{{ $height := index $height_map ($cover_config.height | default "medium") | default "320px" }}
+
+{{/* Style configuration */}}
+{{ $style := $cover_config.style | default "gradient" }}
+
+{{/* Position configuration */}}
+{{ $position := $cover_config.position | default (dict) }}
+{{ $x := $position.x | default 50 }}
+{{ $y := $position.y | default 50 }}
+
+{{/* Overlay configuration */}}
+{{ $overlay := $cover_config.overlay | default (dict) }}
+{{ $overlay_enabled := $overlay.enabled | default true }}
+{{ $overlay_type := $overlay.type | default "gradient" }}
+{{ $overlay_opacity := $overlay.opacity | default 0.3 }}
+{{ $overlay_color := $overlay.color | default "#000000" }}
+{{ $overlay_gradient := $overlay.gradient | default "bottom" }}
+
+{{/* Fade configuration */}}
+{{ $fade := $cover_config.fade | default (dict) }}
+{{ $fade_enabled := $fade.enabled | default true }}
+{{ $fade_height := $fade.height | default "60px" }}
+{{ $fade_blur := $fade.blur | default false }}
+
+{{/* Icon configuration */}}
+{{ $icon_config := $cover_config.icon | default (dict) }}
+{{ $has_icon := or $icon_config.name $icon_config.emoji }}
+{{ $icon_position := $icon_config.position | default "overlap" }}
+{{ $icon_size := $icon_config.size | default "large" }}
+{{ $icon_style := $icon_config.style | default "glass" }}
+{{ $icon_border := $icon_config.border | default true }}
+
+{{/* Responsive hide on mobile */}}
+{{ $hide_mobile := false }}
+{{ if $cover_config.responsive }}
+ {{ $hide_mobile = $cover_config.responsive.hide_on_mobile | default false }}
+{{ end }}
+
+{{/* Process cover image */}}
+{{ $cover_width := 2560 }}
+{{ $cover_height := 520 }}
+{{ if eq ($cover_config.height | default "medium") "small" }}
+ {{ $cover_height = 320 }}
+{{ else if eq ($cover_config.height | default "medium") "large" }}
+ {{ $cover_height = 768 }}
+{{ else if eq ($cover_config.height | default "medium") "full" }}
+ {{ $cover_height = 1080 }}
+{{ end }}
+
+{{/* Generate responsive images */}}
+{{ $processed_cover := $cover }}
+{{ $srcset := "" }}
+{{ if ne $cover.MediaType.SubType "gif" }}
+ {{ $fill_spec := printf "%dx%d Center q85 webp" $cover_width $cover_height }}
+ {{ $processed_cover = $cover.Fill $fill_spec }}
+
+ {{ $variants := slice }}
+ {{ $widths := slice 640 768 1024 1366 1920 2560 }}
+ {{ range $widths }}
+ {{ $w := . }}
+ {{ $h := div (mul $w $cover_height) $cover_width }}
+ {{ $variant_spec := printf "%dx%d Center q85 webp" $w $h }}
+ {{ $variant := $cover.Fill $variant_spec }}
+ {{ $variants = $variants | append (printf "%s %dw" $variant.RelPermalink $w) }}
+ {{ end }}
+ {{ $srcset = delimit $variants ", " }}
+{{ end }}
+
+{{/* Build CSS classes based on style */}}
+{{ $cover_classes := "article-cover relative w-full" }}
+{{ if $hide_mobile }}
+ {{ $cover_classes = printf "%s hidden md:block" $cover_classes }}
+{{ end }}
+
+{{/* Container styles */}}
+{{ $container_style := printf "height: %s;" $height }}
+
+{{/* Calculate mask style for fade */}}
+{{ $mask_style := "" }}
+{{ if and $fade_enabled (ne $style "minimal") }}
+ {{/* Mask fades from black (visible) to transparent (invisible) at the bottom */}}
+ {{ $mask_style = printf "-webkit-mask-image: linear-gradient(to bottom, black calc(100%% - %s), transparent 100%%); mask-image: linear-gradient(to bottom, black calc(100%% - %s), transparent 100%%);" $fade_height $fade_height }}
+{{ end }}
+
+
+
+
+ {{/* Cover Image */}}
+
+ {{ if ne $cover.MediaType.SubType "gif" }}
+

+ {{ else }}
+

+ {{ end }}
+
+
+ {{/* Overlay Effects */}}
+ {{ if and $overlay_enabled (ne $style "minimal") }}
+ {{ if eq $style "gradient" }}
+
+ {{ else if eq $style "glass" }}
+
+ {{ else if eq $style "blur" }}
+
+ {{ else if eq $overlay_type "gradient" }}
+ {{ if eq $overlay_gradient "bottom" }}
+
+ {{ else if eq $overlay_gradient "top" }}
+
+ {{ else if eq $overlay_gradient "both" }}
+
+ {{ else if eq $overlay_gradient "radial" }}
+
+ {{ end }}
+ {{ else if eq $overlay_type "solid" }}
+
+ {{ end }}
+ {{ end }}
+
+ {{/* Blur Effect at Bottom (Optional) */}}
+ {{ if and $fade_enabled $fade_blur (ne $style "minimal") }}
+ {{/* Taller blur zone with mask to create smooth blur transition before fade out */}}
+ {{/* Semi-transparent background needed for backdrop-filter to render */}}
+
+ {{ end }}
+
+ {{/* Caption */}}
+ {{ with $cover_config.caption }}
+
+ {{ . | markdownify }}
+
+ {{ end }}
+
+
+ {{/* Icon Overlay - Only if position is "overlap" - NOW UNCLIPPED */}}
+ {{ if and $has_icon (eq $icon_position "overlap") }}
+ {{ $icon_size_map := dict "small" "w-8 h-8" "medium" "w-12 h-12" "large" "w-16 h-16" }}
+ {{ $icon_size_class := index $icon_size_map $icon_size | default "w-16 h-16" }}
+ {{ $icon_padding := cond (eq $icon_size "small") "p-1.5" (cond (eq $icon_size "medium") "p-2" "p-3") }}
+
+
+
+ {{ if $icon_config.emoji }}
+ {{ $icon_config.emoji }}
+ {{ else if $icon_config.name }}
+ {{ partial "functions/get_icon.html" (dict "name" $icon_config.name "attributes" (printf "class=\"%s text-gray-700 dark:text-gray-200\"" $icon_size_class)) }}
+ {{ end }}
+
+
+ {{ end }}
+
+
+
+{{/* Icon Below Cover - If position is "below" */}}
+{{ if and $has_icon (eq $icon_position "below") }}
+ {{ $icon_size_map := dict "small" "w-8 h-8" "medium" "w-12 h-12" "large" "w-16 h-16" }}
+ {{ $icon_size_class := index $icon_size_map $icon_size | default "w-16 h-16" }}
+ {{ $icon_padding := cond (eq $icon_size "small") "p-1.5" (cond (eq $icon_size "medium") "p-2" "p-3") }}
+
+
+
+ {{ if $icon_config.emoji }}
+ {{ $icon_config.emoji }}
+ {{ else if $icon_config.name }}
+ {{ partial "functions/get_icon.html" (dict "name" $icon_config.name "attributes" (printf "class=\"%s text-gray-700 dark:text-gray-200\"" $icon_size_class)) }}
+ {{ end }}
+
+
+{{ end }}
+
+{{ end }}
diff --git a/modules/blox-tailwind/layouts/_partials/functions/get_cover_image.html b/modules/blox-tailwind/layouts/_partials/functions/get_cover_image.html
new file mode 100644
index 00000000..4d7c7038
--- /dev/null
+++ b/modules/blox-tailwind/layouts/_partials/functions/get_cover_image.html
@@ -0,0 +1,41 @@
+{{/* HugoBlox function to retrieve the cover image */}}
+{{/* Inputs: page context */}}
+{{/* Output: image resource, or nil if not found */}}
+
+{{/*
+ Cover image is searched in this order:
+ 1. Search for a file `*cover*` in the page directory
+ 2. Search for a file `.Params.cover.image` in the page directory
+ 3. Search for a file `.Params.cover.image` in the `assets/media/` directory
+*/}}
+
+{{/* Search for an image "*cover*" in page folder */}}
+{{ $resource := (.Resources.ByType "image").GetMatch "*cover*" }}
+{{ if eq $resource nil }}
+ {{/* Otherwise fall back to the image file specified in front matter */}}
+ {{ $filename := "" }}
+ {{ if .Params.cover }}
+ {{ if reflect.IsMap .Params.cover }}
+ {{ $filename = .Params.cover.image }}
+ {{ else }}
+ {{/* Support simple string format: cover: "image.jpg" */}}
+ {{ $filename = .Params.cover }}
+ {{ end }}
+ {{ end }}
+ {{ if $filename }}
+ {{/* Check if it's a remote URL */}}
+ {{ if or (strings.HasPrefix $filename "http://") (strings.HasPrefix $filename "https://") }}
+ {{ $resource = resources.GetRemote $filename }}
+ {{ if eq $resource nil }}
+ {{ warnf "Remote cover image not found: %s" $filename }}
+ {{ end }}
+ {{ else }}
+ {{/* Search in page folder */}}
+ {{ $resource = (.Resources.ByType "image").GetMatch $filename }}
+ {{/* Otherwise in `assets/media/` folder */}}
+ {{ if eq $resource nil }} {{ $resource = resources.GetMatch (path.Join "media" $filename) }} {{ end }}
+ {{ end }}
+ {{ end }}
+{{ end }}
+
+{{ return $resource }}
diff --git a/modules/blox-tailwind/layouts/_partials/functions/get_icon.html b/modules/blox-tailwind/layouts/_partials/functions/get_icon.html
index 9d832519..da660fa1 100644
--- a/modules/blox-tailwind/layouts/_partials/functions/get_icon.html
+++ b/modules/blox-tailwind/layouts/_partials/functions/get_icon.html
@@ -14,6 +14,7 @@
{{ $attrs := .attributes | default "" }}
{{/* Convert SVG sizing to emoji text sizing with important specificity */}}
{{ if in $attrs "w-24" }}{{ $attrs = replaceRE "w-24 h-24" "" $attrs }}{{ $attrs = printf "%s text-8xl leading-none" $attrs }}{{ end }}
+ {{ if in $attrs "w-16" }}{{ $attrs = replaceRE "w-16 h-16" "" $attrs }}{{ $attrs = printf "%s text-5xl leading-none" $attrs }}{{ end }}
{{ if in $attrs "w-12" }}{{ $attrs = replaceRE "w-12 h-12" "" $attrs }}{{ $attrs = printf "%s text-5xl leading-none" $attrs }}{{ end }}
{{ if in $attrs "w-8" }}{{ $attrs = replaceRE "w-8 h-8" "" $attrs }}{{ $attrs = printf "%s text-4xl leading-none" $attrs }}{{ end }}
{{ if in $attrs "w-6" }}{{ $attrs = replaceRE "w-6 h-6" "" $attrs }}{{ $attrs = printf "%s text-2xl leading-none" $attrs }}{{ end }}
@@ -43,37 +44,55 @@
{{ $icon = printf `` (cast.ToString $icon_size) (cast.ToString $icon_size) $icon_body }}
{{ end }}
{{ else }}
- {{ with index (index site.Data.icons.hb "icons") $name -}}
- {{ $icon = . }}
+ {{/* Icon NOT found in pack - check if it's a direct Unicode emoji */}}
+ {{ if not (strings.Contains $.name "/") }}
+ {{/* No explicit pack specified (defaulted to hero) - treat as direct Unicode emoji */}}
+ {{ $attrs := .attributes | default "" }}
+ {{/* Convert SVG sizing to emoji text sizing (same as emoji pack) */}}
+ {{ if in $attrs "w-24" }}{{ $attrs = replaceRE "w-24 h-24" "text-8xl" $attrs }}{{ end }}
+ {{ if in $attrs "w-16" }}{{ $attrs = replaceRE "w-16 h-16" "text-5xl" $attrs }}{{ end }}
+ {{ if in $attrs "w-12" }}{{ $attrs = replaceRE "w-12 h-12" "text-5xl" $attrs }}{{ end }}
+ {{ if in $attrs "w-8" }}{{ $attrs = replaceRE "w-8 h-8" "text-4xl" $attrs }}{{ end }}
+ {{ if in $attrs "w-6" }}{{ $attrs = replaceRE "w-6 h-6" "text-2xl" $attrs }}{{ end }}
+ {{ if in $attrs "w-5" }}{{ $attrs = replaceRE "w-5 h-5" "text-xl" $attrs }}{{ end }}
+ {{ if in $attrs "w-4" }}{{ $attrs = replaceRE "w-4 h-4" "text-lg" $attrs }}{{ end }}
+ {{/* Add centering and line-height classes if not already present */}}
+ {{ if not (in $attrs "flex") }}{{ $attrs = replaceRE "class=\"" "class=\"flex items-center justify-center leading-none " $attrs }}{{ end }}
+ {{ $icon = printf "%s" $attrs $.name }}
{{ else }}
- {{- $prefix := printf "media/icons/%s/" $pack -}}
- {{- $icon_path := printf "%s%s.svg" $prefix $name -}}
- {{- if not (fileExists (printf "assets/%s" $icon_path)) -}}
- {{ $pageRef := "unknown location" }}
- {{ if .Page }}
- {{ if .Page.File }}
- {{ if .Page.File.Path }}
- {{ $pageRef = .Page.File.Path }}
+ {{/* Explicit pack specified but icon not found - continue with fallback logic */}}
+ {{ with index (index site.Data.icons.hb "icons") $name -}}
+ {{ $icon = . }}
+ {{ else }}
+ {{- $prefix := printf "media/icons/%s/" $pack -}}
+ {{- $icon_path := printf "%s%s.svg" $prefix $name -}}
+ {{- if not (fileExists (printf "assets/%s" $icon_path)) -}}
+ {{ $pageRef := "unknown location" }}
+ {{ if .Page }}
+ {{ if .Page.File }}
+ {{ if .Page.File.Path }}
+ {{ $pageRef = .Page.File.Path }}
+ {{ else if .Page.RelPermalink }}
+ {{ $pageRef = .Page.RelPermalink }}
+ {{ end }}
{{ else if .Page.RelPermalink }}
{{ $pageRef = .Page.RelPermalink }}
{{ end }}
- {{ else if .Page.RelPermalink }}
- {{ $pageRef = .Page.RelPermalink }}
{{ end }}
+ {{ partial "functions/logger" (dict
+ "page" .Page
+ "level" "warn"
+ "id" (printf "missing-icon-%s-%s" $pack $name)
+ "message" (printf "The icon `%s` was not found in `assets/media/icons/%s/`. Referenced in: %s" $name $pack $pageRef)
+ )
+ }}
+ {{- end -}}
+ {{- $icon_resource := resources.Get $icon_path -}}
+ {{ if $icon_resource }}
+ {{ $icon = $icon_resource.Content }}
+ {{ else }}
+ {{ $icon = index (index site.Data.icons.brands "icons") "hugo" }}
{{ end }}
- {{ partial "functions/logger" (dict
- "page" .Page
- "level" "warn"
- "id" (printf "missing-icon-%s-%s" $pack $name)
- "message" (printf "The icon `%s` was not found in `assets/media/icons/%s/`. Referenced in: %s" $name $pack $pageRef)
- )
- }}
- {{- end -}}
- {{- $icon_resource := resources.Get $icon_path -}}
- {{ if $icon_resource }}
- {{ $icon = $icon_resource.Content }}
- {{ else }}
- {{ $icon = index (index site.Data.icons.brands "icons") "hugo" }}
{{ end }}
{{ end }}
{{ end }}
diff --git a/modules/blox-tailwind/layouts/baseof.html b/modules/blox-tailwind/layouts/baseof.html
index 76dd8d20..9e29cc75 100644
--- a/modules/blox-tailwind/layouts/baseof.html
+++ b/modules/blox-tailwind/layouts/baseof.html
@@ -22,7 +22,8 @@
{{ end }}
{{ end }}
-
+ {{ $cover := partial "functions/get_cover_image.html" . }}
+
{{ block "main" . }}{{ end }}