feat: port Markdown Slides template from Wowchemy/Bootstrap

This commit is contained in:
George Cushen committed 2026-01-06 20:12:26 +00:00
1 parent c792830419
commit bfdcb6603c
53 files changed
+6587 -209

No files matched your search

+14 -9
View File
@@ -11,6 +11,9 @@
import * as params from "@params";
// Guard against null/undefined slides params - ensures decks render with sensible defaults
const slides = params.slides || {};
// Enable core slide features.
var enabledPlugins = [RevealMarkdown, RevealSearch, RevealNotes, RevealMath.KaTeX, RevealZoom];
@@ -38,15 +41,17 @@ const keysToCamelCase = (o) => {
// reveal configurations can be included in front matter under slides.reveal
var pluginOptions = {};
if (typeof params.slides.reveal_options !== "undefined") {
pluginOptions = params.slides.reveal_options;
if (typeof slides.reveal_options !== "undefined") {
pluginOptions = slides.reveal_options;
}
pluginOptions = keysToCamelCase(pluginOptions);
//enable menu by default if not set
// Menu disabled by default (reveal.js-menu@2.1.0 has ESM compatibility issues)
// Menu plugin no longer maintained as of 2026-01 (last updated 6 years ago).
// TODO: consider removing this plugin as it's unlikely to work with modern browsers.
if (typeof pluginOptions.menu_enabled === "undefined") {
pluginOptions.menu_enabled = true;
pluginOptions.menu_enabled = false;
}
// configure menu if enabled
@@ -59,18 +64,18 @@ pluginOptions.plugins = enabledPlugins;
Reveal.initialize(pluginOptions);
// Disable Mermaid by default.
if (typeof params.slides.diagram === "undefined") {
params.slides.diagram = false;
if (typeof slides.diagram === "undefined") {
slides.diagram = false;
}
// Configure Mermaid only if diagrams are enabled.
if (params.slides.diagram) {
if (slides.diagram) {
//mermaid options
// mermaid: front matter configuration can be used to set mermaid options
// You can also use directives (see mermaid documentation)
let mermaidOptions = {};
if (typeof params.slides.diagram_options !== "undefined") {
mermaidOptions = params.slides.diagram_options;
if (typeof slides.diagram_options !== "undefined") {
mermaidOptions = slides.diagram_options;
}
// `startOnLoad` must be false since diagrams are lazily rendered.
+15
View File
@@ -0,0 +1,15 @@
outputFormats:
Present:
mediaType: text/html
baseName: index
isHTML: true
permalinkable: true
path: present
cascade:
- _target:
path: "/slides/**"
kind: "page"
outputs:
- HTML
- Present
+38 -70
View File
@@ -1,78 +1,46 @@
<!DOCTYPE html>
<html lang="{{ site.LanguageCode | default "en-us" }}">
{{/*
Slides Baseof - Landing page chrome only
Presentation output lives in baseof.present.html
*/}}
<!doctype html>
{{- $language_code := site.LanguageCode | default "en-us" -}}
<html lang="{{$language_code}}" {{ if in site.Data.i18n.rtl.rtl $language_code }}dir="rtl"{{else}}dir="ltr"{{end}}
data-wc-theme-default="{{ (lower site.Params.hugoblox.theme.mode) | default "system" }}"
data-hbb-relurl="{{ "" | relURL }}">
<head>
{{/* Asset versions */}}
{{ $cdn_url_reveal := "https://cdn.jsdelivr.net/npm/reveal.js@5.0.4" }}
{{ $mermaid_version := "9.1.3" }}
{{ $mermaid_sri := "sha256-TIYL00Rhw/8WaoUhYTLX9SKIEFdXxg+yMWSLVUbhiLg=" }}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="generator" content="HugoBlox Kit {{with site.Data.hugoblox.version}}{{.}}{{end}}">
{{ with site.Home.OutputFormats.Get "WebAppManifest" }}
<link rel="manifest" href="{{ .RelPermalink }}">
{{ end }}
<link rel="icon" type="image/png" href="{{ "media/icon.png" | relURL }}">
<link rel="apple-touch-icon" type="image/png" href="{{ "media/icon.png" | relURL }}">
<link rel="canonical" href="{{ .Permalink }}">
<title>{{ .Title }} | {{ site.Title }}</title>
{{/* Load Reveal CSS */}}
<link rel="stylesheet" href="{{ $cdn_url_reveal }}/dist/reveal.min.css">
{{- $theme := $.Param "slides.theme" | default "black" -}}
<link rel="stylesheet" href="{{ $cdn_url_reveal }}/dist/theme/{{ $theme }}.min.css">
{{/* Hugo Chroma Syntax Highlighter Initialization */}}
{{ $hl_theme := $.Param "slides.highlight_style" | default "dracula" }}
{{ $hl_theme_path := printf "css/libs/chroma/%s.css" $hl_theme }}
{{ if not (fileExists (printf "assets/%s" $hl_theme_path)) }}
{{ errorf "Hugo Chroma themes %s.css not found in `css/libs/chroma/`. See https://gohugo.io/commands/hugo_gen_chromastyles/" $hl_theme }}
{{ else }}
{{ $hl_theme_css := resources.Get $hl_theme_path | minify }}
<link rel="stylesheet" href="{{ $hl_theme_css.RelPermalink }}">
{{ end }}
{{/* Load any Reveal CSS customizations */}}
{{ $custom_reveal_css := printf "css/reveal_custom.css" }}
{{ if (fileExists (printf "assets/%s" $custom_reveal_css)) }}
{{ $css_custom := resources.Get "css/reveal_custom.css" }}
{{ $style := slice $css_custom | resources.Concat "css/reveal_custom.css" | resources.Minify }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
{{ end }}
<title>{{ .Title }} | {{ site.Params.hugoblox.identity.name | default site.Title }}</title>
{{ partial "site_head" . }}
</head>
<body>
{{ block "main" . }}{{ end }}
{{/* Load Reveal and built-in plugins. */}}
<script src="{{ $cdn_url_reveal }}/dist/reveal.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/markdown/markdown.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/notes/notes.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/search/search.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/math/math.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/zoom/zoom.min.js" crossorigin="anonymous"></script>
{{/* Load third-party Reveal plugins. */}}
{{ if $.Param "slides.reveal_options.menu_enabled" | default true}}
<script src="https://cdn.jsdelivr.net/npm/reveal.js-menu@2.1.0/plugin.js" integrity="sha256-M6JwAjnRAWmi+sbXURR/yAhWZKYhAw7YXnnLvIxrdGs=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/reveal.js-menu@2.1.0/menu.js" integrity="sha256-l14dklFcW5mWar6w/9KaW0fWVerf3mYr7Wt0+rXzFAA=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js-menu@2.1.0/menu.css" integrity="sha256-0fU8HKLaTjgzfaV9CgSqbsN8ilA3zo6zK1M6rlgULd8=" crossorigin="anonymous">
{{ end }}
{{/* Load Mermaid. */}}
{{ if $.Param "slides.diagram" | default false}}
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\" title=\"mermaid\"></script>" (printf "https://cdn.jsdelivr.net/npm/mermaid@%s/dist/mermaid.min.js" $mermaid_version) $mermaid_sri | safeHTML }}
{{ end }}
{{/* Initialize slides. */}}
{{ $slidejs := resources.Get "js/hugoblox-slides.js" | js.Build (dict "params" (dict "slides" $.Params.slides )) }}
<script src="{{ $slidejs.RelPermalink }}"></script>
<body class="bg-[var(--color-background)] text-[var(--color-foreground)] page-wrapper" id="top">
{{/* Landing page chrome */}}
<div id="page-bg"></div>
<div class="page-header {{ if site.Params.hugoblox.header.sticky }}sticky top-0 z-30{{ end }}">
{{ $show_navbar := ((site.Params.hugoblox.header.enable | default true) | and (ne .Params.header.navbar.enable false)) | or .Params.header.navbar.enable }}
{{ if $show_navbar }}
{{- $header_config := .Params.header.navbar | default site.Params.hugoblox.header | default dict -}}
{{- $block_name := partial "hbx/resolve-block-param" (dict "config" $header_config "context" "header") | default "navbar" -}}
{{ $block_path := printf "components/headers/%s.html" $block_name }}
{{ if templates.Exists (printf "_partials/%s" $block_path) }}
{{ partial $block_path . }}
{{ end }}
{{ end }}
</div>
<div class="page-body my-10">
{{ block "main" . }}{{ end }}
</div>
<div class="page-footer">
{{ partial "site_footer" . }}
</div>
{{ partial "notification-container" . }}
{{ partial "functions/get_hook" (dict "hook" "body-end" "context" .) }}
</body>
</html>
@@ -0,0 +1,86 @@
{{/*
Present Baseof - Presentation output only
*/}}
<!doctype html>
{{- $language_code := site.LanguageCode | default "en-us" -}}
<html lang="{{$language_code}}" {{ if in site.Data.i18n.rtl.rtl $language_code }}dir="rtl"{{else}}dir="ltr"{{end}}>
<head>
{{/* Asset versions */}}
{{ $cdn_url_reveal := "https://cdn.jsdelivr.net/npm/reveal.js@5.0.4" }}
{{ $mermaid_version := "9.1.3" }}
{{ $mermaid_sri := "sha256-TIYL00Rhw/8WaoUhYTLX9SKIEFdXxg+yMWSLVUbhiLg=" }}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="generator" content="HugoBlox Kit {{with site.Data.hugoblox.version}}{{.}}{{end}}">
{{ with site.Home.OutputFormats.Get "WebAppManifest" }}
<link rel="manifest" href="{{ .RelPermalink }}">
{{ end }}
<link rel="icon" type="image/png" href="{{ "media/icon.png" | relURL }}">
<link rel="apple-touch-icon" type="image/png" href="{{ "media/icon.png" | relURL }}">
{{ $landing_url := .Permalink }}
{{ with .OutputFormats.Get "HTML" }}
{{ $landing_url = .Permalink }}
{{ end }}
<link rel="canonical" href="{{ $landing_url }}">
<meta name="robots" content="noindex, nofollow">
<title>{{ .Title }} | {{ site.Params.hugoblox.identity.name | default site.Title }}</title>
{{/* Load Reveal CSS */}}
<link rel="stylesheet" href="{{ $cdn_url_reveal }}/dist/reveal.min.css">
{{- $theme := $.Param "slides.theme" | default "black" -}}
<link rel="stylesheet" href="{{ $cdn_url_reveal }}/dist/theme/{{ $theme }}.min.css">
{{/* Hugo Chroma Syntax Highlighter */}}
{{ $hl_theme := $.Param "slides.highlight_style" | default "dracula" }}
{{ $hl_theme_path := printf "css/libs/chroma/%s.css" $hl_theme }}
{{ if fileExists (printf "assets/%s" $hl_theme_path) }}
{{ $hl_theme_css := resources.Get $hl_theme_path | minify }}
<link rel="stylesheet" href="{{ $hl_theme_css.RelPermalink }}">
{{ end }}
{{/* Reveal print styles (paper or PDF) */}}
<script>
(function() {
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
const params = new URLSearchParams(window.location.search);
const mode = params.has("print-pdf") ? "pdf" : "paper";
link.href = "{{ $cdn_url_reveal }}/dist/print/" + mode + ".css";
document.head.appendChild(link);
})();
</script>
</head>
<body>
{{ block "main" . }}{{ end }}
{{/* Load Reveal and plugins */}}
<script src="{{ $cdn_url_reveal }}/dist/reveal.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/markdown/markdown.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/notes/notes.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/search/search.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/math/math.min.js" crossorigin="anonymous"></script>
<script src="{{ $cdn_url_reveal }}/plugin/zoom/zoom.min.js" crossorigin="anonymous"></script>
{{/* Load Mermaid if enabled */}}
{{ if $.Param "slides.diagram" | default false}}
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\" title=\"mermaid\"></script>" (printf "https://cdn.jsdelivr.net/npm/mermaid@%s/dist/mermaid.min.js" $mermaid_version) $mermaid_sri | safeHTML }}
{{ end }}
{{/* Initialize Reveal */}}
{{ $slidejs := resources.Get "js/hugoblox-slides.js" | js.Build (dict "params" (dict "slides" $.Params.slides )) | fingerprint }}
<script src="{{ $slidejs.RelPermalink }}"></script>
{{ partial "functions/get_hook" (dict "hook" "body-end" "context" .) }}
</body>
</html>
+212
View File
@@ -0,0 +1,212 @@
{{ define "main" }}
{{/* Enable Alpine.js for tag filtering */}}
{{ .Store.Set "has_alpine" true }}
{{/* Collect all unique tags from slides */}}
{{ $allTags := slice }}
{{ $featuredPages := slice }}
{{ $regularPages := slice }}
{{ $search_placeholder := i18n "slides_search_placeholder" | default "Search decks by title..." }}
{{ $all_label := i18n "all" | default "All" }}
{{ $no_decks_label := i18n "slides_no_decks" | default "No slide decks found" }}
{{ $no_matches_label := i18n "slides_no_matches" | default "No decks match your filters" }}
{{ $clear_filters_label := i18n "clear_filters" | default "Clear filters" }}
{{ range .Pages }}
{{ if .Params.tags }}
{{ range .Params.tags }}
{{ $allTags = $allTags | append . }}
{{ end }}
{{ end }}
{{ if or .Params.featured .Params.pinned }}
{{ $featuredPages = $featuredPages | append . }}
{{ else }}
{{ $regularPages = $regularPages | append . }}
{{ end }}
{{ end }}
{{ $allTags = $allTags | uniq | sort }}
{{/* Sort featured by date descending, then regular by date descending */}}
{{ $featuredPages = sort $featuredPages ".Date" "desc" }}
{{ $regularPages = sort $regularPages ".Date" "desc" }}
{{ $sortedPages := $featuredPages | append $regularPages }}
<div class="container mx-auto px-4 sm:px-6 max-w-6xl py-8" x-data="slidesFilter()">
{{/* Page Header */}}
<div class="mb-8">
<h1 class="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-zinc-100 mb-2">{{ .Title }}</h1>
{{ with .Content }}
<div class="prose prose-zinc dark:prose-invert max-w-none">
{{ . }}
</div>
{{ end }}
</div>
{{/* Search and Filter Bar */}}
<div class="mb-8 space-y-4">
{{/* Search input */}}
<div class="relative max-w-md">
<input
type="text"
x-model="searchQuery"
@input="filterDecks()"
placeholder="{{ $search_placeholder }}"
class="w-full pl-10 pr-4 py-2.5 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-shadow"
>
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
{{/* Tag Filter Chips */}}
{{ if $allTags }}
<div class="flex flex-wrap gap-2">
<button
@click="setTag('all')"
:class="activeTag === 'all' ? 'bg-primary-600 text-white' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700'"
class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors"
>
{{ $all_label }}
</button>
{{ range $allTags }}
<button
@click="setTag({{ . | jsonify }})"
:class="activeTag === {{ . | jsonify }} ? 'bg-primary-600 text-white' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700'"
class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors"
>
{{ . }}
</button>
{{ end }}
</div>
{{ end }}
</div>
{{/* Decks Grid */}}
{{ $view := .Params.view | default "slides-gallery" }}
{{ $config := dict
"columns" (.Params.columns | default 2)
"len" (len $sortedPages)
"fill_image" (.Params.fill_image | default true)
"show_date" (.Params.show_date | default true)
}}
{{ partial "functions/render_view" (dict "fragment" "start" "page" . "item" . "view" $view "config" $config) }}
{{ range $index, $item := $sortedPages }}
{{ $tags := $item.Params.tags | default (slice) }}
{{ $tagsJson := $tags | jsonify }}
{{ $authorList := slice }}
{{ with $item.Params.authors }}
{{ if reflect.IsSlice . }}
{{ $authorList = . }}
{{ else }}
{{ $authorList = slice . }}
{{ end }}
{{ end }}
{{ $authorNames := slice }}
{{ range $authorList }}
{{ $profile := partial "functions/get_author_profile" . }}
{{ $authorName := . }}
{{ if $profile.has_data }}
{{ $authorName = $profile.title }}
{{ end }}
{{ $authorNames = $authorNames | append $authorName }}
{{ end }}
{{ $searchParts := slice $item.Title }}
{{ with $item.Params.venue }}
{{ $searchParts = $searchParts | append . }}
{{ end }}
{{ if gt (len $authorNames) 0 }}
{{ $searchParts = $searchParts | append (delimit $authorNames " ") }}
{{ end }}
{{ if gt (len $tags) 0 }}
{{ $searchParts = $searchParts | append (delimit $tags " ") }}
{{ end }}
{{ $searchText := (delimit $searchParts " ") | lower }}
<div
class="deck-card"
data-tags="{{ $tagsJson }}"
data-search="{{ $searchText }}"
>
{{ partial "functions/render_view" (dict "page" $ "item" $item "view" $view "index" $index "config" $config) }}
</div>
{{ end }}
{{ partial "functions/render_view" (dict "fragment" "end" "page" . "item" . "view" $view) }}
{{/* Empty state */}}
{{ if eq (len $sortedPages) 0 }}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="mt-4 text-zinc-600 dark:text-zinc-400">{{ $no_decks_label }}</p>
</div>
{{ end }}
{{/* No matches empty state */}}
<div
x-show="noMatches"
x-cloak
class="text-center py-12"
>
<svg class="mx-auto h-12 w-12 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="mt-4 text-zinc-600 dark:text-zinc-400">{{ $no_matches_label }}</p>
<button @click="clearFilters()" class="mt-2 text-primary-600 dark:text-primary-400 hover:underline">
{{ $clear_filters_label }}
</button>
</div>
</div>
<script>
function slidesFilter() {
return {
activeTag: 'all',
searchQuery: '',
noMatches: false,
setTag(tag) {
this.activeTag = tag;
this.filterDecks();
},
clearFilters() {
this.activeTag = 'all';
this.searchQuery = '';
this.filterDecks();
},
filterDecks() {
const cards = document.querySelectorAll('.deck-card');
let visibleCount = 0;
const query = this.searchQuery.toLowerCase().trim();
cards.forEach(card => {
const tags = JSON.parse(card.dataset.tags || '[]');
const searchText = card.dataset.search || '';
const matchesTag = this.activeTag === 'all' || tags.includes(this.activeTag);
const matchesSearch = query === '' || searchText.includes(query);
if (matchesTag && matchesSearch) {
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
this.noMatches = visibleCount === 0 && cards.length > 0;
}
}
}
</script>
{{ end }}
@@ -0,0 +1,14 @@
{{/*
Presenter Layout - Full-screen Reveal.js presentation
Uses parent page's slide content
Route: /slides/<deck>/present/
*/}}
{{/* Get parent deck page (the actual slides content) */}}
{{ $deckPage := .Parent }}
{{ if not $deckPage }}
{{ $deckPage = . }}
{{ end }}
{{/* Render using modules/slides presentation layout */}}
{{ partial "slides" (union (slice $deckPage) $deckPage.Pages) }}
+333 -1
View File
@@ -1,3 +1,335 @@
{{/*
Smart Slides Single Layout
Landing page for slide decks.
Presentation output is rendered via the present output format.
*/}}
{{ define "main" }}
{{ partial "slides" (union (slice .Page) .Pages) }}
{{ $present_format := .OutputFormats.Get "present" }}
{{ $back_to_slides_label := i18n "back_to_slides" | default "Back to Slides" }}
{{ $featured_label := i18n "featured" | default "Featured" }}
{{ $slides_label := i18n "slides" | default "Slides" }}
{{ $abstract_label := i18n "callout_abstract" | default "Abstract" }}
{{ $topics_label := i18n "topics" | default "Topics" }}
{{ $related_resources_label := i18n "related_resources" | default "Related Resources" }}
{{ $present_fullscreen_label := i18n "present_fullscreen" | default "Present (fullscreen)" }}
{{ $copy_link_label := i18n "copy_link" | default "Copy Link" }}
{{ $copy_citation_label := i18n "copy_citation" | default "Copy citation" }}
{{ $copied_label := i18n "btn_copied" | default "Copied" }}
{{ $export_pdf_label := i18n "export_pdf_print" | default "Export PDF (print)" }}
{{ $print_hint := i18n "slides_print_hint" | default "Opens in print mode - use browser Print > Save as PDF" }}
{{ $how_to_cite_label := i18n "how_to_cite" | default "How to cite" }}
{{ $citation_label := i18n "citation" | default "Citation" }}
{{ $doi_label := i18n "btn_doi" | default "DOI" }}
{{ $bibtex_label := i18n "bibtex" | default "BibTeX" }}
{{ $download_cite_bib_label := i18n "download_cite_bib" | default "Download cite.bib" }}
{{ $lecture_label := i18n "lecture" | default "Lecture" }}
{{/* LANDING PAGE CONTENT */}}
<div id="deck-landing">
{{/* Get slide metadata */}}
{{ $authors := .Params.authors | default (slice) }}
{{ $venue := .Params.venue }}
{{ $event := .Params.event }}
{{ $course := .Params.course }}
{{ $lecture := .Params.lecture_number }}
{{ $tags := .Params.tags | default (slice) }}
{{ $topics := .Params.topics | default $tags }}
{{ $description := .Params.abstract | default .Params.summary | default .Description }}
{{ $citation := .Params.citation }}
{{ $authorNames := slice }}
{{ $authorList := slice }}
{{ with $authors }}
{{ if reflect.IsSlice . }}
{{ $authorList = . }}
{{ else }}
{{ $authorList = slice . }}
{{ end }}
{{ end }}
{{ range $authorList }}
{{ $profile := partial "functions/get_author_profile" . }}
{{ $authorName := . }}
{{ if $profile.has_data }}
{{ $authorName = $profile.title }}
{{ end }}
{{ $authorNames = $authorNames | append $authorName }}
{{ end }}
{{/* Build links using HugoBlox standard structure */}}
{{ $builtLinks := partial "functions/build_links.html" (dict "page" . "is_list" false) }}
{{/* Filter links by type for Related Resources section */}}
{{ $resourceLinks := slice }}
{{ $citationLinks := slice }}
{{ range $builtLinks }}
{{ if in (slice "pdf" "preprint" "code" "video" "dataset" "demo" "site") .type }}
{{ $resourceLinks = $resourceLinks | append . }}
{{ else if in (slice "doi" "bibtex") .type }}
{{ $citationLinks = $citationLinks | append . }}
{{ end }}
{{ end }}
{{ $citation_copy := "" }}
{{ if $citation }}
{{ $citation_copy = $citation }}
{{ else }}
{{ range $citationLinks }}
{{ if and (eq $citation_copy "") (eq .type "doi") }}
{{ $citation_copy = .url }}
{{ else if and (eq $citation_copy "") (eq .type "bibtex") }}
{{ $citation_copy = .url }}
{{ end }}
{{ end }}
{{ end }}
{{ $has_citation_copy := ne $citation_copy "" }}
{{/* Calculate slide count */}}
{{ $content := .RawContent }}
{{ $slideCount := 1 }}
{{ $separators := findRE "(?m)^---$" $content }}
{{ if $separators }}
{{ $slideCount = add (len $separators) 1 }}
{{ if hasPrefix $content "---" }}
{{ $slideCount = sub $slideCount 1 }}
{{ end }}
{{ end }}
<div class="container mx-auto px-4 sm:px-6 max-w-5xl py-12" x-data="{ copied: false, showCitation: false, copyText: {{ $copy_link_label | jsonify }}, citationCopied: false, citationCopyText: {{ $copy_citation_label | jsonify }}, citationValue: {{ $citation_copy | jsonify }} }">
{{/* Back to library link */}}
<a href="{{ "/slides/" | relURL }}" class="inline-flex items-center gap-2 text-sm text-zinc-600 dark:text-zinc-400 hover:text-primary-600 dark:hover:text-primary-400 mb-6 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
{{ $back_to_slides_label }}
</a>
{{/* Main content card */}}
<div class="bg-white dark:bg-zinc-900 rounded-2xl shadow-lg ring-1 ring-zinc-900/5 dark:ring-white/10 overflow-hidden mb-8">
{{/* Hero section with gradient */}}
<div class="relative px-8 py-12 bg-gradient-to-br from-primary-600 to-primary-800 dark:from-primary-700 dark:to-primary-900 text-white">
{{/* Featured badge */}}
{{ if or .Params.featured .Params.pinned }}
<div class="absolute top-4 right-4 px-3 py-1 rounded-full text-xs font-semibold bg-white/20 backdrop-blur-sm flex items-center gap-1.5">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
{{ $featured_label }}
</div>
{{ end }}
<h1 class="text-3xl md:text-4xl font-bold mb-4">{{ .Title }}</h1>
{{/* Meta info */}}
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-white/80">
{{ if gt (len $authorNames) 0 }}
<div class="flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
{{ delimit $authorNames ", " }}
</div>
{{ end }}
{{ with $venue }}
<div class="flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
{{ . }}
</div>
{{ end }}
{{ with $event }}
<div class="flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
</svg>
{{ . }}
</div>
{{ end }}
{{ if $course }}
<div class="flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
{{ $course }}{{ with $lecture }} - {{ $lecture_label }} {{ . }}{{ end }}
</div>
{{ end }}
<div class="flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ .Date | time.Format "January 2006" }}
</div>
<div class="flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
{{ $slideCount }} {{ $slides_label | lower }}
</div>
</div>
</div>
{{/* Content section */}}
<div class="px-8 py-8">
{{/* Description/Abstract */}}
{{ with $description }}
<div class="mb-6">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2">{{ $abstract_label }}</h2>
<p class="text-zinc-600 dark:text-zinc-400 leading-relaxed">{{ . }}</p>
</div>
{{ end }}
{{/* Topics/Tags */}}
{{ if $topics }}
<div class="mb-6">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2">{{ $topics_label }}</h2>
<div class="flex flex-wrap gap-2">
{{ range $topics }}
<a href="{{ "/slides/" | relURL }}" class="px-3 py-1 rounded-full text-sm font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-primary-100 dark:hover:bg-primary-900/40 hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
{{ . }}
</a>
{{ end }}
</div>
</div>
{{ end }}
{{/* Related Resources - Use HugoBlox standard links */}}
{{ if $resourceLinks }}
<div class="mb-6">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2">{{ $related_resources_label }}</h2>
<div class="flex flex-wrap gap-3">
{{ range $resourceLinks }}
{{ $linkLabel := .label }}
{{ with .labelKey }}
{{ $linkLabel = i18n . | default $linkLabel }}
{{ end }}
{{ $target := "" }}
{{ $rel := "" }}
{{ $scheme := (urls.Parse .url).Scheme }}
{{ if in (slice "http" "https") $scheme }}
{{ $target = "_blank" }}
{{ $rel = "noopener" }}
{{ end }}
<a href="{{ .url }}" {{ with $target }}target="{{ . }}"{{ end }} {{ with $rel }}rel="{{ . }}"{{ end }} class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors">
{{ partial "functions/get_icon" (dict "name" (.icon | default "hero/link") "attributes" "class='w-4 h-4'") }}
{{ $linkLabel }}
</a>
{{ end }}
</div>
</div>
{{ end }}
{{/* Share Panel */}}
<div class="flex flex-wrap gap-3 pt-6 border-t border-zinc-100 dark:border-zinc-800 mb-6">
{{/* Open Presenter */}}
{{ with $present_format }}
<a href="{{ .RelPermalink }}" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-primary-600 hover:bg-primary-700 text-white font-semibold transition-colors shadow-lg shadow-primary-500/25 cursor-pointer">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
{{ $present_fullscreen_label }}
</a>
{{ end }}
{{/* Copy Link */}}
<button
@click="navigator.clipboard.writeText('{{ .Permalink }}'); copied = true; copyText = {{ $copied_label | jsonify }}; setTimeout(() => { copied = false; copyText = {{ $copy_link_label | jsonify }}; }, 2000)"
class="inline-flex items-center gap-2 px-4 py-3 rounded-lg bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" x-show="!copied">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" x-show="copied" x-cloak>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span x-text="copyText">{{ $copy_link_label }}</span>
</button>
{{/* Copy Citation */}}
{{ if $has_citation_copy }}
<button
@click="navigator.clipboard.writeText(citationValue); citationCopied = true; citationCopyText = {{ $copied_label | jsonify }}; setTimeout(() => { citationCopied = false; citationCopyText = {{ $copy_citation_label | jsonify }}; }, 2000)"
class="inline-flex items-center gap-2 px-4 py-3 rounded-lg bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" x-show="!citationCopied">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" x-show="citationCopied" x-cloak>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span x-text="citationCopyText">{{ $copy_citation_label }}</span>
</button>
{{ end }}
{{/* Print PDF */}}
{{ with $present_format }}
<a href="{{ .RelPermalink }}?print-pdf" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-4 py-3 rounded-lg bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors" title="{{ $print_hint }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
{{ $export_pdf_label }}
</a>
{{ end }}
</div>
{{/* Citation Section - Use HugoBlox standard links + citation field */}}
{{ if or $citation $citationLinks }}
<div class="pt-6 border-t border-zinc-100 dark:border-zinc-800">
<button
@click="showCitation = !showCitation"
class="flex items-center gap-2 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
>
<svg class="w-4 h-4 transition-transform" :class="showCitation && 'rotate-90'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
{{ $how_to_cite_label }}
</button>
<div x-show="showCitation" x-transition class="mt-3 space-y-3">
{{ with $citation }}
<div>
<h4 class="text-xs font-semibold text-zinc-500 dark:text-zinc-400 mb-1">{{ $citation_label }}</h4>
<pre class="p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg text-sm text-zinc-700 dark:text-zinc-300 overflow-x-auto"><code>{{ . }}</code></pre>
</div>
{{ end }}
{{/* DOI and BibTeX from standard links structure */}}
{{ range $citationLinks }}
{{ if eq .type "doi" }}
<div>
<h4 class="text-xs font-semibold text-zinc-500 dark:text-zinc-400 mb-1">{{ $doi_label }}</h4>
<a href="{{ .url }}" target="_blank" rel="noopener" class="text-sm text-primary-600 dark:text-primary-400 hover:underline">
{{ .url | replaceRE "^https://doi.org/" "" }}
</a>
</div>
{{ else if eq .type "bibtex" }}
<div>
<h4 class="text-xs font-semibold text-zinc-500 dark:text-zinc-400 mb-1">{{ $bibtex_label }}</h4>
<a href="{{ .url }}" class="text-sm text-primary-600 dark:text-primary-400 hover:underline">
{{ $download_cite_bib_label }}
</a>
</div>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
</div>
</div>
{{ end }}
@@ -0,0 +1,3 @@
{{ define "main" }}
{{ partial "slides" (union (slice .Page) .Pages) }}
{{ end }}