Compare commits

...

20 Commits
v0.1.1 ... main

Author SHA1 Message Date
Alex Dunmow
c04ea9cbf4 bump to 0.5.0 2026-07-06 20:46:29 +08:00
Alex Dunmow
396b3b443f feat(art-deco): reshoot gallery with bundled Cinzel/Cormorant typography (home/about/article x light+dark)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:46:21 +08:00
Alex Dunmow
1fdc6ae792 bump to 0.4.1 2026-07-06 20:43:04 +08:00
Alex Dunmow
1b7f273118 feat(art-deco): bundle Cinzel/Cormorant @font-face + declare required_icon_packs
Host does not consume LoadedPlugin.BundledFonts, so declare @font-face rules
against served /templates/art-deco/fonts/*.woff2 and point preset
typography.fontHeading/body at template:art-deco:<Family>. Declare
required_icon_packs [lucide, simple-icons] so sprite URLs resolve on fresh install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:43:04 +08:00
Alex Dunmow
025b13dd48 bump to 0.4.0 2026-07-06 15:54:13 +08:00
Alex Dunmow
f2c8b3643c fix(art-deco): Wave B remediation — drop stray reservation strip + header divider, unbreak hero override, size icon SVGs, richer seed articles + gold-room media
- master_pages: remove orphaned reservation strip (marquee) and header fan divider (default) — the strip stacked under the navbar as a duplicate full-width band
- hero override: eyebrow/headline/sub/desc use |get:"text" so bare-string fields no longer error the whole block to empty (was rendering a dead band above home content)
- divider override: replace ::icon:sm:: shortcode (rendered an unsized <use> SVG that ballooned — the broken About 'tally-mark' divider) with an explicit width/height-sized inline SVG
- SVG geometry guard: explicit width/height on all <use> icon SVGs across overrides (testimonial stars + inherited set)
- seed: expand 3 blog articles to ~9 paragraphs (1926 supper-club voice) so articles fill the viewport; wire gold-room.svg media into the private-dining block
- re-capture 6 gallery shots (home/about/article x light+dark) on a fresh reprovisioned instance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:53:47 +08:00
Alex Dunmow
e67437985e bump to 0.3.1 2026-07-06 15:47:03 +08:00
Alex Dunmow
a441b4cccf bump to 0.3.0 2026-07-06 09:06:16 +08:00
Alex Dunmow
a1acb0f886 feat: Wave B gallery screenshots (home/about/article × light+dark) + demo-home preview
Captured from a live dev gallery instance with demo content installed
(WO-TF-027 pilot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:06:09 +08:00
Alex Dunmow
620644f386 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
def29b6e9c refactor: replace hand-rolled utility CSS with Tailwind classes (host build now scans theme templates) 2026-07-05 17:54:25 +08:00
Alex Dunmow
3b3adfdf43 fix(art-deco): remediate empty-media balloons, dead whitespace + mobile layout
- Hero: image-less heroes no longer reserve min-h-[75vh]; use guaranteed
  .deco-hero-compact padding (kills the tall empty band above the fold).
- Landing template: stop double-wrapping main in max-w-5xl/py-16/text-center
  (double padding + clamped full-bleed sections + forced centering); render
  section blocks and the footer full-bleed so each self-manages width/rhythm.
- private_dining: with no image, collapse the 2-col grid to a single centred
  column instead of stranding text beside an empty media half; route the
  capacity/minimum figures through guaranteed .deco-stats gap so values no
  longer collide ("14From").
- Reservation strip: populate phone/hours/book link so it reads as a proper
  gilded bar instead of a cramped empty dark strip under the nav.
- default/article templates: full-bleed header + footer (navbar no longer
  narrowed to max-w-3xl/5xl; dark footer spans edge-to-edge, not a floating
  box with dead vertical space).
- chef_signature: trim md:py-24 -> md:py-20 for tighter section rhythm.
- Layout-critical rules added as explicit CSS (manifest css.input_css_append
  + assets/style.css) rather than JIT-fragile theme-template Tailwind combos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:23:35 +08:00
Alex Dunmow
4e4344298c bump to 0.2.1 2026-07-05 16:44:47 +08:00
Alex Dunmow
674c2d15b3 docs: publishing README 2026-07-05 13:45:45 +08:00
Alex Dunmow
03c49a9a83 feat(art-deco): Wave A supper-club theme — 49 overrides, 7 templates, seed
WO-TF-011: dress the fine-dining / supper-club persona (gilded symmetry,
ziggurat frames, sunbursts, hairline gold rules; champagne/ivory light,
gold-on-lacquer dark) against the frozen Wave-0 builtin schemas.

Overrides (templates/overrides/art-deco/): all 49 mandatory builtin keys,
each re-skinned from the builtin default with every content field, provider
field, custom tag ({% img %}/{% button %}/{% form %}/{% signup_form %}/
{% auth_form %} wiring) and JS (navbar drawer, gallery lightbox, countdown,
stats count-up, video/audio facade, announcement dismiss) preserved verbatim.
Auth trio reproduces the untouchable /api/auth/* endpoints and field names;
captcha widget injected from _captcha_widget. manifest.yaml declares all 49.

Page templates: added blog-index, contact, auth (now 7 total) in Deco chrome.

Blocks rework (spec D5): footer/hero(marquee)/gallery/divider/pull-quote theme
blocks deleted — folded into overrides of the builtin keys. Kept persona
furniture reservation + menu; added private_dining + chef_signature (4 blocks).

Presets: 4 dual-mode (mode "both"), all 19 tokens, light AND dark tuned.
Fonts: bundle Cinzel + Cormorant Garamond (OFL) as Latin woff2 subsets in
assets/fonts/ (license recorded in OFL.txt); all font-family via var().
Email wrapper themed. required_icon_packs=["lucide"] + RECOMMENDED_ICONS.md.

Seed (demo): home/about/blog(3 posts)/contact/login pages in supper-club voice;
contact-form targets seeded contact_submissions data table; row_inserted
workflow emails admins.

Verified: ninja plugin build (codeless), verify, archive-check, and
check-safety all exit 0. Visual quality UNVERIFIED until Wave B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 10:51:02 +08:00
Alex Dunmow
a7141e39ae fix: align email wrapper vars to the engine contract; multi-line address via |lines
Email context is the established pongo-engine mapping (site_name/site_url/
support_email flat, colors.* camelCase) — the loader renders these through
the cms pongoengine, not an ad-hoc snake_case doc. Footer regains per-line
address <div>s via the host 'lines' filter (parity case footer_full now
byte-identical; 40/41 static parity, the remaining case is the
impossible-input all-colors-empty email edge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:43:06 +08:00
Alex Dunmow
5075aa81c9 feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Convert the Art Deco theme from a wasm (Go+templ) plugin to a fully codeless
.bnp: pure declaration the host runs, no plugin.wasm, no block/core dependency.

- blocks/: blocks.yaml (7 fully-qualified art-deco:* keys) + per-block
  .schema.json (moved from schemas/) + .ninjatpl transcribed byte-for-byte
  (modulo whitespace) from the templ sources.
- templates/art-deco/{default,landing,article,full-width}.ninjatpl — page
  layouts.
- templates/overrides/art-deco/{heading,text,button,image}.ninjatpl.
- templates/email/art-deco.ninjatpl.
- manifest.yaml (system/page templates, template_overrides, email_wrappers,
  theme_presets, bundled_fonts, master_pages, css input_css_append) +
  master_pages.json.
- plugin.mod bumped to 0.2.0; [compatibility] block_core dropped (codeless has
  no core dependency).
- Deleted all Go/templ/*_templ.go, go.mod/go.sum; Makefile rewritten to the
  codeless build/verify/publish targets.

Render parity verified against the prior templ output (static pongo2 harness,
whitespace-normalised): all 7 blocks, 4 overrides and the email wrapper match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:24:55 +08:00
Alex Dunmow
57e16e8ec7 chore: repo hygiene for the wasm era (WO-WZ-015 fix round)
Replace the retired .so Makefile with the wasm/.bnp convention (build /
verify / archive-check / publish via ninja plugin build), gitignore *.so
and *.bnp artifacts, and refresh the stale [compatibility] block_core
constraint to the v0.15 era.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:03:07 +08:00
Alex Dunmow
c52fce32e0 wasm port (WO-WZ-015): reactor-mode wasip1 guest, core v0.15.2
Add wasmguest.Serve(Registration) boilerplate and bump block/core to v0.15.2
for the .so->wasm migration. Built + published as .bnp to the dev registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:11:12 +08:00
Alex Dunmow
8fae1ff15c bump to 0.1.2 2026-07-03 22:04:39 +08:00
142 changed files with 2409 additions and 4636 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
tmp/ tmp/
.idea/ .idea/
.vscode/ .vscode/
*.bnp

View File

@ -1,32 +1,55 @@
# Art Deco — local build helpers (.so plugin workflow). # art-deco — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
# #
# `make` produces art-deco.so via CGO go build -buildmode=plugin. This is the # This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# local-only build used by check-safety. The full `rebuild` target intentionally # (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# does NOT ship here — production deployment goes through the upstream gotham-style # an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# Makefile, which is reserved for human-run cycles, not autonomous agents. # Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
#
# Usage:
# make build # pack art-deco-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry)
# make clean # remove build artefacts
.PHONY: all clean templ help .PHONY: build verify archive-check publish clean help
PLUGIN_NAME := art-deco PLUGIN_NAME := art-deco
TEMPL_BIN := $(HOME)/go/bin/templ NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Default target: build the .so locally. # Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
all: $(PLUGIN_NAME).so # + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build:
$(NINJA) plugin build --dir .
# Local plugin build (no container). # Validate the built .bnp exactly as the registry/loader will.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod verify:
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so . $(NINJA) plugin verify $(BNP)
# Regenerate templ Go files from .templ sources. # Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
templ: archive-check:
$(TEMPL_BIN) generate @T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK"
# Publish the prebuilt .bnp to the active registry.
# Pass --host https://my.blockninja.dev for the dev orchestrator.
publish: build
$(NINJA) plugin publish --bnp $(BNP)
# Remove build artefacts. # Remove build artefacts.
clean: clean:
rm -f $(PLUGIN_NAME).so rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help: help:
@echo "Targets:" @echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)" @echo " build Pack the codeless .bnp (no wasm)"
@echo " templ Regenerate templ Go files" @echo " verify Validate the built .bnp (loader-identical checks)"
@echo " clean Remove build artefacts" @echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry"
@echo " clean Remove build artefacts"

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Art Deco
A dressed-up theme in champagne gold over jet black, with ivory detailing and geometric fan motifs. Tall engraved headings sit above a slim serif body, so the whole thing carries the mood of a 1920s grand hotel lobby. It reads formal and glamorous, and it stays easy to read.
## Good for
We built Art Deco with hospitality and celebration in mind. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Hotels and fine-dining restaurants
- Wedding and event venues
- Jewellers and luxury boutiques
- Cocktail bars and supper clubs
## What you get
- Four color presets, each with a tuned light mode and a tuned dark mode: Champagne Noir, Ivory & Rose Gold, Velvet Onyx, and Emerald & Gilt.
- Cinzel headings and Cormorant Garamond body, bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a reservation strip, a menu, a private-dining section, and a chef's signature.
- A demo site that installs when you activate the theme: a home page, an about page, a short journal, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.

View File

@ -1,53 +1,36 @@
# Recommended Fonts — Art Deco # Recommended Fonts — Art Deco
This theme ships `fonts.json = []` (Wave-1 policy, see `themes/docs/FONTS.md`). This theme **bundles** its display and body families as subset woff2 files in
The intended typographic identity is achieved by an admin assigning the `assets/fonts/` (see `fonts.json`). They install as `source=template` rows and
following Google Fonts to the theme's typography slots after install. are pre-selected for the theme's typography slots — no admin action is required
for the intended look. The families below are the fallbacks baked into every
`var(--font-heading|body|mono, …)` call, so the theme still reads correctly if
an admin reassigns a slot.
## Picker workflow ## Bundled (shipped in this theme)
1. Sign in as an admin and open **Settings → Typography**. | Slot | Family | Weights | File(s) | License |
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. | Heading | **Cinzel** | 400, 700 | `assets/fonts/cinzel-*.woff2` | SIL OFL 1.1 |
| Body | **Cormorant Garamond** | 400, 400 italic, 600, 700 | `assets/fonts/cormorant-garamond-*.woff2` | SIL OFL 1.1 |
## Display heading — Italiana License record and copyright attribution: `assets/fonts/OFL.txt`. The woff2s are
Latin subsets of the upstream OFL families (subsetting is permitted by the OFL).
- **Source**: `google:Italiana` ## Mono slot (not bundled — assign if needed)
- **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 The theme uses `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)` for
reservation IDs and tabular figures. To match the intended look, an admin can
assign a mono family:
- **Source**: `google:Cinzel` (also acceptable per spec §3) 1. Sign in as an admin, open **Settings → Typography**.
Pick this if Italiana feels too thin at small sizes. 2. Switch to the **Google Fonts** tab and add **JetBrains Mono**.
3. Assign it to the **Mono** slot.
## Body — Cormorant Garamond If left unset, the CSS falls back to the system monospace stack.
- **Source**: `google:Cormorant Garamond` ## Alternatives (optional)
- **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 - Heading: `google:Italiana` — a thinner, higher-contrast display cut; already
the second fallback in the heading `var()` list.
- **Source**: `google:JetBrains Mono` - Body: the system serif stack (`Georgia, serif`) is the final fallback.
- **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.

24
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,24 @@
# Recommended Icons — Art Deco
This theme references icons from a single bundled pack. `plugin.mod` declares
it in `required_icon_packs`; until the loader auto-installs declared packs, an
admin should confirm it is enabled before activation.
## Required pack
- **lucide** — ships bundled with the CMS (v0.460). Enabled by default. No
action needed in most installs; if icons render as empty boxes, open
`/admin/icons` and confirm **Lucide** is installed and enabled.
## Icons used by the theme chrome and overrides
All via the sprite pipeline (`<svg><use href="/icons/lucide.svg#<name>"/></svg>`
or the `::lucide:<name>::` shorthand):
`arrow-right`, `check`, `chevron-down`, `chevron-left`, `chevron-right`, `x`,
`play`, `download`, `navigation`, `map-pin`, `diamond`, `star`, `flag`,
`globe`.
Block content that stores an icon (e.g. feature-grid items, social links,
contact channels) may reference any installed pack as a `pack:name` string;
the schemas default to `lucide` so the theme renders without extra setup.

23
assets/fonts/OFL.txt Normal file
View File

@ -0,0 +1,23 @@
Bundled fonts — license record (SIL Open Font License, Version 1.1)
====================================================================
The woff2 files in this directory are subsets (Latin) of the following
open-source families, both distributed under the SIL Open Font License 1.1
(https://openfontlicense.org). Subsetting and format conversion (TTF to
woff2) are expressly permitted by the OFL; the derived subsets remain under
the same license.
Cinzel
cinzel-400.woff2, cinzel-700.woff2
Copyright (c) Natanael Gama (https://ndiscover.com), with Reserved Font
Name "Cinzel".
Cormorant Garamond
cormorant-garamond-400.woff2, cormorant-garamond-400-italic.woff2,
cormorant-garamond-600.woff2, cormorant-garamond-700.woff2
Copyright (c) Christian Thalmann, Catharsis Fonts
(https://catharsisfonts.com), with Reserved Font Name "Cormorant".
The full SIL Open Font License 1.1 text applies to each family and is
available at https://openfontlicense.org. This file records the license and
copyright attribution as required for redistribution under the OFL.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

32
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,32 @@
# Art Deco theme block definitions (codeless .bnp, WO-WZ-021 / WO-TF-011).
# Only TRUE persona furniture remains as theme blocks. Everything that
# duplicated a builtin (footer, hero/marquee, gallery, divider, pull-quote)
# is now a template OVERRIDE of the builtin key (templates/overrides/art-deco/).
# Keys are fully qualified (art-deco:<key>) — the codeless loader registers
# definitions verbatim; the qualified keys match the data-block attributes,
# master_pages.json and seed references.
blocks:
- key: art-deco:reservation
title: Reservation Strip
description: Sticky gold-on-black reservation strip with phone, OpenTable CTA and trading hours.
category: navigation
schema: reservation.schema.json
template: reservation.ninjatpl
- key: art-deco:menu
title: Tasting Menu
description: Symmetric menu card listing courses with tasting note and price.
category: content
schema: menu.schema.json
template: menu.ninjatpl
- key: art-deco:private_dining
title: Private Dining
description: Private dining room showcase — framed image with room details and an enquiry CTA.
category: content
schema: private_dining.schema.json
template: private_dining.ninjatpl
- key: art-deco:chef_signature
title: Chef's Signature
description: Centred signature-dish feature with a chef's note between sunburst crests.
category: content
schema: chef_signature.schema.json
template: chef_signature.ninjatpl

View File

@ -0,0 +1,11 @@
{# chef_signature persona block — centred signature-dish feature with sunburst crests. Media via {% img %}. #}
<section data-block="art-deco:chef_signature" class="py-16 md:py-20 bg-background text-foreground">
<div class="mx-auto max-w-3xl px-4 text-center">
{% if eyebrow %}<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-10"></span><span class="deco-caps text-xs" style="color:hsl(var(--primary));">{{ eyebrow }}</span><span class="deco-rule w-10"></span></div>{% endif %}
{% if image %}<div class="deco-step mx-auto mb-8 max-w-md">{% img image alt=dish class="w-full h-auto object-cover" %}</div>{% endif %}
<div class="h-6 w-40 mx-auto deco-sunburst" aria-hidden="true"></div>
{% if dish %}<h2 class="deco-caps mt-6 text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ dish }}</h2>{% endif %}
{% if note %}<div class="mt-4 text-xl italic leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ note|safe }}</div>{% endif %}
{% if chef %}<p class="deco-caps mt-6 text-xs" style="color:hsl(var(--primary));">{{ chef }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Chef's Signature",
"description": "Centred signature-dish feature with a chef's note between sunburst crests.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"x-editor": "text",
"default": "Chef's signature"
},
"dish": {
"type": "string",
"title": "Dish name",
"x-editor": "text",
"default": ""
},
"note": {
"type": "string",
"title": "Chef's note",
"description": "A short tasting note or story about the dish.",
"x-editor": "richtext",
"default": ""
},
"chef": {
"type": "string",
"title": "Chef name",
"x-editor": "text",
"default": ""
},
"image": {
"type": "string",
"title": "Dish image",
"x-editor": "media",
"default": ""
}
}
}

8
blocks/menu.ninjatpl Normal file
View File

@ -0,0 +1,8 @@
<section data-block="art-deco:menu" class="py-16">
<div class="max-w-3xl mx-auto px-4">
<div class="deco-frame text-center" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground));">
{% if title %}<h2 class="deco-caps deco-underline mb-8 text-3xl md:text-4xl" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ title }}</h2>{% endif %}
{% if courses %}<ul class="space-y-6 text-left">{% for course in courses %}<li class="flex items-start justify-between gap-6 border-b" style="border-color: hsl(var(--border));"><div class="flex-1"><h3 class="deco-caps text-lg" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ course.name }}</h3>{% if course.description %}<div class="mt-1 italic" style="color: hsl(var(--muted-foreground));">{{ course.description|safe }}</div>{% endif %}</div>{% if course.price %}<span class="deco-caps text-base" style="font-family: var(--font-mono, &quot;JetBrains Mono&quot;, ui-monospace, monospace); color: hsl(var(--primary));">{{ course.price }}</span>{% endif %}</li>{% endfor %}</ul>{% else %}<p class="text-muted-foreground" style="color: hsl(var(--muted-foreground));">Add courses to start composing this menu.</p>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1,14 @@
{# private_dining persona block — framed room image + gilded details panel. Media via {% img %}. With no image, collapses to a single centred column so the empty media half never balloons. #}
{% set hasImage = image %}
<section data-block="art-deco:private_dining" class="py-16 md:py-20 bg-background text-foreground">
<div class="mx-auto grid items-center gap-10 px-4 {% if hasImage %}max-w-6xl md:grid-cols-2 md:gap-16{% else %}max-w-3xl{% endif %}">
{% if hasImage %}<div class="{% if mediaPosition == "right" %}md:order-2{% else %}md:order-1{% endif %}"><div class="deco-step">{% img image alt=title class="w-full h-auto object-cover" %}</div></div>{% endif %}
<div class="{% if hasImage %}{% if mediaPosition == "right" %}md:order-1{% else %}md:order-2{% endif %}{% else %}text-center{% endif %}">
{% if eyebrow %}<div class="mb-3 flex items-center gap-4 {% if not hasImage %}justify-center{% endif %}"><span class="deco-rule w-10"></span><span class="deco-caps text-xs" style="color:hsl(var(--primary));">{{ eyebrow }}</span>{% if not hasImage %}<span class="deco-rule w-10"></span>{% endif %}</div>{% endif %}
{% if title %}<h2 class="deco-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
{% if description %}<div class="mt-4 space-y-4 text-lg italic {% if not hasImage %}mx-auto max-w-xl{% endif %}" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ description|safe }}</div>{% endif %}
{% if seats or minimum %}<dl class="flex flex-wrap gap-x-12 gap-y-3 mt-6 {% if not hasImage %}justify-center{% endif %}">{% if seats %}<div><dt class="deco-caps text-xs" style="color:hsl(var(--primary));">Capacity</dt><dd class="mt-1">{{ seats }}</dd></div>{% endif %}{% if minimum %}<div><dt class="deco-caps text-xs" style="color:hsl(var(--primary));">Minimum</dt><dd class="mt-1">{{ minimum }}</dd></div>{% endif %}</dl>{% endif %}
{% if enquireUrl %}<a href="{{ enquireUrl }}" class="deco-pill mt-8 inline-flex items-center gap-2">{{ enquireLabel|default:"Enquire" }}</a>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Private Dining",
"description": "Private dining room showcase — framed image with room details and an enquiry CTA.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small caps label above the heading.",
"x-editor": "text",
"default": "Private dining"
},
"title": {
"type": "string",
"title": "Room name",
"x-editor": "text",
"default": ""
},
"description": {
"type": "string",
"title": "Description",
"description": "Room description and what the experience includes.",
"x-editor": "richtext",
"default": ""
},
"image": {
"type": "string",
"title": "Room image",
"x-editor": "media",
"default": ""
},
"seats": {
"type": "string",
"title": "Seats",
"description": "e.g. 'Seats 2 to 14'.",
"x-editor": "text",
"default": ""
},
"minimum": {
"type": "string",
"title": "Minimum spend",
"description": "e.g. 'From $2,400'.",
"x-editor": "text",
"default": ""
},
"enquireLabel": {
"type": "string",
"title": "Enquire label",
"x-editor": "text",
"default": "Enquire"
},
"enquireUrl": {
"type": "string",
"title": "Enquire URL",
"x-editor": "link",
"default": ""
},
"mediaPosition": {
"type": "string",
"title": "Image side",
"enum": ["left", "right"],
"x-editor": "select",
"default": "left"
}
}
}

View File

@ -0,0 +1,10 @@
<div data-block="art-deco:reservation" class="sticky top-0 z-30 w-full deco-grain" style="background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-bottom: 1px solid hsl(var(--primary));">
<div class="max-w-5xl mx-auto px-4 py-2 flex flex-wrap items-center justify-between gap-2 text-sm">
<div class="flex items-center gap-3 deco-caps" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--primary));">
<span>Reservations</span>
{% if phone %}<span aria-hidden="true" style="color: hsl(var(--primary) / 0.5);">·</span><a href="tel:{{ phone }}" class="deco-link" style="border-bottom-color: hsl(var(--primary));">{{ phone }}</a>{% endif %}
</div>
{% if hours %}<div class="flex flex-wrap items-center gap-3" style="color: hsl(var(--background));">{% for line in hours %}{% if not forloop.First %}<span aria-hidden="true" style="color: hsl(var(--primary) / 0.5);">·</span>{% endif %}<span>{{ line }}</span>{% endfor %}</div>{% endif %}
{% if openTable %}<a href="{{ openTable }}" class="deco-pill" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));">Book a Table</a>{% endif %}
</div>
</div>

View File

@ -1,41 +0,0 @@
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()
}

View File

@ -1,36 +0,0 @@
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 != "" {
<a
data-block-override="art-deco:button"
href={ templ.SafeURL(data.URL) }
class="deco-pill inline-block"
style={ buttonVariantStyle(data.Variant) }
>
{ data.Label }
</a>
} else {
<button
type="button"
data-block-override="art-deco:button"
class="deco-pill"
style={ buttonVariantStyle(data.Variant) }
>
{ data.Label }
</button>
}
}

View File

@ -1,125 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<a data-block-override=\"art-deco:button\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 20, Col: 33}
}
_, 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, "\" class=\"deco-pill inline-block\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(buttonVariantStyle(data.Variant))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 22, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"button\" data-block-override=\"art-deco:button\" class=\"deco-pill\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(buttonVariantStyle(data.Variant))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 31, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 33, Col: 15}
}
_, 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, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,43 +0,0 @@
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()
}

View File

@ -1,26 +0,0 @@
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) {
<div
data-block="art-deco:divider_fan"
data-variant={ data.Variant }
class="w-full py-6 flex justify-center"
role="presentation"
aria-hidden="true"
>
<div class={ "h-10 w-full max-w-xl", dividerVariantClass(data.Variant) }></div>
</div>
}

View File

@ -1,88 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<div data-block=\"art-deco:divider_fan\" data-variant=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Variant)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `divider_fan.templ`, Line: 19, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"w-full py-6 flex justify-center\" role=\"presentation\" aria-hidden=\"true\">")
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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `divider_fan.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,178 +0,0 @@
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) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Cormorant Garamond', 'Cormorant', Georgia, serif;", edBg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
{ emailCtx.PreviewText }
</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 32px 10px; background-color: %s;", edBg(emailCtx)) }>
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", edBg(emailCtx), edBorder(emailCtx)) }>
<tr>
<td align="center" style={ fmt.Sprintf("padding: 0; background-color: %s;", edCardDark(emailCtx)) }>
<img
src={ flatGoldSunburstSVG(edPrimary(emailCtx)) }
alt=""
width="600"
height="80"
style={ fmt.Sprintf("display: block; width: 100%%; max-width: 600px; height: 80px; background-color: %s;", edCardDark(emailCtx)) }
/>
if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; padding: 24px 16px; font-family: 'Italiana', 'Cinzel', Georgia, serif; font-size: 28px; text-transform: uppercase; letter-spacing: 0.18em; color: %s;", edPrimary(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<tr>
<td class="content-padding" style={ fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.7;", edFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 0 48px;") }>
<hr style={ fmt.Sprintf("border: none; border-top: 1px solid %s; margin: 0;", edPrimary(emailCtx)) }/>
</td>
</tr>
<tr>
<td align="center" style={ fmt.Sprintf("padding: 24px 48px; color: %s; font-size: 13px;", edMutedFg(emailCtx)) }>
<p style={ fmt.Sprintf("margin: 0 0 8px; font-family: 'Italiana', 'Cinzel', Georgia, serif; text-transform: uppercase; letter-spacing: 0.18em; color: %s;", edFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</p>
if emailCtx.SiteSettings.SiteURL != "" {
<p style="margin: 0 0 8px;">
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px solid %s;", edPrimary(emailCtx), edPrimary(emailCtx)) }>
{ emailCtx.SiteSettings.SiteURL }
</a>
</p>
}
if emailCtx.SiteSettings.SupportEmail != "" {
<p style={ fmt.Sprintf("margin: 12px 0 0; font-style: italic; color: %s;", edMutedFg(emailCtx)) }>
P.S. Reservations &amp; enquiries: { emailCtx.SiteSettings.SupportEmail }
</p>
}
if emailCtx.UnsubscribeURL != "" {
<p style="margin: 16px 0 0; font-size: 11px;">
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", edMutedFg(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

View File

@ -1,463 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// 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, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><!--[if mso]>\n\t\t\t<noscript>\n\t\t\t\t<xml>\n\t\t\t\t\t<o:OfficeDocumentSettings>\n\t\t\t\t\t\t<o:PixelsPerInch>96</o:PixelsPerInch>\n\t\t\t\t\t</o:OfficeDocumentSettings>\n\t\t\t\t</xml>\n\t\t\t</noscript>\n\t\t\t<![endif]--><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "</title><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li, blockquote {\n\t\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t\t}\n\t\t\t\ttable, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }\n\t\t\t\timg { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\ta[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.content-padding { padding-left: 24px !important; padding-right: 24px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Cormorant Garamond', 'Cormorant', Georgia, serif;", edBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 110, Col: 155}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 10px; background-color: %s;", edBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 118, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><table role=\"presentation\" class=\"email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", edBg(emailCtx), edBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 119, Col: 232}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 0; background-color: %s;", edCardDark(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 121, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(flatGoldSunburstSVG(edPrimary(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 123, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" alt=\"\" width=\"600\" height=\"80\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("display: block; width: 100%%; max-width: 600px; height: 80px; background-color: %s;", edCardDark(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 127, Col: 138}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; padding: 24px 16px; font-family: 'Italiana', 'Cinzel', Georgia, serif; font-size: 28px; text-transform: uppercase; letter-spacing: 0.18em; color: %s;", edPrimary(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 130, Col: 218}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td></tr><tr><td class=\"content-padding\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.7;", edFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 137, Col: 140}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
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, "</td></tr><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 0 48px;"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 142, Col: 51}
}
_, 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, 18, "\"><hr style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("border: none; border-top: 1px solid %s; margin: 0;", edPrimary(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 143, Col: 107}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"></td></tr><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 24px 48px; color: %s; font-size: 13px;", edMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 147, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"><p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 8px; font-family: 'Italiana', 'Cinzel', Georgia, serif; text-transform: uppercase; letter-spacing: 0.18em; color: %s;", edFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 148, Col: 180}
}
_, 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, 21, "\">")
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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p style=\"margin: 0 0 8px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 templ.SafeURL
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 153, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px solid %s;", edPrimary(emailCtx), edPrimary(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 153, Col: 195}
}
_, 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, 25, "\">")
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, "</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.SiteSettings.SupportEmail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 12px 0 0; font-style: italic; color: %s;", edMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 159, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">P.S. Reservations &amp; 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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<p style=\"margin: 16px 0 0; font-size: 11px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 templ.SafeURL
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 165, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", edMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 165, Col: 139}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,58 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem 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),
}
}

View File

@ -1 +1,20 @@
[] [
{
"name": "Cinzel",
"family": "Cinzel",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/cinzel-400.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/cinzel-700.woff2" }
]
},
{
"name": "Cormorant Garamond",
"family": "Cormorant Garamond",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/cormorant-garamond-400.woff2" },
{ "weight": "400", "style": "italic", "file": "fonts/cormorant-garamond-400-italic.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/cormorant-garamond-600.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/cormorant-garamond-700.woff2" }
]
}
]

View File

@ -1,47 +0,0 @@
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()
}

View File

@ -1,59 +0,0 @@
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) {
<section data-block="art-deco:footer" class="py-12 deco-grain">
<div class="max-w-3xl mx-auto px-4 text-center">
<hr class="deco-rule mb-8"/>
if len(addressLines(data.Address)) > 0 {
<address class="not-italic mb-4" style="color: hsl(var(--muted-foreground));">
for _, line := range addressLines(data.Address) {
<div>{ line }</div>
}
</address>
}
if data.ReservationsEmail != "" {
<p class="mb-4">
<a href={ templ.SafeURL("mailto:" + data.ReservationsEmail) } class="deco-link">{ data.ReservationsEmail }</a>
</p>
}
if data.MenuName != "" {
<nav
aria-label="Footer"
class="deco-caps text-xs mb-4"
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>
<span>{ data.MenuName }</span>
</nav>
}
if data.ShowSocial {
<div class="flex justify-center gap-4 mb-4" style="color: hsl(var(--primary));">
<span class="deco-caps text-xs" aria-hidden="true">Instagram</span>
<span aria-hidden="true">·</span>
<span class="deco-caps text-xs" aria-hidden="true">Facebook</span>
<span aria-hidden="true">·</span>
<span class="deco-caps text-xs" aria-hidden="true">LinkedIn</span>
</div>
}
<hr class="deco-rule mt-8"/>
</div>
</section>
}

View File

@ -1,149 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "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, "<section data-block=\"art-deco:footer\" class=\"py-12 deco-grain\"><div class=\"max-w-3xl mx-auto px-4 text-center\"><hr class=\"deco-rule mb-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(addressLines(data.Address)) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<address class=\"not-italic mb-4\" style=\"color: hsl(var(--muted-foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, line := range addressLines(data.Address) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div>")
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</address>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ReservationsEmail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"mb-4\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("mailto:" + data.ReservationsEmail))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 35, Col: 64}
}
_, 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, "\" class=\"deco-link\">")
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, "</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.MenuName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<nav aria-label=\"Footer\" class=\"deco-caps text-xs mb-4\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 44, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowSocial {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"flex justify-center gap-4 mb-4\" style=\"color: hsl(var(--primary));\"><span class=\"deco-caps text-xs\" aria-hidden=\"true\">Instagram</span> <span aria-hidden=\"true\">·</span> <span class=\"deco-caps text-xs\" aria-hidden=\"true\">Facebook</span> <span aria-hidden=\"true\">·</span> <span class=\"deco-caps text-xs\" aria-hidden=\"true\">LinkedIn</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<hr class=\"deco-rule mt-8\"></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,48 +0,0 @@
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()
}

View File

@ -1,54 +0,0 @@
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) {
<section data-block="art-deco:gallery_fan" class="py-16">
<div class="max-w-6xl mx-auto px-4">
<div class="relative deco-grain">
<div class="absolute inset-x-0 -top-2 h-12 pointer-events-none deco-sunburst" aria-hidden="true"></div>
<div class={ "grid gap-4 pt-12", galleryFanGridCols(data.Columns) }>
if len(data.Images) == 0 {
<div class="col-span-full text-center text-sm py-12" style="color: hsl(var(--muted-foreground));">
Add images to populate the fan gallery.
</div>
}
for _, img := range data.Images {
<figure class="deco-step">
if img.Src != "" {
<img src={ img.Src } alt={ img.Caption } class="block w-full h-auto object-cover" style="background-color: hsl(var(--muted));"/>
} else {
<img src={ fallbackImage() } alt="" class="block w-full h-auto" style="background-color: hsl(var(--muted));"/>
}
if img.Caption != "" {
<figcaption
class="mt-2 text-center italic text-sm"
style="color: hsl(var(--muted-foreground));"
>
{ img.Caption }
</figcaption>
}
</figure>
}
</div>
<div class="absolute inset-x-0 -bottom-2 h-12 pointer-events-none deco-sunburst rotate-180" aria-hidden="true"></div>
</div>
</div>
</section>
}

View File

@ -1,169 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<section data-block=\"art-deco:gallery_fan\" class=\"py-16\"><div class=\"max-w-6xl mx-auto px-4\"><div class=\"relative deco-grain\"><div class=\"absolute inset-x-0 -top-2 h-12 pointer-events-none deco-sunburst\" aria-hidden=\"true\"></div>")
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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_fan.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Images) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"col-span-full text-center text-sm py-12\" style=\"color: hsl(var(--muted-foreground));\">Add images to populate the fan gallery.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, img := range data.Images {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figure class=\"deco-step\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if img.Src != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(img.Src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_fan.templ`, Line: 35, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(img.Caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_fan.templ`, Line: 35, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"block w-full h-auto object-cover\" style=\"background-color: hsl(var(--muted));\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(fallbackImage())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_fan.templ`, Line: 37, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" alt=\"\" class=\"block w-full h-auto\" style=\"background-color: hsl(var(--muted));\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if img.Caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<figcaption class=\"mt-2 text-center italic text-sm\" style=\"color: hsl(var(--muted-foreground));\">")
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, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><div class=\"absolute inset-x-0 -bottom-2 h-12 pointer-events-none deco-sunburst rotate-180\" aria-hidden=\"true\"></div></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

20
go.mod
View File

@ -1,20 +0,0 @@
module git.dev.alexdunmow.com/block/themes/art-deco
go 1.26.4
require (
git.dev.alexdunmow.com/block/core v0.14.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
)

44
go.sum
View File

@ -1,44 +0,0 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
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=

View File

@ -1,44 +0,0 @@
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
}

View File

@ -1,69 +0,0 @@
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:
<h1
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(1), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h1>
case 2:
<h2
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(2), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h2>
case 3:
<h3
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(3), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h3>
case 4:
<h4
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(4), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h4>
case 5:
<h5
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(5), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h5>
case 6:
<h6
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(6), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h6>
default:
<h2
data-block-override="art-deco:heading"
class={ "deco-caps deco-underline", headingSizeClass(2), data.ExtraCls }
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>{ data.Text }</h2>
}
}

View File

@ -1,311 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<h1 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h1>")
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, "<h2 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h2>")
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, "<h3 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h3>")
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, "<h4 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h4>")
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, "<h5 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h5>")
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, "<h6 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h6>")
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, "<h2 data-block-override=\"art-deco:heading\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,59 +0,0 @@
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
}

View File

@ -1,43 +0,0 @@
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()
}

View File

@ -1,30 +0,0 @@
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) {
<figure data-block-override="art-deco:image" class="my-8">
<div class={ imageFrameClass(data.Frame) }>
<img src={ data.Src } alt={ data.Alt } class="block w-full h-auto"/>
</div>
if data.Caption != "" {
<figcaption
class="mt-2 italic text-center text-sm"
style="color: hsl(var(--muted-foreground));"
>
{ data.Caption }
</figcaption>
}
</figure>
}

View File

@ -1,124 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<figure data-block-override=\"art-deco:image\" class=\"my-8\">")
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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 19, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 19, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"block w-full h-auto\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<figcaption class=\"mt-2 italic text-center text-sm\" style=\"color: hsl(var(--muted-foreground));\">")
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, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

337
manifest.yaml Normal file
View File

@ -0,0 +1,337 @@
# Art Deco theme — codeless .bnp manifest (WO-WZ-021 Phase B).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs: [lucide, simple-icons]
system_templates:
- 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:
- system: art-deco
key: default
title: Default
description: "Centered symmetric layout with gold rule header and footer."
slots: [header, main, footer]
- system: art-deco
key: landing
title: Marquee Landing
description: "Wide hero with sunburst overlay, reservation strip, CTA."
slots: [marquee, main, cta, footer]
- system: art-deco
key: article
title: Editorial
description: "Narrow editorial column for press, menus and stories."
slots: [header, main, aside, footer]
- system: art-deco
key: full-width
title: Grand Ballroom
description: "Edge-to-edge gallery layout for venues and lookbooks."
slots: [header, main, footer]
- system: art-deco
key: blog-index
title: Editorial Index
description: "Blog index with gilded masthead, featured and post-grid slots."
slots: [header, featured, main, footer]
- system: art-deco
key: contact
title: Reservations
description: "Contact layout with optional aside for hours and location."
slots: [header, main, aside, footer]
- system: art-deco
key: auth
title: Members
description: "Centred auth shell with wordmark and sunburst crest."
slots: [main, footer]
template_overrides:
# Chrome
- { template: art-deco, block: navbar }
- { template: art-deco, block: footer }
- { template: art-deco, block: utility-bar }
- { template: art-deco, block: announcement-bar }
# Marketing
- { template: art-deco, block: hero }
- { template: art-deco, block: feature-grid }
- { template: art-deco, block: rich-section }
- { template: art-deco, block: stats }
- { template: art-deco, block: testimonial }
- { template: art-deco, block: logo-strip }
- { template: art-deco, block: pricing }
- { template: art-deco, block: team }
- { template: art-deco, block: faq }
- { template: art-deco, block: timeline }
- { template: art-deco, block: cta }
- { template: art-deco, block: countdown }
# Primitives
- { template: art-deco, block: heading }
- { template: art-deco, block: text }
- { template: art-deco, block: rich-text }
- { template: art-deco, block: image }
- { template: art-deco, block: quote }
- { template: art-deco, block: divider }
- { template: art-deco, block: button }
- { template: art-deco, block: card }
- { template: art-deco, block: gallery }
- { template: art-deco, block: video-embed }
- { template: art-deco, block: audio-embed }
# Commerce / local
- { template: art-deco, block: menu-list }
- { template: art-deco, block: event-list }
- { template: art-deco, block: contact-form }
- { template: art-deco, block: contact-card }
- { template: art-deco, block: hours-location }
- { template: art-deco, block: social-links }
# Blog family
- { template: art-deco, block: blog-hero }
- { template: art-deco, block: featured-posts }
- { template: art-deco, block: related-posts }
- { template: art-deco, block: popular-posts }
- { template: art-deco, block: category-list }
- { template: art-deco, block: post-hero }
- { template: art-deco, block: post-metadata }
- { template: art-deco, block: author-bio }
- { template: art-deco, block: author-bio-hero }
- { template: art-deco, block: newsletter-signup }
- { template: art-deco, block: breadcrumbs }
- { template: art-deco, block: sidebar-nav }
# Auth surface
- { template: art-deco, block: auth-form }
- { template: art-deco, block: password-reset-form }
- { template: art-deco, block: auth-status }
# System
- { template: art-deco, block: page-suggestions }
email_wrappers:
- art-deco
css:
input_css_append: |
/* 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, <fallback>).
*/
/* Bundled display faces. The host does not yet emit @font-face for
* bundled_fonts (LoadedPlugin.BundledFonts is unconsumed), so the theme
* declares its own faces against the served artifact paths
* (/templates/art-deco/fonts/...). Presets set typography.fontHeading/body
* to template:art-deco:<Family> so --font-heading/--font-body resolve to
* these families. */
@font-face { font-family: 'Cinzel'; src: url('/templates/art-deco/fonts/cinzel-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Cinzel'; src: url('/templates/art-deco/fonts/cinzel-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/art-deco/fonts/cormorant-garamond-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/art-deco/fonts/cormorant-garamond-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/art-deco/fonts/cormorant-garamond-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/art-deco/fonts/cormorant-garamond-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@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));
}
}

View File

@ -1,43 +0,0 @@
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()
}

