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" }} + {{ $alt_text }} + {{ else }} + {{ $alt_text }} + {{ 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 `%s` (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 }}