themes-cyberpunk/template.templ
Alex Dunmow 313ebaf296 initial: theme plugin cyberpunk
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/cyberpunk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 14:11:25 +08:00

238 lines
8.5 KiB
Plaintext

package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries the rendered slots and assorted page chrome data passed
// from the CMS into a Cyberpunk page template.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
// parseCyberpunkPageData converts the loose doc map the renderer is handed into a typed PageData.
func parseCyberpunkPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok && t != "" {
title = t
}
slots := map[string]string{}
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS, _ := doc["theme_css"].(string)
structuredData, _ := doc["structured_data"].(string)
cssHash, _ := doc["css_hash"].(string)
pageviewNonce, _ := doc["pageview_nonce"].(string)
themeMode := "dark"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: bn.ParseSiteSettings(doc),
PageMeta: bn.ParsePageMeta(doc),
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: bn.ParseEngagementConfig(doc),
}
}
// scanlineOverlay renders the global scanline overlay element (fixed, pointer-events: none).
templ scanlineOverlay() {
<div aria-hidden="true" class="scanlines pointer-events-none fixed inset-0 z-50" data-cyberpunk-scanlines></div>
}
// Cyberpunk renders the default page template.
templ Cyberpunk(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/cyberpunk/css/cyberpunk.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="cyberpunk-body min-h-screen flex flex-col antialiased" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
@scanlineOverlay()
<header class="cyberpunk-chrome w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.85); border-bottom: 1px solid hsl(var(--border));">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full max-w-6xl mx-auto px-4 py-12">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center cyberpunk-empty" style="color: hsl(var(--muted-foreground));">
<p class="cyberpunk-mono">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto" style="border-top: 1px solid hsl(var(--border));">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// CyberpunkLanding renders the marketing landing template with hero/features/cta/footer slots.
templ CyberpunkLanding(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/cyberpunk/css/cyberpunk.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="cyberpunk-body min-h-screen flex flex-col antialiased" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
@scanlineOverlay()
<section class="w-full">
@templ.Raw(data.Slots["hero"])
</section>
<section class="w-full max-w-6xl mx-auto px-4 py-16">
@templ.Raw(data.Slots["features"])
</section>
<section class="w-full">
@templ.Raw(data.Slots["cta"])
</section>
<footer class="w-full mt-auto" style="border-top: 1px solid hsl(var(--border));">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// CyberpunkArticle renders the article template with mono-metadata header and a code-friendly aside.
templ CyberpunkArticle(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/cyberpunk/css/cyberpunk.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="cyberpunk-body min-h-screen flex flex-col antialiased" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
@scanlineOverlay()
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="max-w-4xl mx-auto px-4 py-6 cyberpunk-mono text-xs uppercase tracking-widest">
@templ.Raw(data.Slots["header"])
</div>
</header>
<main class="flex-grow w-full max-w-4xl mx-auto px-4 py-12 grid gap-10 lg:grid-cols-[1fr_240px]">
<article class="cyberpunk-prose prose prose-invert max-w-none">
@templ.Raw(data.Slots["main"])
</article>
<aside class="cyberpunk-aside cyberpunk-mono text-sm" style="color: hsl(var(--muted-foreground));">
@templ.Raw(data.Slots["aside"])
</aside>
</main>
<footer class="w-full mt-auto" style="border-top: 1px solid hsl(var(--border));">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// CyberpunkFullWidth renders the edge-to-edge template (no max-width clamping on main).
templ CyberpunkFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/cyberpunk/css/cyberpunk.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="cyberpunk-body min-h-screen flex flex-col antialiased" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
@scanlineOverlay()
<header class="cyberpunk-chrome w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.85); border-bottom: 1px solid hsl(var(--border));">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center cyberpunk-mono" style="color: hsl(var(--muted-foreground));">
<p>No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto" style="border-top: 1px solid hsl(var(--border));">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// RenderCyberpunk renders the default Cyberpunk page template.
func RenderCyberpunk(ctx context.Context, doc map[string]any) templ.Component {
return Cyberpunk(parseCyberpunkPageData(doc))
}
// RenderCyberpunkLanding renders the Cyberpunk landing page template.
func RenderCyberpunkLanding(ctx context.Context, doc map[string]any) templ.Component {
return CyberpunkLanding(parseCyberpunkPageData(doc))
}
// RenderCyberpunkArticle renders the Cyberpunk article page template.
func RenderCyberpunkArticle(ctx context.Context, doc map[string]any) templ.Component {
return CyberpunkArticle(parseCyberpunkPageData(doc))
}
// RenderCyberpunkFullWidth renders the Cyberpunk full-width page template.
func RenderCyberpunkFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return CyberpunkFullWidth(parseCyberpunkPageData(doc))
}