View File

@ -1,42 +0,0 @@
package main
// marqueeHeroComponent renders the wide hero with stepped ziggurat frame and sunburst overlay.
templ marqueeHeroComponent(data MarqueeHeroData) {
<section data-block="art-deco:marquee_hero" class="relative w-full overflow-hidden deco-grain" style="background-color: hsl(var(--foreground)); color: hsl(var(--background));">
if data.Image != "" {
<div class="absolute inset-0 z-0" aria-hidden="true">
<img src={ data.Image } alt="" class="w-full h-full object-cover" style="opacity: 0.5;"/>
<div class="absolute inset-0" style="background-color: hsl(var(--foreground) / 0.55);"></div>
</div>
}
<div class="absolute inset-x-0 top-0 h-24 z-10 pointer-events-none deco-sunburst" aria-hidden="true"></div>
<div class="relative z-20 max-w-4xl mx-auto px-4 py-32 text-center">
<div class="deco-frame inline-block px-12 py-10" style="background-color: hsl(var(--foreground) / 0.6); border-color: hsl(var(--primary));">
if data.Eyebrow != "" {
<p class="deco-caps mb-4 text-xs" style="color: hsl(var(--primary));">{ data.Eyebrow }</p>
}
if data.Title != "" {
<h1
class="deco-caps deco-underline mb-6 text-5xl md:text-6xl lg:text-7xl"
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--background));"
>
{ data.Title }
</h1>
}
if data.Subtitle != "" {
<div
class="text-lg md:text-xl mb-8 max-w-2xl mx-auto"
style="color: hsl(var(--background) / 0.9);"
>
@templ.Raw(data.Subtitle)
</div>
}
if data.CTALabel != "" && data.CTAHref != "" {
<a href={ templ.SafeURL(data.CTAHref) } class="deco-pill" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));">
{ data.CTALabel }
</a>
}
</div>
</div>
</section>
}

