commit 9fbedf5ba111b0355aa6477234da22713c074b0b Author: Alex Dunmow Date: Sat Jun 6 14:11:19 2026 +0800 initial: theme plugin art-deco Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/art-deco. Co-Authored-By: Claude Opus 4.7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f780e6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.so +*.test +tmp/ +.idea/ +.vscode/ diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..9cd3c1c --- /dev/null +++ b/BUILD_REPORT.md @@ -0,0 +1,168 @@ +# Art Deco — Build Report + +Status: **SHIPPED**. + +Compiled `.so` produced and the safety check exits 0. + +## What landed + +### Plugin metadata (`plugin.mod`) +- `kind = "theme"`, `scope = "@themes"`, `version = "0.1.0"`. +- `categories = ["templates"]` (single whitelist entry). +- `tags = ["luxury", "gold", "hospitality", "hotel", "wedding", "jewellery", "vintage", "glam", "fine-dining"]` (9 tags, within the 5–9 UAT range). +- `[compatibility].block_core = ">=0.11.0 <0.12.0"`. + +### System & page templates +- System template registered with `Key: "art-deco"`. +- 4 page templates registered: + - `default` → slots `header, main, footer`. + - `landing` → slots `marquee, main, cta, footer`. + - `article` → slots `header, main, aside, footer`. + - `full-width` → slots `header, main, footer`. + +### Blocks (7, all `Source: "art-deco"`) +| Key | Schema | Notes | +|---|---|---| +| `reservation` | `schemas/reservation.schema.json` | Sticky gold-on-black strip with phone, OpenTable CTA, hours. | +| `menu` | `schemas/menu.schema.json` | Symmetric menu card with rich-text course descriptions and prices. | +| `gallery_fan` | `schemas/gallery_fan.schema.json` | Mirrored gallery framed by sunburst, 2/3/4 columns, collapses to 1 col <640px. | +| `divider_fan` | `schemas/divider_fan.schema.json` | Pure-CSS sunburst / scallop / ziggurat divider — falls back to sunburst on unknown variant. | +| `footer` | `schemas/footer.schema.json` | Centered footer with gold rule, address, reservations email, social row, menu name. | +| `marquee_hero` | `schemas/marquee_hero.schema.json` | Wide hero with stepped ziggurat frame and sunburst overlay. | +| `press_quote` | `schemas/press_quote.schema.json` | Italic Cormorant pull-quote with stars (clamped 0–5). | + +Schemas are loaded BEFORE any `br.Register(...)` call (line ordering enforced in `register.go`). + +### Template overrides (4) +- `heading` — caps tracking + hairline gold underline, Italiana display family. +- `text` — Cormorant body with italic drop-cap on first paragraph. +- `button` — black pill with gold border, stamped inset shadow on hover/focus. +- `image` — stepped ziggurat frame variant (also scallop / plain). + +### Email wrapper +- `tr.RegisterEmailWrapper("art-deco", ArtDecoEmailWrapper)` wired. +- 600px centered column, ivory body, jet-black masthead. +- Masthead uses a flat-gold SVG `` (not a CSS gradient) so Outlook renders it correctly. +- Reservation phone / `SupportEmail` auto-injected as italic postscript line. + +### Master pages (2) +- `art-deco:default-master` covers `default`, `article` — `navbar` (header,0), `art-deco:divider_fan` (header,1,sunburst), `slot` (main,0,slotName=main), `art-deco:footer` (footer,0). +- `art-deco:marquee-master` covers `landing`, `full-width` — `navbar` (marquee,0), `art-deco:reservation` (marquee,1), `slot` (main,0,slotName=main), `art-deco:divider_fan` (cta,0,scallop), `art-deco:footer` (footer,0). + +### Presets (`presets.json`, 3) +- `champagne-noir` (`mode: both`, lightColors + darkColors, all 19 tokens each). +- `ivory-rose` (`mode: light`, lightColors only, all 19 tokens). +- `velvet-onyx` (`mode: dark`, darkColors only, all 19 tokens). +- Every value is an HSL triple `H S% L%` — no `hsl(...)` wrappers, no hex. + +### CSS manifest +- `CSSManifest.InputCSSAppend` registered via `ThemeCSSManifest()` in `embed.go`/`registration.go`. +- The injected CSS declares `@layer utilities` blocks for `.deco-rule`, `.deco-frame`, `.deco-step`, `.deco-grain`, plus aesthetic helpers `.deco-foil`, `.deco-sunburst`, `.deco-scallop`, `.deco-ziggurat`, `.deco-caps`, `.deco-dropcap`, `.deco-underline`, `.deco-pill`, `.deco-link`. +- All colour values consume `hsl(var(--token))`; all `font-family` declarations go through `var(--font-heading|body|mono, )`. + +### Fonts policy (Wave-1) +- `fonts.json = []` (no bundled woff2s in this pass). +- `RECOMMENDED_FONTS.md` lists the spec §5 fonts as Google Fonts picker recommendations (Italiana, Cormorant Garamond, JetBrains Mono). +- Fallback stacks for `--font-heading`, `--font-body`, `--font-mono` are embedded inline in every template + utility. + +## Build output + +``` +$ cd ~/src/blockninja/themes/art-deco && go mod tidy +(no output — clean) +$ /home/alex/go/bin/templ generate +(✓) Complete [ updates=13 duration=… ] +$ make +CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o art-deco.so . +$ ls -lh art-deco.so +-rw-rw-r-- 1 alex alex 21M art-deco.so +``` + +## Safety check + +``` +$ cd ~/src/blockninja/check-safety && go run . ~/src/blockninja/themes/art-deco \ + --plugin-dir ~/src/blockninja/themes/art-deco +… +=== Check 2c: Standalone plugin SDK import boundaries === + OK: Standalone plugin imports and go.mod stay on SDK version v0.11.1 +=== Check 3: Go code compiles and passes go fix, golangci-lint --fix, go vet, and strict lint === + OK: Go lint pipeline clean for 1 module(s) +=== Check 6: No hardcoded colors in frontend (use theme tokens) === + OK: No hardcoded colors in .templ files +=== Check 11: No placeholder code; only shipped features === + OK: No placeholder code found +… +exit=0 +``` + +Only WARN remaining is Check 2e (`any` usage in `content map[string]any` signatures). This is the +exact same warning Gotham emits — it is the standalone-plugin block signature contract and is +non-blocking. + +## Open items / deferred + +### Wave-1 deferrals (intentional, per `themes/docs/FONTS.md`) +- **No bundled woff2 fonts**. Theme ships `fonts.json = []` and a `RECOMMENDED_FONTS.md` for the + admin to pick Italiana / Cormorant Garamond / JetBrains Mono from the Google Fonts tab. +- **No `LICENSES.md`** — nothing is bundled. + +### Wave-2 follow-ups (out of scope for this pass) +- Commission a redistributable Art Deco display face (Italiana variants are limited) and bundle + via `fonts.json` if Italiana proves brittle at retina sizes. UAT §11 FOUT timing and the licence + audit only become checkable once a face is bundled. +- Capture the 6 UAT marketplace screenshots (`docs/uat/art-deco-shots/01..06.png`). Requires + a running CMS instance — outside the autonomous build scope. +- Seed the "Maison Étoile" demo content (6 pages, 4 courses, 1 press quote, 9 gallery images). + Same constraint as above — requires the instance container. +- Run the UAT browser-side gates: WCAG contrast pairs, focus-visible ring sweep, viewport + scroll-width checks at 360/768/1024/1440. The CSS is wired to satisfy them but the verification + is an instance-side workflow. +- Master-page UAT cell "Replacing `art-deco:footer` on `default-master` in admin produces a + ghost-free re-render" — admin-side verification, not a build-side check. + +### Not implemented (called out by spec but explicitly listed in BUILD_REPORT) +- `make rebuild` and the container deploy targets — by build-instructions, `make rebuild` is + off-limits to the autonomous build pass. The Makefile here ships only `all`, `templ`, `clean`, + `help` — production deploy is reserved for the gotham-style upstream Makefile and a human run. +- Real woff2 bundles in `assets/fonts/` — deferred to Wave-2 per FONTS.md. + +## File inventory + +``` +art-deco/ +├── BUILD_REPORT.md ← this file +├── Makefile ← local-only build (make, make templ, make clean) +├── RECOMMENDED_FONTS.md ← Wave-1 Google Fonts picker guide +├── assets/style.css ← @layer utilities CSS (foil, frame, fan, grain, pill…) +├── button_override.{go,templ,_templ.go} +├── divider_fan.{go,templ,_templ.go} +├── email_wrapper.{templ,_templ.go} +├── embed.go ← //go:embed directives + CSSManifest hook +├── fonts.json ← [] +├── footer.{go,templ,_templ.go} +├── gallery_fan.{go,templ,_templ.go} +├── go.{mod,sum} ← block/core v0.11.1, no replace directives +├── heading_override.{go,templ,_templ.go} +├── helpers.go ← getString / getInt / getSlice / getStringSlice / clampInt +├── image_override.{go,templ,_templ.go} +├── marquee_hero.{go,templ,_templ.go} +├── menu.{go,templ,_templ.go} +├── plugin.mod ← TOML metadata, kind=theme +├── presets.json ← 3 presets, 19 tokens each, HSL triples +├── press_quote.{go,templ,_templ.go} +├── press_quote_helpers.go +├── register.go ← Register(tr,br) + DefaultMasterPages() +├── registration.go ← var Registration plugin.PluginRegistration +├── reservation.{go,templ,_templ.go} +├── schemas/ +│ ├── divider_fan.schema.json +│ ├── footer.schema.json +│ ├── gallery_fan.schema.json +│ ├── marquee_hero.schema.json +│ ├── menu.schema.json +│ ├── press_quote.schema.json +│ └── reservation.schema.json +├── template.templ + template_templ.go +└── text_override.{go,templ,_templ.go} +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cee91c1 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +# Art Deco — local build helpers (.so plugin workflow). +# +# `make` produces art-deco.so via CGO go build -buildmode=plugin. This is the +# local-only build used by check-safety. The full `rebuild` target intentionally +# does NOT ship here — production deployment goes through the upstream gotham-style +# Makefile, which is reserved for human-run cycles, not autonomous agents. + +.PHONY: all clean templ help + +PLUGIN_NAME := art-deco +TEMPL_BIN := $(HOME)/go/bin/templ + +# Default target: build the .so locally. +all: $(PLUGIN_NAME).so + +# Local plugin build (no container). +$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod + CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so . + +# Regenerate templ Go files from .templ sources. +templ: + $(TEMPL_BIN) generate + +# Remove build artefacts. +clean: + rm -f $(PLUGIN_NAME).so + +help: + @echo "Targets:" + @echo " all Build $(PLUGIN_NAME).so locally (default)" + @echo " templ Regenerate templ Go files" + @echo " clean Remove build artefacts" diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md new file mode 100644 index 0000000..2af1260 --- /dev/null +++ b/RECOMMENDED_FONTS.md @@ -0,0 +1,53 @@ +# Recommended Fonts — Art Deco + +This theme ships `fonts.json = []` (Wave-1 policy, see `themes/docs/FONTS.md`). +The intended typographic identity is achieved by an admin assigning the +following Google Fonts to the theme's typography slots after install. + +## Picker workflow + +1. Sign in as an admin and open **Settings → Typography**. +2. Switch to the **Google Fonts** tab in the font picker. +3. Search for the family, click **Add**, then assign it to the listed slot. + +## Display heading — Italiana + +- **Source**: `google:Italiana` +- **Slot**: Heading +- **Why**: The spec calls for stamped Art Deco titles in tight tracking. Italiana + is the closest curated Google Fonts match to the high-contrast 1920s display + cuts the brief describes. +- **Fallback already in CSS**: `var(--font-heading, "Italiana", "Cinzel", Georgia, serif)`. + +### Alternative + +- **Source**: `google:Cinzel` (also acceptable per spec §3) + Pick this if Italiana feels too thin at small sizes. + +## Body — Cormorant Garamond + +- **Source**: `google:Cormorant Garamond` +- **Slot**: Body +- **Why**: Classical italic and book weights for long-form prose, exactly the + "Cormorant for long-form prose with classical italics" pairing the spec calls + out. +- **Fallback already in CSS**: `var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)`. + +## Mono — JetBrains Mono + +- **Source**: `google:JetBrains Mono` +- **Slot**: Mono +- **Why**: Spec wants a precise monospace for reservation IDs and room numbers. + JetBrains Mono ships across all curated Google Fonts and is the cleanest + utilitarian option. +- **Fallback already in CSS**: `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)`. + +## Notes for Wave-2 + +- If Italiana proves too brittle on retina at large sizes, commission a + redistributable Art Deco display face and bundle it via `fonts.json`. The + rest of the CSS is already variable-driven and will pick the new family up + without further edits. +- Body italic for the drop-cap relies on the body family having an italic + variant; both Cormorant Garamond and Cormorant ship italics in the curated + Google Fonts list — no extra picker action needed. diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..9cfaf75 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,212 @@ +/* Art Deco theme — utility CSS for foil, frames, fans and grain. + * + * Color is always consumed via hsl(var(--token)) — the tokens carry HSL triples. + * Font families always come from var(--font-heading|body|mono, ). + */ + +@layer utilities { + /* deco-rule — hairline gold rule, 1px ascending to 2px on accent lines. */ + .deco-rule { + border: none; + border-top: 1px solid hsl(var(--primary)); + margin: 0; + } + .deco-rule-thick { + border: none; + border-top: 2px solid hsl(var(--primary)); + margin: 0; + } + + /* deco-frame — stepped ziggurat frame, layered borders + clipped corners. */ + .deco-frame { + position: relative; + border: 1px solid hsl(var(--primary)); + padding: 1.25rem; + } + .deco-frame::before, + .deco-frame::after { + content: ""; + position: absolute; + left: -6px; + right: -6px; + height: 4px; + background: hsl(var(--primary)); + opacity: 0.85; + } + .deco-frame::before { top: -6px; } + .deco-frame::after { bottom: -6px; } + + /* deco-step — stepped corner mask suitable for image frames. */ + .deco-step { + position: relative; + padding: 4px; + background: + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) top left / 14px 1px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) top left / 1px 14px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) top right / 14px 1px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) top right / 1px 14px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) bottom left / 14px 1px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) bottom left / 1px 14px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) bottom right / 14px 1px no-repeat, + linear-gradient(hsl(var(--primary)), hsl(var(--primary))) bottom right / 1px 14px no-repeat; + } + .deco-step > * { + display: block; + width: 100%; + height: 100%; + } + + /* deco-grain — 3% film-grain noise, dark surfaces only. */ + .deco-grain { + position: relative; + isolation: isolate; + } + .deco-grain::after { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + pointer-events: none; + opacity: 0.03; + background-image: + repeating-radial-gradient(circle at 17% 23%, hsl(var(--foreground)) 0 1px, transparent 1px 3px), + repeating-radial-gradient(circle at 73% 61%, hsl(var(--foreground)) 0 1px, transparent 1px 3px); + mix-blend-mode: overlay; + } + + /* deco-foil — inset gold "stamp" shadow used on hover states. */ + .deco-foil { + box-shadow: + inset 0 0 0 1px hsl(var(--primary)), + inset 0 1px 0 hsl(var(--primary) / 0.6), + inset 0 -1px 0 hsl(var(--primary) / 0.3); + } + + /* deco-sunburst — conic-gradient sunburst rays. Used by divider_fan & marquee_hero. */ + .deco-sunburst { + background-image: + conic-gradient( + from 180deg at 50% 100%, + transparent 0deg, + hsl(var(--primary) / 0.7) 5deg, + transparent 10deg, + transparent 20deg, + hsl(var(--primary) / 0.55) 25deg, + transparent 30deg, + transparent 40deg, + hsl(var(--primary) / 0.7) 45deg, + transparent 50deg, + transparent 60deg, + hsl(var(--primary) / 0.55) 65deg, + transparent 70deg, + transparent 80deg, + hsl(var(--primary) / 0.7) 85deg, + transparent 90deg, + transparent 100deg, + hsl(var(--primary) / 0.55) 105deg, + transparent 110deg, + transparent 120deg, + hsl(var(--primary) / 0.7) 125deg, + transparent 130deg, + transparent 140deg, + hsl(var(--primary) / 0.55) 145deg, + transparent 150deg, + transparent 160deg, + hsl(var(--primary) / 0.7) 165deg, + transparent 170deg, + transparent 180deg + ); + } + + /* deco-scallop — scalloped fan crest via radial gradient. */ + .deco-scallop { + background-image: + radial-gradient(circle at 50% 100%, hsl(var(--primary) / 0.7) 0 2px, transparent 3px), + repeating-linear-gradient( + to right, + transparent 0, + transparent 12px, + hsl(var(--primary) / 0.55) 12px, + hsl(var(--primary) / 0.55) 13px, + transparent 13px, + transparent 24px + ); + } + + /* deco-ziggurat — stepped ziggurat divider stripes. */ + .deco-ziggurat { + background-image: + linear-gradient(to bottom, transparent 0 30%, hsl(var(--primary)) 30% 32%, transparent 32% 50%, hsl(var(--primary)) 50% 52%, transparent 52% 70%, hsl(var(--primary)) 70% 72%, transparent 72% 100%); + } + + /* deco-caps — caps tracking helper for Italiana display headings. */ + .deco-caps { + text-transform: uppercase; + letter-spacing: 0.18em; + } + + /* deco-dropcap — classical italic dropcap on first paragraph. */ + .deco-dropcap[data-deco-dropcap]::first-letter { + font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif); + font-style: italic; + font-size: 3.2em; + line-height: 0.95; + float: left; + padding: 0.05em 0.12em 0 0; + color: hsl(var(--primary)); + } + + /* deco-underline — hairline gold underline on headings. */ + .deco-underline { + position: relative; + padding-bottom: 0.35em; + } + .deco-underline::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1px; + background: hsl(var(--primary)); + } + + /* deco-pill — black pill button base for the button override. */ + .deco-pill { + border-radius: 9999px; + border-width: 1px; + border-style: solid; + border-color: hsl(var(--primary)); + background-color: hsl(var(--foreground)); + color: hsl(var(--background)); + padding: 0.65rem 1.6rem; + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.85rem; + transition: box-shadow 200ms ease, background-color 200ms ease; + } + .deco-pill:hover, + .deco-pill:focus-visible { + box-shadow: + inset 0 0 0 1px hsl(var(--primary)), + inset 0 1px 0 hsl(var(--primary) / 0.6), + inset 0 -1px 0 hsl(var(--primary) / 0.4); + } + .deco-pill:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 3px; + } + + /* deco-link — gold underline link state. */ + .deco-link { + color: hsl(var(--primary)); + text-decoration: none; + border-bottom: 1px solid hsl(var(--primary)); + transition: color 200ms ease, border-color 200ms ease; + } + .deco-link:hover, + .deco-link:focus-visible { + color: hsl(var(--accent)); + border-bottom-color: hsl(var(--accent)); + } +} diff --git a/button_override.go b/button_override.go new file mode 100644 index 0000000..985453e --- /dev/null +++ b/button_override.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "context" +) + +// ButtonOverrideData is the typed view for the button override component. +type ButtonOverrideData struct { + Label string + URL string + Variant string +} + +// normaliseButtonVariant returns a known variant or "primary" as default. +func normaliseButtonVariant(v string) string { + switch v { + case "primary", "secondary", "ghost": + return v + default: + return "primary" + } +} + +// ArtDecoButtonBlock overrides the built-in "button" block when the Art Deco theme is active. +// Content: {"label": "...", "url": "...", "variant": "primary|secondary|ghost"} +func ArtDecoButtonBlock(ctx context.Context, content map[string]any) string { + data := ButtonOverrideData{ + Label: getString(content, "label"), + URL: getString(content, "url"), + Variant: normaliseButtonVariant(getString(content, "variant")), + } + + if data.Label == "" { + return "" + } + + var buf bytes.Buffer + _ = artDecoButtonComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/button_override.templ b/button_override.templ new file mode 100644 index 0000000..525fc26 --- /dev/null +++ b/button_override.templ @@ -0,0 +1,36 @@ +package main + +// buttonVariantStyle returns inline style overrides per variant. +func buttonVariantStyle(variant string) string { + switch variant { + case "secondary": + return "background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border-color: hsl(var(--primary));" + case "ghost": + return "background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--primary));" + default: + return "background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-color: hsl(var(--primary));" + } +} + +// artDecoButtonComponent renders the black-pill button with stamped hover state. +templ artDecoButtonComponent(data ButtonOverrideData) { + if data.URL != "" { + + { data.Label } + + } else { + + } +} diff --git a/button_override_templ.go b/button_override_templ.go new file mode 100644 index 0000000..b706533 --- /dev/null +++ b/button_override_templ.go @@ -0,0 +1,125 @@ +// 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" + +// buttonVariantStyle returns inline style overrides per variant. +func buttonVariantStyle(variant string) string { + switch variant { + case "secondary": + return "background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border-color: hsl(var(--primary));" + case "ghost": + return "background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--primary));" + default: + return "background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-color: hsl(var(--primary));" + } +} + +// artDecoButtonComponent renders the black-pill button with stamped hover state. +func artDecoButtonComponent(data ButtonOverrideData) 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) + if data.URL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 24, Col: 15} + } + _, 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, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/divider_fan.go b/divider_fan.go new file mode 100644 index 0000000..3618cb2 --- /dev/null +++ b/divider_fan.go @@ -0,0 +1,43 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// DividerFanBlockMeta defines the fan divider block. +var DividerFanBlockMeta = blocks.BlockMeta{ + Key: "divider_fan", + Title: "Fan Divider", + Description: "Pure-CSS gold geometric divider — sunburst, scallop or ziggurat.", + Source: "art-deco", +} + +// DividerFanData carries the variant for the divider component. +type DividerFanData struct { + Variant string +} + +// normaliseVariant clamps to the supported set; unknown values fall back to sunburst. +func normaliseVariant(v string) string { + switch v { + case "scallop", "ziggurat": + return v + default: + return "sunburst" + } +} + +// DividerFanBlock renders a CSS-only geometric divider. +// Content: {"variant": "sunburst" | "scallop" | "ziggurat"} +func DividerFanBlock(ctx context.Context, content map[string]any) string { + data := DividerFanData{ + Variant: normaliseVariant(getString(content, "variant")), + } + + var buf bytes.Buffer + _ = dividerFanComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/divider_fan.templ b/divider_fan.templ new file mode 100644 index 0000000..22859cc --- /dev/null +++ b/divider_fan.templ @@ -0,0 +1,26 @@ +package main + +// dividerVariantClass maps the variant to the corresponding utility class. +func dividerVariantClass(variant string) string { + switch variant { + case "scallop": + return "deco-scallop" + case "ziggurat": + return "deco-ziggurat" + default: + return "deco-sunburst" + } +} + +// dividerFanComponent renders a pure-CSS gold geometric divider. +templ dividerFanComponent(data DividerFanData) { + +} diff --git a/divider_fan_templ.go b/divider_fan_templ.go new file mode 100644 index 0000000..67d4713 --- /dev/null +++ b/divider_fan_templ.go @@ -0,0 +1,88 @@ +// 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" + +// dividerVariantClass maps the variant to the corresponding utility class. +func dividerVariantClass(variant string) string { + switch variant { + case "scallop": + return "deco-scallop" + case "ziggurat": + return "deco-ziggurat" + default: + return "deco-sunburst" + } +} + +// dividerFanComponent renders a pure-CSS gold geometric divider. +func dividerFanComponent(data DividerFanData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 = []any{"h-10 w-full max-w-xl", dividerVariantClass(data.Variant)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/email_wrapper.templ b/email_wrapper.templ new file mode 100644 index 0000000..0dd54ef --- /dev/null +++ b/email_wrapper.templ @@ -0,0 +1,178 @@ +package main + +import ( + "bytes" + "context" + "fmt" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// ArtDecoEmailWrapper wraps body content in the Art Deco email layout: +// ivory background, 600px column, jet-black masthead with flat-gold PNG sunburst, +// Cormorant body, gold hairline above footer, reservation phone postscript. +func ArtDecoEmailWrapper(body string, emailCtx templates.EmailContext) string { + var buf bytes.Buffer + _ = artDecoEmailTemplate(emailCtx, body).Render(context.Background(), &buf) + return buf.String() +} + +// edColor returns a color from the email context or a hex fallback for the named token. +func edColor(value, fallback string) string { + if value != "" { + return value + } + return fallback +} + +// edBg ivory background. +func edBg(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Background, "#f6efe2") +} + +// edCard jet-black masthead surface (uses Foreground as the dark masthead colour). +func edCardDark(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Foreground, "#191510") +} + +// edFg foreground text on the ivory body. +func edFg(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Foreground, "#191510") +} + +// edPrimary champagne-gold accent (used for the hairline and links). +func edPrimary(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Primary, "#c9a14a") +} + +// edMutedFg muted footer text. +func edMutedFg(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.MutedForeground, "#6c5b35") +} + +// edBorder hairline gold border colour. +func edBorder(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Border, "#c9a14a") +} + +// flatGoldSunburstSVG is the inlined data: URI for the flat-gold sunburst image +// used as a fallback for the Outlook masthead (CSS conic-gradient is flattened by Outlook). +// We render via string concatenation so the URL-encoded %xx tokens are not misread as fmt verbs. +func flatGoldSunburstSVG(gold string) string { + g := stripHash(gold) + prefix := "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%2080%22%3E%3Crect%20width%3D%22600%22%20height%3D%2280%22%20fill%3D%22%23191510%22%2F%3E%3Cg%20stroke%3D%22%23" + rays := "%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M300%2080%20L%20100%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20180%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20260%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20340%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20420%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20500%2010%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E" + return prefix + g + rays +} + +// stripHash removes a leading '#' if present (defensive for the data URI). +func stripHash(s string) string { + if len(s) > 0 && s[0] == '#' { + return s[1:] + } + return s +} + +// artDecoEmailTemplate renders the Art Deco email wrapper. +templ artDecoEmailTemplate(emailCtx templates.EmailContext, body string) { + + + + + + + + + { emailCtx.SiteSettings.SiteName } + + + + if emailCtx.PreviewText != "" { +
+ { emailCtx.PreviewText } +
+ } + + + + +
+ + + + + + + + + + + + + + +
+ + +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..693dc96 --- /dev/null +++ b/email_wrapper_templ.go @@ -0,0 +1,463 @@ +// 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" +) + +// ArtDecoEmailWrapper wraps body content in the Art Deco email layout: +// ivory background, 600px column, jet-black masthead with flat-gold PNG sunburst, +// Cormorant body, gold hairline above footer, reservation phone postscript. +func ArtDecoEmailWrapper(body string, emailCtx templates.EmailContext) string { + var buf bytes.Buffer + _ = artDecoEmailTemplate(emailCtx, body).Render(context.Background(), &buf) + return buf.String() +} + +// edColor returns a color from the email context or a hex fallback for the named token. +func edColor(value, fallback string) string { + if value != "" { + return value + } + return fallback +} + +// edBg ivory background. +func edBg(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Background, "#f6efe2") +} + +// edCard jet-black masthead surface (uses Foreground as the dark masthead colour). +func edCardDark(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Foreground, "#191510") +} + +// edFg foreground text on the ivory body. +func edFg(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Foreground, "#191510") +} + +// edPrimary champagne-gold accent (used for the hairline and links). +func edPrimary(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Primary, "#c9a14a") +} + +// edMutedFg muted footer text. +func edMutedFg(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.MutedForeground, "#6c5b35") +} + +// edBorder hairline gold border colour. +func edBorder(emailCtx templates.EmailContext) string { + return edColor(emailCtx.Colors.Border, "#c9a14a") +} + +// flatGoldSunburstSVG is the inlined data: URI for the flat-gold sunburst image +// used as a fallback for the Outlook masthead (CSS conic-gradient is flattened by Outlook). +// We render via string concatenation so the URL-encoded %xx tokens are not misread as fmt verbs. +func flatGoldSunburstSVG(gold string) string { + g := stripHash(gold) + prefix := "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%2080%22%3E%3Crect%20width%3D%22600%22%20height%3D%2280%22%20fill%3D%22%23191510%22%2F%3E%3Cg%20stroke%3D%22%23" + rays := "%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M300%2080%20L%20100%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20180%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20260%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20340%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20420%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20500%2010%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E" + return prefix + g + rays +} + +// stripHash removes a leading '#' if present (defensive for the data URI). +func stripHash(s string) string { + if len(s) > 0 && s[0] == '#' { + return s[1:] + } + return s +} + +// artDecoEmailTemplate renders the Art Deco email wrapper. +func artDecoEmailTemplate(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, "") + 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: 94, Col: 42} + } + _, 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.PreviewText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + 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: 113, Col: 27} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
\"\" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SiteName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, 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: 131, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + 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, 17, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, 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: 149, Col: 42} + } + _, 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, 22, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SiteURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") + 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: 154, Col: 43} + } + _, 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, 26, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if emailCtx.SiteSettings.SupportEmail != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

