Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/noir. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
177 lines
5.7 KiB
Go
177 lines
5.7 KiB
Go
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,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|