feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
This commit is contained in:
parent
e3a471a3df
commit
2c0428dce9
40
Makefile
40
Makefile
@ -1,26 +1,27 @@
|
|||||||
# noir — build & publish helpers (wasm .bnp workflow)
|
# noir — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||||
#
|
#
|
||||||
# The theme compiles to a wasip1 reactor-mode wasm module packed into a .bnp
|
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
||||||
# artifact (`ninja plugin build`). The legacy .so path is retired (WO-WZ-015):
|
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
||||||
# the CMS is wasm-native and can no longer load .so plugins. Distribution is
|
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
||||||
# publish-to-registry -> InstallFromRegistry (hot-load, no restart).
|
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
|
||||||
|
# classifies the repo as codeless (no Go at the root) and packs the artifact
|
||||||
|
# with manifest.pb (codeless: true) and NO plugin.wasm.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make build # compile wasm + pack noir-<version>.bnp
|
# make build # pack noir-<version>.bnp (codeless — no wasm)
|
||||||
# make verify # validate the built .bnp (layout/abi/name/size)
|
# make verify # validate the built .bnp (loader-identical checks)
|
||||||
# make archive-check # prove a clean `git archive HEAD` compiles to wasm
|
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
||||||
# make publish # ninja plugin publish --bnp (to the active registry)
|
# make publish # ninja plugin publish --bnp (to the active registry)
|
||||||
# make templ # regenerate templ Go files
|
|
||||||
# make clean # remove build artefacts
|
# make clean # remove build artefacts
|
||||||
|
|
||||||
.PHONY: build verify archive-check publish templ clean help
|
.PHONY: build verify archive-check publish clean help
|
||||||
|
|
||||||
PLUGIN_NAME := noir
|
PLUGIN_NAME := noir
|
||||||
NINJA := ninja
|
NINJA := ninja
|
||||||
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
||||||
|
|
||||||
# Compile to wasm and pack the .bnp (GOOS=wasip1 GOARCH=wasm, DESCRIBE probe,
|
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
||||||
# tar.zst of plugin.wasm + plugin.mod + manifest.pb + schemas/ + assets/).
|
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
||||||
build:
|
build:
|
||||||
$(NINJA) plugin build --dir .
|
$(NINJA) plugin build --dir .
|
||||||
|
|
||||||
@ -28,12 +29,12 @@ build:
|
|||||||
verify:
|
verify:
|
||||||
$(NINJA) plugin verify $(BNP)
|
$(NINJA) plugin verify $(BNP)
|
||||||
|
|
||||||
# Prove a clean `git archive HEAD` (what publish ships) compiles to wasm.
|
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
||||||
archive-check:
|
archive-check:
|
||||||
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
||||||
trap 'rm -rf "$$T"' EXIT && \
|
trap 'rm -rf "$$T"' EXIT && \
|
||||||
git archive HEAD | tar -x -C "$$T" && \
|
git archive HEAD | tar -x -C "$$T" && \
|
||||||
cd "$$T" && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o "$$T/check.wasm" . && \
|
cd "$$T" && $(NINJA) plugin build --dir . && \
|
||||||
echo "archive-check OK"
|
echo "archive-check OK"
|
||||||
|
|
||||||
# Publish the prebuilt .bnp to the active registry.
|
# Publish the prebuilt .bnp to the active registry.
|
||||||
@ -41,19 +42,14 @@ archive-check:
|
|||||||
publish: build
|
publish: build
|
||||||
$(NINJA) plugin publish --bnp $(BNP)
|
$(NINJA) plugin publish --bnp $(BNP)
|
||||||
|
|
||||||
# Regenerate templ Go files locally.
|
|
||||||
templ:
|
|
||||||
templ generate
|
|
||||||
|
|
||||||
# Remove build artefacts.
|
# Remove build artefacts.
|
||||||
clean:
|
clean:
|
||||||
rm -f $(PLUGIN_NAME)-*.bnp $(PLUGIN_NAME).so plugin.wasm
|
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " build Compile wasm + pack the .bnp"
|
@echo " build Pack the codeless .bnp (no wasm)"
|
||||||
@echo " verify Validate the built .bnp (loader-identical checks)"
|
@echo " verify Validate the built .bnp (loader-identical checks)"
|
||||||
@echo " archive-check Prove a clean git archive HEAD compiles to wasm"
|
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||||
@echo " publish Publish the .bnp to the active registry"
|
@echo " publish Publish the .bnp to the active registry"
|
||||||
@echo " templ Regenerate templ Go files"
|
|
||||||
@echo " clean Remove build artefacts"
|
@echo " clean Remove build artefacts"
|
||||||
|
|||||||
42
blocks/blocks.yaml
Normal file
42
blocks/blocks.yaml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Noir theme block definitions (codeless .bnp, WO-WZ-021).
|
||||||
|
# Keys are fully qualified (noir:<key>) because the codeless loader registers
|
||||||
|
# definitions verbatim — there is no PluginBlockRegistry prefixing in the
|
||||||
|
# codeless path (unlike the wasm/templ path). The qualified keys match the
|
||||||
|
# hardcoded data-block attributes and the master_pages.json references.
|
||||||
|
blocks:
|
||||||
|
- key: noir:lightbox_gallery
|
||||||
|
title: Lightbox Gallery
|
||||||
|
description: Grid of photographs that expand into a full-viewport lightbox overlay on click.
|
||||||
|
category: content
|
||||||
|
schema: lightbox_gallery.schema.json
|
||||||
|
template: lightbox_gallery.ninjatpl
|
||||||
|
- key: noir:contact_sheet
|
||||||
|
title: Contact Sheet
|
||||||
|
description: Numbered photograph frames with sprocket motif evoking a darkroom contact sheet.
|
||||||
|
category: content
|
||||||
|
schema: contact_sheet.schema.json
|
||||||
|
template: contact_sheet.ninjatpl
|
||||||
|
- key: noir:case_study
|
||||||
|
title: Project Case Study
|
||||||
|
description: Long-form project spread with sticky meta rail and image-led prose.
|
||||||
|
category: layout
|
||||||
|
schema: case_study.schema.json
|
||||||
|
template: case_study.ninjatpl
|
||||||
|
- key: noir:caption_strip
|
||||||
|
title: Caption Strip
|
||||||
|
description: Full-width 10px mono caption strip with a label on the left and supporting text on the right.
|
||||||
|
category: navigation
|
||||||
|
schema: caption_strip.schema.json
|
||||||
|
template: caption_strip.ninjatpl
|
||||||
|
- key: noir:image_pair
|
||||||
|
title: Image Pair
|
||||||
|
description: 50/50 photograph diptych with a shared caption beneath both images.
|
||||||
|
category: content
|
||||||
|
schema: image_pair.schema.json
|
||||||
|
template: image_pair.ninjatpl
|
||||||
|
- key: noir:footer
|
||||||
|
title: Noir Footer
|
||||||
|
description: Dissolved bottom rail with optional colophon and social links.
|
||||||
|
category: layout
|
||||||
|
schema: footer.schema.json
|
||||||
|
template: footer.ninjatpl
|
||||||
1
blocks/caption_strip.ninjatpl
Normal file
1
blocks/caption_strip.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div data-block="noir:caption_strip" class="noir-caption-strip"><span class="tracked-mono">{{ label }}</span><span class="tracked-mono">{{ right }}</span></div>
|
||||||
1
blocks/case_study.ninjatpl
Normal file
1
blocks/case_study.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<section data-block="noir:case_study" class="py-12"><div class="max-w-6xl mx-auto px-6 grid grid-cols-1 md:grid-cols-[14rem_1fr] gap-10"><aside class="noir-meta-rail tracked-mono space-y-3">{% if client %}<div><div style="color: hsl(var(--mutedForeground));">Client</div><div class="mt-1" style="color: hsl(var(--foreground));">{{ client }}</div></div>{% endif %}{% if year|integer > 0 %}<div><div style="color: hsl(var(--mutedForeground));">Year</div><div class="mt-1" style="color: hsl(var(--foreground));">{{ year|integer }}</div></div>{% endif %}{% if credits %}<div><div style="color: hsl(var(--mutedForeground));">Credits</div><ul class="mt-1 space-y-1" style="color: hsl(var(--foreground));">{% for credit in credits %}<li>{{ credit }}</li>{% endfor %}</ul></div>{% endif %}</aside><div class="min-w-0">{% if title %}<h1 class="noir-display mb-10" style="font-size: clamp(2.5rem, 5vw, 4rem); line-height: 1.05; color: hsl(var(--foreground));">{{ title }}</h1>{% endif %}{% if images %}<div class="space-y-8">{% for img in images %}{% if img %}<figure class="noir-figure"><img src="{{ img }}" alt="{{ title }}" loading="lazy" class="w-full h-auto block"></figure>{% endif %}{% endfor %}</div>{% else %}<div class="hairline p-12 text-center tracked-mono" style="color: hsl(var(--mutedForeground));">Add photographs to the case study spread.</div>{% endif %}</div></div></section>
|
||||||
1
blocks/contact_sheet.ninjatpl
Normal file
1
blocks/contact_sheet.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<section data-block="noir:contact_sheet" class="py-12"><div class="max-w-6xl mx-auto px-6">{% if not items %}<div class="hairline p-12 text-center tracked-mono" style="color: hsl(var(--mutedForeground));">Add frames to populate the contact sheet.</div>{% else %}<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">{% for item in items %}<figure class="noir-sprocket noir-figure" data-frame-number="{% if item.frame %}{{ item.frame }}{% else %}{% if forloop.Counter < 10 %}0{% endif %}{{ forloop.Counter }}{% endif %}"><span class="noir-frame-number tracked-mono" style="color: hsl(var(--mutedForeground));">{% if item.frame %}{{ item.frame }}{% else %}{% if forloop.Counter < 10 %}0{% endif %}{{ forloop.Counter }}{% endif %}</span>{% if item.image %}<img src="{{ item.image }}" alt="{{ item.label }}" loading="lazy" class="w-full h-auto block mt-2">{% else %}<div class="aspect-square hairline" style="background-color: hsl(var(--muted));"></div>{% endif %}{% if item.label %}<figcaption class="tracked-mono mt-2" style="color: hsl(var(--mutedForeground));">{{ item.label }}</figcaption>{% endif %}</figure>{% endfor %}</div>{% endif %}</div></section>
|
||||||
1
blocks/footer.ninjatpl
Normal file
1
blocks/footer.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div data-block="noir:footer" class="hairline-t" style="padding: 1rem 1.5rem;"><div class="max-w-6xl mx-auto flex items-center justify-between gap-4 flex-wrap">{% if showColophon != "false" and showColophon != false %}<p class="tracked-mono" style="color: hsl(var(--mutedForeground));">Designed and printed in the darkroom.</p>{% else %}<span></span>{% endif %}{% if social %}<ul class="flex items-center gap-4 list-none p-0 m-0">{% for link in social %}<li><a href="{{ link.url }}" rel="noopener noreferrer" target="_blank" class="tracked-mono" style="color: hsl(var(--mutedForeground)); text-decoration: none;">{{ link.text }}</a></li>{% endfor %}</ul>{% endif %}</div></div>
|
||||||
1
blocks/image_pair.ninjatpl
Normal file
1
blocks/image_pair.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<section data-block="noir:image_pair" class="py-12"><div class="max-w-6xl mx-auto px-6"><div class="grid grid-cols-1 md:grid-cols-2 gap-4" style="grid-template-columns: 1fr 1fr;">{% if left %}<figure class="noir-figure"><img src="{{ left }}" alt="{{ caption }}" loading="lazy" class="w-full h-auto block"></figure>{% else %}<div class="hairline aspect-[4/5]" style="background-color: hsl(var(--muted));"></div>{% endif %}{% if right %}<figure class="noir-figure"><img src="{{ right }}" alt="{{ caption }}" loading="lazy" class="w-full h-auto block"></figure>{% else %}<div class="hairline aspect-[4/5]" style="background-color: hsl(var(--muted));"></div>{% endif %}</div>{% if caption %}<p class="tracked-mono mt-3 text-center" style="color: hsl(var(--mutedForeground));">{{ caption }}</p>{% endif %}</div></section>
|
||||||
1
blocks/lightbox_gallery.ninjatpl
Normal file
1
blocks/lightbox_gallery.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<section data-block="noir:lightbox_gallery" class="py-12"><div class="max-w-6xl mx-auto px-6">{% if not items %}<div class="hairline p-12 text-center tracked-mono" style="color: hsl(var(--mutedForeground));">Add photographs to populate the gallery.</div>{% else %}<div class="grid gap-4 {% if columns|integer == 2 %}grid-cols-1 md:grid-cols-2{% elif columns|integer == 4 %}grid-cols-1 md:grid-cols-2 lg:grid-cols-4{% else %}grid-cols-1 md:grid-cols-2 lg:grid-cols-3{% endif %}">{% for item in items %}<figure class="noir-figure"><button type="button" data-noir-lightbox-trigger data-src="{{ item.image }}" data-alt="{{ item.caption }}" data-caption="{{ item.caption }}" class="block w-full p-0 m-0 cursor-zoom-in bg-transparent border-0" style="background: transparent;" aria-label="{% if item.caption %}Open photograph: {{ item.caption }}{% else %}Open photograph {{ forloop.Counter }}{% endif %}"><img src="{{ item.image }}" alt="{{ item.caption }}" loading="lazy" class="w-full h-auto block"></button>{% if item.caption %}<figcaption class="tracked-mono mt-2" style="color: hsl(var(--mutedForeground));">{{ item.caption }}</figcaption>{% endif %}</figure>{% endfor %}</div>{% endif %}</div></section>
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CaptionStripBlockMeta defines the Noir caption strip block.
|
|
||||||
var CaptionStripBlockMeta = blocks.BlockMeta{
|
|
||||||
Key: "caption_strip",
|
|
||||||
Title: "Caption Strip",
|
|
||||||
Description: "Full-width 10px mono caption strip with a label on the left and supporting text on the right.",
|
|
||||||
Source: "noir",
|
|
||||||
Category: blocks.CategoryNavigation,
|
|
||||||
}
|
|
||||||
|
|
||||||
// CaptionStripBlock renders a thin caption strip.
|
|
||||||
// Content shape: {"label":"INDEX","right":"© Studio"}
|
|
||||||
func CaptionStripBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
data := CaptionStripData{
|
|
||||||
Label: getString(content, "label"),
|
|
||||||
Right: getString(content, "right"),
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = captionStripComponent(data).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CaptionStripData holds the parsed view-model.
|
|
||||||
type CaptionStripData struct {
|
|
||||||
Label string
|
|
||||||
Right string
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// captionStripComponent renders the 10px mono caption strip.
|
|
||||||
templ captionStripComponent(data CaptionStripData) {
|
|
||||||
<div data-block="noir:caption_strip" class="noir-caption-strip">
|
|
||||||
<span class="tracked-mono">{ data.Label }</span>
|
|
||||||
<span class="tracked-mono">{ data.Right }</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// captionStripComponent renders the 10px mono caption strip.
|
|
||||||
func captionStripComponent(data CaptionStripData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"noir:caption_strip\" class=\"noir-caption-strip\"><span class=\"tracked-mono\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 string
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `caption_strip.templ`, Line: 6, Col: 41}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <span class=\"tracked-mono\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Right)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `caption_strip.templ`, Line: 7, Col: 41}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CaseStudyBlockMeta defines the Noir long-form project case study block.
|
|
||||||
var CaseStudyBlockMeta = blocks.BlockMeta{
|
|
||||||
Key: "case_study",
|
|
||||||
Title: "Project Case Study",
|
|
||||||
Description: "Long-form project spread with sticky meta rail and image-led prose.",
|
|
||||||
Source: "noir",
|
|
||||||
Category: blocks.CategoryLayout,
|
|
||||||
}
|
|
||||||
|
|
||||||
// CaseStudyBlock renders a project case study.
|
|
||||||
// Content shape: {"title":"...","client":"...","year":2025,"credits":["...","..."],"images":["...","..."]}
|
|
||||||
func CaseStudyBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
data := CaseStudyData{
|
|
||||||
Title: getString(content, "title"),
|
|
||||||
Client: getString(content, "client"),
|
|
||||||
Year: getInt(content, "year", 0),
|
|
||||||
Credits: getStringSlice(content, "credits"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, img := range getStringSlice(content, "images") {
|
|
||||||
if img == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data.Images = append(data.Images, blocks.ResolveMediaPath(img))
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = caseStudyComponent(data).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CaseStudyData holds the parsed view-model for the case study block.
|
|
||||||
type CaseStudyData struct {
|
|
||||||
Title string
|
|
||||||
Client string
|
|
||||||
Year int
|
|
||||||
Credits []string
|
|
||||||
Images []string
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// caseStudyComponent renders a project case-study spread with a sticky meta rail.
|
|
||||||
templ caseStudyComponent(data CaseStudyData) {
|
|
||||||
<section data-block="noir:case_study" class="py-12">
|
|
||||||
<div class="max-w-6xl mx-auto px-6 grid grid-cols-1 md:grid-cols-[14rem_1fr] gap-10">
|
|
||||||
<aside class="noir-meta-rail tracked-mono space-y-3">
|
|
||||||
if data.Client != "" {
|
|
||||||
<div>
|
|
||||||
<div style="color: hsl(var(--mutedForeground));">Client</div>
|
|
||||||
<div class="mt-1" style="color: hsl(var(--foreground));">{ data.Client }</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
if data.Year > 0 {
|
|
||||||
<div>
|
|
||||||
<div style="color: hsl(var(--mutedForeground));">Year</div>
|
|
||||||
<div class="mt-1" style="color: hsl(var(--foreground));">{ intToString(data.Year) }</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
if len(data.Credits) > 0 {
|
|
||||||
<div>
|
|
||||||
<div style="color: hsl(var(--mutedForeground));">Credits</div>
|
|
||||||
<ul class="mt-1 space-y-1" style="color: hsl(var(--foreground));">
|
|
||||||
for _, credit := range data.Credits {
|
|
||||||
<li>{ credit }</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</aside>
|
|
||||||
<div class="min-w-0">
|
|
||||||
if data.Title != "" {
|
|
||||||
<h1 class="noir-display mb-10" style="font-size: clamp(2.5rem, 5vw, 4rem); line-height: 1.05; color: hsl(var(--foreground));">
|
|
||||||
{ data.Title }
|
|
||||||
</h1>
|
|
||||||
}
|
|
||||||
if len(data.Images) > 0 {
|
|
||||||
<div class="space-y-8">
|
|
||||||
for _, img := range data.Images {
|
|
||||||
<figure class="noir-figure">
|
|
||||||
<img src={ img } alt={ data.Title } loading="lazy" class="w-full h-auto block"/>
|
|
||||||
</figure>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="hairline p-12 text-center tracked-mono" style="color: hsl(var(--mutedForeground));">
|
|
||||||
Add photographs to the case study spread.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// caseStudyComponent renders a project case-study spread with a sticky meta rail.
|
|
||||||
func caseStudyComponent(data CaseStudyData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"noir:case_study\" class=\"py-12\"><div class=\"max-w-6xl mx-auto px-6 grid grid-cols-1 md:grid-cols-[14rem_1fr] gap-10\"><aside class=\"noir-meta-rail tracked-mono space-y-3\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if data.Client != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div><div style=\"color: hsl(var(--mutedForeground));\">Client</div><div class=\"mt-1\" style=\"color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 string
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Client)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `case_study.templ`, Line: 11, Col: 76}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if data.Year > 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div><div style=\"color: hsl(var(--mutedForeground));\">Year</div><div class=\"mt-1\" style=\"color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(intToString(data.Year))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `case_study.templ`, Line: 17, Col: 87}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(data.Credits) > 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div><div style=\"color: hsl(var(--mutedForeground));\">Credits</div><ul class=\"mt-1 space-y-1\" style=\"color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for _, credit := range data.Credits {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<li>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(credit)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `case_study.templ`, Line: 25, Col: 20}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</li>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</ul></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</aside><div class=\"min-w-0\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if data.Title != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<h1 class=\"noir-display mb-10\" style=\"font-size: clamp(2.5rem, 5vw, 4rem); line-height: 1.05; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var5 string
|
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `case_study.templ`, Line: 34, Col: 18}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h1>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(data.Images) > 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"space-y-8\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for _, img := range data.Images {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<figure class=\"noir-figure\"><img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var6 string
|
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(img)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `case_study.templ`, Line: 41, Col: 22}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var7 string
|
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Title)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `case_study.templ`, Line: 41, Col: 41}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" loading=\"lazy\" class=\"w-full h-auto block\"></figure>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"hairline p-12 text-center tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">Add photographs to the case study spread.</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></div></section>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContactSheetBlockMeta defines the Noir contact sheet block.
|
|
||||||
var ContactSheetBlockMeta = blocks.BlockMeta{
|
|
||||||
Key: "contact_sheet",
|
|
||||||
Title: "Contact Sheet",
|
|
||||||
Description: "Numbered photograph frames with sprocket motif evoking a darkroom contact sheet.",
|
|
||||||
Source: "noir",
|
|
||||||
Category: blocks.CategoryContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContactSheetBlock renders a contact-sheet grid.
|
|
||||||
// Content shape: {"items":[{"image":"...","frame":"01","label":"..."}, ...]}
|
|
||||||
func ContactSheetBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
items := getSlice(content, "items")
|
|
||||||
|
|
||||||
var frames []ContactFrame
|
|
||||||
for i, item := range items {
|
|
||||||
frame := getString(item, "frame")
|
|
||||||
if frame == "" {
|
|
||||||
frame = padFrame(i + 1)
|
|
||||||
}
|
|
||||||
frames = append(frames, ContactFrame{
|
|
||||||
Image: blocks.ResolveMediaPath(getString(item, "image")),
|
|
||||||
Frame: frame,
|
|
||||||
Label: getString(item, "label"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = contactSheetComponent(frames).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContactFrame represents a single numbered frame in the contact sheet.
|
|
||||||
type ContactFrame struct {
|
|
||||||
Image string
|
|
||||||
Frame string
|
|
||||||
Label string
|
|
||||||
}
|
|
||||||
|
|
||||||
// padFrame zero-pads a frame number to two digits ("01", "02", ..., "10").
|
|
||||||
func padFrame(n int) string {
|
|
||||||
if n < 10 {
|
|
||||||
return "0" + intToString(n)
|
|
||||||
}
|
|
||||||
return intToString(n)
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// contactSheetComponent renders the contact sheet grid with sprocket frames.
|
|
||||||
templ contactSheetComponent(frames []ContactFrame) {
|
|
||||||
<section data-block="noir:contact_sheet" class="py-12">
|
|
||||||
<div class="max-w-6xl mx-auto px-6">
|
|
||||||
if len(frames) == 0 {
|
|
||||||
<div class="hairline p-12 text-center tracked-mono" style="color: hsl(var(--mutedForeground));">
|
|
||||||
Add frames to populate the contact sheet.
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
for _, frame := range frames {
|
|
||||||
<figure class="noir-sprocket noir-figure" data-frame-number={ frame.Frame }>
|
|
||||||
<span class="noir-frame-number tracked-mono" style="color: hsl(var(--mutedForeground));">
|
|
||||||
{ frame.Frame }
|
|
||||||
</span>
|
|
||||||
if frame.Image != "" {
|
|
||||||
<img
|
|
||||||
src={ frame.Image }
|
|
||||||
alt={ frame.Label }
|
|
||||||
loading="lazy"
|
|
||||||
class="w-full h-auto block mt-2"
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
<div class="aspect-square hairline" style="background-color: hsl(var(--muted));"></div>
|
|
||||||
}
|
|
||||||
if frame.Label != "" {
|
|
||||||
<figcaption class="tracked-mono mt-2" style="color: hsl(var(--mutedForeground));">
|
|
||||||
{ frame.Label }
|
|
||||||
</figcaption>
|
|
||||||
}
|
|
||||||
</figure>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// contactSheetComponent renders the contact sheet grid with sprocket frames.
|
|
||||||
func contactSheetComponent(frames []ContactFrame) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"noir:contact_sheet\" class=\"py-12\"><div class=\"max-w-6xl mx-auto px-6\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if len(frames) == 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hairline p-12 text-center tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">Add frames to populate the contact sheet.</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for _, frame := range frames {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<figure class=\"noir-sprocket noir-figure\" data-frame-number=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 string
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(frame.Frame)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `contact_sheet.templ`, Line: 14, Col: 79}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><span class=\"noir-frame-number tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(frame.Frame)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `contact_sheet.templ`, Line: 16, Col: 21}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span> ")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if frame.Image != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(frame.Image)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `contact_sheet.templ`, Line: 20, Col: 26}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var5 string
|
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(frame.Label)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `contact_sheet.templ`, Line: 21, Col: 26}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" loading=\"lazy\" class=\"w-full h-auto block mt-2\"> ")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"aspect-square hairline\" style=\"background-color: hsl(var(--muted));\"></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if frame.Label != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<figcaption class=\"tracked-mono mt-2\" style=\"color: hsl(var(--mutedForeground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var6 string
|
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(frame.Label)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `contact_sheet.templ`, Line: 30, Col: 22}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</figcaption>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</figure>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></section>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NoirEmailWrapper wraps body content in a pure black 600px canvas with a
|
|
||||||
// Tenor Sans masthead and a mono caption strip above the footer.
|
|
||||||
func NoirEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirEmailTemplate renders the inline-styled HTML email wrapper.
|
|
||||||
templ noirEmailTemplate(emailCtx templates.EmailContext, body string) {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<meta name="x-apple-disable-message-reformatting"/>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
|
||||||
<title>{ emailCtx.SiteSettings.SiteName }</title>
|
|
||||||
</head>
|
|
||||||
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", noirEmailBg(emailCtx)) }>
|
|
||||||
if emailCtx.PreviewText != "" {
|
|
||||||
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{ emailCtx.PreviewText }</div>
|
|
||||||
}
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("background-color: %s;", noirEmailBg(emailCtx)) }>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding: 48px 12px;">
|
|
||||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("width: 600px; max-width: 600px; background-color: %s; border: 1px solid %s;", noirEmailCard(emailCtx), noirEmailBorder(emailCtx)) }>
|
|
||||||
<!-- Masthead -->
|
|
||||||
<tr>
|
|
||||||
<td align="left" style={ fmt.Sprintf("padding: 28px 32px; border-bottom: 1px solid %s;", noirEmailBorder(emailCtx)) }>
|
|
||||||
if emailCtx.SiteSettings.SiteName != "" {
|
|
||||||
<div style={ fmt.Sprintf("margin: 0; font-size: 18px; font-weight: 400; font-family: 'Tenor Sans', Georgia, serif; letter-spacing: 0.05em; color: %s;", noirEmailFg(emailCtx)) }>
|
|
||||||
{ emailCtx.SiteSettings.SiteName }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Cover image (16:10) -->
|
|
||||||
if emailCtx.SiteSettings.LogoURL != "" {
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 0; line-height: 0;">
|
|
||||||
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } width="600" height="375" style="display: block; width: 600px; height: 375px; object-fit: cover; max-width: 100%;"/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
<!-- Body -->
|
|
||||||
<tr>
|
|
||||||
<td style={ fmt.Sprintf("padding: 32px; color: %s; font-size: 16px; line-height: 1.7;", noirEmailFg(emailCtx)) }>
|
|
||||||
@templ.Raw(body)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Mono caption strip -->
|
|
||||||
<tr>
|
|
||||||
<td style={ fmt.Sprintf("padding: 12px 32px; border-top: 1px solid %s; border-bottom: 1px solid %s; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", noirEmailBorder(emailCtx), noirEmailBorder(emailCtx), noirEmailMutedFg(emailCtx)) }>
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
||||||
<tr>
|
|
||||||
<td align="left">
|
|
||||||
if emailCtx.SiteSettings.SiteName != "" {
|
|
||||||
{ "© " + emailCtx.SiteSettings.SiteName }
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td align="right">
|
|
||||||
if emailCtx.UnsubscribeURL != "" {
|
|
||||||
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", noirEmailMutedFg(emailCtx)) }>
|
|
||||||
Unsubscribe
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Footer -->
|
|
||||||
<tr>
|
|
||||||
<td align="center" style={ fmt.Sprintf("padding: 20px 32px; color: %s; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;", noirEmailMutedFg(emailCtx)) }>
|
|
||||||
if emailCtx.SiteSettings.SiteURL != "" {
|
|
||||||
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", noirEmailMutedFg(emailCtx)) }>
|
|
||||||
{ emailCtx.SiteSettings.SiteURL }
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirEmailBg returns the canvas colour (pure black per spec) with a fallback
|
|
||||||
// derived from the EmailContext.
|
|
||||||
func noirEmailBg(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Background != "" {
|
|
||||||
return emailCtx.Colors.Background
|
|
||||||
}
|
|
||||||
return "#000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailCard(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Card != "" {
|
|
||||||
return emailCtx.Colors.Card
|
|
||||||
}
|
|
||||||
return "#0a0a0a"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailFg(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Foreground != "" {
|
|
||||||
return emailCtx.Colors.Foreground
|
|
||||||
}
|
|
||||||
return "#f5f5f5"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailMutedFg(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.MutedForeground != "" {
|
|
||||||
return emailCtx.Colors.MutedForeground
|
|
||||||
}
|
|
||||||
return "#8c8c8c"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailBorder(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Border != "" {
|
|
||||||
return emailCtx.Colors.Border
|
|
||||||
}
|
|
||||||
return "#1f1f1f"
|
|
||||||
}
|
|
||||||
@ -1,401 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NoirEmailWrapper wraps body content in a pure black 600px canvas with a
|
|
||||||
// Tenor Sans masthead and a mono caption strip above the footer.
|
|
||||||
func NoirEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirEmailTemplate renders the inline-styled HTML email wrapper.
|
|
||||||
func noirEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 string
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 28, Col: 41}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title></head><body style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", noirEmailBg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 30, Col: 177}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if emailCtx.PreviewText != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 32, Col: 101}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var5 string
|
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s;", noirEmailBg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 34, Col: 152}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><tr><td align=\"center\" style=\"padding: 48px 12px;\"><table role=\"presentation\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var6 string
|
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: 600px; max-width: 600px; background-color: %s; border: 1px solid %s;", noirEmailCard(emailCtx), noirEmailBorder(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 37, Col: 237}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><!-- Masthead --><tr><td align=\"left\" style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var7 string
|
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 28px 32px; border-bottom: 1px solid %s;", noirEmailBorder(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 40, Col: 122}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if emailCtx.SiteSettings.SiteName != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var8 string
|
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 18px; font-weight: 400; font-family: 'Tenor Sans', Georgia, serif; letter-spacing: 0.05em; color: %s;", noirEmailFg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 42, Col: 183}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var9 string
|
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 43, Col: 42}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td></tr><!-- Cover image (16:10) -->")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if emailCtx.SiteSettings.LogoURL != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<tr><td style=\"padding: 0; line-height: 0;\"><img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var10 string
|
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 52, Col: 49}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var11 string
|
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 52, Col: 88}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" width=\"600\" height=\"375\" style=\"display: block; width: 600px; height: 375px; object-fit: cover; max-width: 100%;\"></td></tr>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<!-- Body --><tr><td style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var12 string
|
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px; color: %s; font-size: 16px; line-height: 1.7;", noirEmailFg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 58, Col: 117}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td></tr><!-- Mono caption strip --><tr><td style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var13 string
|
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 12px 32px; border-top: 1px solid %s; border-bottom: 1px solid %s; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", noirEmailBorder(emailCtx), noirEmailBorder(emailCtx), noirEmailMutedFg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 64, Col: 321}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"left\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if emailCtx.SiteSettings.SiteName != "" {
|
|
||||||
var templ_7745c5c3_Var14 string
|
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs("© " + emailCtx.SiteSettings.SiteName)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 69, Col: 52}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td align=\"right\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if emailCtx.UnsubscribeURL != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a href=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var15 templ.SafeURL
|
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 74, Col: 60}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var16 string
|
|
||||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", noirEmailMutedFg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 74, Col: 147}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">Unsubscribe</a>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td></tr></table></td></tr><!-- Footer --><tr><td align=\"center\" style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var17 string
|
|
||||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 20px 32px; color: %s; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;", noirEmailMutedFg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 85, Col: 227}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if emailCtx.SiteSettings.SiteURL != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var18 templ.SafeURL
|
|
||||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 87, Col: 63}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" style=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var19 string
|
|
||||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", noirEmailMutedFg(emailCtx)))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 87, Col: 150}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var20 string
|
|
||||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 88, Col: 41}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td></tr></table></td></tr></table></body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirEmailBg returns the canvas colour (pure black per spec) with a fallback
|
|
||||||
// derived from the EmailContext.
|
|
||||||
func noirEmailBg(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Background != "" {
|
|
||||||
return emailCtx.Colors.Background
|
|
||||||
}
|
|
||||||
return "#000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailCard(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Card != "" {
|
|
||||||
return emailCtx.Colors.Card
|
|
||||||
}
|
|
||||||
return "#0a0a0a"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailFg(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Foreground != "" {
|
|
||||||
return emailCtx.Colors.Foreground
|
|
||||||
}
|
|
||||||
return "#f5f5f5"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailMutedFg(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.MutedForeground != "" {
|
|
||||||
return emailCtx.Colors.MutedForeground
|
|
||||||
}
|
|
||||||
return "#8c8c8c"
|
|
||||||
}
|
|
||||||
|
|
||||||
func noirEmailBorder(emailCtx templates.EmailContext) string {
|
|
||||||
if emailCtx.Colors.Border != "" {
|
|
||||||
return emailCtx.Colors.Border
|
|
||||||
}
|
|
||||||
return "#1f1f1f"
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
64
embed.go
64
embed.go
@ -1,64 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/*
|
|
||||||
var assetsFS embed.FS
|
|
||||||
|
|
||||||
//go:embed schemas/*
|
|
||||||
var schemasFS embed.FS
|
|
||||||
|
|
||||||
//go:embed presets.json
|
|
||||||
var presetsData []byte
|
|
||||||
|
|
||||||
//go:embed fonts.json
|
|
||||||
var fontsData []byte
|
|
||||||
|
|
||||||
//go:embed plugin.mod
|
|
||||||
var pluginModBytes []byte
|
|
||||||
|
|
||||||
// Assets returns the embedded assets filesystem.
|
|
||||||
func Assets() fs.FS {
|
|
||||||
sub, _ := fs.Sub(assetsFS, "assets")
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schemas returns the embedded schemas filesystem.
|
|
||||||
func Schemas() fs.FS {
|
|
||||||
sub, _ := fs.Sub(schemasFS, "schemas")
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssetsHandler returns an http.Handler that serves the embedded assets.
|
|
||||||
func AssetsHandler() http.Handler {
|
|
||||||
return http.FileServer(http.FS(Assets()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ThemePresets returns the embedded theme presets JSON.
|
|
||||||
func ThemePresets() []byte {
|
|
||||||
return presetsData
|
|
||||||
}
|
|
||||||
|
|
||||||
// BundledFonts returns the embedded fonts manifest JSON.
|
|
||||||
func BundledFonts() []byte {
|
|
||||||
return fontsData
|
|
||||||
}
|
|
||||||
|
|
||||||
// ThemeCSSManifest exposes Noir's custom utility CSS (.bleed, .tracked-mono,
|
|
||||||
// .hairline, lightbox keyframes) to the host Tailwind input so the classes
|
|
||||||
// survive the content scanner.
|
|
||||||
func ThemeCSSManifest() *plugin.CSSManifest {
|
|
||||||
css, err := assetsFS.ReadFile("assets/style.css")
|
|
||||||
if err != nil {
|
|
||||||
return &plugin.CSSManifest{}
|
|
||||||
}
|
|
||||||
return &plugin.CSSManifest{
|
|
||||||
InputCSSAppend: string(css),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
footer.go
48
footer.go
@ -1,48 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FooterBlockMeta defines the Noir dissolved-rail footer block.
|
|
||||||
var FooterBlockMeta = blocks.BlockMeta{
|
|
||||||
Key: "footer",
|
|
||||||
Title: "Noir Footer",
|
|
||||||
Description: "Dissolved bottom rail with optional colophon and social links.",
|
|
||||||
Source: "noir",
|
|
||||||
Category: blocks.CategoryLayout,
|
|
||||||
}
|
|
||||||
|
|
||||||
// FooterBlock renders the Noir footer.
|
|
||||||
// Content shape: {"showColophon":"true","social":[{"text":"Instagram","url":"..."}, ...]}
|
|
||||||
func FooterBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
data := FooterData{
|
|
||||||
ShowColophon: getBoolish(content, "showColophon", true),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range getSlice(content, "social") {
|
|
||||||
data.Social = append(data.Social, FooterLink{
|
|
||||||
Text: getString(item, "text"),
|
|
||||||
URL: getString(item, "url"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = footerComponent(data).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FooterData holds the parsed view-model for the Noir footer.
|
|
||||||
type FooterData struct {
|
|
||||||
ShowColophon bool
|
|
||||||
Social []FooterLink
|
|
||||||
}
|
|
||||||
|
|
||||||
// FooterLink is a single social-link entry.
|
|
||||||
type FooterLink struct {
|
|
||||||
Text string
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
33
footer.templ
33
footer.templ
@ -1,33 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// footerComponent renders the Noir footer rail.
|
|
||||||
templ footerComponent(data FooterData) {
|
|
||||||
<div data-block="noir:footer" class="hairline-t" style="padding: 1rem 1.5rem;">
|
|
||||||
<div class="max-w-6xl mx-auto flex items-center justify-between gap-4 flex-wrap">
|
|
||||||
if data.ShowColophon {
|
|
||||||
<p class="tracked-mono" style="color: hsl(var(--mutedForeground));">
|
|
||||||
Designed and printed in the darkroom.
|
|
||||||
</p>
|
|
||||||
} else {
|
|
||||||
<span></span>
|
|
||||||
}
|
|
||||||
if len(data.Social) > 0 {
|
|
||||||
<ul class="flex items-center gap-4 list-none p-0 m-0">
|
|
||||||
for _, link := range data.Social {
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={ templ.SafeURL(link.URL) }
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
class="tracked-mono"
|
|
||||||
style="color: hsl(var(--mutedForeground)); text-decoration: none;"
|
|
||||||
>
|
|
||||||
{ link.Text }
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// footerComponent renders the Noir footer rail.
|
|
||||||
func footerComponent(data FooterData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"noir:footer\" class=\"hairline-t\" style=\"padding: 1rem 1.5rem;\"><div class=\"max-w-6xl mx-auto flex items-center justify-between gap-4 flex-wrap\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if data.ShowColophon {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">Designed and printed in the darkroom.</p>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span></span> ")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(data.Social) > 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<ul class=\"flex items-center gap-4 list-none p-0 m-0\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for _, link := range data.Social {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li><a href=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 templ.SafeURL
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(link.URL))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 19, Col: 38}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" rel=\"noopener noreferrer\" target=\"_blank\" class=\"tracked-mono\" style=\"color: hsl(var(--mutedForeground)); text-decoration: none;\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(link.Text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 25, Col: 19}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a></li>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</ul>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
20
go.mod
20
go.mod
@ -1,20 +0,0 @@
|
|||||||
module git.dev.alexdunmow.com/block/themes/noir
|
|
||||||
|
|
||||||
go 1.26.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.dev.alexdunmow.com/block/core v0.15.2
|
|
||||||
github.com/a-h/templ v0.3.1020
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
connectrpc.com/connect v1.20.0 // indirect
|
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
|
||||||
golang.org/x/text v0.36.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
|
||||||
)
|
|
||||||
46
go.sum
46
go.sum
@ -1,46 +0,0 @@
|
|||||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
|
||||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.15.2 h1:qTjJxa1pClxwehkdTxXiJDpHQLr5DoXBUCa40QRO0L8=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.15.2/go.mod h1:A+mgA9oeWoPhXruv4Mjpywwl2A9B5I3cKzlJjalBdlE=
|
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
|
||||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
|
||||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
|
||||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
75
helpers.go
75
helpers.go
@ -1,75 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
// getString extracts a string value from content map. Returns "" if missing or wrong type.
|
|
||||||
func getString(content map[string]any, key string) string {
|
|
||||||
if v, ok := content[key].(string); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// getInt extracts an int value from content map (handles float64 from JSON).
|
|
||||||
func getInt(content map[string]any, key string, defaultVal int) int {
|
|
||||||
if v, ok := content[key].(float64); ok {
|
|
||||||
return int(v)
|
|
||||||
}
|
|
||||||
if v, ok := content[key].(int); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if v, ok := content[key].(string); ok {
|
|
||||||
if n, err := strconv.Atoi(v); err == nil {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultVal
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSlice extracts a slice of maps from content. Returns nil if missing.
|
|
||||||
func getSlice(content map[string]any, key string) []map[string]any {
|
|
||||||
if v, ok := content[key].([]any); ok {
|
|
||||||
result := make([]map[string]any, 0, len(v))
|
|
||||||
for _, item := range v {
|
|
||||||
if m, ok := item.(map[string]any); ok {
|
|
||||||
result = append(result, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getStringSlice extracts a slice of strings from content. Non-string entries are coerced via fmt.Sprintf.
|
|
||||||
// Returns nil if missing.
|
|
||||||
func getStringSlice(content map[string]any, key string) []string {
|
|
||||||
if v, ok := content[key].([]any); ok {
|
|
||||||
result := make([]string, 0, len(v))
|
|
||||||
for _, item := range v {
|
|
||||||
if s, ok := item.(string); ok {
|
|
||||||
result = append(result, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
if v, ok := content[key].([]string); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBoolish accepts "true"/"false" strings and booleans. Returns defaultVal otherwise.
|
|
||||||
func getBoolish(content map[string]any, key string, defaultVal bool) bool {
|
|
||||||
if v, ok := content[key].(bool); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if v, ok := content[key].(string); ok {
|
|
||||||
switch v {
|
|
||||||
case "true":
|
|
||||||
return true
|
|
||||||
case "false":
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultVal
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ImagePairBlockMeta defines the Noir diptych block.
|
|
||||||
var ImagePairBlockMeta = blocks.BlockMeta{
|
|
||||||
Key: "image_pair",
|
|
||||||
Title: "Image Pair",
|
|
||||||
Description: "50/50 photograph diptych with a shared caption beneath both images.",
|
|
||||||
Source: "noir",
|
|
||||||
Category: blocks.CategoryContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImagePairBlock renders a two-photograph diptych.
|
|
||||||
// Content shape: {"left":"...","right":"...","caption":"..."}
|
|
||||||
func ImagePairBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
data := ImagePairData{
|
|
||||||
Left: blocks.ResolveMediaPath(getString(content, "left")),
|
|
||||||
Right: blocks.ResolveMediaPath(getString(content, "right")),
|
|
||||||
Caption: getString(content, "caption"),
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = imagePairComponent(data).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImagePairData holds the parsed view-model for the diptych.
|
|
||||||
type ImagePairData struct {
|
|
||||||
Left string
|
|
||||||
Right string
|
|
||||||
Caption string
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// imagePairComponent renders a 50/50 photograph diptych with a shared caption.
|
|
||||||
templ imagePairComponent(data ImagePairData) {
|
|
||||||
<section data-block="noir:image_pair" class="py-12">
|
|
||||||
<div class="max-w-6xl mx-auto px-6">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" style="grid-template-columns: 1fr 1fr;">
|
|
||||||
if data.Left != "" {
|
|
||||||
<figure class="noir-figure">
|
|
||||||
<img src={ data.Left } alt={ data.Caption } loading="lazy" class="w-full h-auto block"/>
|
|
||||||
</figure>
|
|
||||||
} else {
|
|
||||||
<div class="hairline aspect-[4/5]" style="background-color: hsl(var(--muted));"></div>
|
|
||||||
}
|
|
||||||
if data.Right != "" {
|
|
||||||
<figure class="noir-figure">
|
|
||||||
<img src={ data.Right } alt={ data.Caption } loading="lazy" class="w-full h-auto block"/>
|
|
||||||
</figure>
|
|
||||||
} else {
|
|
||||||
<div class="hairline aspect-[4/5]" style="background-color: hsl(var(--muted));"></div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
if data.Caption != "" {
|
|
||||||
<p class="tracked-mono mt-3 text-center" style="color: hsl(var(--mutedForeground));">
|
|
||||||
{ data.Caption }
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// imagePairComponent renders a 50/50 photograph diptych with a shared caption.
|
|
||||||
func imagePairComponent(data ImagePairData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"noir:image_pair\" class=\"py-12\"><div class=\"max-w-6xl mx-auto px-6\"><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\" style=\"grid-template-columns: 1fr 1fr;\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if data.Left != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<figure class=\"noir-figure\"><img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 string
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Left)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_pair.templ`, Line: 10, Col: 26}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_pair.templ`, Line: 10, Col: 47}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" loading=\"lazy\" class=\"w-full h-auto block\"></figure>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"hairline aspect-[4/5]\" style=\"background-color: hsl(var(--muted));\"></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if data.Right != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<figure class=\"noir-figure\"><img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Right)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_pair.templ`, Line: 17, Col: 27}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var5 string
|
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_pair.templ`, Line: 17, Col: 48}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" loading=\"lazy\" class=\"w-full h-auto block\"></figure>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"hairline aspect-[4/5]\" style=\"background-color: hsl(var(--muted));\"></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if data.Caption != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"tracked-mono mt-3 text-center\" style=\"color: hsl(var(--mutedForeground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var6 string
|
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_pair.templ`, Line: 25, Col: 19}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></section>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LightboxGalleryBlockMeta defines the Noir lightbox gallery block.
|
|
||||||
var LightboxGalleryBlockMeta = blocks.BlockMeta{
|
|
||||||
Key: "lightbox_gallery",
|
|
||||||
Title: "Lightbox Gallery",
|
|
||||||
Description: "Grid of photographs that expand into a full-viewport lightbox overlay on click.",
|
|
||||||
Source: "noir",
|
|
||||||
Category: blocks.CategoryContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// LightboxGalleryBlock renders the gallery from content.items + content.columns.
|
|
||||||
// Content shape: {"items":[{"image":"...","caption":"..."}, ...], "columns": 2|3|4}
|
|
||||||
func LightboxGalleryBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
items := getSlice(content, "items")
|
|
||||||
cols := getInt(content, "columns", 3)
|
|
||||||
if cols != 2 && cols != 3 && cols != 4 {
|
|
||||||
cols = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries []LightboxItem
|
|
||||||
for _, item := range items {
|
|
||||||
entries = append(entries, LightboxItem{
|
|
||||||
Image: blocks.ResolveMediaPath(getString(item, "image")),
|
|
||||||
Caption: getString(item, "caption"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = lightboxGalleryComponent(entries, cols).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LightboxItem represents one photograph in the gallery.
|
|
||||||
type LightboxItem struct {
|
|
||||||
Image string
|
|
||||||
Caption string
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// lightboxGalleryComponent renders the gallery grid with lightbox triggers.
|
|
||||||
templ lightboxGalleryComponent(items []LightboxItem, cols int) {
|
|
||||||
<section data-block="noir:lightbox_gallery" class="py-12">
|
|
||||||
<div class="max-w-6xl mx-auto px-6">
|
|
||||||
if len(items) == 0 {
|
|
||||||
<div class="hairline p-12 text-center tracked-mono" style="color: hsl(var(--mutedForeground));">
|
|
||||||
Add photographs to populate the gallery.
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class={ "grid gap-4", lightboxGridCols(cols) }>
|
|
||||||
for i, item := range items {
|
|
||||||
<figure class="noir-figure">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-noir-lightbox-trigger
|
|
||||||
data-src={ item.Image }
|
|
||||||
data-alt={ item.Caption }
|
|
||||||
data-caption={ item.Caption }
|
|
||||||
class="block w-full p-0 m-0 cursor-zoom-in bg-transparent border-0"
|
|
||||||
style="background: transparent;"
|
|
||||||
aria-label={ lightboxAriaLabel(i, item.Caption) }
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={ item.Image }
|
|
||||||
alt={ item.Caption }
|
|
||||||
loading="lazy"
|
|
||||||
class="w-full h-auto block"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
if item.Caption != "" {
|
|
||||||
<figcaption class="tracked-mono mt-2" style="color: hsl(var(--mutedForeground));">
|
|
||||||
{ item.Caption }
|
|
||||||
</figcaption>
|
|
||||||
}
|
|
||||||
</figure>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
// lightboxGridCols returns Tailwind grid-template-columns utility for the requested columns.
|
|
||||||
func lightboxGridCols(cols int) string {
|
|
||||||
switch cols {
|
|
||||||
case 2:
|
|
||||||
return "grid-cols-1 md:grid-cols-2"
|
|
||||||
case 4:
|
|
||||||
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
|
|
||||||
default: // 3
|
|
||||||
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lightboxAriaLabel builds an accessible button label for each gallery entry.
|
|
||||||
func lightboxAriaLabel(index int, caption string) string {
|
|
||||||
if caption != "" {
|
|
||||||
return "Open photograph: " + caption
|
|
||||||
}
|
|
||||||
return "Open photograph " + intToString(index+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// intToString avoids importing strconv into a templ-generated file.
|
|
||||||
func intToString(n int) string {
|
|
||||||
if n == 0 {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
neg := false
|
|
||||||
if n < 0 {
|
|
||||||
neg = true
|
|
||||||
n = -n
|
|
||||||
}
|
|
||||||
var b [20]byte
|
|
||||||
i := len(b)
|
|
||||||
for n > 0 {
|
|
||||||
i--
|
|
||||||
b[i] = byte('0' + n%10)
|
|
||||||
n /= 10
|
|
||||||
}
|
|
||||||
if neg {
|
|
||||||
i--
|
|
||||||
b[i] = '-'
|
|
||||||
}
|
|
||||||
return string(b[i:])
|
|
||||||
}
|
|
||||||
@ -1,229 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// lightboxGalleryComponent renders the gallery grid with lightbox triggers.
|
|
||||||
func lightboxGalleryComponent(items []LightboxItem, cols int) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"noir:lightbox_gallery\" class=\"py-12\"><div class=\"max-w-6xl mx-auto px-6\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if len(items) == 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"hairline p-12 text-center tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">Add photographs to populate the gallery.</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var templ_7745c5c3_Var2 = []any{"grid gap-4", lightboxGridCols(cols)}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for i, item := range items {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figure class=\"noir-figure\"><button type=\"button\" data-noir-lightbox-trigger data-src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Image)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 18, Col: 29}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" data-alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var5 string
|
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 19, Col: 31}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" data-caption=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var6 string
|
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 20, Col: 35}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"block w-full p-0 m-0 cursor-zoom-in bg-transparent border-0\" style=\"background: transparent;\" aria-label=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var7 string
|
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(lightboxAriaLabel(i, item.Caption))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 23, Col: 55}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var8 string
|
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Image)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 26, Col: 25}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var9 string
|
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 27, Col: 27}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" loading=\"lazy\" class=\"w-full h-auto block\"></button> ")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if item.Caption != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<figcaption class=\"tracked-mono mt-2\" style=\"color: hsl(var(--mutedForeground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var10 string
|
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `lightbox_gallery.templ`, Line: 34, Col: 23}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</figcaption>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</figure>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></section>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// lightboxGridCols returns Tailwind grid-template-columns utility for the requested columns.
|
|
||||||
func lightboxGridCols(cols int) string {
|
|
||||||
switch cols {
|
|
||||||
case 2:
|
|
||||||
return "grid-cols-1 md:grid-cols-2"
|
|
||||||
case 4:
|
|
||||||
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
|
|
||||||
default: // 3
|
|
||||||
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lightboxAriaLabel builds an accessible button label for each gallery entry.
|
|
||||||
func lightboxAriaLabel(index int, caption string) string {
|
|
||||||
if caption != "" {
|
|
||||||
return "Open photograph: " + caption
|
|
||||||
}
|
|
||||||
return "Open photograph " + intToString(index+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// intToString avoids importing strconv into a templ-generated file.
|
|
||||||
func intToString(n int) string {
|
|
||||||
if n == 0 {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
neg := false
|
|
||||||
if n < 0 {
|
|
||||||
neg = true
|
|
||||||
n = -n
|
|
||||||
}
|
|
||||||
var b [20]byte
|
|
||||||
i := len(b)
|
|
||||||
for n > 0 {
|
|
||||||
i--
|
|
||||||
b[i] = byte('0' + n%10)
|
|
||||||
n /= 10
|
|
||||||
}
|
|
||||||
if neg {
|
|
||||||
i--
|
|
||||||
b[i] = '-'
|
|
||||||
}
|
|
||||||
return string(b[i:])
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
17
main.go
17
main.go
@ -1,17 +0,0 @@
|
|||||||
//go:build wasip1
|
|
||||||
|
|
||||||
// Package main compiles the noir theme to a wasip1 reactor-mode wasm guest
|
|
||||||
// (WO-WZ-015). The .so build path is retired; `ninja plugin build` produces the
|
|
||||||
// .bnp artifact the registry serves.
|
|
||||||
//
|
|
||||||
// Reactor mode: the host runs `_initialize` once per pooled instance, which
|
|
||||||
// runs this init func (hence wasmguest.Serve) before any bn_invoke. Serve is
|
|
||||||
// non-blocking; all theme work then arrives over the ABI hook calls. main is
|
|
||||||
// never called.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "git.dev.alexdunmow.com/block/core/plugin/wasmguest"
|
|
||||||
|
|
||||||
func init() { wasmguest.Serve(Registration) }
|
|
||||||
|
|
||||||
func main() {} // never called — reactor mode
|
|
||||||
279
manifest.yaml
Normal file
279
manifest.yaml
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
# Noir theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
|
||||||
|
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||||
|
theme_presets: presets.json
|
||||||
|
bundled_fonts: fonts.json
|
||||||
|
master_pages: master_pages.json
|
||||||
|
|
||||||
|
system_templates:
|
||||||
|
- key: noir
|
||||||
|
title: Noir
|
||||||
|
description: "Silver-on-black photography theme with full-bleed imagery, micro mono labels, lightbox galleries and contact-sheet layouts."
|
||||||
|
|
||||||
|
page_templates:
|
||||||
|
- system: noir
|
||||||
|
key: default
|
||||||
|
title: Default
|
||||||
|
description: "Centred gallery page with a thin masthead and dissolved footer."
|
||||||
|
slots: [header, main, footer]
|
||||||
|
- system: noir
|
||||||
|
key: landing
|
||||||
|
title: Landing
|
||||||
|
description: "Edge-to-edge hero image, micro caption strip, minimal CTA."
|
||||||
|
slots: [hero, main, cta, footer]
|
||||||
|
- system: noir
|
||||||
|
key: article
|
||||||
|
title: Article / Project
|
||||||
|
description: "Long-form case study with sticky caption rail and image-led prose."
|
||||||
|
slots: [header, main, aside, footer]
|
||||||
|
- system: noir
|
||||||
|
key: full-width
|
||||||
|
title: Full Width
|
||||||
|
description: "Contact-sheet or lightbox grid, no horizontal padding."
|
||||||
|
slots: [header, main, footer]
|
||||||
|
|
||||||
|
template_overrides:
|
||||||
|
- { template: noir, block: heading }
|
||||||
|
- { template: noir, block: text }
|
||||||
|
- { template: noir, block: image }
|
||||||
|
- { template: noir, block: button }
|
||||||
|
- { template: noir, block: card }
|
||||||
|
|
||||||
|
email_wrappers:
|
||||||
|
- noir
|
||||||
|
|
||||||
|
css:
|
||||||
|
input_css_append: |
|
||||||
|
/* ============================================================
|
||||||
|
Noir Theme — Silver-on-black photography
|
||||||
|
Custom utility CSS injected via CSSManifest.InputCSSAppend.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* --- Typography slots ---------------------------------------- */
|
||||||
|
/* Body & page typography flow through the theme font variables.
|
||||||
|
Fallback stacks reflect the spec's intended faces (Tenor Sans,
|
||||||
|
Inter, JetBrains Mono) without hardcoding the family name. */
|
||||||
|
|
||||||
|
.noir-page {
|
||||||
|
font-family: var(--font-body, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-display {
|
||||||
|
font-family: var(--font-heading, "Tenor Sans", Georgia, serif);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- .tracked-mono ------------------------------------------- */
|
||||||
|
/* 10px JetBrains Mono, +60 tracking equivalent (0.18em), uppercase.
|
||||||
|
Used for captions, frame numbers, and the caption strip. */
|
||||||
|
|
||||||
|
.tracked-mono {
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", "Consolas", "SFMono-Regular", monospace);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracked-mono-sm {
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", "Consolas", "SFMono-Regular", monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- .bleed -------------------------------------------------- */
|
||||||
|
/* Edge-to-edge utility — breaks out of a max-width container. */
|
||||||
|
|
||||||
|
.bleed {
|
||||||
|
width: 100vw;
|
||||||
|
margin-left: calc((100vw - 100%) / 2 * -1);
|
||||||
|
margin-right: calc((100vw - 100%) / 2 * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- .hairline ----------------------------------------------- */
|
||||||
|
/* 1px almost-invisible border using the border token at 40% alpha. */
|
||||||
|
|
||||||
|
.hairline {
|
||||||
|
border: 1px solid hsl(var(--border) / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hairline-t { border-top: 1px solid hsl(var(--border) / 0.4); }
|
||||||
|
.hairline-b { border-bottom: 1px solid hsl(var(--border) / 0.4); }
|
||||||
|
|
||||||
|
/* --- Surface tokens used by overrides ------------------------ */
|
||||||
|
|
||||||
|
.noir-surface {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-card {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid hsl(var(--border) / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons render as hairline outlines that invert on hover.
|
||||||
|
Override block uses these classes directly. */
|
||||||
|
|
||||||
|
.noir-btn {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: transparent;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
border: 1px solid hsl(var(--foreground));
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", "Consolas", "SFMono-Regular", monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 0.18s ease, color 0.18s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-btn:hover,
|
||||||
|
.noir-btn:focus-visible {
|
||||||
|
background-color: hsl(var(--primary));
|
||||||
|
color: hsl(var(--primary-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-btn:focus-visible {
|
||||||
|
outline: 2px solid hsl(var(--ring));
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Caption strip ------------------------------------------- */
|
||||||
|
|
||||||
|
.noir-caption-strip {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
border-top: 1px solid hsl(var(--border) / 0.4);
|
||||||
|
border-bottom: 1px solid hsl(var(--border) / 0.4);
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--mutedForeground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Contact sheet sprocket motif ---------------------------- */
|
||||||
|
|
||||||
|
.noir-sprocket {
|
||||||
|
position: relative;
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border) / 0.4);
|
||||||
|
padding: 1.25rem 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-sprocket::before,
|
||||||
|
.noir-sprocket::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 0.5rem;
|
||||||
|
/* Sprocket holes drawn as repeating radial-gradient circles
|
||||||
|
using the muted foreground token at low alpha. */
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
hsl(var(--mutedForeground) / 0.85) 0,
|
||||||
|
hsl(var(--mutedForeground) / 0.85) 25%,
|
||||||
|
transparent 27%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
background-size: 1rem 0.5rem;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-position: 0 50%;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-sprocket::before { top: 0; }
|
||||||
|
.noir-sprocket::after { bottom: 0; }
|
||||||
|
|
||||||
|
.noir-frame-number {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.4rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Lightbox overlay ---------------------------------------- */
|
||||||
|
|
||||||
|
[data-noir-lightbox] {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: none;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem;
|
||||||
|
animation: noirFadeIn 180ms ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-noir-lightbox][aria-hidden="false"] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-noir-lightbox] img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-noir-lightbox] .noir-lightbox-caption {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: hsl(var(--mutedForeground));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-noir-lightbox] .noir-lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes noirFadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Sticky case-study meta rail ----------------------------- */
|
||||||
|
|
||||||
|
.noir-meta-rail {
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
|
align-self: start;
|
||||||
|
color: hsl(var(--mutedForeground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Image full-bleed override ------------------------------- */
|
||||||
|
|
||||||
|
.noir-figure {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-figure img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noir-figure figcaption {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: hsl(var(--mutedForeground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Reduced motion ------------------------------------------ */
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
[data-noir-lightbox] { animation: none; }
|
||||||
|
}
|
||||||
24
master_pages.json
Normal file
24
master_pages.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "noir:default-master",
|
||||||
|
"title": "Noir Default Master",
|
||||||
|
"page_templates": ["default", "article"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "navbar", "title": "Masthead", "content": { "menuName": "main", "variant": "minimal" }, "slot": "header", "sort_order": 0 },
|
||||||
|
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main", "placeholder": "Photographs go here" }, "slot": "main", "sort_order": 0 },
|
||||||
|
{ "block_key": "noir:caption_strip", "title": "Caption Strip", "content": { "label": "INDEX", "right": "© studio" }, "slot": "footer", "sort_order": 0 },
|
||||||
|
{ "block_key": "noir:footer", "title": "Footer", "content": { "showColophon": "true" }, "slot": "footer", "sort_order": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "noir:gallery-master",
|
||||||
|
"title": "Noir Gallery Master",
|
||||||
|
"page_templates": ["landing", "full-width"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "navbar", "title": "Dissolved Masthead", "content": { "menuName": "main", "variant": "dissolved" }, "slot": "header", "sort_order": 0 },
|
||||||
|
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main", "placeholder": "Photographs go here" }, "slot": "main", "sort_order": 0 },
|
||||||
|
{ "block_key": "noir:contact_sheet_footer", "title": "Contact Sheet Footer", "content": { "items": [] }, "slot": "footer", "sort_order": 0 },
|
||||||
|
{ "block_key": "noir:footer", "title": "Footer", "content": { "showColophon": "true" }, "slot": "footer", "sort_order": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
99
overrides.go
99
overrides.go
@ -1,99 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NoirHeadingBlock overrides the built-in heading.
|
|
||||||
// Renders with the display-serif family, no underline, generous tracking.
|
|
||||||
func NoirHeadingBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
text := getString(content, "text")
|
|
||||||
textClass := getString(content, "textClass")
|
|
||||||
level := parseHeadingLevel(content)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirHeadingComponent(level, text, textClass).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseHeadingLevel parses the heading level from content (1-6, default 2).
|
|
||||||
func parseHeadingLevel(content map[string]any) int {
|
|
||||||
if level, ok := content["level"].(float64); ok {
|
|
||||||
l := int(level)
|
|
||||||
if l >= 1 && l <= 6 {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if level, ok := content["level"].(int); ok {
|
|
||||||
if level >= 1 && level <= 6 {
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if level, ok := content["level"].(string); ok {
|
|
||||||
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirTextBlock overrides the built-in text block.
|
|
||||||
// Renders with the humanist sans family and generous leading.
|
|
||||||
func NoirTextBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
text := getString(content, "text")
|
|
||||||
class := getString(content, "class")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirTextComponent(text, class).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirImageBlock overrides the built-in image block.
|
|
||||||
// Always full-bleed inside its container with a mono caption beneath.
|
|
||||||
func NoirImageBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
src := blocks.ResolveMediaPath(getString(content, "src"))
|
|
||||||
alt := getString(content, "alt")
|
|
||||||
caption := getString(content, "caption")
|
|
||||||
if caption == "" {
|
|
||||||
caption = getString(content, "title")
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirImageComponent(src, alt, caption).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirButtonBlock overrides the built-in button.
|
|
||||||
// Renders as a hairline 1px outline, no fill, hover inverts.
|
|
||||||
func NoirButtonBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
text := getString(content, "text")
|
|
||||||
if text == "" {
|
|
||||||
text = getString(content, "label")
|
|
||||||
}
|
|
||||||
href := getString(content, "url")
|
|
||||||
if href == "" {
|
|
||||||
href = getString(content, "href")
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirButtonComponent(text, href).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirCardBlock overrides the built-in card.
|
|
||||||
// Transparent background, hairline border only.
|
|
||||||
func NoirCardBlock(ctx context.Context, content map[string]any) string {
|
|
||||||
title := getString(content, "title")
|
|
||||||
body := getString(content, "body")
|
|
||||||
if body == "" {
|
|
||||||
body = getString(content, "text")
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_ = noirCardComponent(title, body).Render(ctx, &buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// noirHeadingComponent renders a heading using the display-serif font slot.
|
|
||||||
templ noirHeadingComponent(level int, text, textClass string) {
|
|
||||||
switch level {
|
|
||||||
case 1:
|
|
||||||
<h1 class={ "noir-display", textClass } style="font-size: clamp(2.5rem, 5vw, 4rem); line-height: 1.05; letter-spacing: 0.005em; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h1>
|
|
||||||
case 2:
|
|
||||||
<h2 class={ "noir-display", textClass } style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h2>
|
|
||||||
case 3:
|
|
||||||
<h3 class={ "noir-display", textClass } style="font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h3>
|
|
||||||
case 4:
|
|
||||||
<h4 class={ "noir-display", textClass } style="font-size: 1.5rem; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h4>
|
|
||||||
case 5:
|
|
||||||
<h5 class={ "noir-display", textClass } style="font-size: 1.25rem; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h5>
|
|
||||||
case 6:
|
|
||||||
<h6 class={ "noir-display", textClass } style="font-size: 1.125rem; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h6>
|
|
||||||
default:
|
|
||||||
<h2 class={ "noir-display", textClass } style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1; color: hsl(var(--foreground));">
|
|
||||||
{ text }
|
|
||||||
</h2>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirTextComponent renders text with the humanist sans family and generous leading.
|
|
||||||
// The body family resolves through --font-body; the surrounding .noir-page already
|
|
||||||
// supplies the fallback stack from style.css, so we only need spacing here.
|
|
||||||
templ noirTextComponent(text, class string) {
|
|
||||||
<div class={ "noir-text", class } style="line-height: 1.7; color: hsl(var(--foreground)); max-width: 65ch;">
|
|
||||||
@templ.Raw(text)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirImageComponent renders an image full-bleed within its container with a mono caption.
|
|
||||||
templ noirImageComponent(src, alt, caption string) {
|
|
||||||
<figure class="noir-figure bleed">
|
|
||||||
if src != "" {
|
|
||||||
<img src={ src } alt={ alt } loading="lazy" class="w-full h-auto block"/>
|
|
||||||
}
|
|
||||||
if caption != "" {
|
|
||||||
<figcaption class="tracked-mono mt-2 px-6" style="color: hsl(var(--mutedForeground));">
|
|
||||||
{ caption }
|
|
||||||
</figcaption>
|
|
||||||
}
|
|
||||||
</figure>
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirButtonComponent renders the hairline outline button.
|
|
||||||
templ noirButtonComponent(text, href string) {
|
|
||||||
if href != "" {
|
|
||||||
<a href={ templ.SafeURL(href) } class="noir-btn">
|
|
||||||
{ text }
|
|
||||||
</a>
|
|
||||||
} else {
|
|
||||||
<button type="button" class="noir-btn">
|
|
||||||
{ text }
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirCardComponent renders the transparent hairline-bordered card.
|
|
||||||
templ noirCardComponent(title, body string) {
|
|
||||||
<div class="noir-card" style="padding: 1.5rem;">
|
|
||||||
if title != "" {
|
|
||||||
<h3 class="noir-display" style="font-size: 1.5rem; margin-bottom: 0.75rem; color: hsl(var(--foreground));">
|
|
||||||
{ title }
|
|
||||||
</h3>
|
|
||||||
}
|
|
||||||
if body != "" {
|
|
||||||
<div style="color: hsl(var(--foreground)); line-height: 1.6;">
|
|
||||||
@templ.Raw(body)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@ -1,577 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
// noirHeadingComponent renders a heading using the display-serif font slot.
|
|
||||||
func noirHeadingComponent(level int, text, textClass string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
switch level {
|
|
||||||
case 1:
|
|
||||||
var templ_7745c5c3_Var2 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"font-size: clamp(2.5rem, 5vw, 4rem); line-height: 1.05; letter-spacing: 0.005em; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 8, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
case 2:
|
|
||||||
var templ_7745c5c3_Var5 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var6 string
|
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var7 string
|
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 12, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
var templ_7745c5c3_Var8 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var9 string
|
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var10 string
|
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 16, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
var templ_7745c5c3_Var11 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h4 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var12 string
|
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"font-size: 1.5rem; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var13 string
|
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 20, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h4>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
case 5:
|
|
||||||
var templ_7745c5c3_Var14 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h5 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var15 string
|
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"font-size: 1.25rem; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var16 string
|
|
||||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 24, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h5>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
case 6:
|
|
||||||
var templ_7745c5c3_Var17 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h6 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var18 string
|
|
||||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" style=\"font-size: 1.125rem; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var19 string
|
|
||||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 28, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h6>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var templ_7745c5c3_Var20 = []any{"noir-display", textClass}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h2 class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var21 string
|
|
||||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var22 string
|
|
||||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 32, Col: 10}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h2>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirTextComponent renders text with the humanist sans family and generous leading.
|
|
||||||
// The body family resolves through --font-body; the surrounding .noir-page already
|
|
||||||
// supplies the fallback stack from style.css, so we only need spacing here.
|
|
||||||
func noirTextComponent(text, class string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var23 == nil {
|
|
||||||
templ_7745c5c3_Var23 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
var templ_7745c5c3_Var24 = []any{"noir-text", class}
|
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var25 string
|
|
||||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var24).String())
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 1, Col: 0}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" style=\"line-height: 1.7; color: hsl(var(--foreground)); max-width: 65ch;\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirImageComponent renders an image full-bleed within its container with a mono caption.
|
|
||||||
func noirImageComponent(src, alt, caption string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var26 == nil {
|
|
||||||
templ_7745c5c3_Var26 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<figure class=\"noir-figure bleed\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if src != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<img src=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var27 string
|
|
||||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 50, Col: 17}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" alt=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var28 string
|
|
||||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 50, Col: 29}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" loading=\"lazy\" class=\"w-full h-auto block\"> ")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if caption != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<figcaption class=\"tracked-mono mt-2 px-6\" style=\"color: hsl(var(--mutedForeground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var29 string
|
|
||||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(caption)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 54, Col: 13}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</figcaption>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</figure>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirButtonComponent renders the hairline outline button.
|
|
||||||
func noirButtonComponent(text, href string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var30 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var30 == nil {
|
|
||||||
templ_7745c5c3_Var30 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
if href != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a href=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var31 templ.SafeURL
|
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 63, Col: 31}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"noir-btn\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var32 string
|
|
||||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 64, Col: 9}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<button type=\"button\" class=\"noir-btn\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var33 string
|
|
||||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 68, Col: 9}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</button>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirCardComponent renders the transparent hairline-bordered card.
|
|
||||||
func noirCardComponent(title, body string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var34 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var34 == nil {
|
|
||||||
templ_7745c5c3_Var34 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"noir-card\" style=\"padding: 1.5rem;\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if title != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<h3 class=\"noir-display\" style=\"font-size: 1.5rem; margin-bottom: 0.75rem; color: hsl(var(--foreground));\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var35 string
|
|
||||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `overrides.templ`, Line: 78, Col: 11}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h3>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if body != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<div style=\"color: hsl(var(--foreground)); line-height: 1.6;\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@ -2,11 +2,8 @@
|
|||||||
name = "noir"
|
name = "noir"
|
||||||
display_name = "Noir"
|
display_name = "Noir"
|
||||||
scope = "@themes"
|
scope = "@themes"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
description = "Silver-on-black photography theme with full-bleed imagery, micro mono labels, lightbox galleries and contact-sheet layouts."
|
description = "Silver-on-black photography theme with full-bleed imagery, micro mono labels, lightbox galleries and contact-sheet layouts."
|
||||||
kind = "theme"
|
kind = "theme"
|
||||||
categories = ["templates"]
|
categories = ["templates"]
|
||||||
tags = ["noir", "monochrome", "photography", "portfolio", "gallery", "filmmaker", "fashion", "darkroom"]
|
tags = ["noir", "monochrome", "photography", "portfolio", "gallery", "filmmaker", "fashion", "darkroom"]
|
||||||
|
|
||||||
[compatibility]
|
|
||||||
block_core = ">=0.15.0 <0.16.0"
|
|
||||||
|
|||||||
176
register.go
176
register.go
@ -1,176 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
"git.dev.alexdunmow.com/block/core/plugin"
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
// wrap adapts a templ-returning render function to templates.TemplateFunc.
|
|
||||||
// templ.Component already implements templates.HTMLComponent via Render.
|
|
||||||
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
|
|
||||||
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
|
|
||||||
return f(ctx, doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register wires the Noir system template, page templates, blocks, overrides,
|
|
||||||
// and email wrapper into the host registries.
|
|
||||||
//
|
|
||||||
// Order matters: br.LoadSchemasFromFS(Schemas()) is called BEFORE any
|
|
||||||
// br.Register call so the schema metadata binds correctly.
|
|
||||||
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
|
||||||
// System template.
|
|
||||||
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
|
|
||||||
Key: "noir",
|
|
||||||
Title: "Noir",
|
|
||||||
Description: "Silver-on-black photography theme with full-bleed imagery, micro mono labels, lightbox galleries and contact-sheet layouts.",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Page templates (4).
|
|
||||||
if err := tr.RegisterPageTemplate("noir", templates.PageTemplateMeta{
|
|
||||||
Key: "default",
|
|
||||||
Title: "Default",
|
|
||||||
Description: "Centred gallery page with a thin masthead and dissolved footer.",
|
|
||||||
Slots: []string{"header", "main", "footer"},
|
|
||||||
}, wrap(RenderNoirDefault)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tr.RegisterPageTemplate("noir", templates.PageTemplateMeta{
|
|
||||||
Key: "landing",
|
|
||||||
Title: "Landing",
|
|
||||||
Description: "Edge-to-edge hero image, micro caption strip, minimal CTA.",
|
|
||||||
Slots: []string{"hero", "main", "cta", "footer"},
|
|
||||||
}, wrap(RenderNoirLanding)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tr.RegisterPageTemplate("noir", templates.PageTemplateMeta{
|
|
||||||
Key: "article",
|
|
||||||
Title: "Article / Project",
|
|
||||||
Description: "Long-form case study with sticky caption rail and image-led prose.",
|
|
||||||
Slots: []string{"header", "main", "aside", "footer"},
|
|
||||||
}, wrap(RenderNoirArticle)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tr.RegisterPageTemplate("noir", templates.PageTemplateMeta{
|
|
||||||
Key: "full-width",
|
|
||||||
Title: "Full Width",
|
|
||||||
Description: "Contact-sheet or lightbox grid, no horizontal padding.",
|
|
||||||
Slots: []string{"header", "main", "footer"},
|
|
||||||
}, wrap(RenderNoirFullWidth)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schemas must load BEFORE block registration so metadata binds.
|
|
||||||
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme-specific blocks (6). Registered with unqualified keys —
|
|
||||||
// addressed as "noir:<key>" at runtime.
|
|
||||||
br.Register(LightboxGalleryBlockMeta, LightboxGalleryBlock)
|
|
||||||
br.Register(ContactSheetBlockMeta, ContactSheetBlock)
|
|
||||||
br.Register(CaseStudyBlockMeta, CaseStudyBlock)
|
|
||||||
br.Register(CaptionStripBlockMeta, CaptionStripBlock)
|
|
||||||
br.Register(ImagePairBlockMeta, ImagePairBlock)
|
|
||||||
br.Register(FooterBlockMeta, FooterBlock)
|
|
||||||
|
|
||||||
// Template overrides (5) — active only when this theme is selected.
|
|
||||||
br.RegisterTemplateOverride("noir", "heading", NoirHeadingBlock)
|
|
||||||
br.RegisterTemplateOverride("noir", "text", NoirTextBlock)
|
|
||||||
br.RegisterTemplateOverride("noir", "image", NoirImageBlock)
|
|
||||||
br.RegisterTemplateOverride("noir", "button", NoirButtonBlock)
|
|
||||||
br.RegisterTemplateOverride("noir", "card", NoirCardBlock)
|
|
||||||
|
|
||||||
// Branded email wrapper.
|
|
||||||
tr.RegisterEmailWrapper("noir", NoirEmailWrapper)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultMasterPages returns the two master pages Noir provisions on first load.
|
|
||||||
//
|
|
||||||
// noir:default-master covers the `default` and `article` page templates.
|
|
||||||
// noir:gallery-master covers `landing` and `full-width`, swapping the
|
|
||||||
// caption strip for a contact-sheet-style footer and using the dissolved navbar.
|
|
||||||
func DefaultMasterPages() []plugin.MasterPageDefinition {
|
|
||||||
return []plugin.MasterPageDefinition{
|
|
||||||
{
|
|
||||||
Key: "noir:default-master",
|
|
||||||
Title: "Noir Default Master",
|
|
||||||
PageTemplates: []string{"default", "article"},
|
|
||||||
Blocks: []plugin.MasterPageBlock{
|
|
||||||
{
|
|
||||||
BlockKey: "navbar",
|
|
||||||
Title: "Masthead",
|
|
||||||
Content: map[string]any{"menuName": "main", "variant": "minimal"},
|
|
||||||
Slot: "header",
|
|
||||||
SortOrder: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlockKey: "slot",
|
|
||||||
Title: "Main Slot",
|
|
||||||
Content: map[string]any{"slotName": "main", "placeholder": "Photographs go here"},
|
|
||||||
Slot: "main",
|
|
||||||
SortOrder: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlockKey: "noir:caption_strip",
|
|
||||||
Title: "Caption Strip",
|
|
||||||
Content: map[string]any{"label": "INDEX", "right": "© studio"},
|
|
||||||
Slot: "footer",
|
|
||||||
SortOrder: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlockKey: "noir:footer",
|
|
||||||
Title: "Footer",
|
|
||||||
Content: map[string]any{"showColophon": "true"},
|
|
||||||
Slot: "footer",
|
|
||||||
SortOrder: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "noir:gallery-master",
|
|
||||||
Title: "Noir Gallery Master",
|
|
||||||
PageTemplates: []string{"landing", "full-width"},
|
|
||||||
Blocks: []plugin.MasterPageBlock{
|
|
||||||
{
|
|
||||||
BlockKey: "navbar",
|
|
||||||
Title: "Dissolved Masthead",
|
|
||||||
Content: map[string]any{"menuName": "main", "variant": "dissolved"},
|
|
||||||
Slot: "header",
|
|
||||||
SortOrder: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlockKey: "slot",
|
|
||||||
Title: "Main Slot",
|
|
||||||
Content: map[string]any{"slotName": "main", "placeholder": "Photographs go here"},
|
|
||||||
Slot: "main",
|
|
||||||
SortOrder: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlockKey: "noir:contact_sheet_footer",
|
|
||||||
Title: "Contact Sheet Footer",
|
|
||||||
Content: map[string]any{"items": []any{}},
|
|
||||||
Slot: "footer",
|
|
||||||
SortOrder: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlockKey: "noir:footer",
|
|
||||||
Title: "Footer",
|
|
||||||
Content: map[string]any{"showColophon": "true"},
|
|
||||||
Slot: "footer",
|
|
||||||
SortOrder: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
"git.dev.alexdunmow.com/block/core/plugin"
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Registration is the compile-time plugin registration for the Noir theme.
|
|
||||||
var Registration = plugin.PluginRegistration{
|
|
||||||
Name: "noir",
|
|
||||||
Version: plugin.ParseModVersion(pluginModBytes),
|
|
||||||
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
|
||||||
return Register(tr, br)
|
|
||||||
},
|
|
||||||
Assets: func() http.Handler { return AssetsHandler() },
|
|
||||||
Schemas: func() fs.FS { return Schemas() },
|
|
||||||
ThemePresets: func() []byte { return ThemePresets() },
|
|
||||||
BundledFonts: func() []byte { return BundledFonts() },
|
|
||||||
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
|
|
||||||
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
|
|
||||||
}
|
|
||||||
288
template.templ
288
template.templ
@ -1,288 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NoirPageData holds the parsed view-model for every Noir page template.
|
|
||||||
type NoirPageData 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNoirPageData(doc map[string]any) NoirPageData {
|
|
||||||
title := "Untitled"
|
|
||||||
if t, ok := doc["title"].(string); ok {
|
|
||||||
title = t
|
|
||||||
}
|
|
||||||
|
|
||||||
slots := make(map[string]string)
|
|
||||||
if s, ok := doc["slots"].(map[string]string); ok {
|
|
||||||
slots = s
|
|
||||||
}
|
|
||||||
|
|
||||||
themeCSS := ""
|
|
||||||
if tc, ok := doc["theme_css"].(string); ok {
|
|
||||||
themeCSS = tc
|
|
||||||
}
|
|
||||||
|
|
||||||
structuredData := ""
|
|
||||||
if sd, ok := doc["structured_data"].(string); ok {
|
|
||||||
structuredData = sd
|
|
||||||
}
|
|
||||||
|
|
||||||
cssHash := ""
|
|
||||||
if ch, ok := doc["css_hash"].(string); ok {
|
|
||||||
cssHash = ch
|
|
||||||
}
|
|
||||||
|
|
||||||
pageviewNonce := ""
|
|
||||||
if pn, ok := doc["pageview_nonce"].(string); ok {
|
|
||||||
pageviewNonce = pn
|
|
||||||
}
|
|
||||||
|
|
||||||
themeMode := "dark"
|
|
||||||
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
|
||||||
themeMode = tm
|
|
||||||
}
|
|
||||||
|
|
||||||
siteSettings := bn.ParseSiteSettings(doc)
|
|
||||||
pageMeta := bn.ParsePageMeta(doc)
|
|
||||||
engagementConfig := bn.ParseEngagementConfig(doc)
|
|
||||||
|
|
||||||
return NoirPageData{
|
|
||||||
Title: title,
|
|
||||||
Slots: slots,
|
|
||||||
ThemeMode: themeMode,
|
|
||||||
ThemeCSS: themeCSS,
|
|
||||||
SiteSettings: siteSettings,
|
|
||||||
PageMeta: pageMeta,
|
|
||||||
StructuredData: structuredData,
|
|
||||||
CSSHash: cssHash,
|
|
||||||
PageviewNonce: pageviewNonce,
|
|
||||||
EngagementConfig: engagementConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirHead emits the shared <head> block plus the lightbox-bootstrap script.
|
|
||||||
templ noirHead(data NoirPageData) {
|
|
||||||
@bn.Head(bn.HeadData{
|
|
||||||
Title: data.Title,
|
|
||||||
Settings: data.SiteSettings,
|
|
||||||
PageMeta: data.PageMeta,
|
|
||||||
ThemeMode: data.ThemeMode,
|
|
||||||
ThemeCSS: data.ThemeCSS,
|
|
||||||
PluginStyles: []string{"/templates/noir/style.css"},
|
|
||||||
StructuredData: data.StructuredData,
|
|
||||||
CSSHash: data.CSSHash,
|
|
||||||
PageviewNonce: data.PageviewNonce,
|
|
||||||
EngagementConfig: data.EngagementConfig,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirLightboxScript injects the vanilla keyboard-aware lightbox handler.
|
|
||||||
// Stays tiny (no deps) and uses the data attributes set by lightbox_gallery.
|
|
||||||
templ noirLightboxScript() {
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
if (window.__noirLightboxInit) return;
|
|
||||||
window.__noirLightboxInit = true;
|
|
||||||
var overlay = null;
|
|
||||||
function ensureOverlay() {
|
|
||||||
if (overlay) return overlay;
|
|
||||||
overlay = document.createElement('div');
|
|
||||||
overlay.setAttribute('data-noir-lightbox', '');
|
|
||||||
overlay.setAttribute('aria-hidden', 'true');
|
|
||||||
overlay.setAttribute('role', 'dialog');
|
|
||||||
overlay.setAttribute('aria-modal', 'true');
|
|
||||||
overlay.innerHTML = '<button type="button" class="noir-lightbox-close tracked-mono" aria-label="Close">Close</button><img alt="" /><div class="noir-lightbox-caption tracked-mono"></div>';
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });
|
|
||||||
overlay.querySelector('.noir-lightbox-close').addEventListener('click', close);
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (overlay.getAttribute('aria-hidden') === 'false' && e.key === 'Escape') close();
|
|
||||||
});
|
|
||||||
return overlay;
|
|
||||||
}
|
|
||||||
function open(src, alt, caption) {
|
|
||||||
var o = ensureOverlay();
|
|
||||||
var img = o.querySelector('img');
|
|
||||||
img.src = src;
|
|
||||||
img.alt = alt || '';
|
|
||||||
o.querySelector('.noir-lightbox-caption').textContent = caption || '';
|
|
||||||
o.setAttribute('aria-hidden', 'false');
|
|
||||||
o.querySelector('.noir-lightbox-close').focus();
|
|
||||||
}
|
|
||||||
function close() {
|
|
||||||
if (!overlay) return;
|
|
||||||
overlay.setAttribute('aria-hidden', 'true');
|
|
||||||
}
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
var t = e.target.closest('[data-noir-lightbox-trigger]');
|
|
||||||
if (!t) return;
|
|
||||||
e.preventDefault();
|
|
||||||
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
|
||||||
});
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
||||||
var t = document.activeElement;
|
|
||||||
if (t && t.matches && t.matches('[data-noir-lightbox-trigger]')) {
|
|
||||||
e.preventDefault();
|
|
||||||
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirDefault — centred gallery page with thin masthead and dissolved footer.
|
|
||||||
templ NoirDefault(data NoirPageData) {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
@noirHead(data)
|
|
||||||
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="default">
|
|
||||||
@bn.AdminBypassBanner(data.SiteSettings)
|
|
||||||
<header class="w-full hairline-b">
|
|
||||||
<div class="max-w-5xl mx-auto px-6">
|
|
||||||
@templ.Raw(data.Slots["header"])
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="flex-grow max-w-5xl mx-auto w-full px-6 py-12">
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
@templ.Raw(main)
|
|
||||||
} else {
|
|
||||||
<div class="py-24 text-center">
|
|
||||||
<p class="tracked-mono" style="color: hsl(var(--mutedForeground));">No photographs assigned to this page.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">
|
|
||||||
@templ.Raw(data.Slots["footer"])
|
|
||||||
</footer>
|
|
||||||
@noirLightboxScript()
|
|
||||||
@bn.BodyEnd(data.SiteSettings)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirLanding — edge-to-edge hero, micro caption strip, minimal CTA.
|
|
||||||
templ NoirLanding(data NoirPageData) {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
@noirHead(data)
|
|
||||||
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="landing">
|
|
||||||
@bn.AdminBypassBanner(data.SiteSettings)
|
|
||||||
<section class="w-full">
|
|
||||||
@templ.Raw(data.Slots["hero"])
|
|
||||||
</section>
|
|
||||||
<main class="flex-grow">
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
<div class="max-w-5xl mx-auto px-6 py-16">
|
|
||||||
@templ.Raw(main)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</main>
|
|
||||||
<section class="w-full">
|
|
||||||
@templ.Raw(data.Slots["cta"])
|
|
||||||
</section>
|
|
||||||
<footer class="w-full mt-auto">
|
|
||||||
@templ.Raw(data.Slots["footer"])
|
|
||||||
</footer>
|
|
||||||
@noirLightboxScript()
|
|
||||||
@bn.BodyEnd(data.SiteSettings)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirArticle — long-form case study with sticky caption rail and image-led prose.
|
|
||||||
templ NoirArticle(data NoirPageData) {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
@noirHead(data)
|
|
||||||
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="article">
|
|
||||||
@bn.AdminBypassBanner(data.SiteSettings)
|
|
||||||
<header class="w-full hairline-b">
|
|
||||||
<div class="max-w-5xl mx-auto px-6">
|
|
||||||
@templ.Raw(data.Slots["header"])
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="flex-grow max-w-5xl mx-auto w-full px-6 py-12">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-[12rem_1fr] gap-8">
|
|
||||||
<aside class="noir-meta-rail">
|
|
||||||
@templ.Raw(data.Slots["aside"])
|
|
||||||
</aside>
|
|
||||||
<article class="min-w-0">
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
@templ.Raw(main)
|
|
||||||
} else {
|
|
||||||
<div class="py-24 text-center">
|
|
||||||
<p class="tracked-mono" style="color: hsl(var(--mutedForeground));">No content assigned to this article.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">
|
|
||||||
@templ.Raw(data.Slots["footer"])
|
|
||||||
</footer>
|
|
||||||
@noirLightboxScript()
|
|
||||||
@bn.BodyEnd(data.SiteSettings)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirFullWidth — contact-sheet or lightbox grid, no horizontal padding.
|
|
||||||
templ NoirFullWidth(data NoirPageData) {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
@noirHead(data)
|
|
||||||
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="full-width">
|
|
||||||
@bn.AdminBypassBanner(data.SiteSettings)
|
|
||||||
<header class="w-full hairline-b">
|
|
||||||
<div class="max-w-5xl mx-auto px-6">
|
|
||||||
@templ.Raw(data.Slots["header"])
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="flex-grow w-full">
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
@templ.Raw(main)
|
|
||||||
} else {
|
|
||||||
<div class="max-w-5xl mx-auto py-24 px-6 text-center">
|
|
||||||
<p class="tracked-mono" style="color: hsl(var(--mutedForeground));">No photographs assigned to this page.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">
|
|
||||||
@templ.Raw(data.Slots["footer"])
|
|
||||||
</footer>
|
|
||||||
@noirLightboxScript()
|
|
||||||
@bn.BodyEnd(data.SiteSettings)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirDefault(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirDefault(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirLanding(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirLanding(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirArticle(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirArticle(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirFullWidth(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
@ -1,548 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NoirPageData holds the parsed view-model for every Noir page template.
|
|
||||||
type NoirPageData 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNoirPageData(doc map[string]any) NoirPageData {
|
|
||||||
title := "Untitled"
|
|
||||||
if t, ok := doc["title"].(string); ok {
|
|
||||||
title = t
|
|
||||||
}
|
|
||||||
|
|
||||||
slots := make(map[string]string)
|
|
||||||
if s, ok := doc["slots"].(map[string]string); ok {
|
|
||||||
slots = s
|
|
||||||
}
|
|
||||||
|
|
||||||
themeCSS := ""
|
|
||||||
if tc, ok := doc["theme_css"].(string); ok {
|
|
||||||
themeCSS = tc
|
|
||||||
}
|
|
||||||
|
|
||||||
structuredData := ""
|
|
||||||
if sd, ok := doc["structured_data"].(string); ok {
|
|
||||||
structuredData = sd
|
|
||||||
}
|
|
||||||
|
|
||||||
cssHash := ""
|
|
||||||
if ch, ok := doc["css_hash"].(string); ok {
|
|
||||||
cssHash = ch
|
|
||||||
}
|
|
||||||
|
|
||||||
pageviewNonce := ""
|
|
||||||
if pn, ok := doc["pageview_nonce"].(string); ok {
|
|
||||||
pageviewNonce = pn
|
|
||||||
}
|
|
||||||
|
|
||||||
themeMode := "dark"
|
|
||||||
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
|
||||||
themeMode = tm
|
|
||||||
}
|
|
||||||
|
|
||||||
siteSettings := bn.ParseSiteSettings(doc)
|
|
||||||
pageMeta := bn.ParsePageMeta(doc)
|
|
||||||
engagementConfig := bn.ParseEngagementConfig(doc)
|
|
||||||
|
|
||||||
return NoirPageData{
|
|
||||||
Title: title,
|
|
||||||
Slots: slots,
|
|
||||||
ThemeMode: themeMode,
|
|
||||||
ThemeCSS: themeCSS,
|
|
||||||
SiteSettings: siteSettings,
|
|
||||||
PageMeta: pageMeta,
|
|
||||||
StructuredData: structuredData,
|
|
||||||
CSSHash: cssHash,
|
|
||||||
PageviewNonce: pageviewNonce,
|
|
||||||
EngagementConfig: engagementConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirHead emits the shared <head> block plus the lightbox-bootstrap script.
|
|
||||||
func noirHead(data NoirPageData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = bn.Head(bn.HeadData{
|
|
||||||
Title: data.Title,
|
|
||||||
Settings: data.SiteSettings,
|
|
||||||
PageMeta: data.PageMeta,
|
|
||||||
ThemeMode: data.ThemeMode,
|
|
||||||
ThemeCSS: data.ThemeCSS,
|
|
||||||
PluginStyles: []string{"/templates/noir/style.css"},
|
|
||||||
StructuredData: data.StructuredData,
|
|
||||||
CSSHash: data.CSSHash,
|
|
||||||
PageviewNonce: data.PageviewNonce,
|
|
||||||
EngagementConfig: data.EngagementConfig,
|
|
||||||
}).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// noirLightboxScript injects the vanilla keyboard-aware lightbox handler.
|
|
||||||
// Stays tiny (no deps) and uses the data attributes set by lightbox_gallery.
|
|
||||||
func noirLightboxScript() templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var2 == nil {
|
|
||||||
templ_7745c5c3_Var2 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script>\n\t\t(function() {\n\t\t if (window.__noirLightboxInit) return;\n\t\t window.__noirLightboxInit = true;\n\t\t var overlay = null;\n\t\t function ensureOverlay() {\n\t\t if (overlay) return overlay;\n\t\t overlay = document.createElement('div');\n\t\t overlay.setAttribute('data-noir-lightbox', '');\n\t\t overlay.setAttribute('aria-hidden', 'true');\n\t\t overlay.setAttribute('role', 'dialog');\n\t\t overlay.setAttribute('aria-modal', 'true');\n\t\t overlay.innerHTML = '<button type=\"button\" class=\"noir-lightbox-close tracked-mono\" aria-label=\"Close\">Close</button><img alt=\"\" /><div class=\"noir-lightbox-caption tracked-mono\"></div>';\n\t\t document.body.appendChild(overlay);\n\t\t overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });\n\t\t overlay.querySelector('.noir-lightbox-close').addEventListener('click', close);\n\t\t document.addEventListener('keydown', function(e) {\n\t\t if (overlay.getAttribute('aria-hidden') === 'false' && e.key === 'Escape') close();\n\t\t });\n\t\t return overlay;\n\t\t }\n\t\t function open(src, alt, caption) {\n\t\t var o = ensureOverlay();\n\t\t var img = o.querySelector('img');\n\t\t img.src = src;\n\t\t img.alt = alt || '';\n\t\t o.querySelector('.noir-lightbox-caption').textContent = caption || '';\n\t\t o.setAttribute('aria-hidden', 'false');\n\t\t o.querySelector('.noir-lightbox-close').focus();\n\t\t }\n\t\t function close() {\n\t\t if (!overlay) return;\n\t\t overlay.setAttribute('aria-hidden', 'true');\n\t\t }\n\t\t document.addEventListener('click', function(e) {\n\t\t var t = e.target.closest('[data-noir-lightbox-trigger]');\n\t\t if (!t) return;\n\t\t e.preventDefault();\n\t\t open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));\n\t\t });\n\t\t document.addEventListener('keydown', function(e) {\n\t\t if (e.key !== 'Enter' && e.key !== ' ') return;\n\t\t var t = document.activeElement;\n\t\t if (t && t.matches && t.matches('[data-noir-lightbox-trigger]')) {\n\t\t e.preventDefault();\n\t\t open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));\n\t\t }\n\t\t });\n\t\t})();\n\t</script>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirDefault — centred gallery page with thin masthead and dissolved footer.
|
|
||||||
func NoirDefault(data NoirPageData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var3 == nil {
|
|
||||||
templ_7745c5c3_Var3 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!doctype html><html lang=\"en\" class=\"dark\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirHead(data).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<body class=\"noir-page noir-surface antialiased min-h-screen flex flex-col\" data-noir-template=\"default\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<header class=\"w-full hairline-b\"><div class=\"max-w-5xl mx-auto px-6\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></header><main class=\"flex-grow max-w-5xl mx-auto w-full px-6 py-12\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"py-24 text-center\"><p class=\"tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">No photographs assigned to this page.</p></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</main><footer class=\"w-full mt-auto\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</footer>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirLightboxScript().Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirLanding — edge-to-edge hero, micro caption strip, minimal CTA.
|
|
||||||
func NoirLanding(data NoirPageData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var4 == nil {
|
|
||||||
templ_7745c5c3_Var4 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!doctype html><html lang=\"en\" class=\"dark\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirHead(data).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<body class=\"noir-page noir-surface antialiased min-h-screen flex flex-col\" data-noir-template=\"landing\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<section class=\"w-full\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</section><main class=\"flex-grow\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"max-w-5xl mx-auto px-6 py-16\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</main><section class=\"w-full\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</section><footer class=\"w-full mt-auto\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</footer>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirLightboxScript().Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirArticle — long-form case study with sticky caption rail and image-led prose.
|
|
||||||
func NoirArticle(data NoirPageData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var5 == nil {
|
|
||||||
templ_7745c5c3_Var5 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<!doctype html><html lang=\"en\" class=\"dark\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirHead(data).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<body class=\"noir-page noir-surface antialiased min-h-screen flex flex-col\" data-noir-template=\"article\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<header class=\"w-full hairline-b\"><div class=\"max-w-5xl mx-auto px-6\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div></header><main class=\"flex-grow max-w-5xl mx-auto w-full px-6 py-12\"><div class=\"grid grid-cols-1 md:grid-cols-[12rem_1fr] gap-8\"><aside class=\"noir-meta-rail\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["aside"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside><article class=\"min-w-0\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"py-24 text-center\"><p class=\"tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">No content assigned to this article.</p></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</article></div></main><footer class=\"w-full mt-auto\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</footer>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirLightboxScript().Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoirFullWidth — contact-sheet or lightbox grid, no horizontal padding.
|
|
||||||
func NoirFullWidth(data NoirPageData) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var6 == nil {
|
|
||||||
templ_7745c5c3_Var6 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<!doctype html><html lang=\"en\" class=\"dark\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirHead(data).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<body class=\"noir-page noir-surface antialiased min-h-screen flex flex-col\" data-noir-template=\"full-width\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<header class=\"w-full hairline-b\"><div class=\"max-w-5xl mx-auto px-6\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div></header><main class=\"flex-grow w-full\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
|
||||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"max-w-5xl mx-auto py-24 px-6 text-center\"><p class=\"tracked-mono\" style=\"color: hsl(var(--mutedForeground));\">No photographs assigned to this page.</p></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</main><footer class=\"w-full mt-auto\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</footer>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = noirLightboxScript().Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirDefault(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirDefault(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirLanding(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirLanding(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirArticle(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirArticle(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderNoirFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
|
||||||
return NoirFullWidth(parseNoirPageData(doc))
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
52
templates/email/noir.ninjatpl
Normal file
52
templates/email/noir.ninjatpl
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>{{ site_name }}</title>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: {{ colors.background|default:"#000000" }}; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;">
|
||||||
|
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ colors.background|default:"#000000" }};">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 48px 12px;">
|
||||||
|
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style="width: 600px; max-width: 600px; background-color: {{ colors.card|default:"#0a0a0a" }}; border: 1px solid {{ colors.border|default:"#1f1f1f" }};">
|
||||||
|
<!-- Masthead -->
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding: 28px 32px; border-bottom: 1px solid {{ colors.border|default:"#1f1f1f" }};">
|
||||||
|
{% if site_name %}<div style="margin: 0; font-size: 18px; font-weight: 400; font-family: &#39;Tenor Sans&#39;, Georgia, serif; letter-spacing: 0.05em; color: {{ colors.foreground|default:"#f5f5f5" }};">{{ site_name }}</div>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Cover image (16:10) -->
|
||||||
|
{% if logo_url %}<tr>
|
||||||
|
<td style="padding: 0; line-height: 0;"><img src="{{ logo_url }}" alt="{{ site_name }}" width="600" height="375" style="display: block; width: 600px; height: 375px; object-fit: cover; max-width: 100%;"></td>
|
||||||
|
</tr>{% endif %}
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 32px; color: {{ colors.foreground|default:"#f5f5f5" }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Mono caption strip -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px 32px; border-top: 1px solid {{ colors.border|default:"#1f1f1f" }}; border-bottom: 1px solid {{ colors.border|default:"#1f1f1f" }}; font-family: &#39;JetBrains Mono&#39;, Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#8c8c8c" }};">
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td align="left">{% if site_name %}© {{ site_name }}{% endif %}</td>
|
||||||
|
<td align="right">{% if unsubscribe_url %}<a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#8c8c8c" }}; text-decoration: none;">Unsubscribe</a>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 20px 32px; color: {{ colors.mutedForeground|default:"#8c8c8c" }}; font-family: &#39;JetBrains Mono&#39;, Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;">
|
||||||
|
{% if site_url %}<a href="{{ site_url }}" style="color: {{ colors.mutedForeground|default:"#8c8c8c" }}; text-decoration: none;">{{ site_url }}</a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
68
templates/noir/article.ninjatpl
Normal file
68
templates/noir/article.ninjatpl
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="article">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full hairline-b">
|
||||||
|
<div class="max-w-5xl mx-auto px-6">{{ slots.header|safe }}</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow max-w-5xl mx-auto w-full px-6 py-12">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-[12rem_1fr] gap-8">
|
||||||
|
<aside class="noir-meta-rail">{{ slots.aside|safe }}</aside>
|
||||||
|
<article class="min-w-0">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-24 text-center"><p class="tracked-mono" style="color: hsl(var(--mutedForeground));">No content assigned to this article.</p></div>{% endif %}</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
if (window.__noirLightboxInit) return;
|
||||||
|
window.__noirLightboxInit = true;
|
||||||
|
var overlay = null;
|
||||||
|
function ensureOverlay() {
|
||||||
|
if (overlay) return overlay;
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.setAttribute('data-noir-lightbox', '');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.innerHTML = '<button type="button" class="noir-lightbox-close tracked-mono" aria-label="Close">Close</button><img alt="" /><div class="noir-lightbox-caption tracked-mono"></div>';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });
|
||||||
|
overlay.querySelector('.noir-lightbox-close').addEventListener('click', close);
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (overlay.getAttribute('aria-hidden') === 'false' && e.key === 'Escape') close();
|
||||||
|
});
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
function open(src, alt, caption) {
|
||||||
|
var o = ensureOverlay();
|
||||||
|
var img = o.querySelector('img');
|
||||||
|
img.src = src;
|
||||||
|
img.alt = alt || '';
|
||||||
|
o.querySelector('.noir-lightbox-caption').textContent = caption || '';
|
||||||
|
o.setAttribute('aria-hidden', 'false');
|
||||||
|
o.querySelector('.noir-lightbox-close').focus();
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
if (!overlay) return;
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var t = e.target.closest('[data-noir-lightbox-trigger]');
|
||||||
|
if (!t) return;
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
var t = document.activeElement;
|
||||||
|
if (t && t.matches && t.matches('[data-noir-lightbox-trigger]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
templates/noir/default.ninjatpl
Normal file
65
templates/noir/default.ninjatpl
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="default">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full hairline-b">
|
||||||
|
<div class="max-w-5xl mx-auto px-6">{{ slots.header|safe }}</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow max-w-5xl mx-auto w-full px-6 py-12">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-24 text-center"><p class="tracked-mono" style="color: hsl(var(--mutedForeground));">No photographs assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
if (window.__noirLightboxInit) return;
|
||||||
|
window.__noirLightboxInit = true;
|
||||||
|
var overlay = null;
|
||||||
|
function ensureOverlay() {
|
||||||
|
if (overlay) return overlay;
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.setAttribute('data-noir-lightbox', '');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.innerHTML = '<button type="button" class="noir-lightbox-close tracked-mono" aria-label="Close">Close</button><img alt="" /><div class="noir-lightbox-caption tracked-mono"></div>';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });
|
||||||
|
overlay.querySelector('.noir-lightbox-close').addEventListener('click', close);
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (overlay.getAttribute('aria-hidden') === 'false' && e.key === 'Escape') close();
|
||||||
|
});
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
function open(src, alt, caption) {
|
||||||
|
var o = ensureOverlay();
|
||||||
|
var img = o.querySelector('img');
|
||||||
|
img.src = src;
|
||||||
|
img.alt = alt || '';
|
||||||
|
o.querySelector('.noir-lightbox-caption').textContent = caption || '';
|
||||||
|
o.setAttribute('aria-hidden', 'false');
|
||||||
|
o.querySelector('.noir-lightbox-close').focus();
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
if (!overlay) return;
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var t = e.target.closest('[data-noir-lightbox-trigger]');
|
||||||
|
if (!t) return;
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
var t = document.activeElement;
|
||||||
|
if (t && t.matches && t.matches('[data-noir-lightbox-trigger]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
templates/noir/full-width.ninjatpl
Normal file
65
templates/noir/full-width.ninjatpl
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="full-width">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full hairline-b">
|
||||||
|
<div class="max-w-5xl mx-auto px-6">{{ slots.header|safe }}</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-5xl mx-auto py-24 px-6 text-center"><p class="tracked-mono" style="color: hsl(var(--mutedForeground));">No photographs assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
if (window.__noirLightboxInit) return;
|
||||||
|
window.__noirLightboxInit = true;
|
||||||
|
var overlay = null;
|
||||||
|
function ensureOverlay() {
|
||||||
|
if (overlay) return overlay;
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.setAttribute('data-noir-lightbox', '');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.innerHTML = '<button type="button" class="noir-lightbox-close tracked-mono" aria-label="Close">Close</button><img alt="" /><div class="noir-lightbox-caption tracked-mono"></div>';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });
|
||||||
|
overlay.querySelector('.noir-lightbox-close').addEventListener('click', close);
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (overlay.getAttribute('aria-hidden') === 'false' && e.key === 'Escape') close();
|
||||||
|
});
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
function open(src, alt, caption) {
|
||||||
|
var o = ensureOverlay();
|
||||||
|
var img = o.querySelector('img');
|
||||||
|
img.src = src;
|
||||||
|
img.alt = alt || '';
|
||||||
|
o.querySelector('.noir-lightbox-caption').textContent = caption || '';
|
||||||
|
o.setAttribute('aria-hidden', 'false');
|
||||||
|
o.querySelector('.noir-lightbox-close').focus();
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
if (!overlay) return;
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var t = e.target.closest('[data-noir-lightbox-trigger]');
|
||||||
|
if (!t) return;
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
var t = document.activeElement;
|
||||||
|
if (t && t.matches && t.matches('[data-noir-lightbox-trigger]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
templates/noir/landing.ninjatpl
Normal file
64
templates/noir/landing.ninjatpl
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="noir-page noir-surface antialiased min-h-screen flex flex-col" data-noir-template="landing">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<section class="w-full">{{ slots.hero|safe }}</section>
|
||||||
|
<main class="flex-grow">
|
||||||
|
{% if slots.main %}<div class="max-w-5xl mx-auto px-6 py-16">{{ slots.main|safe }}</div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<section class="w-full">{{ slots.cta|safe }}</section>
|
||||||
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
if (window.__noirLightboxInit) return;
|
||||||
|
window.__noirLightboxInit = true;
|
||||||
|
var overlay = null;
|
||||||
|
function ensureOverlay() {
|
||||||
|
if (overlay) return overlay;
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.setAttribute('data-noir-lightbox', '');
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.innerHTML = '<button type="button" class="noir-lightbox-close tracked-mono" aria-label="Close">Close</button><img alt="" /><div class="noir-lightbox-caption tracked-mono"></div>';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); });
|
||||||
|
overlay.querySelector('.noir-lightbox-close').addEventListener('click', close);
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (overlay.getAttribute('aria-hidden') === 'false' && e.key === 'Escape') close();
|
||||||
|
});
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
function open(src, alt, caption) {
|
||||||
|
var o = ensureOverlay();
|
||||||
|
var img = o.querySelector('img');
|
||||||
|
img.src = src;
|
||||||
|
img.alt = alt || '';
|
||||||
|
o.querySelector('.noir-lightbox-caption').textContent = caption || '';
|
||||||
|
o.setAttribute('aria-hidden', 'false');
|
||||||
|
o.querySelector('.noir-lightbox-close').focus();
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
if (!overlay) return;
|
||||||
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var t = e.target.closest('[data-noir-lightbox-trigger]');
|
||||||
|
if (!t) return;
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
var t = document.activeElement;
|
||||||
|
if (t && t.matches && t.matches('[data-noir-lightbox-trigger]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
open(t.getAttribute('data-src'), t.getAttribute('data-alt'), t.getAttribute('data-caption'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
templates/overrides/noir/button.ninjatpl
Normal file
1
templates/overrides/noir/button.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% if url or href %}<a href="{% if url %}{{ url }}{% else %}{{ href }}{% endif %}" class="noir-btn">{% if text %}{{ text }}{% else %}{{ label }}{% endif %}</a>{% else %}<button type="button" class="noir-btn">{% if text %}{{ text }}{% else %}{{ label }}{% endif %}</button>{% endif %}
|
||||||
1
templates/overrides/noir/card.ninjatpl
Normal file
1
templates/overrides/noir/card.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div class="noir-card" style="padding: 1.5rem;">{% if title %}<h3 class="noir-display" style="font-size: 1.5rem; margin-bottom: 0.75rem; color: hsl(var(--foreground));">{{ title }}</h3>{% endif %}{% if body %}<div style="color: hsl(var(--foreground)); line-height: 1.6;">{{ body|safe }}</div>{% elif text %}<div style="color: hsl(var(--foreground)); line-height: 1.6;">{{ text|safe }}</div>{% endif %}</div>
|
||||||
1
templates/overrides/noir/heading.ninjatpl
Normal file
1
templates/overrides/noir/heading.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% if level|integer == 1 %}<h1 class="noir-display {{ textClass }}" style="font-size: clamp(2.5rem, 5vw, 4rem); line-height: 1.05; letter-spacing: 0.005em; color: hsl(var(--foreground));">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="noir-display {{ textClass }}" style="font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2; color: hsl(var(--foreground));">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="noir-display {{ textClass }}" style="font-size: 1.5rem; color: hsl(var(--foreground));">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="noir-display {{ textClass }}" style="font-size: 1.25rem; color: hsl(var(--foreground));">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="noir-display {{ textClass }}" style="font-size: 1.125rem; color: hsl(var(--foreground));">{{ text }}</h6>{% else %}<h2 class="noir-display {{ textClass }}" style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1; color: hsl(var(--foreground));">{{ text }}</h2>{% endif %}
|
||||||
1
templates/overrides/noir/image.ninjatpl
Normal file
1
templates/overrides/noir/image.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<figure class="noir-figure bleed">{% if src %}<img src="{{ src }}" alt="{{ alt }}" loading="lazy" class="w-full h-auto block">{% endif %}{% if caption %}<figcaption class="tracked-mono mt-2 px-6" style="color: hsl(var(--mutedForeground));">{{ caption }}</figcaption>{% elif title %}<figcaption class="tracked-mono mt-2 px-6" style="color: hsl(var(--mutedForeground));">{{ title }}</figcaption>{% endif %}</figure>
|
||||||
1
templates/overrides/noir/text.ninjatpl
Normal file
1
templates/overrides/noir/text.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div class="noir-text {{ class }}" style="line-height: 1.7; color: hsl(var(--foreground)); max-width: 65ch;">{{ text|safe }}</div>
|
||||||
Loading…
x
Reference in New Issue
Block a user