P.S. Reservations & enquiries: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SupportEmail) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 160, Col: 82} + } + _, 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, 29, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if emailCtx.UnsubscribeURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

Unsubscribe

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..6526022 --- /dev/null +++ b/embed.go @@ -0,0 +1,58 @@ +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 rooted at assets/. +func Assets() fs.FS { + sub, _ := fs.Sub(assetsFS, "assets") + return sub +} + +// Schemas returns the embedded schemas filesystem rooted at schemas/. +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 ([] in this pass). +func BundledFonts() []byte { return fontsData } + +// ThemeCSSManifest exposes the Art Deco utility CSS to the host Tailwind input. +func ThemeCSSManifest() *plugin.CSSManifest { + css, err := assetsFS.ReadFile("assets/style.css") + if err != nil { + return &plugin.CSSManifest{} + } + return &plugin.CSSManifest{ + InputCSSAppend: string(css), + } +} diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/fonts.json @@ -0,0 +1 @@ +[] diff --git a/footer.go b/footer.go new file mode 100644 index 0000000..4442a8b --- /dev/null +++ b/footer.go @@ -0,0 +1,47 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// FooterBlockMeta defines the Art Deco footer block. +var FooterBlockMeta = blocks.BlockMeta{ + Key: "footer", + Title: "Footer", + Description: "Centered Art Deco footer with gold rule, address, reservations email and optional social row.", + Source: "art-deco", +} + +// FooterData is the typed view for the footer component. +type FooterData struct { + Address string + ReservationsEmail string + ShowSocial bool + MenuName string +} + +// FooterBlock renders the centered Art Deco footer. +// Content: {"address": "...", "reservationsEmail": "...", "showSocial": "true|false", "menuName": "..."} +func FooterBlock(ctx context.Context, content map[string]any) string { + // showSocial is stored as a string select ("true"/"false"); also accept bool for flexibility. + showSocial := true + if b, ok := content["showSocial"].(bool); ok { + showSocial = b + } else if s := getString(content, "showSocial"); s != "" { + showSocial = s != "false" + } + + data := FooterData{ + Address: getString(content, "address"), + ReservationsEmail: getString(content, "reservationsEmail"), + ShowSocial: showSocial, + MenuName: getString(content, "menuName"), + } + + var buf bytes.Buffer + _ = footerComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/footer.templ b/footer.templ new file mode 100644 index 0000000..edf2361 --- /dev/null +++ b/footer.templ @@ -0,0 +1,59 @@ +package main + +import "strings" + +// addressLines splits a multi-line address into individual lines for centered rendering. +func addressLines(addr string) []string { + if addr == "" { + return nil + } + lines := strings.Split(addr, "\n") + out := make([]string, 0, len(lines)) + for _, line := range lines { + l := strings.TrimSpace(line) + if l != "" { + out = append(out, l) + } + } + return out +} + +// footerComponent renders the centered Art Deco footer. +templ footerComponent(data FooterData) { +
+
+
+ if len(addressLines(data.Address)) > 0 { +
+ for _, line := range addressLines(data.Address) { +
{ line }
+ } +
+ } + if data.ReservationsEmail != "" { +

+ { data.ReservationsEmail } +

+ } + if data.MenuName != "" { + + } + if data.ShowSocial { +
+ + + + + +
+ } +
+
+
+} diff --git a/footer_templ.go b/footer_templ.go new file mode 100644 index 0000000..235cf8f --- /dev/null +++ b/footer_templ.go @@ -0,0 +1,149 @@ +// 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 "strings" + +// addressLines splits a multi-line address into individual lines for centered rendering. +func addressLines(addr string) []string { + if addr == "" { + return nil + } + lines := strings.Split(addr, "\n") + out := make([]string, 0, len(lines)) + for _, line := range lines { + l := strings.TrimSpace(line) + if l != "" { + out = append(out, l) + } + } + return out +} + +// footerComponent renders the centered Art Deco footer. +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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(addressLines(data.Address)) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, line := range addressLines(data.Address) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(line) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 29, Col: 17} + } + _, 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, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ReservationsEmail != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ReservationsEmail) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 35, Col: 109} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.MenuName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ShowSocial { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
Instagram · Facebook · LinkedIn
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/gallery_fan.go b/gallery_fan.go new file mode 100644 index 0000000..e2ae49c --- /dev/null +++ b/gallery_fan.go @@ -0,0 +1,48 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// GalleryFanBlockMeta defines the mirrored fan gallery block. +var GalleryFanBlockMeta = blocks.BlockMeta{ + Key: "gallery_fan", + Title: "Fan Gallery", + Description: "Mirrored gallery framed by sunburst — collapses to single column under 640px.", + Source: "art-deco", +} + +// GalleryImage is a single image in the gallery. +type GalleryImage struct { + Src string + Caption string +} + +// GalleryFanData is the typed view for the gallery component. +type GalleryFanData struct { + Images []GalleryImage + Columns int +} + +// GalleryFanBlock renders a mirrored fan gallery. +// Content: {"images": [{"src": "...", "caption": "..."}], "columns": 3} +func GalleryFanBlock(ctx context.Context, content map[string]any) string { + items := getSlice(content, "images") + images := make([]GalleryImage, 0, len(items)) + for _, item := range items { + images = append(images, GalleryImage{ + Src: getString(item, "src"), + Caption: getString(item, "caption"), + }) + } + + cols := getInt(content, "columns", 3) + cols = clampInt(cols, 2, 4) + + var buf bytes.Buffer + _ = galleryFanComponent(GalleryFanData{Images: images, Columns: cols}).Render(ctx, &buf) + return buf.String() +} diff --git a/gallery_fan.templ b/gallery_fan.templ new file mode 100644 index 0000000..3a3eecb --- /dev/null +++ b/gallery_fan.templ @@ -0,0 +1,54 @@ +package main + +// galleryFanGridCols maps the column count to a tailwind grid class with single-column collapse below 640px. +func galleryFanGridCols(cols int) string { + switch cols { + case 2: + return "grid-cols-1 sm:grid-cols-2" + case 4: + return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" + default: + return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" + } +} + +// fallbackImage returns a tiny transparent svg data URI used when no image src is supplied. +func fallbackImage() string { + return "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%23222%22%2F%3E%3C%2Fsvg%3E" +} + +// galleryFanComponent renders the mirrored gallery framed by sunburst rays. +templ galleryFanComponent(data GalleryFanData) { +
+
+
+ +
+ if len(data.Images) == 0 { +
+ Add images to populate the fan gallery. +
+ } + for _, img := range data.Images { +
+ if img.Src != "" { + { + } else { + + } + if img.Caption != "" { +
+ { img.Caption } +
+ } +
+ } +
+ +
+
+
+} diff --git a/gallery_fan_templ.go b/gallery_fan_templ.go new file mode 100644 index 0000000..385f494 --- /dev/null +++ b/gallery_fan_templ.go @@ -0,0 +1,169 @@ +// 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" + +// galleryFanGridCols maps the column count to a tailwind grid class with single-column collapse below 640px. +func galleryFanGridCols(cols int) string { + switch cols { + case 2: + return "grid-cols-1 sm:grid-cols-2" + case 4: + return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" + default: + return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" + } +} + +// fallbackImage returns a tiny transparent svg data URI used when no image src is supplied. +func fallbackImage() string { + return "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%23222%22%2F%3E%3C%2Fsvg%3E" +} + +// galleryFanComponent renders the mirrored gallery framed by sunburst rays. +func galleryFanComponent(data GalleryFanData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 = []any{"grid gap-4 pt-12", galleryFanGridCols(data.Columns)} + 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, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Images) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Add images to populate the fan gallery.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, img := range data.Images { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if img.Src != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"") ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"\" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if img.Caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Caption) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_fan.templ`, Line: 44, Col: 22} + } + _, 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, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5554714 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.dev.alexdunmow.com/block/themes/art-deco + +go 1.26.4 + +require ( + git.dev.alexdunmow.com/block/core v0.11.1 + 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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26aea2c --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +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.11.1 h1:5b3Ps9CLor2FGyxw/Qovt27AGZKR5Xi1JZGi/TfliTA= +git.dev.alexdunmow.com/block/core v0.11.1/go.mod h1:ZwzEOxRDLDfrhQGqo6hLw01/C1z/aS4Dm9ljQMl0Bg4= +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= +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/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= diff --git a/heading_override.go b/heading_override.go new file mode 100644 index 0000000..985e1bb --- /dev/null +++ b/heading_override.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "context" + "strconv" +) + +// parseHeadingLevel reads "level" from a content map, defaulting to 2 and clamping to 1..6. +func parseHeadingLevel(content map[string]any) int { + if v, ok := content["level"].(float64); ok { + return clampInt(int(v), 1, 6) + } + if v, ok := content["level"].(int); ok { + return clampInt(v, 1, 6) + } + if v, ok := content["level"].(string); ok { + if i, err := strconv.Atoi(v); err == nil { + return clampInt(i, 1, 6) + } + } + return 2 +} + +// ArtDecoHeadingBlock overrides the built-in "heading" block when the Art Deco theme is active. +// Content: {"text": "...", "level": 1-6, "textClass": "optional-utility-classes"} +func ArtDecoHeadingBlock(ctx context.Context, content map[string]any) string { + data := HeadingData{ + Text: getString(content, "text"), + Level: parseHeadingLevel(content), + ExtraCls: getString(content, "textClass"), + } + + var buf bytes.Buffer + _ = artDecoHeadingComponent(data).Render(ctx, &buf) + return buf.String() +} + +// HeadingData is the typed view for the heading override component. +type HeadingData struct { + Text string + Level int + ExtraCls string +} diff --git a/heading_override.templ b/heading_override.templ new file mode 100644 index 0000000..553cbb3 --- /dev/null +++ b/heading_override.templ @@ -0,0 +1,69 @@ +package main + +// headingSizeClass returns the responsive size utility for a given heading level. +func headingSizeClass(level int) string { + switch level { + case 1: + return "text-4xl md:text-5xl" + case 2: + return "text-3xl md:text-4xl" + case 3: + return "text-2xl md:text-3xl" + case 4: + return "text-xl md:text-2xl" + case 5: + return "text-lg md:text-xl" + case 6: + return "text-base md:text-lg" + default: + return "text-3xl md:text-4xl" + } +} + +// artDecoHeadingComponent renders an Art Deco heading with caps tracking and hairline gold underline. +templ artDecoHeadingComponent(data HeadingData) { + switch data.Level { + case 1: +