View File

@ -1,152 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<section data-block=\"art-deco:marquee_hero\" class=\"relative w-full overflow-hidden deco-grain\" style=\"background-color: hsl(var(--foreground)); color: hsl(var(--background));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"absolute inset-0 z-0\" aria-hidden=\"true\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee_hero.templ`, Line: 8, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"\" class=\"w-full h-full object-cover\" style=\"opacity: 0.5;\"><div class=\"absolute inset-0\" style=\"background-color: hsl(var(--foreground) / 0.55);\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"absolute inset-x-0 top-0 h-24 z-10 pointer-events-none deco-sunburst\" aria-hidden=\"true\"></div><div class=\"relative z-20 max-w-4xl mx-auto px-4 py-32 text-center\"><div class=\"deco-frame inline-block px-12 py-10\" style=\"background-color: hsl(var(--foreground) / 0.6); border-color: hsl(var(--primary));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Eyebrow != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"deco-caps mb-4 text-xs\" style=\"color: hsl(var(--primary));\">")
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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h1 class=\"deco-caps deco-underline mb-6 text-5xl md:text-6xl lg:text-7xl\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--background));\">")
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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Subtitle != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"text-lg md:text-xl mb-8 max-w-2xl mx-auto\" style=\"color: hsl(var(--background) / 0.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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.CTALabel != "" && data.CTAHref != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.CTAHref))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee_hero.templ`, Line: 35, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"deco-pill\" style=\"background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));\">")
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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

