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:" 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, }, }, }, } }