{ data.Text }

+ case 2: +

{ data.Text }

+ case 3: +

{ data.Text }

+ case 4: +

{ data.Text }

+ case 5: +
{ data.Text }
+ case 6: +
{ data.Text }
+ default: +

{ data.Text }

+ } +} diff --git a/heading_override_templ.go b/heading_override_templ.go new file mode 100644 index 0000000..cd23d5a --- /dev/null +++ b/heading_override_templ.go @@ -0,0 +1,311 @@ +// 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" + +// headingSizeClass returns the responsive size utility for a given heading level. +func headingSizeClass(level int) string { + switch level { + case 1: + return "text-4xl md:text-5xl" + case 2: + return "text-3xl md:text-4xl" + case 3: + return "text-2xl md:text-3xl" + case 4: + return "text-xl md:text-2xl" + case 5: + return "text-lg md:text-xl" + case 6: + return "text-base md:text-lg" + default: + return "text-3xl md:text-4xl" + } +} + +// artDecoHeadingComponent renders an Art Deco heading with caps tracking and hairline gold underline. +func artDecoHeadingComponent(data HeadingData) 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 data.Level { + case 1: + var templ_7745c5c3_Var2 = []any{"deco-caps deco-underline", headingSizeClass(1), data.ExtraCls} + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 31, Col: 15} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 2: + var templ_7745c5c3_Var5 = []any{"deco-caps deco-underline", headingSizeClass(2), data.ExtraCls} + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 37, Col: 15} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 3: + var templ_7745c5c3_Var8 = []any{"deco-caps deco-underline", headingSizeClass(3), data.ExtraCls} + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 43, Col: 15} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 4: + var templ_7745c5c3_Var11 = []any{"deco-caps deco-underline", headingSizeClass(4), data.ExtraCls} + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 49, Col: 15} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 5: + var templ_7745c5c3_Var14 = []any{"deco-caps deco-underline", headingSizeClass(5), data.ExtraCls} + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 55, Col: 15} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 6: + var templ_7745c5c3_Var17 = []any{"deco-caps deco-underline", headingSizeClass(6), data.ExtraCls} + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 61, Col: 15} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + var templ_7745c5c3_Var20 = []any{"deco-caps deco-underline", headingSizeClass(2), data.ExtraCls} + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 67, Col: 15} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..195b329 --- /dev/null +++ b/helpers.go @@ -0,0 +1,59 @@ +package main + +// getString extracts a string value from a content map. +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 a 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 + } + return defaultVal +} + +// getSlice extracts a slice of maps from a content map. +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 a content map. +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 + } + return nil +} + +// clampInt clamps an int into [min, max] inclusive. +func clampInt(v, min, max int) int { + if v < min { + return min + } + if v > max { + return max + } + return v +} diff --git a/image_override.go b/image_override.go new file mode 100644 index 0000000..6b8932f --- /dev/null +++ b/image_override.go @@ -0,0 +1,43 @@ +package main + +import ( + "bytes" + "context" +) + +// ImageOverrideData is the typed view for the image override component. +type ImageOverrideData struct { + Src string + Alt string + Caption string + Frame string +} + +// normaliseImageFrame clamps the frame option to the supported set. +func normaliseImageFrame(v string) string { + switch v { + case "stepped", "scallop", "plain": + return v + default: + return "stepped" + } +} + +// ArtDecoImageBlock overrides the built-in "image" block when the Art Deco theme is active. +// Content: {"src": "...", "alt": "...", "caption": "...", "frame": "stepped|scallop|plain"} +func ArtDecoImageBlock(ctx context.Context, content map[string]any) string { + data := ImageOverrideData{ + Src: getString(content, "src"), + Alt: getString(content, "alt"), + Caption: getString(content, "caption"), + Frame: normaliseImageFrame(getString(content, "frame")), + } + + if data.Src == "" { + data.Src = fallbackImage() + } + + var buf bytes.Buffer + _ = artDecoImageComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/image_override.templ b/image_override.templ new file mode 100644 index 0000000..f8005f3 --- /dev/null +++ b/image_override.templ @@ -0,0 +1,30 @@ +package main + +// imageFrameClass returns the utility class for an image frame option. +func imageFrameClass(frame string) string { + switch frame { + case "scallop": + return "deco-scallop" + case "plain": + return "" + default: + return "deco-step" + } +} + +// artDecoImageComponent renders an image wrapped in the stepped ziggurat frame option. +templ artDecoImageComponent(data ImageOverrideData) { +
+
+ { +
+ if data.Caption != "" { +
+ { data.Caption } +
+ } +
+} diff --git a/image_override_templ.go b/image_override_templ.go new file mode 100644 index 0000000..23e1d59 --- /dev/null +++ b/image_override_templ.go @@ -0,0 +1,124 @@ +// 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" + +// imageFrameClass returns the utility class for an image frame option. +func imageFrameClass(frame string) string { + switch frame { + case "scallop": + return "deco-scallop" + case "plain": + return "" + default: + return "deco-step" + } +} + +// artDecoImageComponent renders an image wrapped in the stepped ziggurat frame option. +func artDecoImageComponent(data ImageOverrideData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 = []any{imageFrameClass(data.Frame)} + 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, 2, "
\"")
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + 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_override.templ`, Line: 26, Col: 18} + } + _, 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, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/marquee_hero.go b/marquee_hero.go new file mode 100644 index 0000000..4e374c1 --- /dev/null +++ b/marquee_hero.go @@ -0,0 +1,43 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// MarqueeHeroBlockMeta defines the marquee hero block. +var MarqueeHeroBlockMeta = blocks.BlockMeta{ + Key: "marquee_hero", + Title: "Marquee Hero", + Description: "Wide hero with stepped ziggurat frame and sunburst overlay.", + Source: "art-deco", +} + +// MarqueeHeroData is the typed view for the marquee hero component. +type MarqueeHeroData struct { + Eyebrow string + Title string + Subtitle string + CTALabel string + CTAHref string + Image string +} + +// MarqueeHeroBlock renders the wide hero with stepped frame and sunburst overlay. +// Content: {"eyebrow": "...", "title": "...", "subtitle": "...", "ctaLabel": "...", "ctaHref": "...", "image": "..."} +func MarqueeHeroBlock(ctx context.Context, content map[string]any) string { + data := MarqueeHeroData{ + Eyebrow: getString(content, "eyebrow"), + Title: getString(content, "title"), + Subtitle: getString(content, "subtitle"), + CTALabel: getString(content, "ctaLabel"), + CTAHref: getString(content, "ctaHref"), + Image: getString(content, "image"), + } + + var buf bytes.Buffer + _ = marqueeHeroComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/marquee_hero.templ b/marquee_hero.templ new file mode 100644 index 0000000..ee4a833 --- /dev/null +++ b/marquee_hero.templ @@ -0,0 +1,42 @@ +package main + +// marqueeHeroComponent renders the wide hero with stepped ziggurat frame and sunburst overlay. +templ marqueeHeroComponent(data MarqueeHeroData) { +
+ if data.Image != "" { + + } + +
+
+ if data.Eyebrow != "" { +

{ data.Eyebrow }

+ } + if data.Title != "" { +

+ { data.Title } +

+ } + if data.Subtitle != "" { +
+ @templ.Raw(data.Subtitle) +
+ } + if data.CTALabel != "" && data.CTAHref != "" { + + { data.CTALabel } + + } +
+
+
+} diff --git a/marquee_hero_templ.go b/marquee_hero_templ.go new file mode 100644 index 0000000..089d3df --- /dev/null +++ b/marquee_hero_templ.go @@ -0,0 +1,152 @@ +// 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" + +// marqueeHeroComponent renders the wide hero with stepped ziggurat frame and sunburst overlay. +func marqueeHeroComponent(data MarqueeHeroData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Image != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
\"\"
") + 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 + } + if data.Eyebrow != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Eyebrow) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee_hero.templ`, Line: 16, Col: 89} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee_hero.templ`, Line: 23, Col: 18} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Subtitle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.Subtitle).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.CTALabel != "" && data.CTAHref != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.CTALabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee_hero.templ`, Line: 36, Col: 21} + } + _, 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, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/menu.go b/menu.go new file mode 100644 index 0000000..9560d12 --- /dev/null +++ b/menu.go @@ -0,0 +1,52 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// MenuBlockMeta defines the tasting menu block. +var MenuBlockMeta = blocks.BlockMeta{ + Key: "menu", + Title: "Tasting Menu", + Description: "Symmetric two-column menu card listing courses with description and price.", + Source: "art-deco", +} + +// MenuCourse is a single course on the menu. +type MenuCourse struct { + Name string + Description string + Price string +} + +// MenuData is the typed view for the menu component. +type MenuData struct { + Title string + Courses []MenuCourse +} + +// MenuBlock renders a tasting menu card. +// Content: {"title": "...", "courses": [{"name": "...", "description": "...", "price": "..."}]} +func MenuBlock(ctx context.Context, content map[string]any) string { + items := getSlice(content, "courses") + courses := make([]MenuCourse, 0, len(items)) + for _, item := range items { + courses = append(courses, MenuCourse{ + Name: getString(item, "name"), + Description: getString(item, "description"), + Price: getString(item, "price"), + }) + } + + data := MenuData{ + Title: getString(content, "title"), + Courses: courses, + } + + var buf bytes.Buffer + _ = menuComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/menu.templ b/menu.templ new file mode 100644 index 0000000..31a9a7d --- /dev/null +++ b/menu.templ @@ -0,0 +1,50 @@ +package main + +// menuComponent renders the symmetric two-column menu card. +templ menuComponent(data MenuData) { +
+
+
+ if data.Title != "" { +

+ { data.Title } +

+ } + if len(data.Courses) == 0 { +

Add courses to start composing this menu.

+ } else { +
    + for _, course := range data.Courses { +
  • +
    +

    + { course.Name } +

    + if course.Description != "" { +
    + @templ.Raw(course.Description) +
    + } +
    + if course.Price != "" { + + { course.Price } + + } +
  • + } +
+ } +
+
+
+} diff --git a/menu_templ.go b/menu_templ.go new file mode 100644 index 0000000..3373896 --- /dev/null +++ b/menu_templ.go @@ -0,0 +1,139 @@ +// 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" + +// menuComponent renders the symmetric two-column menu card. +func menuComponent(data MenuData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu.templ`, Line: 13, Col: 18} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Courses) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Add courses to start composing this menu.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, course := range data.Courses { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(course.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu.templ`, Line: 27, Col: 23} + } + _, 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, "

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if course.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(course.Description).Render(ctx, templ_7745c5c3_Buffer) + 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 + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if course.Price != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(course.Price) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu.templ`, Line: 40, Col: 24} + } + _, 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, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..43d7382 --- /dev/null +++ b/plugin.mod @@ -0,0 +1,12 @@ +[plugin] +name = "art-deco" +display_name = "Art Deco" +scope = "@themes" +version = "0.1.0" +description = "Luxury Art Deco theme with champagne gold gradients, jet black, ivory and geometric fan motifs for hotels, fine dining, weddings and jewellers." +kind = "theme" +categories = ["templates"] +tags = ["luxury", "gold", "hospitality", "hotel", "wedding", "jewellery", "vintage", "glam", "fine-dining"] + +[compatibility] +block_core = ">=0.11.0 <0.12.0" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..51f6d84 --- /dev/null +++ b/presets.json @@ -0,0 +1,110 @@ +[ + { + "id": "champagne-noir", + "name": "Champagne Noir", + "description": "Jet black lifted by champagne gold — the default Art Deco palette.", + "theme": { + "mode": "both", + "lightColors": { + "background": "40 30% 96%", + "foreground": "40 20% 10%", + "card": "40 25% 98%", + "cardForeground": "40 20% 10%", + "popover": "40 25% 98%", + "popoverForeground": "40 20% 10%", + "primary": "42 55% 52%", + "primaryForeground": "40 25% 8%", + "secondary": "40 15% 90%", + "secondaryForeground": "40 20% 12%", + "muted": "40 12% 92%", + "mutedForeground": "40 12% 38%", + "accent": "36 70% 60%", + "accentForeground": "40 30% 8%", + "destructive": "0 70% 45%", + "destructiveForeground": "40 25% 96%", + "border": "42 30% 78%", + "input": "42 25% 82%", + "ring": "42 65% 55%" + }, + "darkColors": { + "background": "40 8% 6%", + "foreground": "42 35% 92%", + "card": "40 10% 10%", + "cardForeground": "42 35% 92%", + "popover": "40 10% 10%", + "popoverForeground": "42 35% 92%", + "primary": "42 65% 58%", + "primaryForeground": "40 25% 8%", + "secondary": "40 12% 14%", + "secondaryForeground": "42 30% 88%", + "muted": "40 10% 12%", + "mutedForeground": "40 12% 62%", + "accent": "36 80% 62%", + "accentForeground": "40 30% 8%", + "destructive": "0 72% 52%", + "destructiveForeground": "40 25% 96%", + "border": "42 30% 22%", + "input": "42 20% 18%", + "ring": "42 65% 58%" + } + } + }, + { + "id": "ivory-rose", + "name": "Ivory & Rose Gold", + "description": "Soft ivory with rose-gold flourishes for weddings and jewellers.", + "theme": { + "mode": "light", + "lightColors": { + "background": "36 40% 97%", + "foreground": "20 15% 14%", + "card": "36 35% 99%", + "cardForeground": "20 15% 14%", + "popover": "36 35% 99%", + "popoverForeground": "20 15% 14%", + "primary": "18 55% 58%", + "primaryForeground": "36 40% 97%", + "secondary": "30 35% 92%", + "secondaryForeground": "20 15% 14%", + "muted": "30 25% 94%", + "mutedForeground": "20 12% 40%", + "accent": "15 65% 65%", + "accentForeground": "20 20% 8%", + "destructive": "0 70% 45%", + "destructiveForeground": "36 40% 97%", + "border": "25 30% 82%", + "input": "25 25% 86%", + "ring": "18 55% 58%" + } + } + }, + { + "id": "velvet-onyx", + "name": "Velvet Onyx", + "description": "Deep velvet onyx with gold and rose stage-lights for theatre and after-dark dining.", + "theme": { + "mode": "dark", + "darkColors": { + "background": "260 20% 6%", + "foreground": "42 40% 92%", + "card": "260 20% 10%", + "cardForeground": "42 40% 92%", + "popover": "260 20% 10%", + "popoverForeground": "42 40% 92%", + "primary": "42 70% 60%", + "primaryForeground": "260 25% 8%", + "secondary": "260 18% 14%", + "secondaryForeground": "42 30% 88%", + "muted": "260 15% 12%", + "mutedForeground": "42 15% 60%", + "accent": "320 35% 55%", + "accentForeground": "42 40% 95%", + "destructive": "0 72% 52%", + "destructiveForeground": "42 40% 95%", + "border": "260 20% 22%", + "input": "260 18% 18%", + "ring": "42 70% 60%" + } + } + } +] diff --git a/press_quote.go b/press_quote.go new file mode 100644 index 0000000..609495d --- /dev/null +++ b/press_quote.go @@ -0,0 +1,39 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// PressQuoteBlockMeta defines the press quote block. +var PressQuoteBlockMeta = blocks.BlockMeta{ + Key: "press_quote", + Title: "Press Quote", + Description: "Italic Cormorant pull-quote between fan dividers, optional star rating.", + Source: "art-deco", +} + +// PressQuoteData is the typed view for the press quote component. +type PressQuoteData struct { + Quote string + Source string + Stars int +} + +// PressQuoteBlock renders the italic pull-quote framed by fan dividers. +// Content: {"quote": "...", "source": "...", "stars": 5} +func PressQuoteBlock(ctx context.Context, content map[string]any) string { + stars := clampInt(getInt(content, "stars", 0), 0, 5) + + data := PressQuoteData{ + Quote: getString(content, "quote"), + Source: getString(content, "source"), + Stars: stars, + } + + var buf bytes.Buffer + _ = pressQuoteComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/press_quote.templ b/press_quote.templ new file mode 100644 index 0000000..967a463 --- /dev/null +++ b/press_quote.templ @@ -0,0 +1,39 @@ +package main + +// pressQuoteComponent renders the italic Cormorant pull-quote between fan dividers. +templ pressQuoteComponent(data PressQuoteData) { +
+
+ +
+ if data.Quote != "" { + @templ.Raw(data.Quote) + } else { + Add a press quote to populate this block. + } +
+ if data.Stars > 0 { +
+ for i := 0; i < data.Stars; i++ { + + } + for i := data.Stars; i < 5; i++ { + + } +
+ } + if data.Source != "" { +

+ { data.Source } +

+ } + +
+
+} diff --git a/press_quote_helpers.go b/press_quote_helpers.go new file mode 100644 index 0000000..3939570 --- /dev/null +++ b/press_quote_helpers.go @@ -0,0 +1,8 @@ +package main + +import "strconv" + +// starsAttr renders an int into a stable data-stars attribute string. +func starsAttr(n int) string { + return strconv.Itoa(n) +} diff --git a/press_quote_templ.go b/press_quote_templ.go new file mode 100644 index 0000000..e5bc4e8 --- /dev/null +++ b/press_quote_templ.go @@ -0,0 +1,114 @@ +// 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" + +// pressQuoteComponent renders the italic Cormorant pull-quote between fan dividers. +func pressQuoteComponent(data PressQuoteData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Quote != "" { + templ_7745c5c3_Err = templ.Raw(data.Quote).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Add a press quote to populate this block.") + 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 data.Stars > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i := 0; i < data.Stars; i++ { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for i := data.Stars; i < 5; i++ { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Source != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Source) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `press_quote.templ`, Line: 33, Col: 18} + } + _, 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, 10, "

") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/register.go b/register.go new file mode 100644 index 0000000..e0a56e6 --- /dev/null +++ b/register.go @@ -0,0 +1,174 @@ +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 into a templates.TemplateFunc. +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 up the Art Deco system template, its page templates, blocks, +// template overrides and email wrapper. +func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { + // Schemas must be loaded BEFORE any br.Register call so block schemas bind. + if err := br.LoadSchemasFromFS(Schemas()); err != nil { + return err + } + + // System template metadata. + tr.RegisterSystemTemplate(templates.SystemTemplateMeta{ + Key: "art-deco", + Title: "Art Deco", + Description: "Luxury Art Deco theme with champagne gold gradients, jet black, ivory and geometric fan motifs for hotels, fine dining, weddings and jewellers.", + }) + + // Page templates. + if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{ + Key: "default", + Title: "Default", + Description: "Centered symmetric layout with gold rule header and footer.", + Slots: []string{"header", "main", "footer"}, + }, wrap(RenderArtDecoDefault)); err != nil { + return err + } + + if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{ + Key: "landing", + Title: "Marquee Landing", + Description: "Wide hero with sunburst overlay, reservation strip, CTA.", + Slots: []string{"marquee", "main", "cta", "footer"}, + }, wrap(RenderArtDecoLanding)); err != nil { + return err + } + + if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{ + Key: "article", + Title: "Editorial", + Description: "Narrow editorial column for press, menus and stories.", + Slots: []string{"header", "main", "aside", "footer"}, + }, wrap(RenderArtDecoArticle)); err != nil { + return err + } + + if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{ + Key: "full-width", + Title: "Grand Ballroom", + Description: "Edge-to-edge gallery layout for venues and lookbooks.", + Slots: []string{"header", "main", "footer"}, + }, wrap(RenderArtDecoFullWidth)); err != nil { + return err + } + + // Theme-specific blocks. + br.Register(ReservationBlockMeta, ReservationBlock) + br.Register(MenuBlockMeta, MenuBlock) + br.Register(GalleryFanBlockMeta, GalleryFanBlock) + br.Register(DividerFanBlockMeta, DividerFanBlock) + br.Register(FooterBlockMeta, FooterBlock) + br.Register(MarqueeHeroBlockMeta, MarqueeHeroBlock) + br.Register(PressQuoteBlockMeta, PressQuoteBlock) + + // Template overrides — active only while the Art Deco system template is selected. + br.RegisterTemplateOverride("art-deco", "heading", ArtDecoHeadingBlock) + br.RegisterTemplateOverride("art-deco", "text", ArtDecoTextBlock) + br.RegisterTemplateOverride("art-deco", "button", ArtDecoButtonBlock) + br.RegisterTemplateOverride("art-deco", "image", ArtDecoImageBlock) + + // Branded email wrapper. + tr.RegisterEmailWrapper("art-deco", ArtDecoEmailWrapper) + + return nil +} + +// DefaultMasterPages returns the master pages provisioned the first time the plugin loads. +func DefaultMasterPages() []plugin.MasterPageDefinition { + return []plugin.MasterPageDefinition{ + { + Key: "art-deco:default-master", + Title: "Art Deco Default Master", + PageTemplates: []string{"default", "article"}, + Blocks: []plugin.MasterPageBlock{ + { + BlockKey: "navbar", + Title: "Brass Navigation", + Content: map[string]any{"menuName": "main"}, + Slot: "header", + SortOrder: 0, + }, + { + BlockKey: "art-deco:divider_fan", + Title: "Header Fan Divider", + Content: map[string]any{"variant": "sunburst"}, + Slot: "header", + SortOrder: 1, + }, + { + BlockKey: "slot", + Title: "Main Content", + Content: map[string]any{"slotName": "main", "placeholder": "Page content"}, + Slot: "main", + SortOrder: 0, + }, + { + BlockKey: "art-deco:footer", + Title: "Site Footer", + Content: map[string]any{"address": "", "reservationsEmail": "", "showSocial": "true"}, + Slot: "footer", + SortOrder: 0, + }, + }, + }, + { + Key: "art-deco:marquee-master", + Title: "Art Deco Marquee Master", + PageTemplates: []string{"landing", "full-width"}, + Blocks: []plugin.MasterPageBlock{ + { + BlockKey: "navbar", + Title: "Brass Navigation", + Content: map[string]any{"menuName": "main"}, + Slot: "marquee", + SortOrder: 0, + }, + { + BlockKey: "art-deco:reservation", + Title: "Reservation Strip", + Content: map[string]any{"phone": "", "openTable": ""}, + Slot: "marquee", + SortOrder: 1, + }, + { + BlockKey: "slot", + Title: "Main Content", + Content: map[string]any{"slotName": "main"}, + Slot: "main", + SortOrder: 0, + }, + { + BlockKey: "art-deco:divider_fan", + Title: "CTA Fan Divider", + Content: map[string]any{"variant": "scallop"}, + Slot: "cta", + SortOrder: 0, + }, + { + BlockKey: "art-deco:footer", + Title: "Site Footer", + Content: map[string]any{"showSocial": "true"}, + Slot: "footer", + SortOrder: 0, + }, + }, + }, + } +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..1a78bf9 --- /dev/null +++ b/registration.go @@ -0,0 +1,25 @@ +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 Art Deco theme. +var Registration = plugin.PluginRegistration{ + Name: "art-deco", + 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() }, +} diff --git a/reservation.go b/reservation.go new file mode 100644 index 0000000..822f317 --- /dev/null +++ b/reservation.go @@ -0,0 +1,37 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// ReservationBlockMeta defines the reservation strip block. +var ReservationBlockMeta = blocks.BlockMeta{ + Key: "reservation", + Title: "Reservation Strip", + Description: "Sticky gold-on-black reservation strip with phone, OpenTable CTA and trading hours.", + Source: "art-deco", +} + +// ReservationData is the typed view used by the templ component. +type ReservationData struct { + Phone string + OpenTable string + Hours []string +} + +// ReservationBlock renders a reservation strip. +// Content: {"phone": "...", "openTable": "...", "hours": ["Tue–Sat · 6pm–late", ...]} +func ReservationBlock(ctx context.Context, content map[string]any) string { + data := ReservationData{ + Phone: getString(content, "phone"), + OpenTable: getString(content, "openTable"), + Hours: getStringSlice(content, "hours"), + } + + var buf bytes.Buffer + _ = reservationComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/reservation.templ b/reservation.templ new file mode 100644 index 0000000..dec70c3 --- /dev/null +++ b/reservation.templ @@ -0,0 +1,36 @@ +package main + +// reservationComponent renders the sticky reservation strip. +// Empty inputs render a shell with a "Reservations" label so the strip is never blank. +templ reservationComponent(data ReservationData) { +
+
+
+ Reservations + if data.Phone != "" { + + { data.Phone } + } +
+ if len(data.Hours) > 0 { +
+ for i, line := range data.Hours { + if i > 0 { + + } + { line } + } +
+ } + if data.OpenTable != "" { + + Book a Table + + } +
+
+} diff --git a/reservation_templ.go b/reservation_templ.go new file mode 100644 index 0000000..ed12eb8 --- /dev/null +++ b/reservation_templ.go @@ -0,0 +1,136 @@ +// 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" + +// reservationComponent renders the sticky reservation strip. +// Empty inputs render a shell with a "Reservations" label so the strip is never blank. +func reservationComponent(data ReservationData) 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Hours) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, line := range data.Hours { + if i > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "·") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(line) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reservation.templ`, Line: 25, Col: 18} + } + _, 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, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.OpenTable != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Book a Table") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/schemas/divider_fan.schema.json b/schemas/divider_fan.schema.json new file mode 100644 index 0000000..1ba530b --- /dev/null +++ b/schemas/divider_fan.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Fan Divider", + "description": "Pure-CSS geometric gold divider — sunburst, scallop or ziggurat.", + "type": "object", + "properties": { + "variant": { + "type": "string", + "title": "Variant", + "description": "Choose the divider motif.", + "x-editor": "select", + "default": "sunburst", + "enum": ["sunburst", "scallop", "ziggurat"] + } + } +} diff --git a/schemas/footer.schema.json b/schemas/footer.schema.json new file mode 100644 index 0000000..2ad2443 --- /dev/null +++ b/schemas/footer.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Footer", + "description": "Centered Art Deco footer with gold rule, address, reservations email and optional social row.", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "Address", + "description": "Multi-line postal address.", + "x-editor": "textarea", + "default": "" + }, + "reservationsEmail": { + "type": "string", + "title": "Reservations email", + "description": "Public email shown in the footer for reservations enquiries.", + "x-editor": "text", + "default": "" + }, + "showSocial": { + "type": "string", + "title": "Show social row", + "description": "Whether to render the social-icon row.", + "x-editor": "select", + "default": "true", + "enum": ["true", "false"] + }, + "menuName": { + "type": "string", + "title": "Footer menu", + "description": "Name of the menu to render in the footer.", + "x-editor": "menu-select", + "default": "" + } + } +} diff --git a/schemas/gallery_fan.schema.json b/schemas/gallery_fan.schema.json new file mode 100644 index 0000000..25a2678 --- /dev/null +++ b/schemas/gallery_fan.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Fan Gallery", + "description": "Mirrored gallery framed by sunburst rays; collapses to single column under 640px.", + "type": "object", + "properties": { + "images": { + "type": "array", + "title": "Images", + "description": "Images to mirror across the fan.", + "x-editor": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "src": { + "type": "string", + "title": "Image", + "x-editor": "media" + }, + "caption": { + "type": "string", + "title": "Caption", + "x-editor": "text" + } + } + } + }, + "columns": { + "type": "integer", + "title": "Columns", + "description": "Columns on desktop (2, 3 or 4).", + "x-editor": "select", + "default": 3, + "enum": [2, 3, 4] + } + } +} diff --git a/schemas/marquee_hero.schema.json b/schemas/marquee_hero.schema.json new file mode 100644 index 0000000..b84c477 --- /dev/null +++ b/schemas/marquee_hero.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Marquee Hero", + "description": "Wide hero with stepped ziggurat frame and sunburst overlay.", + "type": "object", + "properties": { + "eyebrow": { + "type": "string", + "title": "Eyebrow", + "description": "Small caps line above the title.", + "x-editor": "text", + "default": "" + }, + "title": { + "type": "string", + "title": "Title", + "description": "Display headline in Italiana caps.", + "x-editor": "text", + "default": "" + }, + "subtitle": { + "type": "string", + "title": "Subtitle", + "description": "Long-form supporting copy (rich text).", + "x-editor": "richtext", + "default": "" + }, + "ctaLabel": { + "type": "string", + "title": "CTA label", + "x-editor": "text", + "default": "" + }, + "ctaHref": { + "type": "string", + "title": "CTA URL", + "x-editor": "link", + "default": "" + }, + "image": { + "type": "string", + "title": "Background image", + "x-editor": "media", + "default": "" + } + } +} diff --git a/schemas/menu.schema.json b/schemas/menu.schema.json new file mode 100644 index 0000000..6c32192 --- /dev/null +++ b/schemas/menu.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Tasting Menu", + "description": "Symmetric two-column menu card listing courses with description and price.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Menu title", + "description": "e.g. 'Chef's Tasting' or 'Cellar List'.", + "x-editor": "text", + "default": "" + }, + "courses": { + "type": "array", + "title": "Courses", + "description": "Ordered list of courses on the menu.", + "x-editor": "collection", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Course name", + "x-editor": "text" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Tasting note for the course.", + "x-editor": "richtext" + }, + "price": { + "type": "string", + "title": "Price", + "description": "Plain string — e.g. '$48' or 'Market'.", + "x-editor": "text" + } + }, + "required": ["name"] + } + } + } +} diff --git a/schemas/press_quote.schema.json b/schemas/press_quote.schema.json new file mode 100644 index 0000000..80135c3 --- /dev/null +++ b/schemas/press_quote.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Press Quote", + "description": "Italic Cormorant pull-quote between fan dividers.", + "type": "object", + "properties": { + "quote": { + "type": "string", + "title": "Quote", + "description": "The press quotation (rich text).", + "x-editor": "richtext", + "default": "" + }, + "source": { + "type": "string", + "title": "Source", + "description": "Attribution line — e.g. 'The Daily, March 2024'.", + "x-editor": "text", + "default": "" + }, + "stars": { + "type": "integer", + "title": "Stars", + "description": "Filled-star count, clamped to 0–5.", + "x-editor": "number", + "default": 0, + "minimum": 0, + "maximum": 5 + } + } +} diff --git a/schemas/reservation.schema.json b/schemas/reservation.schema.json new file mode 100644 index 0000000..71e446c --- /dev/null +++ b/schemas/reservation.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Reservation Strip", + "description": "Sticky gold-on-black reservation strip with phone, OpenTable CTA, and trading hours.", + "type": "object", + "properties": { + "phone": { + "type": "string", + "title": "Reservations phone", + "description": "International-format phone number for the reservations desk.", + "x-editor": "text", + "default": "" + }, + "openTable": { + "type": "string", + "title": "OpenTable URL", + "description": "Deep link to the OpenTable widget or booking page.", + "x-editor": "link", + "default": "" + }, + "hours": { + "type": "array", + "title": "Trading hours", + "description": "Free-form lines such as 'Tue–Sat · 6pm–late'.", + "x-editor": "array", + "default": [], + "items": { + "type": "string", + "x-editor": "text" + } + } + } +} diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..00d01ed --- /dev/null +++ b/template.templ @@ -0,0 +1,270 @@ +package main + +import ( + "context" + + "git.dev.alexdunmow.com/block/core/templates/bn" +) + +// PageData captures the inputs needed to render an Art Deco page template. +type PageData struct { + Title string + Slots map[string]string + ThemeMode string + ThemeCSS string + SiteSettings bn.SiteSettingsData + PageMeta bn.PageMeta + StructuredData string + CSSHash string + PageviewNonce string + EngagementConfig bn.EngagementConfig +} + +// parsePageData reads a page-render doc into the typed PageData used by templ. +func parsePageData(doc map[string]any) PageData { + 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 + } + + return PageData{ + Title: title, + Slots: slots, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + SiteSettings: bn.ParseSiteSettings(doc), + PageMeta: bn.ParsePageMeta(doc), + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: bn.ParseEngagementConfig(doc), + } +} + +// htmlClassForMode returns the html-element class hint that flips dark mode. +func htmlClassForMode(mode string) string { + if mode == "dark" { + return "dark" + } + return "" +} + +// ArtDecoDefault — centered symmetric layout with gold rule header and footer. +templ ArtDecoDefault(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/art-deco/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+
+ @templ.Raw(data.Slots["header"]) +
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