23
master_pages.json Normal file
View File

@ -0,0 +1,23 @@
[
{
"key": "art-deco:default-master",
"title": "Art Deco Default Master",
"page_templates": ["default", "article", "blog-index", "contact"],
"blocks": [
{ "block_key": "navbar", "title": "Gilded Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Maison Doré" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "centered", "companyName": "Maison Doré", "tagline": "A supper club in the grand manner." }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "art-deco:marquee-master",
"title": "Art Deco Marquee Master",
"page_templates": ["landing", "full-width"],
"blocks": [
{ "block_key": "navbar", "title": "Gilded Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Maison Doré" } }, "slot": "marquee", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "divider", "title": "CTA Fan Divider", "content": { "variant": "ornament", "icon": "lucide:diamond" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "centered", "companyName": "Maison Doré", "tagline": "A supper club in the grand manner." }, "slot": "footer", "sort_order": 0 }
]
}
]

52
menu.go
View File

@ -1,52 +0,0 @@
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()
}

View File

@ -1,50 +0,0 @@
package main
// menuComponent renders the symmetric two-column menu card.
templ menuComponent(data MenuData) {
<section data-block="art-deco:menu" class="py-16">
<div class="max-w-3xl mx-auto px-4">
<div class="deco-frame text-center" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground));">
if data.Title != "" {
<h2
class="deco-caps deco-underline mb-8 text-3xl md:text-4xl"
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>
{ data.Title }
</h2>
}
if len(data.Courses) == 0 {
<p class="text-muted-foreground" style="color: hsl(var(--muted-foreground));">Add courses to start composing this menu.</p>
} else {
<ul class="space-y-6 text-left">
for _, course := range data.Courses {
<li class="flex items-start justify-between gap-6 border-b" style="border-color: hsl(var(--border));">
<div class="flex-1">
<h3
class="deco-caps text-lg"
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"
>
{ course.Name }
</h3>
if course.Description != "" {
<div class="mt-1 italic" style="color: hsl(var(--muted-foreground));">
@templ.Raw(course.Description)
</div>
}
</div>
if course.Price != "" {
<span
class="deco-caps text-base"
style="font-family: var(--font-mono, &quot;JetBrains Mono&quot;, ui-monospace, monospace); color: hsl(var(--primary));"
>
{ course.Price }
</span>
}
</li>
}
</ul>
}
</div>
</div>
</section>
}

View File

@ -1,139 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<section data-block=\"art-deco:menu\" class=\"py-16\"><div class=\"max-w-3xl mx-auto px-4\"><div class=\"deco-frame text-center\" style=\"background-color: hsl(var(--card)); color: hsl(var(--card-foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"deco-caps deco-underline mb-8 text-3xl md:text-4xl\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.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, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Courses) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"text-muted-foreground\" style=\"color: hsl(var(--muted-foreground));\">Add courses to start composing this menu.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<ul class=\"space-y-6 text-left\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, course := range data.Courses {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<li class=\"flex items-start justify-between gap-6 border-b\" style=\"border-color: hsl(var(--border));\"><div class=\"flex-1\"><h3 class=\"deco-caps text-lg\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(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, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if course.Description != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mt-1 italic\" style=\"color: hsl(var(--muted-foreground));\">")
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if course.Price != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"deco-caps text-base\" style=\"font-family: var(--font-mono, &quot;JetBrains Mono&quot;, ui-monospace, monospace); color: hsl(var(--primary));\">")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -2,11 +2,8 @@
name = "art-deco" name = "art-deco"
display_name = "Art Deco" display_name = "Art Deco"
scope = "@themes" scope = "@themes"
version = "0.1.1" version = "0.5.0"
description = "Luxury Art Deco theme with champagne gold gradients, jet black, ivory and geometric fan motifs for hotels, fine dining, weddings and jewellers." 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" kind = "theme"
categories = ["templates"] categories = ["templates"]
tags = ["luxury", "gold", "hospitality", "hotel", "wedding", "jewellery", "vintage", "glam", "fine-dining"] tags = ["luxury", "gold", "hospitality", "hotel", "wedding", "jewellery", "vintage", "glam", "fine-dining"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

View File

@ -5,6 +5,12 @@
"description": "Jet black lifted by champagne gold — the default Art Deco palette.", "description": "Jet black lifted by champagne gold — the default Art Deco palette.",
"theme": { "theme": {
"mode": "both", "mode": "both",
"typography": {
"fontHeading": "template:art-deco:Cinzel",
"fontBody": "template:art-deco:Cormorant Garamond",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": { "lightColors": {
"background": "40 30% 96%", "background": "40 30% 96%",
"foreground": "40 20% 10%", "foreground": "40 20% 10%",
@ -52,9 +58,15 @@
{ {
"id": "ivory-rose", "id": "ivory-rose",
"name": "Ivory & Rose Gold", "name": "Ivory & Rose Gold",
"description": "Soft ivory with rose-gold flourishes for weddings and jewellers.", "description": "Soft ivory by day, warm ember by night, with rose-gold flourishes for weddings and jewellers.",
"theme": { "theme": {
"mode": "light", "mode": "both",
"typography": {
"fontHeading": "template:art-deco:Cinzel",
"fontBody": "template:art-deco:Cormorant Garamond",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": { "lightColors": {
"background": "36 40% 97%", "background": "36 40% 97%",
"foreground": "20 15% 14%", "foreground": "20 15% 14%",
@ -75,15 +87,63 @@
"border": "25 30% 82%", "border": "25 30% 82%",
"input": "25 25% 86%", "input": "25 25% 86%",
"ring": "18 55% 58%" "ring": "18 55% 58%"
},
"darkColors": {
"background": "20 18% 8%",
"foreground": "30 30% 92%",
"card": "20 18% 12%",
"cardForeground": "30 30% 92%",
"popover": "20 18% 12%",
"popoverForeground": "30 30% 92%",
"primary": "18 60% 62%",
"primaryForeground": "20 20% 8%",
"secondary": "20 15% 16%",
"secondaryForeground": "30 25% 88%",
"muted": "20 12% 14%",
"mutedForeground": "25 15% 62%",
"accent": "15 62% 62%",
"accentForeground": "20 20% 8%",
"destructive": "0 72% 52%",
"destructiveForeground": "30 30% 94%",
"border": "20 20% 24%",
"input": "20 18% 20%",
"ring": "18 60% 62%"
} }
} }
}, },
{ {
"id": "velvet-onyx", "id": "velvet-onyx",
"name": "Velvet Onyx", "name": "Velvet Onyx",
"description": "Deep velvet onyx with gold and rose stage-lights for theatre and after-dark dining.", "description": "Velvet-tinted ivory by day, deep onyx and gold stage-light by night, for theatre and after-dark dining.",
"theme": { "theme": {
"mode": "dark", "mode": "both",
"typography": {
"fontHeading": "template:art-deco:Cinzel",
"fontBody": "template:art-deco:Cormorant Garamond",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": {
"background": "280 15% 96%",
"foreground": "260 20% 12%",
"card": "280 12% 99%",
"cardForeground": "260 20% 12%",
"popover": "280 12% 99%",
"popoverForeground": "260 20% 12%",
"primary": "42 60% 48%",
"primaryForeground": "280 20% 8%",
"secondary": "280 15% 92%",
"secondaryForeground": "260 20% 12%",
"muted": "280 12% 93%",
"mutedForeground": "260 12% 40%",
"accent": "320 35% 52%",
"accentForeground": "280 20% 96%",
"destructive": "0 70% 45%",
"destructiveForeground": "280 15% 96%",
"border": "280 15% 82%",
"input": "280 12% 86%",
"ring": "42 60% 48%"
},
"darkColors": { "darkColors": {
"background": "260 20% 6%", "background": "260 20% 6%",
"foreground": "42 40% 92%", "foreground": "42 40% 92%",
@ -106,5 +166,61 @@
"ring": "42 70% 60%" "ring": "42 70% 60%"
} }
} }
},
{
"id": "emerald-gilt",
"name": "Emerald & Gilt",
"description": "Deco emerald and gilt — parchment and jade by day, emerald lacquer and gold by night.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:art-deco:Cinzel",
"fontBody": "template:art-deco:Cormorant Garamond",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": {
"background": "45 35% 95%",
"foreground": "160 25% 12%",
"card": "45 30% 98%",
"cardForeground": "160 25% 12%",
"popover": "45 30% 98%",
"popoverForeground": "160 25% 12%",
"primary": "42 55% 48%",
"primaryForeground": "160 25% 8%",
"secondary": "150 15% 88%",
"secondaryForeground": "160 25% 12%",
"muted": "150 12% 91%",
"mutedForeground": "160 12% 38%",
"accent": "160 40% 30%",
"accentForeground": "45 35% 95%",
"destructive": "0 70% 45%",
"destructiveForeground": "45 35% 95%",
"border": "150 20% 78%",
"input": "150 18% 83%",
"ring": "42 55% 48%"
},
"darkColors": {
"background": "162 25% 6%",
"foreground": "45 35% 92%",
"card": "162 22% 9%",
"cardForeground": "45 35% 92%",
"popover": "162 22% 9%",
"popoverForeground": "45 35% 92%",
"primary": "42 68% 58%",
"primaryForeground": "162 25% 8%",
"secondary": "162 18% 13%",
"secondaryForeground": "45 30% 88%",
"muted": "162 15% 11%",
"mutedForeground": "45 15% 62%",
"accent": "158 45% 45%",
"accentForeground": "162 25% 6%",
"destructive": "0 72% 52%",
"destructiveForeground": "45 35% 95%",
"border": "162 20% 20%",
"input": "162 18% 16%",
"ring": "42 68% 58%"
}
}
} }
] ]

View File

@ -1,39 +0,0 @@
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()
}

View File

@ -1,39 +0,0 @@
package main
// pressQuoteComponent renders the italic Cormorant pull-quote between fan dividers.
templ pressQuoteComponent(data PressQuoteData) {
<section data-block="art-deco:press_quote" class="py-16">
<div class="max-w-3xl mx-auto px-4 text-center">
<div class="h-6 mb-6 deco-sunburst" aria-hidden="true"></div>
<blockquote
class="italic text-2xl md:text-3xl leading-snug"
style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif); font-style: italic; color: hsl(var(--foreground));"
>
if data.Quote != "" {
@templ.Raw(data.Quote)
} else {
<span style="color: hsl(var(--muted-foreground));">Add a press quote to populate this block.</span>
}
</blockquote>
if data.Stars > 0 {
<div class="mt-4 flex justify-center gap-1" aria-label="Star rating" data-stars={ starsAttr(data.Stars) }>
for i := 0; i < data.Stars; i++ {
<span data-deco-star="filled" aria-hidden="true" style="color: hsl(var(--primary));">★</span>
}
for i := data.Stars; i < 5; i++ {
<span data-deco-star="empty" aria-hidden="true" style="color: hsl(var(--muted-foreground));">☆</span>
}
</div>
}
if data.Source != "" {
<p
class="mt-6 deco-caps text-xs"
style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--muted-foreground));"
>
{ data.Source }
</p>
}
<div class="h-6 mt-6 deco-scallop" aria-hidden="true"></div>
</div>
</section>
}

View File

@ -1,8 +0,0 @@
package main
import "strconv"
// starsAttr renders an int into a stable data-stars attribute string.
func starsAttr(n int) string {
return strconv.Itoa(n)
}

View File

@ -1,114 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<section data-block=\"art-deco:press_quote\" class=\"py-16\"><div class=\"max-w-3xl mx-auto px-4 text-center\"><div class=\"h-6 mb-6 deco-sunburst\" aria-hidden=\"true\"></div><blockquote class=\"italic text-2xl md:text-3xl leading-snug\" style=\"font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif); font-style: italic; color: hsl(var(--foreground));\">")
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, "<span style=\"color: hsl(var(--muted-foreground));\">Add a press quote to populate this block.</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</blockquote>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Stars > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"mt-4 flex justify-center gap-1\" aria-label=\"Star rating\" data-stars=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(starsAttr(data.Stars))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `press_quote.templ`, Line: 19, Col: 107}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
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, "<span data-deco-star=\"filled\" aria-hidden=\"true\" style=\"color: hsl(var(--primary));\">★</span> ")
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, "<span data-deco-star=\"empty\" aria-hidden=\"true\" style=\"color: hsl(var(--muted-foreground));\">☆</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Source != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-6 deco-caps text-xs\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--muted-foreground));\">")
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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"h-6 mt-6 deco-scallop\" aria-hidden=\"true\"></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 818 KiB

View File

@ -1,174 +0,0 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function 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,
},
},
},
}
}

View File

@ -1,25 +0,0 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// Registration is the compile-time plugin registration for the 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() },
}

View File

@ -1,37 +0,0 @@
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": ["TueSat · 6pmlate", ...]}
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()
}

View File

@ -1,36 +0,0 @@
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) {
<div
data-block="art-deco:reservation"
class="sticky top-0 z-30 w-full deco-grain"
style="background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-bottom: 1px solid hsl(var(--primary));"
>
<div class="max-w-5xl mx-auto px-4 py-2 flex flex-wrap items-center justify-between gap-2 text-sm">
<div class="flex items-center gap-3 deco-caps" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--primary));">
<span>Reservations</span>
if data.Phone != "" {
<span aria-hidden="true" style="color: hsl(var(--primary) / 0.5);">·</span>
<a href={ templ.SafeURL("tel:" + data.Phone) } class="deco-link" style="border-bottom-color: hsl(var(--primary));">{ data.Phone }</a>
}
</div>
if len(data.Hours) > 0 {
<div class="flex flex-wrap items-center gap-3" style="color: hsl(var(--background));">
for i, line := range data.Hours {
if i > 0 {
<span aria-hidden="true" style="color: hsl(var(--primary) / 0.5);">·</span>
}
<span>{ line }</span>
}
</div>
}
if data.OpenTable != "" {
<a href={ templ.SafeURL(data.OpenTable) } class="deco-pill" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));">
Book a Table
</a>
}
</div>
</div>
}

View File

@ -1,136 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<div data-block=\"art-deco:reservation\" class=\"sticky top-0 z-30 w-full deco-grain\" style=\"background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-bottom: 1px solid hsl(var(--primary));\"><div class=\"max-w-5xl mx-auto px-4 py-2 flex flex-wrap items-center justify-between gap-2 text-sm\"><div class=\"flex items-center gap-3 deco-caps\" style=\"font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--primary));\"><span>Reservations</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Phone != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span aria-hidden=\"true\" style=\"color: hsl(var(--primary) / 0.5);\">·</span> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("tel:" + data.Phone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reservation.templ`, Line: 16, Col: 49}
}
_, 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, "\" class=\"deco-link\" style=\"border-bottom-color: hsl(var(--primary));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Phone)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reservation.templ`, Line: 16, Col: 132}
}
_, 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, 4, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Hours) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"flex flex-wrap items-center gap-3\" style=\"color: hsl(var(--background));\">")
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, "<span aria-hidden=\"true\" style=\"color: hsl(var(--primary) / 0.5);\">·</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <span>")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.OpenTable != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.OpenTable))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reservation.templ`, Line: 30, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"deco-pill\" style=\"background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));\">Book a Table</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,16 +0,0 @@
{
"$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"]
}
}
}

View File

@ -1,37 +0,0 @@
{
"$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": ""
}
}
}

View File

@ -1,38 +0,0 @@
{
"$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]
}
}
}

View File

@ -1,47 +0,0 @@
{
"$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": ""
}
}
}

View File

@ -1,31 +0,0 @@
{
"$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 05.",
"x-editor": "number",
"default": 0,
"minimum": 0,
"maximum": 5
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

64
seed/media/gold-room.svg Normal file
View File

@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" role="img" aria-label="Art Deco gold fan motif on black">
<defs>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e7cd86"/>
<stop offset="0.5" stop-color="#c9a24b"/>
<stop offset="1" stop-color="#9c7a30"/>
</linearGradient>
<radialGradient id="glow" cx="50%" cy="100%" r="90%">
<stop offset="0" stop-color="#241d10"/>
<stop offset="1" stop-color="#0f0e0c"/>
</radialGradient>
</defs>
<!-- ground -->
<rect width="1200" height="800" fill="#0f0e0c"/>
<rect width="1200" height="800" fill="url(#glow)"/>
<!-- sunburst fan rising from lower centre -->
<g transform="translate(600 620)">
<g fill="url(#gold)" opacity="0.9">
<!-- 15 rays across a 180° fan -->
<g>
<path d="M0 0 L-14 -560 L14 -560 Z" transform="rotate(0)"/>
<path d="M0 0 L-11 -520 L11 -520 Z" transform="rotate(11)"/>
<path d="M0 0 L-11 -520 L11 -520 Z" transform="rotate(-11)"/>
<path d="M0 0 L-10 -480 L10 -480 Z" transform="rotate(22)"/>
<path d="M0 0 L-10 -480 L10 -480 Z" transform="rotate(-22)"/>
<path d="M0 0 L-9 -440 L9 -440 Z" transform="rotate(33)"/>
<path d="M0 0 L-9 -440 L9 -440 Z" transform="rotate(-33)"/>
<path d="M0 0 L-8 -400 L8 -400 Z" transform="rotate(45)"/>
<path d="M0 0 L-8 -400 L8 -400 Z" transform="rotate(-45)"/>
<path d="M0 0 L-7 -350 L7 -350 Z" transform="rotate(58)"/>
<path d="M0 0 L-7 -350 L7 -350 Z" transform="rotate(-58)"/>
<path d="M0 0 L-6 -300 L6 -300 Z" transform="rotate(72)"/>
<path d="M0 0 L-6 -300 L6 -300 Z" transform="rotate(-72)"/>
<path d="M0 0 L-5 -250 L5 -250 Z" transform="rotate(86)"/>
<path d="M0 0 L-5 -250 L5 -250 Z" transform="rotate(-86)"/>
</g>
</g>
<!-- concentric arcs -->
<g fill="none" stroke="#c9a24b" stroke-opacity="0.55">
<path d="M-300 0 A300 300 0 0 1 300 0" stroke-width="3"/>
<path d="M-210 0 A210 210 0 0 1 210 0" stroke-width="2"/>
<path d="M-130 0 A130 130 0 0 1 130 0" stroke-width="2"/>
</g>
<!-- central medallion -->
<circle r="46" fill="#0f0e0c" stroke="url(#gold)" stroke-width="3"/>
<path d="M0 -30 L20 0 L0 30 L-20 0 Z" fill="url(#gold)"/>
</g>
<!-- stepped ziggurat border -->
<g fill="none" stroke="url(#gold)" stroke-width="4">
<rect x="40" y="40" width="1120" height="720"/>
<rect x="60" y="60" width="1080" height="680" stroke-width="1.5" stroke-opacity="0.6"/>
</g>
<g fill="#c9a24b">
<rect x="560" y="34" width="80" height="12"/>
<rect x="560" y="754" width="80" height="12"/>
<rect x="34" y="360" width="12" height="80"/>
<rect x="1154" y="360" width="12" height="80"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

209
seed/seed.json Normal file
View File

@ -0,0 +1,209 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "Maison Doré",
"tagline": "A supper club in the grand manner."
}
},
"media": [
{
"id": "d3c0d3c0-a11d-4ec0-9000-000000000001",
"file": "media/gold-room.svg",
"alt": "Gilded Art Deco fan crest — the Gold Room at Maison Doré",
"folder": "Demo"
}
],
"data_tables": [
{
"key": "contact_submissions",
"name": "Reservation Enquiries",
"description": "Seeded contact-form submissions for the Maison Doré demo. A submission becomes a row here and emails the site admins.",
"columns": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "party", "label": "Party size", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Maison Doré",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Hero",
"slot": "main",
"sort_order": 1,
"content": {
"template": "centered",
"eyebrow": "Established 1926",
"headline": "Maison Doré",
"subheadline": "A supper club in the grand manner.",
"description": "Dinner and late suppers, served under gilded ceilings. A short tasting menu, a long cellar, and a room built for the evening.",
"ctaPrimary": { "text": "Reserve a table", "url": "/contact" },
"ctaSecondary": { "text": "Read the menu", "url": "#tasting" }
}
},
{
"block_key": "art-deco:chef_signature",
"title": "Chef's Signature",
"slot": "main",
"sort_order": 2,
"content": {
"eyebrow": "Chef's signature",
"dish": "Gold-leaf oyster, champagne mignonette",
"note": "The dish that opened the room in 1926. We still serve it the same way, on ice, one at a time.",
"chef": "Elise Marchand, Head Chef"
}
},
{
"block_key": "art-deco:menu",
"title": "Tasting Menu",
"slot": "main",
"sort_order": 3,
"content": {
"title": "Chef's Tasting",
"courses": [
{ "name": "Oysters, champagne mignonette", "description": "Half a dozen, on ice.", "price": "$32" },
{ "name": "Consommé doré", "description": "Clarified beef broth, tarragon, gold leaf.", "price": "$24" },
{ "name": "Turbot, beurre blanc", "description": "Line-caught, roasted on the bone.", "price": "$58" },
{ "name": "Crown of lamb", "description": "Herbs, salt-baked roots, jus.", "price": "$62" },
{ "name": "Grand Marnier soufflé", "description": "To finish. Order at the start.", "price": "$28" }
]
}
},
{
"block_key": "art-deco:private_dining",
"title": "Private Dining",
"slot": "main",
"sort_order": 4,
"content": {
"eyebrow": "Private dining",
"title": "The Gold Room",
"image": "d3c0d3c0-a11d-4ec0-9000-000000000001",
"description": "A room of your own behind the main floor. One long table, a dedicated maître d', and the full cellar. We write the menu with you.",
"seats": "Seats 2 to 14",
"minimum": "From $2,400",
"enquireLabel": "Enquire",
"enquireUrl": "/contact",
"mediaPosition": "left"
}
}
]
},
{
"slug": "/about",
"title": "The House",
"template_key": "default",
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The House", "level": 1 } },
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>Maison Doré opened on a narrow street in 1926 and has kept its hours ever since. We take reservations for dinner and hold a few tables back for late suppers.</p><p>The kitchen runs a short tasting menu that changes with the season. The cellar runs long. The room stays the same, which is the point.</p>" } },
{ "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 3, "content": { "variant": "ornament", "icon": "lucide:diamond" } },
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "Come for dinner, stay for the last pour.", "attribution": "The House" } }
]
},
{
"slug": "/blog",
"title": "From the Floor",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Post: Gold Standard", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "The gold standard", "text": "<p>Why one oyster, served one at a time, still opens every table.</p>", "link": "/blog/the-gold-standard", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: A Night", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "A night at the club", "text": "<p>From the first cocktail to the last soufflé. How an evening unfolds.</p>", "link": "/blog/a-night-at-the-club", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: Cellar", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Notes from the cellar", "text": "<p>What we pour and why. A short walk through the list.</p>", "link": "/blog/notes-from-the-cellar", "linkLabel": "Read on" } }
]
},
{
"slug": "/blog/the-gold-standard",
"title": "The gold standard",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "The gold standard", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The oyster came first, in 1926, and it has not left the menu since. Before the tables were dressed, before the band had a name, before the sign above the door had dried, the kitchen had settled on one dish to open the room. We serve it still on ice, one at a time, with a champagne mignonette and a single leaf of gold.</p><p>People ask why only one. The honest answer is that a supper club is a matter of tempo, and the first thing on the table sets it. A dozen oysters is generosity; one oyster, presented as though it were the only one in the house, is an argument about how the evening intends to proceed. Slowly. Deliberately. With attention paid.</p><p>The mignonette is older than the room. A cook we no longer have the name of set down the proportions on the back of a wine list: shallots cut fine enough to disappear, a good vinegar cut with a little champagne, cracked pepper, and nothing else that would announce itself. It has been copied and it has been improved upon, and every improvement has been quietly abandoned. We make it the way it was written.</p><p>The gold leaf is not decoration, whatever the photographs suggest. It is a signature — the way a house marks a dish as its own. A single flake, laid on with a dry brush at the pass, catches the low light of the room and tells you, without a word, where you are sitting. It tastes of nothing. That is the point of it.</p><p>Service is a small ceremony. The plate is carried in two hands, set down without a sound, and left alone. No pepper is offered. No lemon is brought unless it is asked for, and it is not encouraged. The oyster is not garnished at the table; the table is not the place for it. Everything that could be decided has been decided in the kitchen already.</p><p>We take the shells from a single bed on the cold side of the coast, from a grower who has sold to the house for three generations and who still telephones when the water turns. They arrive closed, heavy, and smelling of the sea. A shucker who has been with us longer than most of the room opens them to order, never in advance, because an oyster opened early is an oyster that has begun to forget itself.</p><p>The ice is shaved, not cubed, and banked so the shell sits level and spills nothing. A cook once suggested we set the oyster on rock salt for the look of it. The suggestion was heard and not taken. Salt draws the liquor out; ice keeps it in. The liquor is the dish.</p><p>What follows on the menu changes with the season and the mood of the kitchen — the consommé, the turbot, the crown of lamb, the soufflé ordered at the start. But the oyster does not change, because it is not a course so much as a promise. It says: we have thought about this. We will keep thinking about it, right through to the last pour.</p><p>Regulars have their rituals. One takes it with nothing but the mignonette and closes his eyes. One tips the shell and drinks the liquor first, then eats the oyster after, which the kitchen considers correct. One has ordered it every Friday for eleven years and has never once been offered a menu before it arrives.</p><p>That is the gold standard, and it has very little to do with the gold. It is the standard of doing one small thing exactly, every night, for a hundred years less six, and letting the rest of the meal follow from it. Come for dinner. Start with the oyster. Let the room take it from there.</p>" } }
]
},
{
"slug": "/blog/a-night-at-the-club",
"title": "A night at the club",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "A night at the club", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Doors open at six. For the first hour the room belongs to the staff — cloths pressed, silver counted, candles trimmed to the same height along every table — and then the first guests arrive and it belongs to them until it is dark outside and dark again on the far side of midnight.</p><p>The first cocktail comes before the menu, always. It is a house rule old enough that no one remembers making it. A drink is put in your hand before a decision is asked of you, on the theory that no one should have to choose anything until they have been given a moment to arrive. The moment is the point of a supper club. Everything else is arranged around it.</p><p>Dinner runs five courses and is not to be hurried. The kitchen sends when the table is ready and not before, reads the pace of a room the way a good conductor reads an orchestra, and would rather hold a plate two minutes than land it on a conversation that has not finished. If you are in a hurry, the kitchen will know, and it will be sorry, and it will still not rush the turbot.</p><p>The soufflé is ordered at the start. This surprises first-timers and delights everyone else. A Grand Marnier soufflé cannot be summoned at the end of a meal on a whim; it must be begun while the oysters are still on ice, folded and set and watched, so that it arrives at the table risen and warm at the exact moment the last plate is cleared. To order it early is to plan your evening. We approve of planning your evening.</p><p>Between courses there is room to breathe. The cellar sends up a glass you did not order but should have. The maître d' passes and says nothing, which is its own kind of service. A regular two tables over lifts a glass to a regular she has never spoken to. The room does this on its own; we only set the conditions.</p><p>The band plays after nine. Before then the music is low and comes from nowhere in particular; after nine it comes from the corner and has names attached to it. The songs are old and the tempo is kind. No one is made to dance and a good many people do, in the narrow space between the bar and the last banquette, which was left narrow on purpose.</p><p>Late suppers begin at ten. The full menu closes and a shorter one opens — the oyster still, of course, and a plate of something warm, and whatever the kitchen has kept back for the hours when the room thins to the people who did not want to leave. These are the best hours. The staff will not tell you so, but they are.</p><p>Somewhere past midnight the pace changes again. The band puts down its instruments and picks up a glass. The kitchen sends out what it is eating itself. Conversations that started at separate tables have found their way to the bar. This is not on any menu and cannot be reserved.</p><p>We do not announce a closing time, because the club closes when the last table is ready to leave and not one minute sooner. The candles are replaced if they burn down. The cellar stays open. When the room is finally empty the staff sit where the guests sat, and that is a night at the club — begun with a drink you did not choose, ended at an hour no one agreed on, and worth every course in between.</p>" } }
]
},
{
"slug": "/blog/notes-from-the-cellar",
"title": "Notes from the cellar",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Notes from the cellar", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The list is long and the choices are few, by design. A cellar can be built two ways: wide, to have a little of everything, or deep, to have the right thing in quantity. We have always built ours deep. There are fewer names on our list than on most, and more bottles behind each one.</p><p>We pour champagne by the glass and mean it. Not one champagne — several, opened through the night so the glass in your hand was drawn from a bottle opened within the hour, never the morning. A supper club that will not open a fresh bottle for a single glass is not a supper club we would care to sit in.</p><p>The bench of burgundy is the pride of the room. It was begun by the first owner, who bought more than he could sell in the belief that a cellar is a promise to the future, and it has been added to every year since by people who took the promise seriously. Some of those bottles have been waiting under this floor longer than most of the staff have been alive.</p><p>We keep a small shelf of older bottles for the ones who ask. It is not printed. It is not offered. It exists for the guest who leans in and says, in effect, surprise me, and is prepared to be surprised at the price as well as the pour. The sommelier keeps the shelf in his head and brings it out for the right question.</p><p>The list favours the grower over the label. We would rather pour a careful wine from a name you have never heard than a famous one made carelessly, and we will tell you as much if you let us. A great many of our bottles come from families the house has bought from for decades, some of whom now send the grandchildren of the people who first shook hands on the arrangement.</p><p>Tell the sommelier what you are eating. This is the whole of the method and it is worth more than any list. He is not there to sell you the dear bottle; he is there to put the right glass beside the right plate, and the right glass is very often not the dear one. Trust it. The pairing is the point, and the pairing takes care of itself.</p><p>There is a house style to the pouring, too. Glasses are filled less than you might expect — enough to swirl, never enough to warm — and topped through the course so the wine keeps pace with the food. An empty glass is considered a small failure of attention. A glass filled to the brim is considered a larger one.</p><p>For the table that cannot decide, we keep a pairing that runs the length of the tasting menu, a glass to each course, chosen the same afternoon by whoever is on the floor that night. It is never written down and never twice the same. Order it and you are drinking the sommelier's evening, which is the best thing on the list and the one thing you cannot ask for by name.</p><p>What we pour and why is, in the end, a long answer to a short question, and the short answer is this: we buy carefully, we keep patiently, and we open generously. Come hungry, say what you are eating, and let the cellar do the rest. It has been doing it since 1926 and it has not been wrong often.</p>" } }
]
},
{
"slug": "/contact",
"title": "Reservations",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Reservation Enquiry",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Request a table",
"description": "Tell us the date, the hour and the party. We will confirm by email.",
"submitLabel": "Request a table",
"successMessage": "Thank you. Your table request has been received. We will confirm by email.",
"fields": [
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
{ "name": "party", "label": "Party size", "type": "text", "required": false, "width": "half" },
{ "name": "message", "label": "Date, time and any notes", "type": "textarea", "required": true, "width": "full" }
],
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
}
},
{
"block_key": "hours-location",
"title": "Hours & Location",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Hours",
"hours": [
{ "day": "Tuesday to Thursday", "hours": "6pm to late" },
{ "day": "Friday to Saturday", "hours": "5pm to late" },
{ "day": "Sunday", "hours": "5pm to 10pm" },
{ "day": "Monday", "closed": true }
],
"specialHoursNote": "Late suppers from 10pm, Friday and Saturday.",
"address": "12 Gilt Lane\nThe Old Quarter",
"phone": "+1 555 0126"
}
}
]
},
{
"slug": "/login",
"title": "Members",
"template_key": "auth",
"blocks": [
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } },
{ "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "<p>Members book priority tables and private-room dates.</p>" } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "The House", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "From the Floor", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Reservations", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Reservation enquiry to admins",
"description": "On a reservation-form submission, email the site admins.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,270 +0,0 @@
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) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@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,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
<div class="max-w-5xl mx-auto px-4">
@templ.Raw(data.Slots["header"])
</div>
</header>
<main class="flex-grow max-w-3xl mx-auto w-full px-4 py-12 text-center">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
<div class="max-w-5xl mx-auto px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ArtDecoLanding — Marquee Landing: wide hero, reservation strip, CTA.
templ ArtDecoLanding(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@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,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
@templ.Raw(data.Slots["marquee"])
</section>
<main class="flex-grow">
if main, ok := data.Slots["main"]; ok && main != "" {
<div class="max-w-5xl mx-auto px-4 py-16 text-center">
@templ.Raw(main)
</div>
}
</main>
<section class="w-full">
@templ.Raw(data.Slots["cta"])
</section>
<footer class="w-full mt-auto">
<div class="max-w-5xl mx-auto px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ArtDecoArticle — narrow editorial column for press, menus and stories.
templ ArtDecoArticle(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@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,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
<div class="max-w-3xl mx-auto px-4">
@templ.Raw(data.Slots["header"])
</div>
</header>
<div class="flex-grow grid gap-12 max-w-5xl mx-auto w-full px-4 py-12 md:grid-cols-[2fr_1fr]">
<main class="prose max-w-none">
if main, ok := data.Slots["main"]; ok && main != "" {
<article class="deco-dropcap" data-deco-dropcap="true">
@templ.Raw(main)
</article>
} else {
<div class="py-20 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<aside class="w-full">
@templ.Raw(data.Slots["aside"])
</aside>
</div>
<footer class="w-full mt-auto">
<div class="max-w-5xl mx-auto px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ArtDecoFullWidth — Grand Ballroom: edge-to-edge gallery layout.
templ ArtDecoFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@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,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
<div class="max-w-5xl mx-auto px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// 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))
}

View File

@ -1,607 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// 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, "<!doctype html>")
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, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);\">")
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, "<header class=\"w-full\"><div class=\"max-w-5xl mx-auto px-4\">")
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, "</div></header><main class=\"flex-grow max-w-3xl mx-auto w-full px-4 py-12 text-center\">")
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, "<div class=\"py-20\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</main><footer class=\"w-full mt-auto\"><div class=\"max-w-5xl mx-auto px-4\">")
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, "</div></footer>")
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, "</body></html>")
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, "<!doctype html>")
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, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);\">")
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, "<section class=\"w-full\">")
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, "</section><main class=\"flex-grow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"max-w-5xl mx-auto px-4 py-16 text-center\">")
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</main><section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</section><footer class=\"w-full mt-auto\"><div class=\"max-w-5xl mx-auto px-4\">")
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, "</div></footer>")
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, "</body></html>")
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, "<!doctype html>")
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, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);\">")
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, "<header class=\"w-full\"><div class=\"max-w-3xl mx-auto px-4\">")
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, "</div></header><div class=\"flex-grow grid gap-12 max-w-5xl mx-auto w-full px-4 py-12 md:grid-cols-[2fr_1fr]\"><main class=\"prose max-w-none\">")
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, "<article class=\"deco-dropcap\" data-deco-dropcap=\"true\">")
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, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<div class=\"py-20 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</main><aside class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["aside"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</aside></div><footer class=\"w-full mt-auto\"><div class=\"max-w-5xl mx-auto px-4\">")
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, "</div></footer>")
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, "</body></html>")
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, "<!doctype html>")
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, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")
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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);\">")
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, "<header class=\"w-full\">")
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, "</header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"py-20 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</main><footer class=\"w-full mt-auto\"><div class=\"max-w-5xl mx-auto px-4\">")
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, "</div></footer>")
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, "</body></html>")
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

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<div class="flex-grow grid gap-12 max-w-5xl mx-auto w-full px-4 py-12 md:grid-cols-[2fr_1fr]">
<main class="prose max-w-none">
{% if slots.main %}<article class="deco-dropcap" data-deco-dropcap="true">{{ slots.main|safe }}</article>{% else %}<div class="py-20 text-center"><p class="text-muted-foreground">No content blocks assigned to this page.</p></div>{% endif %}
</main>
<aside class="w-full">{{ slots.aside|safe }}</aside>
</div>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col deco-grain" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<a href="/" class="block text-center" aria-label="{{ site_settings.Title }} home">
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="mx-auto h-12 w-auto">{% else %}<span class="deco-caps text-2xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ site_settings.Title|default:"BlockNinja" }}</span>{% endif %}
</a>
<div class="mx-auto mt-4 mb-6 h-6 w-32 deco-sunburst" aria-hidden="true"></div>
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No form assigned to this page.</p></div>{% endif %}</div>
{% if slots.footer %}<div class="mt-6 text-center text-sm" style="color:hsl(var(--muted-foreground));">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
{% if title %}
<div class="text-center">
<h1 class="deco-caps deco-underline mx-auto w-fit text-4xl md:text-5xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);">{{ title }}</h1>
{% if page_meta.MetaDescription %}<p class="mt-4 text-lg italic" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
<div class="mx-auto mt-6 h-6 w-40 deco-sunburst" aria-hidden="true"></div>
</div>
{% endif %}
{% if slots.featured %}<section class="mt-12">{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section class="mt-12">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No posts published yet.</p></div>{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
{% if slots.aside %}
<div class="grid gap-12 md:grid-cols-[2fr_1fr]">
<div>{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
<aside class="w-full md:border-l md:border-[hsl(var(--primary)/0.3)] md:pl-10">{{ slots.aside|safe }}</aside>
</div>
{% else %}
<div class="max-w-3xl mx-auto">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</div>
{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow max-w-3xl mx-auto w-full px-4 py-12 text-center">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20"><p class="text-muted-foreground">No content blocks assigned to this page.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="text-muted-foreground">No content blocks assigned to this page.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">
<div class="max-w-5xl mx-auto px-4">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
<section class="w-full">{{ slots.marquee|safe }}</section>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% endif %}
</main>
<section class="w-full">{{ slots.cta|safe }}</section>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,64 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<title>{{ site_name }}</title>
<style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style="background-color: {{ colors.background }}; margin: 0; padding: 0; font-family: &amp;#39;Cormorant Garamond&amp;#39;, &amp;#39;Cormorant&amp;#39;, Georgia, serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 32px 10px; background-color: {{ colors.background }};">
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.background }}; border: 1px solid {{ colors.border }};">
<tr>
<td align="center" style="padding: 0; background-color: {{ colors.foreground }};">
<img src="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{{ colors.primary|default:"#c9a14a"|cut:"#" }}%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" alt="" width="600" height="80" style="display: block; width: 100%; max-width: 600px; height: 80px; background-color: {{ colors.foreground }};">
{% if site_name %}<h1 style="margin: 0; padding: 24px 16px; font-family: &amp;#39;Italiana&amp;#39;, &amp;#39;Cinzel&amp;#39;, Georgia, serif; font-size: 28px; text-transform: uppercase; letter-spacing: 0.18em; color: {{ colors.primary }};">{{ site_name }}</h1>{% endif %}
</td>
</tr>
<tr>
<td class="content-padding" style="padding: 40px 48px; color: {{ colors.foreground }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
</tr>
<tr>
<td style="padding: 0 48px;"><hr style="border: none; border-top: 1px solid {{ colors.primary }}; margin: 0;"></td>
</tr>
<tr>
<td align="center" style="padding: 24px 48px; color: {{ colors.mutedForeground }}; font-size: 13px;">
<p style="margin: 0 0 8px; font-family: &amp;#39;Italiana&amp;#39;, &amp;#39;Cinzel&amp;#39;, Georgia, serif; text-transform: uppercase; letter-spacing: 0.18em; color: {{ colors.foreground }};">{{ site_name }}</p>
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.primary }}; text-decoration: none; border-bottom: 1px solid {{ colors.primary }};">{{ site_url }}</a></p>{% endif %}
{% if support_email %}<p style="margin: 12px 0 0; font-style: italic; color: {{ colors.mutedForeground }};">P.S. Reservations &amp; enquiries: {{ support_email }}</p>{% endif %}
{% if unsubscribe_url %}<p style="margin: 16px 0 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,21 @@
{# announcement-bar override — gilded banner. Dismiss <script>, data-bn-annkey and localStorage keying preserved verbatim. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
{% set anncls = "bg-primary/15 text-foreground" %}
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-block-override="art-deco:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-[hsl(var(--primary)/0.4)] {{ anncls }} {{ class }}">
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
<p class="deco-caps text-xs">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed override — gold-framed player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
<figure data-block-override="art-deco:audio-embed" class="bn-audio deco-frame my-8 overflow-hidden bg-card text-card-foreground{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="deco-step h-16 w-16 shrink-0 overflow-hidden bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="deco-caps truncate text-sm" style="font-family:var(--font-heading, 'Cinzel', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</p>{% endif %}{% if artist %}<p class="truncate text-sm italic" style="color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="deco-pill inline-flex items-center gap-2 text-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="deco-link inline-flex items-center gap-1 text-sm">::lucide:download:sm:: Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,33 @@
{# auth-form override — gilded login/register. Wiring is UNTOUCHABLE and reproduced exactly from builtin auth_blocks.go: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message. Captcha widget injected from _captcha_widget when captchaEnabled (server enforces). Fields: default_tab, show_social (social omitted — needs Go/site config; graceful). #}
{% if context.isPublicLoggedIn %}
<div data-block-override="art-deco:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
<a href="/account" class="deco-link mt-3 inline-block">My account</a>
</div>
{% else %}
<div data-block-override="art-deco:auth-form" class="auth-form deco-frame mx-auto max-w-md bg-card p-8">
<div id="auth-message"></div>
<div class="mb-6 flex border-b border-[hsl(var(--primary)/0.3)]">
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.setAttribute('data-active','1');this.nextElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab != "register" %}data-active="1"{% endif %} class="deco-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary">Sign in</button>
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.setAttribute('data-active','1');this.previousElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab == "register" %}data-active="1"{% endif %} class="deco-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary">Register</button>
</div>
<div id="login-panel" class="{% if default_tab == "register" %}hidden{% endif %}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-4"><label class="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="deco-pill w-full" hx-disabled-elt="this">Sign in</button>
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="deco-link">Forgot password?</a></p>
</form>
</div>
<div id="register-panel" class="{% if default_tab != "register" %}hidden{% endif %}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-3"><label class="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-4"><label class="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="deco-pill w-full" hx-disabled-elt="this">Create account</button>
</form>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,31 @@
{# auth-status override — header login state. Logged-out: Sign in / Reserve links. Logged-in: gold monogram + dropdown (logout posts /api/auth/logout). Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="art-deco:auth-status" class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="deco-caps flex items-center gap-2 text-xs opacity-90 hover:opacity-100 px-3 py-1.5 transition-colors">
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full text-[0.7rem] font-bold" style="background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));">{{ context.publicDisplayName|default:context.publicUsername|first|upper }}</span>
<span>{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 deco-frame bg-card py-1 z-50">
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My profile</a>
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My reviews</a>
<hr class="my-1 deco-rule"/>
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-accent">Sign out</button>
</div>
<script>
(function(){
document.addEventListener('click', function(e) {
var menu = document.getElementById('auth-dropdown-menu');
var btn = document.querySelector('.auth-status > button');
if (!menu || !btn) return;
if (!btn.contains(e.target) && !menu.contains(e.target)) { menu.classList.add('hidden'); }
});
})();
</script>
</div>
{% else %}
<div data-block-override="art-deco:auth-status" class="auth-status flex items-center gap-2">
<a href="/login" class="deco-caps text-xs px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-primary transition-colors">Sign in</a>
<a href="/login?tab=register" class="deco-pill text-xs">Reserve</a>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (art-deco) — author-page header from the `authors` provider. #}
{% if author %}
<section data-block-override="art-deco:author-bio-hero" class="deco-rule-thick bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-6 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-12"></span><span class="deco-rule w-12"></span></div>
<h1 class="deco-caps text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="deco-caps mt-2 text-sm text-primary">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="mt-5 max-w-2xl leading-relaxed text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif)">{{ author.bio }}</p>{% endif %}
<div class="mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-6 flex items-center justify-center gap-4">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,14 @@
{# author-bio (art-deco) — byline card sourced from the `post` provider's author_* fields. #}
{% if post.author_name %}
<aside data-block-override="art-deco:author-bio" class="deco-frame bg-card p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="deco-caps mb-4 text-xs text-primary">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-muted ring-1 ring-[hsl(var(--primary)/0.5)]">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="deco-caps text-base text-card-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
<span class="deco-rule mt-2 block w-10"></span>
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif)">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (art-deco) — blog-index header + featured posts from the `posts` provider. #}
<section data-block-override="art-deco:blog-hero" class="bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="mx-auto max-w-2xl text-center">
{% if title %}<h1 class="deco-caps deco-underline text-4xl md:text-5xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif)">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
{% for post in posts %}
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-8{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="deco-step mb-4 block overflow-hidden bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<span class="deco-rule mb-4 block w-12"></span>
<h2 class="deco-caps {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="mt-3 text-sm text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground">{% if post.author_name %}<span class="deco-caps text-primary">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,15 @@
{# breadcrumbs (art-deco) — trail from the `breadcrumbs` provider (page ancestry). #}
{# showHome prepends a Home link; separator is text ("/") for default or a chevron #}
{# for the arrows template. The trailing is_current item renders as plain text. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" data-block-override="art-deco:breadcrumbs" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="flex flex-wrap items-center gap-1 text-xs">
{% if showHome %}<li class="flex items-center"><a href="/" class="deco-caps text-muted-foreground transition-colors hover:text-primary">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
{% for item in breadcrumbs %}
<li class="flex items-center">
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-3.5 w-3.5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-primary">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="deco-caps text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="deco-caps text-muted-foreground transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -0,0 +1,10 @@
{# button override — gold-ringed lacquer pill. Fields (new builtin contract, per OVERRIDE-CONTRACT §4): link, style, label, size, icon, iconPosition, newTab, align. #}
{% with align=align|default:"left" %}
<div data-block-override="art-deco:button" class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
{% if style == "ghost" or style == "link" or style == "secondary" %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="deco-link inline-flex items-center gap-2 text-sm">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
{% else %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="deco-pill inline-flex items-center gap-2">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
{% endif %}
</div>
{% endwith %}

View File

@ -0,0 +1,4 @@
{# card override — angular gold Deco panel. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #}
{% with layout=layout|default:"vertical" %}
<article data-block-override="art-deco:card" class="bn-card deco-frame overflow-hidden bg-card text-card-foreground transition-shadow hover:deco-foil {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="deco-caps text-lg" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm deco-link">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>
{% endwith %}

Some files were not shown because too many files have changed in this diff Show More