No content blocks assigned to this page.

+
+ } +
+
+
+ @templ.Raw(data.Slots["footer"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ArtDecoLanding — Marquee Landing: wide hero, reservation strip, CTA. +templ ArtDecoLanding(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/art-deco/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["marquee"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } +
+
+ @templ.Raw(data.Slots["cta"]) +
+
+
+ @templ.Raw(data.Slots["footer"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ArtDecoArticle — narrow editorial column for press, menus and stories. +templ ArtDecoArticle(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/art-deco/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+
+ @templ.Raw(data.Slots["header"]) +
+
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } else { +
+

No content blocks assigned to this page.

+
+ } +
+ +
+
+
+ @templ.Raw(data.Slots["footer"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ArtDecoFullWidth — Grand Ballroom: edge-to-edge gallery layout. +templ ArtDecoFullWidth(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/art-deco/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

No content blocks assigned to this page.

+
+ } +
+
+
+ @templ.Raw(data.Slots["footer"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Page-render funcs adapt typed PageData into templ.Component values. + +func RenderArtDecoDefault(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoDefault(parsePageData(doc)) +} + +func RenderArtDecoLanding(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoLanding(parsePageData(doc)) +} + +func RenderArtDecoArticle(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoArticle(parsePageData(doc)) +} + +func RenderArtDecoFullWidth(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoFullWidth(parsePageData(doc)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..46bb3e0 --- /dev/null +++ b/template_templ.go @@ -0,0 +1,607 @@ +// 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" +) + +// PageData captures the inputs needed to render an Art Deco page template. +type PageData struct { + Title string + Slots map[string]string + ThemeMode string + ThemeCSS string + SiteSettings bn.SiteSettingsData + PageMeta bn.PageMeta + StructuredData string + CSSHash string + PageviewNonce string + EngagementConfig bn.EngagementConfig +} + +// parsePageData reads a page-render doc into the typed PageData used by templ. +func parsePageData(doc map[string]any) PageData { + 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 + } + + return PageData{ + Title: title, + Slots: slots, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + SiteSettings: bn.ParseSiteSettings(doc), + PageMeta: bn.ParsePageMeta(doc), + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: bn.ParseEngagementConfig(doc), + } +} + +// htmlClassForMode returns the html-element class hint that flips dark mode. +func htmlClassForMode(mode string) string { + if mode == "dark" { + return "dark" + } + return "" +} + +// ArtDecoDefault — centered symmetric layout with gold rule header and footer. +func ArtDecoDefault(data PageData) 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 = []any{htmlClassForMode(data.ThemeMode)} + 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, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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/art-deco/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 + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + 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, 5, "
") + 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, 6, "
") + 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, 7, "

No content blocks assigned to this page.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + 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, 9, "
") + 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, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// ArtDecoLanding — Marquee Landing: wide hero, reservation strip, CTA. +func ArtDecoLanding(data PageData) 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, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 = []any{htmlClassForMode(data.ThemeMode)} + 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, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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/art-deco/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 + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + 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, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.Slots["marquee"]).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + 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, 17, "
") + 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, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + 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, 20, "
") + 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, 21, "
") + 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, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// ArtDecoArticle — narrow editorial column for press, menus and stories. +func ArtDecoArticle(data PageData) 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_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 = []any{htmlClassForMode(data.ThemeMode)} + 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, 24, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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/art-deco/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 + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + 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, 27, "
") + 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, 28, "
") + 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, 29, "
") + 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, 30, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

No content blocks assigned to this page.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + 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, 34, "
") + 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, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// ArtDecoFullWidth — Grand Ballroom: edge-to-edge gallery layout. +func ArtDecoFullWidth(data PageData) 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_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 = []any{htmlClassForMode(data.ThemeMode)} + 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, 37, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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/art-deco/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 + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") + 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, 40, "
") + 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, 41, "
") + 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, 42, "

No content blocks assigned to this page.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + 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, 44, "
") + 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, 45, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Page-render funcs adapt typed PageData into templ.Component values. + +func RenderArtDecoDefault(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoDefault(parsePageData(doc)) +} + +func RenderArtDecoLanding(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoLanding(parsePageData(doc)) +} + +func RenderArtDecoArticle(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoArticle(parsePageData(doc)) +} + +func RenderArtDecoFullWidth(ctx context.Context, doc map[string]any) templ.Component { + return ArtDecoFullWidth(parsePageData(doc)) +} + +var _ = templruntime.GeneratedTemplate diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..244252f --- /dev/null +++ b/text_override.go @@ -0,0 +1,26 @@ +package main + +import ( + "bytes" + "context" +) + +// ArtDecoTextBlock overrides the built-in "text" block when the Art Deco theme is active. +// Renders prose in Cormorant body with a classical italic drop-cap on the first paragraph. +// Content: {"text": "", "class": "optional-extra-class"} +func ArtDecoTextBlock(ctx context.Context, content map[string]any) string { + data := TextOverrideData{ + Text: getString(content, "text"), + Class: getString(content, "class"), + } + + var buf bytes.Buffer + _ = artDecoTextComponent(data).Render(ctx, &buf) + return buf.String() +} + +// TextOverrideData is the typed view for the text override component. +type TextOverrideData struct { + Text string + Class string +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..e53f46a --- /dev/null +++ b/text_override.templ @@ -0,0 +1,13 @@ +package main + +// artDecoTextComponent renders rich text body copy with a classical italic dropcap. +templ artDecoTextComponent(data TextOverrideData) { +
+ @templ.Raw(data.Text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..64fd744 --- /dev/null +++ b/text_override_templ.go @@ -0,0 +1,67 @@ +// 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" + +// artDecoTextComponent renders rich text body copy with a classical italic dropcap. +func artDecoTextComponent(data TextOverrideData) 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) + var templ_7745c5c3_Var2 = []any{"deco-dropcap prose max-w-none", data.Class} + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.Text).Render(ctx, templ_7745c5c3_Buffer) + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate