Compare commits
No commits in common. "main" and "v0.1.1" have entirely different histories.
1
.gitignore
vendored
@ -3,4 +3,3 @@
|
||||
tmp/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.bnp
|
||||
|
||||
59
Makefile
@ -1,55 +1,32 @@
|
||||
# art-deco — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||
# Art Deco — local build helpers (.so plugin workflow).
|
||||
#
|
||||
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
||||
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
||||
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
||||
# 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
|
||||
# `make` produces art-deco.so via CGO go build -buildmode=plugin. This is the
|
||||
# local-only build used by check-safety. The full `rebuild` target intentionally
|
||||
# does NOT ship here — production deployment goes through the upstream gotham-style
|
||||
# Makefile, which is reserved for human-run cycles, not autonomous agents.
|
||||
|
||||
.PHONY: build verify archive-check publish clean help
|
||||
.PHONY: all clean templ help
|
||||
|
||||
PLUGIN_NAME := art-deco
|
||||
NINJA := ninja
|
||||
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
||||
TEMPL_BIN := $(HOME)/go/bin/templ
|
||||
|
||||
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
||||
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
||||
build:
|
||||
$(NINJA) plugin build --dir .
|
||||
# Default target: build the .so locally.
|
||||
all: $(PLUGIN_NAME).so
|
||||
|
||||
# Validate the built .bnp exactly as the registry/loader will.
|
||||
verify:
|
||||
$(NINJA) plugin verify $(BNP)
|
||||
# Local plugin build (no container).
|
||||
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
|
||||
|
||||
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
||||
archive-check:
|
||||
@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)
|
||||
# Regenerate templ Go files from .templ sources.
|
||||
templ:
|
||||
$(TEMPL_BIN) generate
|
||||
|
||||
# Remove build artefacts.
|
||||
clean:
|
||||
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||
rm -f $(PLUGIN_NAME).so
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " build Pack the codeless .bnp (no wasm)"
|
||||
@echo " verify Validate the built .bnp (loader-identical checks)"
|
||||
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||
@echo " publish Publish the .bnp to the active registry"
|
||||
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
||||
@echo " templ Regenerate templ Go files"
|
||||
@echo " clean Remove build artefacts"
|
||||
|
||||
24
README.md
@ -1,24 +0,0 @@
|
||||
# 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.
|
||||
@ -1,36 +1,53 @@
|
||||
# Recommended Fonts — Art Deco
|
||||
|
||||
This theme **bundles** its display and body families as subset woff2 files in
|
||||
`assets/fonts/` (see `fonts.json`). They install as `source=template` rows and
|
||||
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.
|
||||
This theme ships `fonts.json = []` (Wave-1 policy, see `themes/docs/FONTS.md`).
|
||||
The intended typographic identity is achieved by an admin assigning the
|
||||
following Google Fonts to the theme's typography slots after install.
|
||||
|
||||
## Bundled (shipped in this theme)
|
||||
## Picker workflow
|
||||
|
||||
| Slot | Family | Weights | File(s) | License |
|
||||
|------|--------|---------|---------|---------|
|
||||
| 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 |
|
||||
1. Sign in as an admin and open **Settings → Typography**.
|
||||
2. Switch to the **Google Fonts** tab in the font picker.
|
||||
3. Search for the family, click **Add**, then assign it to the listed slot.
|
||||
|
||||
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).
|
||||
## Display heading — Italiana
|
||||
|
||||
## Mono slot (not bundled — assign if needed)
|
||||
- **Source**: `google:Italiana`
|
||||
- **Slot**: Heading
|
||||
- **Why**: The spec calls for stamped Art Deco titles in tight tracking. Italiana
|
||||
is the closest curated Google Fonts match to the high-contrast 1920s display
|
||||
cuts the brief describes.
|
||||
- **Fallback already in CSS**: `var(--font-heading, "Italiana", "Cinzel", Georgia, serif)`.
|
||||
|
||||
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:
|
||||
### Alternative
|
||||
|
||||
1. Sign in as an admin, open **Settings → Typography**.
|
||||
2. Switch to the **Google Fonts** tab and add **JetBrains Mono**.
|
||||
3. Assign it to the **Mono** slot.
|
||||
- **Source**: `google:Cinzel` (also acceptable per spec §3)
|
||||
Pick this if Italiana feels too thin at small sizes.
|
||||
|
||||
If left unset, the CSS falls back to the system monospace stack.
|
||||
## Body — Cormorant Garamond
|
||||
|
||||
## Alternatives (optional)
|
||||
- **Source**: `google:Cormorant Garamond`
|
||||
- **Slot**: Body
|
||||
- **Why**: Classical italic and book weights for long-form prose, exactly the
|
||||
"Cormorant for long-form prose with classical italics" pairing the spec calls
|
||||
out.
|
||||
- **Fallback already in CSS**: `var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)`.
|
||||
|
||||
- Heading: `google:Italiana` — a thinner, higher-contrast display cut; already
|
||||
the second fallback in the heading `var()` list.
|
||||
- Body: the system serif stack (`Georgia, serif`) is the final fallback.
|
||||
## Mono — JetBrains Mono
|
||||
|
||||
- **Source**: `google:JetBrains Mono`
|
||||
- **Slot**: Mono
|
||||
- **Why**: Spec wants a precise monospace for reservation IDs and room numbers.
|
||||
JetBrains Mono ships across all curated Google Fonts and is the cleanest
|
||||
utilitarian option.
|
||||
- **Fallback already in CSS**: `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)`.
|
||||
|
||||
## Notes for Wave-2
|
||||
|
||||
- If Italiana proves too brittle on retina at large sizes, commission a
|
||||
redistributable Art Deco display face and bundle it via `fonts.json`. The
|
||||
rest of the CSS is already variable-driven and will pick the new family up
|
||||
without further edits.
|
||||
- Body italic for the drop-cap relies on the body family having an italic
|
||||
variant; both Cormorant Garamond and Cormorant ship italics in the curated
|
||||
Google Fonts list — no extra picker action needed.
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
# 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.
|
||||
@ -1,23 +0,0 @@
|
||||
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.
|
||||
@ -1,32 +0,0 @@
|
||||
# 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
|
||||
@ -1,11 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,39 +0,0 @@
|
||||
{
|
||||
"$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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
<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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "JetBrains Mono", 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>
|
||||
@ -1,14 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,67 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
<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, "Italiana", "Cinzel", 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>
|
||||
41
button_override.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// ButtonOverrideData is the typed view for the button override component.
|
||||
type ButtonOverrideData struct {
|
||||
Label string
|
||||
URL string
|
||||
Variant string
|
||||
}
|
||||
|
||||
// normaliseButtonVariant returns a known variant or "primary" as default.
|
||||
func normaliseButtonVariant(v string) string {
|
||||
switch v {
|
||||
case "primary", "secondary", "ghost":
|
||||
return v
|
||||
default:
|
||||
return "primary"
|
||||
}
|
||||
}
|
||||
|
||||
// ArtDecoButtonBlock overrides the built-in "button" block when the Art Deco theme is active.
|
||||
// Content: {"label": "...", "url": "...", "variant": "primary|secondary|ghost"}
|
||||
func ArtDecoButtonBlock(ctx context.Context, content map[string]any) string {
|
||||
data := ButtonOverrideData{
|
||||
Label: getString(content, "label"),
|
||||
URL: getString(content, "url"),
|
||||
Variant: normaliseButtonVariant(getString(content, "variant")),
|
||||
}
|
||||
|
||||
if data.Label == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = artDecoButtonComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
36
button_override.templ
Normal file
@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
// buttonVariantStyle returns inline style overrides per variant.
|
||||
func buttonVariantStyle(variant string) string {
|
||||
switch variant {
|
||||
case "secondary":
|
||||
return "background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border-color: hsl(var(--primary));"
|
||||
case "ghost":
|
||||
return "background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--primary));"
|
||||
default:
|
||||
return "background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-color: hsl(var(--primary));"
|
||||
}
|
||||
}
|
||||
|
||||
// artDecoButtonComponent renders the black-pill button with stamped hover state.
|
||||
templ artDecoButtonComponent(data ButtonOverrideData) {
|
||||
if data.URL != "" {
|
||||
<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>
|
||||
}
|
||||
}
|
||||
125
button_override_templ.go
Normal file
@ -0,0 +1,125 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// buttonVariantStyle returns inline style overrides per variant.
|
||||
func buttonVariantStyle(variant string) string {
|
||||
switch variant {
|
||||
case "secondary":
|
||||
return "background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border-color: hsl(var(--primary));"
|
||||
case "ghost":
|
||||
return "background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--primary));"
|
||||
default:
|
||||
return "background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-color: hsl(var(--primary));"
|
||||
}
|
||||
}
|
||||
|
||||
// artDecoButtonComponent renders the black-pill button with stamped hover state.
|
||||
func artDecoButtonComponent(data ButtonOverrideData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if data.URL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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
|
||||
43
divider_fan.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// DividerFanBlockMeta defines the fan divider block.
|
||||
var DividerFanBlockMeta = blocks.BlockMeta{
|
||||
Key: "divider_fan",
|
||||
Title: "Fan Divider",
|
||||
Description: "Pure-CSS gold geometric divider — sunburst, scallop or ziggurat.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// DividerFanData carries the variant for the divider component.
|
||||
type DividerFanData struct {
|
||||
Variant string
|
||||
}
|
||||
|
||||
// normaliseVariant clamps to the supported set; unknown values fall back to sunburst.
|
||||
func normaliseVariant(v string) string {
|
||||
switch v {
|
||||
case "scallop", "ziggurat":
|
||||
return v
|
||||
default:
|
||||
return "sunburst"
|
||||
}
|
||||
}
|
||||
|
||||
// DividerFanBlock renders a CSS-only geometric divider.
|
||||
// Content: {"variant": "sunburst" | "scallop" | "ziggurat"}
|
||||
func DividerFanBlock(ctx context.Context, content map[string]any) string {
|
||||
data := DividerFanData{
|
||||
Variant: normaliseVariant(getString(content, "variant")),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = dividerFanComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
26
divider_fan.templ
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
// dividerVariantClass maps the variant to the corresponding utility class.
|
||||
func dividerVariantClass(variant string) string {
|
||||
switch variant {
|
||||
case "scallop":
|
||||
return "deco-scallop"
|
||||
case "ziggurat":
|
||||
return "deco-ziggurat"
|
||||
default:
|
||||
return "deco-sunburst"
|
||||
}
|
||||
}
|
||||
|
||||
// dividerFanComponent renders a pure-CSS gold geometric divider.
|
||||
templ dividerFanComponent(data DividerFanData) {
|
||||
<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>
|
||||
}
|
||||
88
divider_fan_templ.go
Normal file
@ -0,0 +1,88 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// dividerVariantClass maps the variant to the corresponding utility class.
|
||||
func dividerVariantClass(variant string) string {
|
||||
switch variant {
|
||||
case "scallop":
|
||||
return "deco-scallop"
|
||||
case "ziggurat":
|
||||
return "deco-ziggurat"
|
||||
default:
|
||||
return "deco-sunburst"
|
||||
}
|
||||
}
|
||||
|
||||
// dividerFanComponent renders a pure-CSS gold geometric divider.
|
||||
func dividerFanComponent(data DividerFanData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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
|
||||
178
email_wrapper.templ
Normal file
@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// ArtDecoEmailWrapper wraps body content in the Art Deco email layout:
|
||||
// ivory background, 600px column, jet-black masthead with flat-gold PNG sunburst,
|
||||
// Cormorant body, gold hairline above footer, reservation phone postscript.
|
||||
func ArtDecoEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||
var buf bytes.Buffer
|
||||
_ = artDecoEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// edColor returns a color from the email context or a hex fallback for the named token.
|
||||
func edColor(value, fallback string) string {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// edBg ivory background.
|
||||
func edBg(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Background, "#f6efe2")
|
||||
}
|
||||
|
||||
// edCard jet-black masthead surface (uses Foreground as the dark masthead colour).
|
||||
func edCardDark(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Foreground, "#191510")
|
||||
}
|
||||
|
||||
// edFg foreground text on the ivory body.
|
||||
func edFg(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Foreground, "#191510")
|
||||
}
|
||||
|
||||
// edPrimary champagne-gold accent (used for the hairline and links).
|
||||
func edPrimary(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Primary, "#c9a14a")
|
||||
}
|
||||
|
||||
// edMutedFg muted footer text.
|
||||
func edMutedFg(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.MutedForeground, "#6c5b35")
|
||||
}
|
||||
|
||||
// edBorder hairline gold border colour.
|
||||
func edBorder(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Border, "#c9a14a")
|
||||
}
|
||||
|
||||
// flatGoldSunburstSVG is the inlined data: URI for the flat-gold sunburst image
|
||||
// used as a fallback for the Outlook masthead (CSS conic-gradient is flattened by Outlook).
|
||||
// We render via string concatenation so the URL-encoded %xx tokens are not misread as fmt verbs.
|
||||
func flatGoldSunburstSVG(gold string) string {
|
||||
g := stripHash(gold)
|
||||
prefix := "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%2080%22%3E%3Crect%20width%3D%22600%22%20height%3D%2280%22%20fill%3D%22%23191510%22%2F%3E%3Cg%20stroke%3D%22%23"
|
||||
rays := "%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M300%2080%20L%20100%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20180%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20260%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20340%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20420%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20500%2010%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"
|
||||
return prefix + g + rays
|
||||
}
|
||||
|
||||
// stripHash removes a leading '#' if present (defensive for the data URI).
|
||||
func stripHash(s string) string {
|
||||
if len(s) > 0 && s[0] == '#' {
|
||||
return s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// artDecoEmailTemplate renders the Art Deco email wrapper.
|
||||
templ artDecoEmailTemplate(emailCtx templates.EmailContext, body string) {
|
||||
<!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 & 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>
|
||||
}
|
||||
463
email_wrapper_templ.go
Normal file
@ -0,0 +1,463 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// ArtDecoEmailWrapper wraps body content in the Art Deco email layout:
|
||||
// ivory background, 600px column, jet-black masthead with flat-gold PNG sunburst,
|
||||
// Cormorant body, gold hairline above footer, reservation phone postscript.
|
||||
func ArtDecoEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||
var buf bytes.Buffer
|
||||
_ = artDecoEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// edColor returns a color from the email context or a hex fallback for the named token.
|
||||
func edColor(value, fallback string) string {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// edBg ivory background.
|
||||
func edBg(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Background, "#f6efe2")
|
||||
}
|
||||
|
||||
// edCard jet-black masthead surface (uses Foreground as the dark masthead colour).
|
||||
func edCardDark(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Foreground, "#191510")
|
||||
}
|
||||
|
||||
// edFg foreground text on the ivory body.
|
||||
func edFg(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Foreground, "#191510")
|
||||
}
|
||||
|
||||
// edPrimary champagne-gold accent (used for the hairline and links).
|
||||
func edPrimary(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Primary, "#c9a14a")
|
||||
}
|
||||
|
||||
// edMutedFg muted footer text.
|
||||
func edMutedFg(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.MutedForeground, "#6c5b35")
|
||||
}
|
||||
|
||||
// edBorder hairline gold border colour.
|
||||
func edBorder(emailCtx templates.EmailContext) string {
|
||||
return edColor(emailCtx.Colors.Border, "#c9a14a")
|
||||
}
|
||||
|
||||
// flatGoldSunburstSVG is the inlined data: URI for the flat-gold sunburst image
|
||||
// used as a fallback for the Outlook masthead (CSS conic-gradient is flattened by Outlook).
|
||||
// We render via string concatenation so the URL-encoded %xx tokens are not misread as fmt verbs.
|
||||
func flatGoldSunburstSVG(gold string) string {
|
||||
g := stripHash(gold)
|
||||
prefix := "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%2080%22%3E%3Crect%20width%3D%22600%22%20height%3D%2280%22%20fill%3D%22%23191510%22%2F%3E%3Cg%20stroke%3D%22%23"
|
||||
rays := "%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M300%2080%20L%20100%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20180%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20260%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20340%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20420%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20500%2010%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"
|
||||
return prefix + g + rays
|
||||
}
|
||||
|
||||
// stripHash removes a leading '#' if present (defensive for the data URI).
|
||||
func stripHash(s string) string {
|
||||
if len(s) > 0 && s[0] == '#' {
|
||||
return s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// artDecoEmailTemplate renders the Art Deco email wrapper.
|
||||
func artDecoEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!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 & 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
|
||||
58
embed.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed schemas/*
|
||||
var schemasFS embed.FS
|
||||
|
||||
//go:embed presets.json
|
||||
var presetsData []byte
|
||||
|
||||
//go:embed fonts.json
|
||||
var fontsData []byte
|
||||
|
||||
//go:embed plugin.mod
|
||||
var pluginModBytes []byte
|
||||
|
||||
// Assets returns the embedded assets filesystem rooted at assets/.
|
||||
func Assets() fs.FS {
|
||||
sub, _ := fs.Sub(assetsFS, "assets")
|
||||
return sub
|
||||
}
|
||||
|
||||
// Schemas returns the embedded schemas filesystem rooted at schemas/.
|
||||
func Schemas() fs.FS {
|
||||
sub, _ := fs.Sub(schemasFS, "schemas")
|
||||
return sub
|
||||
}
|
||||
|
||||
// AssetsHandler returns an http.Handler that serves the embedded assets.
|
||||
func AssetsHandler() http.Handler {
|
||||
return http.FileServer(http.FS(Assets()))
|
||||
}
|
||||
|
||||
// ThemePresets returns the embedded theme presets JSON.
|
||||
func ThemePresets() []byte { return presetsData }
|
||||
|
||||
// BundledFonts returns the embedded fonts manifest JSON ([] in this pass).
|
||||
func BundledFonts() []byte { return fontsData }
|
||||
|
||||
// ThemeCSSManifest exposes the Art Deco utility CSS to the host Tailwind input.
|
||||
func ThemeCSSManifest() *plugin.CSSManifest {
|
||||
css, err := assetsFS.ReadFile("assets/style.css")
|
||||
if err != nil {
|
||||
return &plugin.CSSManifest{}
|
||||
}
|
||||
return &plugin.CSSManifest{
|
||||
InputCSSAppend: string(css),
|
||||
}
|
||||
}
|
||||
21
fonts.json
@ -1,20 +1 @@
|
||||
[
|
||||
{
|
||||
"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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
47
footer.go
Normal file
@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// FooterBlockMeta defines the Art Deco footer block.
|
||||
var FooterBlockMeta = blocks.BlockMeta{
|
||||
Key: "footer",
|
||||
Title: "Footer",
|
||||
Description: "Centered Art Deco footer with gold rule, address, reservations email and optional social row.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// FooterData is the typed view for the footer component.
|
||||
type FooterData struct {
|
||||
Address string
|
||||
ReservationsEmail string
|
||||
ShowSocial bool
|
||||
MenuName string
|
||||
}
|
||||
|
||||
// FooterBlock renders the centered Art Deco footer.
|
||||
// Content: {"address": "...", "reservationsEmail": "...", "showSocial": "true|false", "menuName": "..."}
|
||||
func FooterBlock(ctx context.Context, content map[string]any) string {
|
||||
// showSocial is stored as a string select ("true"/"false"); also accept bool for flexibility.
|
||||
showSocial := true
|
||||
if b, ok := content["showSocial"].(bool); ok {
|
||||
showSocial = b
|
||||
} else if s := getString(content, "showSocial"); s != "" {
|
||||
showSocial = s != "false"
|
||||
}
|
||||
|
||||
data := FooterData{
|
||||
Address: getString(content, "address"),
|
||||
ReservationsEmail: getString(content, "reservationsEmail"),
|
||||
ShowSocial: showSocial,
|
||||
MenuName: getString(content, "menuName"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = footerComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
59
footer.templ
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// addressLines splits a multi-line address into individual lines for centered rendering.
|
||||
func addressLines(addr string) []string {
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(addr, "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
l := strings.TrimSpace(line)
|
||||
if l != "" {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// footerComponent renders the centered Art Deco footer.
|
||||
templ footerComponent(data FooterData) {
|
||||
<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, "Italiana", "Cinzel", 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>
|
||||
}
|
||||
149
footer_templ.go
Normal file
@ -0,0 +1,149 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "strings"
|
||||
|
||||
// addressLines splits a multi-line address into individual lines for centered rendering.
|
||||
func addressLines(addr string) []string {
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(addr, "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
l := strings.TrimSpace(line)
|
||||
if l != "" {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// footerComponent renders the centered Art Deco footer.
|
||||
func footerComponent(data FooterData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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, "Italiana", "Cinzel", 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
|
||||
48
gallery_fan.go
Normal file
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// GalleryFanBlockMeta defines the mirrored fan gallery block.
|
||||
var GalleryFanBlockMeta = blocks.BlockMeta{
|
||||
Key: "gallery_fan",
|
||||
Title: "Fan Gallery",
|
||||
Description: "Mirrored gallery framed by sunburst — collapses to single column under 640px.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// GalleryImage is a single image in the gallery.
|
||||
type GalleryImage struct {
|
||||
Src string
|
||||
Caption string
|
||||
}
|
||||
|
||||
// GalleryFanData is the typed view for the gallery component.
|
||||
type GalleryFanData struct {
|
||||
Images []GalleryImage
|
||||
Columns int
|
||||
}
|
||||
|
||||
// GalleryFanBlock renders a mirrored fan gallery.
|
||||
// Content: {"images": [{"src": "...", "caption": "..."}], "columns": 3}
|
||||
func GalleryFanBlock(ctx context.Context, content map[string]any) string {
|
||||
items := getSlice(content, "images")
|
||||
images := make([]GalleryImage, 0, len(items))
|
||||
for _, item := range items {
|
||||
images = append(images, GalleryImage{
|
||||
Src: getString(item, "src"),
|
||||
Caption: getString(item, "caption"),
|
||||
})
|
||||
}
|
||||
|
||||
cols := getInt(content, "columns", 3)
|
||||
cols = clampInt(cols, 2, 4)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = galleryFanComponent(GalleryFanData{Images: images, Columns: cols}).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
54
gallery_fan.templ
Normal file
@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
// galleryFanGridCols maps the column count to a tailwind grid class with single-column collapse below 640px.
|
||||
func galleryFanGridCols(cols int) string {
|
||||
switch cols {
|
||||
case 2:
|
||||
return "grid-cols-1 sm:grid-cols-2"
|
||||
case 4:
|
||||
return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
||||
default:
|
||||
return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
||||
}
|
||||
}
|
||||
|
||||
// fallbackImage returns a tiny transparent svg data URI used when no image src is supplied.
|
||||
func fallbackImage() string {
|
||||
return "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%23222%22%2F%3E%3C%2Fsvg%3E"
|
||||
}
|
||||
|
||||
// galleryFanComponent renders the mirrored gallery framed by sunburst rays.
|
||||
templ galleryFanComponent(data GalleryFanData) {
|
||||
<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>
|
||||
}
|
||||
169
gallery_fan_templ.go
Normal file
@ -0,0 +1,169 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// galleryFanGridCols maps the column count to a tailwind grid class with single-column collapse below 640px.
|
||||
func galleryFanGridCols(cols int) string {
|
||||
switch cols {
|
||||
case 2:
|
||||
return "grid-cols-1 sm:grid-cols-2"
|
||||
case 4:
|
||||
return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
||||
default:
|
||||
return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
||||
}
|
||||
}
|
||||
|
||||
// fallbackImage returns a tiny transparent svg data URI used when no image src is supplied.
|
||||
func fallbackImage() string {
|
||||
return "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%23222%22%2F%3E%3C%2Fsvg%3E"
|
||||
}
|
||||
|
||||
// galleryFanComponent renders the mirrored gallery framed by sunburst rays.
|
||||
func galleryFanComponent(data GalleryFanData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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
Normal file
@ -0,0 +1,20 @@
|
||||
module git.dev.alexdunmow.com/block/themes/art-deco
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
git.dev.alexdunmow.com/block/core v0.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
Normal file
@ -0,0 +1,44 @@
|
||||
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=
|
||||
44
heading_override.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// parseHeadingLevel reads "level" from a content map, defaulting to 2 and clamping to 1..6.
|
||||
func parseHeadingLevel(content map[string]any) int {
|
||||
if v, ok := content["level"].(float64); ok {
|
||||
return clampInt(int(v), 1, 6)
|
||||
}
|
||||
if v, ok := content["level"].(int); ok {
|
||||
return clampInt(v, 1, 6)
|
||||
}
|
||||
if v, ok := content["level"].(string); ok {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return clampInt(i, 1, 6)
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// ArtDecoHeadingBlock overrides the built-in "heading" block when the Art Deco theme is active.
|
||||
// Content: {"text": "...", "level": 1-6, "textClass": "optional-utility-classes"}
|
||||
func ArtDecoHeadingBlock(ctx context.Context, content map[string]any) string {
|
||||
data := HeadingData{
|
||||
Text: getString(content, "text"),
|
||||
Level: parseHeadingLevel(content),
|
||||
ExtraCls: getString(content, "textClass"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = artDecoHeadingComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// HeadingData is the typed view for the heading override component.
|
||||
type HeadingData struct {
|
||||
Text string
|
||||
Level int
|
||||
ExtraCls string
|
||||
}
|
||||
69
heading_override.templ
Normal file
@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
// headingSizeClass returns the responsive size utility for a given heading level.
|
||||
func headingSizeClass(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "text-4xl md:text-5xl"
|
||||
case 2:
|
||||
return "text-3xl md:text-4xl"
|
||||
case 3:
|
||||
return "text-2xl md:text-3xl"
|
||||
case 4:
|
||||
return "text-xl md:text-2xl"
|
||||
case 5:
|
||||
return "text-lg md:text-xl"
|
||||
case 6:
|
||||
return "text-base md:text-lg"
|
||||
default:
|
||||
return "text-3xl md:text-4xl"
|
||||
}
|
||||
}
|
||||
|
||||
// artDecoHeadingComponent renders an Art Deco heading with caps tracking and hairline gold underline.
|
||||
templ artDecoHeadingComponent(data HeadingData) {
|
||||
switch data.Level {
|
||||
case 1:
|
||||
<h1
|
||||
data-block-override="art-deco:heading"
|
||||
class={ "deco-caps deco-underline", headingSizeClass(1), data.ExtraCls }
|
||||
style="font-family: var(--font-heading, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", Georgia, serif); color: hsl(var(--foreground));"
|
||||
>{ data.Text }</h2>
|
||||
}
|
||||
}
|
||||
311
heading_override_templ.go
Normal file
@ -0,0 +1,311 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// headingSizeClass returns the responsive size utility for a given heading level.
|
||||
func headingSizeClass(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "text-4xl md:text-5xl"
|
||||
case 2:
|
||||
return "text-3xl md:text-4xl"
|
||||
case 3:
|
||||
return "text-2xl md:text-3xl"
|
||||
case 4:
|
||||
return "text-xl md:text-2xl"
|
||||
case 5:
|
||||
return "text-lg md:text-xl"
|
||||
case 6:
|
||||
return "text-base md:text-lg"
|
||||
default:
|
||||
return "text-3xl md:text-4xl"
|
||||
}
|
||||
}
|
||||
|
||||
// artDecoHeadingComponent renders an Art Deco heading with caps tracking and hairline gold underline.
|
||||
func artDecoHeadingComponent(data HeadingData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
switch data.Level {
|
||||
case 1:
|
||||
var templ_7745c5c3_Var2 = []any{"deco-caps deco-underline", headingSizeClass(1), data.ExtraCls}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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
|
||||
59
helpers.go
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
// getString extracts a string value from a content map.
|
||||
func getString(content map[string]any, key string) string {
|
||||
if v, ok := content[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getInt extracts an int value from a content map (handles float64 from JSON).
|
||||
func getInt(content map[string]any, key string, defaultVal int) int {
|
||||
if v, ok := content[key].(float64); ok {
|
||||
return int(v)
|
||||
}
|
||||
if v, ok := content[key].(int); ok {
|
||||
return v
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// getSlice extracts a slice of maps from a content map.
|
||||
func getSlice(content map[string]any, key string) []map[string]any {
|
||||
if v, ok := content[key].([]any); ok {
|
||||
result := make([]map[string]any, 0, len(v))
|
||||
for _, item := range v {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
result = append(result, m)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStringSlice extracts a slice of strings from a content map.
|
||||
func getStringSlice(content map[string]any, key string) []string {
|
||||
if v, ok := content[key].([]any); ok {
|
||||
result := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clampInt clamps an int into [min, max] inclusive.
|
||||
func clampInt(v, min, max int) int {
|
||||
if v < min {
|
||||
return min
|
||||
}
|
||||
if v > max {
|
||||
return max
|
||||
}
|
||||
return v
|
||||
}
|
||||
43
image_override.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// ImageOverrideData is the typed view for the image override component.
|
||||
type ImageOverrideData struct {
|
||||
Src string
|
||||
Alt string
|
||||
Caption string
|
||||
Frame string
|
||||
}
|
||||
|
||||
// normaliseImageFrame clamps the frame option to the supported set.
|
||||
func normaliseImageFrame(v string) string {
|
||||
switch v {
|
||||
case "stepped", "scallop", "plain":
|
||||
return v
|
||||
default:
|
||||
return "stepped"
|
||||
}
|
||||
}
|
||||
|
||||
// ArtDecoImageBlock overrides the built-in "image" block when the Art Deco theme is active.
|
||||
// Content: {"src": "...", "alt": "...", "caption": "...", "frame": "stepped|scallop|plain"}
|
||||
func ArtDecoImageBlock(ctx context.Context, content map[string]any) string {
|
||||
data := ImageOverrideData{
|
||||
Src: getString(content, "src"),
|
||||
Alt: getString(content, "alt"),
|
||||
Caption: getString(content, "caption"),
|
||||
Frame: normaliseImageFrame(getString(content, "frame")),
|
||||
}
|
||||
|
||||
if data.Src == "" {
|
||||
data.Src = fallbackImage()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = artDecoImageComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
30
image_override.templ
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
// imageFrameClass returns the utility class for an image frame option.
|
||||
func imageFrameClass(frame string) string {
|
||||
switch frame {
|
||||
case "scallop":
|
||||
return "deco-scallop"
|
||||
case "plain":
|
||||
return ""
|
||||
default:
|
||||
return "deco-step"
|
||||
}
|
||||
}
|
||||
|
||||
// artDecoImageComponent renders an image wrapped in the stepped ziggurat frame option.
|
||||
templ artDecoImageComponent(data ImageOverrideData) {
|
||||
<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>
|
||||
}
|
||||
124
image_override_templ.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// imageFrameClass returns the utility class for an image frame option.
|
||||
func imageFrameClass(frame string) string {
|
||||
switch frame {
|
||||
case "scallop":
|
||||
return "deco-scallop"
|
||||
case "plain":
|
||||
return ""
|
||||
default:
|
||||
return "deco-step"
|
||||
}
|
||||
}
|
||||
|
||||
// artDecoImageComponent renders an image wrapped in the stepped ziggurat frame option.
|
||||
func artDecoImageComponent(data ImageOverrideData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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
@ -1,337 +0,0 @@
|
||||
# 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));
|
||||
}
|
||||
}
|
||||
43
marquee_hero.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// MarqueeHeroBlockMeta defines the marquee hero block.
|
||||
var MarqueeHeroBlockMeta = blocks.BlockMeta{
|
||||
Key: "marquee_hero",
|
||||
Title: "Marquee Hero",
|
||||
Description: "Wide hero with stepped ziggurat frame and sunburst overlay.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// MarqueeHeroData is the typed view for the marquee hero component.
|
||||
type MarqueeHeroData struct {
|
||||
Eyebrow string
|
||||
Title string
|
||||
Subtitle string
|
||||
CTALabel string
|
||||
CTAHref string
|
||||
Image string
|
||||
}
|
||||
|
||||
// MarqueeHeroBlock renders the wide hero with stepped frame and sunburst overlay.
|
||||
// Content: {"eyebrow": "...", "title": "...", "subtitle": "...", "ctaLabel": "...", "ctaHref": "...", "image": "..."}
|
||||
func MarqueeHeroBlock(ctx context.Context, content map[string]any) string {
|
||||
data := MarqueeHeroData{
|
||||
Eyebrow: getString(content, "eyebrow"),
|
||||
Title: getString(content, "title"),
|
||||
Subtitle: getString(content, "subtitle"),
|
||||
CTALabel: getString(content, "ctaLabel"),
|
||||
CTAHref: getString(content, "ctaHref"),
|
||||
Image: getString(content, "image"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = marqueeHeroComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
42
marquee_hero.templ
Normal file
@ -0,0 +1,42 @@
|
||||
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, "Italiana", "Cinzel", 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>
|
||||
}
|
||||
152
marquee_hero_templ.go
Normal file
@ -0,0 +1,152 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// marqueeHeroComponent renders the wide hero with stepped ziggurat frame and sunburst overlay.
|
||||
func marqueeHeroComponent(data MarqueeHeroData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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, "Italiana", "Cinzel", 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
|
||||
@ -1,23 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// MenuBlockMeta defines the tasting menu block.
|
||||
var MenuBlockMeta = blocks.BlockMeta{
|
||||
Key: "menu",
|
||||
Title: "Tasting Menu",
|
||||
Description: "Symmetric two-column menu card listing courses with description and price.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// MenuCourse is a single course on the menu.
|
||||
type MenuCourse struct {
|
||||
Name string
|
||||
Description string
|
||||
Price string
|
||||
}
|
||||
|
||||
// MenuData is the typed view for the menu component.
|
||||
type MenuData struct {
|
||||
Title string
|
||||
Courses []MenuCourse
|
||||
}
|
||||
|
||||
// MenuBlock renders a tasting menu card.
|
||||
// Content: {"title": "...", "courses": [{"name": "...", "description": "...", "price": "..."}]}
|
||||
func MenuBlock(ctx context.Context, content map[string]any) string {
|
||||
items := getSlice(content, "courses")
|
||||
courses := make([]MenuCourse, 0, len(items))
|
||||
for _, item := range items {
|
||||
courses = append(courses, MenuCourse{
|
||||
Name: getString(item, "name"),
|
||||
Description: getString(item, "description"),
|
||||
Price: getString(item, "price"),
|
||||
})
|
||||
}
|
||||
|
||||
data := MenuData{
|
||||
Title: getString(content, "title"),
|
||||
Courses: courses,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = menuComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
50
menu.templ
Normal file
@ -0,0 +1,50 @@
|
||||
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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "JetBrains Mono", ui-monospace, monospace); color: hsl(var(--primary));"
|
||||
>
|
||||
{ course.Price }
|
||||
</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
139
menu_templ.go
Normal file
@ -0,0 +1,139 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// menuComponent renders the symmetric two-column menu card.
|
||||
func menuComponent(data MenuData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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, "Italiana", "Cinzel", 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, "Italiana", "Cinzel", 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, "JetBrains Mono", 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
|
||||
@ -2,8 +2,11 @@
|
||||
name = "art-deco"
|
||||
display_name = "Art Deco"
|
||||
scope = "@themes"
|
||||
version = "0.5.0"
|
||||
version = "0.1.1"
|
||||
description = "Luxury Art Deco theme with champagne gold gradients, jet black, ivory and geometric fan motifs for hotels, fine dining, weddings and jewellers."
|
||||
kind = "theme"
|
||||
categories = ["templates"]
|
||||
tags = ["luxury", "gold", "hospitality", "hotel", "wedding", "jewellery", "vintage", "glam", "fine-dining"]
|
||||
|
||||
[compatibility]
|
||||
block_core = ">=0.11.0 <0.12.0"
|
||||
|
||||
124
presets.json
@ -5,12 +5,6 @@
|
||||
"description": "Jet black lifted by champagne gold — the default Art Deco palette.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:art-deco:Cinzel",
|
||||
"fontBody": "template:art-deco:Cormorant Garamond",
|
||||
"fontSizeBase": "17",
|
||||
"lineHeightBase": "1.6"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "40 30% 96%",
|
||||
"foreground": "40 20% 10%",
|
||||
@ -58,15 +52,9 @@
|
||||
{
|
||||
"id": "ivory-rose",
|
||||
"name": "Ivory & Rose Gold",
|
||||
"description": "Soft ivory by day, warm ember by night, with rose-gold flourishes for weddings and jewellers.",
|
||||
"description": "Soft ivory with rose-gold flourishes for weddings and jewellers.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:art-deco:Cinzel",
|
||||
"fontBody": "template:art-deco:Cormorant Garamond",
|
||||
"fontSizeBase": "17",
|
||||
"lineHeightBase": "1.6"
|
||||
},
|
||||
"mode": "light",
|
||||
"lightColors": {
|
||||
"background": "36 40% 97%",
|
||||
"foreground": "20 15% 14%",
|
||||
@ -87,63 +75,15 @@
|
||||
"border": "25 30% 82%",
|
||||
"input": "25 25% 86%",
|
||||
"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",
|
||||
"name": "Velvet Onyx",
|
||||
"description": "Velvet-tinted ivory by day, deep onyx and gold stage-light by night, for theatre and after-dark dining.",
|
||||
"description": "Deep velvet onyx with gold and rose stage-lights for theatre and after-dark dining.",
|
||||
"theme": {
|
||||
"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%"
|
||||
},
|
||||
"mode": "dark",
|
||||
"darkColors": {
|
||||
"background": "260 20% 6%",
|
||||
"foreground": "42 40% 92%",
|
||||
@ -166,61 +106,5 @@
|
||||
"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%"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
39
press_quote.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// PressQuoteBlockMeta defines the press quote block.
|
||||
var PressQuoteBlockMeta = blocks.BlockMeta{
|
||||
Key: "press_quote",
|
||||
Title: "Press Quote",
|
||||
Description: "Italic Cormorant pull-quote between fan dividers, optional star rating.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// PressQuoteData is the typed view for the press quote component.
|
||||
type PressQuoteData struct {
|
||||
Quote string
|
||||
Source string
|
||||
Stars int
|
||||
}
|
||||
|
||||
// PressQuoteBlock renders the italic pull-quote framed by fan dividers.
|
||||
// Content: {"quote": "...", "source": "...", "stars": 5}
|
||||
func PressQuoteBlock(ctx context.Context, content map[string]any) string {
|
||||
stars := clampInt(getInt(content, "stars", 0), 0, 5)
|
||||
|
||||
data := PressQuoteData{
|
||||
Quote: getString(content, "quote"),
|
||||
Source: getString(content, "source"),
|
||||
Stars: stars,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pressQuoteComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
39
press_quote.templ
Normal file
@ -0,0 +1,39 @@
|
||||
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, "Cormorant Garamond", "Cormorant", 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, "Italiana", "Cinzel", Georgia, serif); color: hsl(var(--muted-foreground));"
|
||||
>
|
||||
{ data.Source }
|
||||
</p>
|
||||
}
|
||||
<div class="h-6 mt-6 deco-scallop" aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
8
press_quote_helpers.go
Normal file
@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
import "strconv"
|
||||
|
||||
// starsAttr renders an int into a stable data-stars attribute string.
|
||||
func starsAttr(n int) string {
|
||||
return strconv.Itoa(n)
|
||||
}
|
||||
114
press_quote_templ.go
Normal file
@ -0,0 +1,114 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// pressQuoteComponent renders the italic Cormorant pull-quote between fan dividers.
|
||||
func pressQuoteComponent(data PressQuoteData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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, "Cormorant Garamond", "Cormorant", 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, "Italiana", "Cinzel", 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
|
||||
BIN
preview.png
|
Before Width: | Height: | Size: 818 KiB After Width: | Height: | Size: 136 KiB |
174
register.go
Normal file
@ -0,0 +1,174 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// wrap adapts a templ-returning render function into a templates.TemplateFunc.
|
||||
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
|
||||
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
|
||||
return f(ctx, doc)
|
||||
}
|
||||
}
|
||||
|
||||
// Register wires up the Art Deco system template, its page templates, blocks,
|
||||
// template overrides and email wrapper.
|
||||
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
// Schemas must be loaded BEFORE any br.Register call so block schemas bind.
|
||||
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// System template metadata.
|
||||
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
|
||||
Key: "art-deco",
|
||||
Title: "Art Deco",
|
||||
Description: "Luxury Art Deco theme with champagne gold gradients, jet black, ivory and geometric fan motifs for hotels, fine dining, weddings and jewellers.",
|
||||
})
|
||||
|
||||
// Page templates.
|
||||
if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{
|
||||
Key: "default",
|
||||
Title: "Default",
|
||||
Description: "Centered symmetric layout with gold rule header and footer.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderArtDecoDefault)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{
|
||||
Key: "landing",
|
||||
Title: "Marquee Landing",
|
||||
Description: "Wide hero with sunburst overlay, reservation strip, CTA.",
|
||||
Slots: []string{"marquee", "main", "cta", "footer"},
|
||||
}, wrap(RenderArtDecoLanding)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{
|
||||
Key: "article",
|
||||
Title: "Editorial",
|
||||
Description: "Narrow editorial column for press, menus and stories.",
|
||||
Slots: []string{"header", "main", "aside", "footer"},
|
||||
}, wrap(RenderArtDecoArticle)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("art-deco", templates.PageTemplateMeta{
|
||||
Key: "full-width",
|
||||
Title: "Grand Ballroom",
|
||||
Description: "Edge-to-edge gallery layout for venues and lookbooks.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderArtDecoFullWidth)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Theme-specific blocks.
|
||||
br.Register(ReservationBlockMeta, ReservationBlock)
|
||||
br.Register(MenuBlockMeta, MenuBlock)
|
||||
br.Register(GalleryFanBlockMeta, GalleryFanBlock)
|
||||
br.Register(DividerFanBlockMeta, DividerFanBlock)
|
||||
br.Register(FooterBlockMeta, FooterBlock)
|
||||
br.Register(MarqueeHeroBlockMeta, MarqueeHeroBlock)
|
||||
br.Register(PressQuoteBlockMeta, PressQuoteBlock)
|
||||
|
||||
// Template overrides — active only while the Art Deco system template is selected.
|
||||
br.RegisterTemplateOverride("art-deco", "heading", ArtDecoHeadingBlock)
|
||||
br.RegisterTemplateOverride("art-deco", "text", ArtDecoTextBlock)
|
||||
br.RegisterTemplateOverride("art-deco", "button", ArtDecoButtonBlock)
|
||||
br.RegisterTemplateOverride("art-deco", "image", ArtDecoImageBlock)
|
||||
|
||||
// Branded email wrapper.
|
||||
tr.RegisterEmailWrapper("art-deco", ArtDecoEmailWrapper)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultMasterPages returns the master pages provisioned the first time the plugin loads.
|
||||
func DefaultMasterPages() []plugin.MasterPageDefinition {
|
||||
return []plugin.MasterPageDefinition{
|
||||
{
|
||||
Key: "art-deco:default-master",
|
||||
Title: "Art Deco Default Master",
|
||||
PageTemplates: []string{"default", "article"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "navbar",
|
||||
Title: "Brass Navigation",
|
||||
Content: map[string]any{"menuName": "main"},
|
||||
Slot: "header",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "art-deco:divider_fan",
|
||||
Title: "Header Fan Divider",
|
||||
Content: map[string]any{"variant": "sunburst"},
|
||||
Slot: "header",
|
||||
SortOrder: 1,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Content",
|
||||
Content: map[string]any{"slotName": "main", "placeholder": "Page content"},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "art-deco:footer",
|
||||
Title: "Site Footer",
|
||||
Content: map[string]any{"address": "", "reservationsEmail": "", "showSocial": "true"},
|
||||
Slot: "footer",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "art-deco:marquee-master",
|
||||
Title: "Art Deco Marquee Master",
|
||||
PageTemplates: []string{"landing", "full-width"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "navbar",
|
||||
Title: "Brass Navigation",
|
||||
Content: map[string]any{"menuName": "main"},
|
||||
Slot: "marquee",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "art-deco:reservation",
|
||||
Title: "Reservation Strip",
|
||||
Content: map[string]any{"phone": "", "openTable": ""},
|
||||
Slot: "marquee",
|
||||
SortOrder: 1,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Content",
|
||||
Content: map[string]any{"slotName": "main"},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "art-deco:divider_fan",
|
||||
Title: "CTA Fan Divider",
|
||||
Content: map[string]any{"variant": "scallop"},
|
||||
Slot: "cta",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "art-deco:footer",
|
||||
Title: "Site Footer",
|
||||
Content: map[string]any{"showSocial": "true"},
|
||||
Slot: "footer",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
25
registration.go
Normal file
@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// Registration is the compile-time plugin registration for the Art Deco theme.
|
||||
var Registration = plugin.PluginRegistration{
|
||||
Name: "art-deco",
|
||||
Version: plugin.ParseModVersion(pluginModBytes),
|
||||
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
return Register(tr, br)
|
||||
},
|
||||
Assets: func() http.Handler { return AssetsHandler() },
|
||||
Schemas: func() fs.FS { return Schemas() },
|
||||
ThemePresets: func() []byte { return ThemePresets() },
|
||||
BundledFonts: func() []byte { return BundledFonts() },
|
||||
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
|
||||
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
|
||||
}
|
||||
37
reservation.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// ReservationBlockMeta defines the reservation strip block.
|
||||
var ReservationBlockMeta = blocks.BlockMeta{
|
||||
Key: "reservation",
|
||||
Title: "Reservation Strip",
|
||||
Description: "Sticky gold-on-black reservation strip with phone, OpenTable CTA and trading hours.",
|
||||
Source: "art-deco",
|
||||
}
|
||||
|
||||
// ReservationData is the typed view used by the templ component.
|
||||
type ReservationData struct {
|
||||
Phone string
|
||||
OpenTable string
|
||||
Hours []string
|
||||
}
|
||||
|
||||
// ReservationBlock renders a reservation strip.
|
||||
// Content: {"phone": "...", "openTable": "...", "hours": ["Tue–Sat · 6pm–late", ...]}
|
||||
func ReservationBlock(ctx context.Context, content map[string]any) string {
|
||||
data := ReservationData{
|
||||
Phone: getString(content, "phone"),
|
||||
OpenTable: getString(content, "openTable"),
|
||||
Hours: getStringSlice(content, "hours"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = reservationComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
36
reservation.templ
Normal file
@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
// reservationComponent renders the sticky reservation strip.
|
||||
// Empty inputs render a shell with a "Reservations" label so the strip is never blank.
|
||||
templ reservationComponent(data ReservationData) {
|
||||
<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, "Italiana", "Cinzel", 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>
|
||||
}
|
||||
136
reservation_templ.go
Normal file
@ -0,0 +1,136 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// reservationComponent renders the sticky reservation strip.
|
||||
// Empty inputs render a shell with a "Reservations" label so the strip is never blank.
|
||||
func reservationComponent(data ReservationData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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, "Italiana", "Cinzel", 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
|
||||
16
schemas/divider_fan.schema.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Fan Divider",
|
||||
"description": "Pure-CSS geometric gold divider — sunburst, scallop or ziggurat.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"title": "Variant",
|
||||
"description": "Choose the divider motif.",
|
||||
"x-editor": "select",
|
||||
"default": "sunburst",
|
||||
"enum": ["sunburst", "scallop", "ziggurat"]
|
||||
}
|
||||
}
|
||||
}
|
||||
37
schemas/footer.schema.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Footer",
|
||||
"description": "Centered Art Deco footer with gold rule, address, reservations email and optional social row.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"title": "Address",
|
||||
"description": "Multi-line postal address.",
|
||||
"x-editor": "textarea",
|
||||
"default": ""
|
||||
},
|
||||
"reservationsEmail": {
|
||||
"type": "string",
|
||||
"title": "Reservations email",
|
||||
"description": "Public email shown in the footer for reservations enquiries.",
|
||||
"x-editor": "text",
|
||||
"default": ""
|
||||
},
|
||||
"showSocial": {
|
||||
"type": "string",
|
||||
"title": "Show social row",
|
||||
"description": "Whether to render the social-icon row.",
|
||||
"x-editor": "select",
|
||||
"default": "true",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
"menuName": {
|
||||
"type": "string",
|
||||
"title": "Footer menu",
|
||||
"description": "Name of the menu to render in the footer.",
|
||||
"x-editor": "menu-select",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
38
schemas/gallery_fan.schema.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Fan Gallery",
|
||||
"description": "Mirrored gallery framed by sunburst rays; collapses to single column under 640px.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"images": {
|
||||
"type": "array",
|
||||
"title": "Images",
|
||||
"description": "Images to mirror across the fan.",
|
||||
"x-editor": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src": {
|
||||
"type": "string",
|
||||
"title": "Image",
|
||||
"x-editor": "media"
|
||||
},
|
||||
"caption": {
|
||||
"type": "string",
|
||||
"title": "Caption",
|
||||
"x-editor": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"type": "integer",
|
||||
"title": "Columns",
|
||||
"description": "Columns on desktop (2, 3 or 4).",
|
||||
"x-editor": "select",
|
||||
"default": 3,
|
||||
"enum": [2, 3, 4]
|
||||
}
|
||||
}
|
||||
}
|
||||
47
schemas/marquee_hero.schema.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Marquee Hero",
|
||||
"description": "Wide hero with stepped ziggurat frame and sunburst overlay.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eyebrow": {
|
||||
"type": "string",
|
||||
"title": "Eyebrow",
|
||||
"description": "Small caps line above the title.",
|
||||
"x-editor": "text",
|
||||
"default": ""
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"description": "Display headline in Italiana caps.",
|
||||
"x-editor": "text",
|
||||
"default": ""
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string",
|
||||
"title": "Subtitle",
|
||||
"description": "Long-form supporting copy (rich text).",
|
||||
"x-editor": "richtext",
|
||||
"default": ""
|
||||
},
|
||||
"ctaLabel": {
|
||||
"type": "string",
|
||||
"title": "CTA label",
|
||||
"x-editor": "text",
|
||||
"default": ""
|
||||
},
|
||||
"ctaHref": {
|
||||
"type": "string",
|
||||
"title": "CTA URL",
|
||||
"x-editor": "link",
|
||||
"default": ""
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"title": "Background image",
|
||||
"x-editor": "media",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
31
schemas/press_quote.schema.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Press Quote",
|
||||
"description": "Italic Cormorant pull-quote between fan dividers.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Quote",
|
||||
"description": "The press quotation (rich text).",
|
||||
"x-editor": "richtext",
|
||||
"default": ""
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"title": "Source",
|
||||
"description": "Attribution line — e.g. 'The Daily, March 2024'.",
|
||||
"x-editor": "text",
|
||||
"default": ""
|
||||
},
|
||||
"stars": {
|
||||
"type": "integer",
|
||||
"title": "Stars",
|
||||
"description": "Filled-star count, clamped to 0–5.",
|
||||
"x-editor": "number",
|
||||
"default": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 818 KiB |
|
Before Width: | Height: | Size: 821 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 358 KiB |
|
Before Width: | Height: | Size: 374 KiB |
@ -1,64 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
209
seed/seed.json
@ -1,209 +0,0 @@
|
||||
{
|
||||
"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 }
|
||||
]
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
270
template.templ
Normal file
@ -0,0 +1,270 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||
)
|
||||
|
||||
// PageData captures the inputs needed to render an Art Deco page template.
|
||||
type PageData struct {
|
||||
Title string
|
||||
Slots map[string]string
|
||||
ThemeMode string
|
||||
ThemeCSS string
|
||||
SiteSettings bn.SiteSettingsData
|
||||
PageMeta bn.PageMeta
|
||||
StructuredData string
|
||||
CSSHash string
|
||||
PageviewNonce string
|
||||
EngagementConfig bn.EngagementConfig
|
||||
}
|
||||
|
||||
// parsePageData reads a page-render doc into the typed PageData used by templ.
|
||||
func parsePageData(doc map[string]any) PageData {
|
||||
title := "Untitled"
|
||||
if t, ok := doc["title"].(string); ok {
|
||||
title = t
|
||||
}
|
||||
|
||||
slots := make(map[string]string)
|
||||
if s, ok := doc["slots"].(map[string]string); ok {
|
||||
slots = s
|
||||
}
|
||||
|
||||
themeCSS := ""
|
||||
if tc, ok := doc["theme_css"].(string); ok {
|
||||
themeCSS = tc
|
||||
}
|
||||
|
||||
structuredData := ""
|
||||
if sd, ok := doc["structured_data"].(string); ok {
|
||||
structuredData = sd
|
||||
}
|
||||
|
||||
cssHash := ""
|
||||
if ch, ok := doc["css_hash"].(string); ok {
|
||||
cssHash = ch
|
||||
}
|
||||
|
||||
pageviewNonce := ""
|
||||
if pn, ok := doc["pageview_nonce"].(string); ok {
|
||||
pageviewNonce = pn
|
||||
}
|
||||
|
||||
themeMode := "dark"
|
||||
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||
themeMode = tm
|
||||
}
|
||||
|
||||
return PageData{
|
||||
Title: title,
|
||||
Slots: slots,
|
||||
ThemeMode: themeMode,
|
||||
ThemeCSS: themeCSS,
|
||||
SiteSettings: bn.ParseSiteSettings(doc),
|
||||
PageMeta: bn.ParsePageMeta(doc),
|
||||
StructuredData: structuredData,
|
||||
CSSHash: cssHash,
|
||||
PageviewNonce: pageviewNonce,
|
||||
EngagementConfig: bn.ParseEngagementConfig(doc),
|
||||
}
|
||||
}
|
||||
|
||||
// htmlClassForMode returns the html-element class hint that flips dark mode.
|
||||
func htmlClassForMode(mode string) string {
|
||||
if mode == "dark" {
|
||||
return "dark"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ArtDecoDefault — centered symmetric layout with gold rule header and footer.
|
||||
templ ArtDecoDefault(data PageData) {
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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, "Cormorant Garamond", "Cormorant", 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, "Cormorant Garamond", "Cormorant", 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, "Cormorant Garamond", "Cormorant", 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))
|
||||
}
|
||||
607
template_templ.go
Normal file
@ -0,0 +1,607 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||
)
|
||||
|
||||
// PageData captures the inputs needed to render an Art Deco page template.
|
||||
type PageData struct {
|
||||
Title string
|
||||
Slots map[string]string
|
||||
ThemeMode string
|
||||
ThemeCSS string
|
||||
SiteSettings bn.SiteSettingsData
|
||||
PageMeta bn.PageMeta
|
||||
StructuredData string
|
||||
CSSHash string
|
||||
PageviewNonce string
|
||||
EngagementConfig bn.EngagementConfig
|
||||
}
|
||||
|
||||
// parsePageData reads a page-render doc into the typed PageData used by templ.
|
||||
func parsePageData(doc map[string]any) PageData {
|
||||
title := "Untitled"
|
||||
if t, ok := doc["title"].(string); ok {
|
||||
title = t
|
||||
}
|
||||
|
||||
slots := make(map[string]string)
|
||||
if s, ok := doc["slots"].(map[string]string); ok {
|
||||
slots = s
|
||||
}
|
||||
|
||||
themeCSS := ""
|
||||
if tc, ok := doc["theme_css"].(string); ok {
|
||||
themeCSS = tc
|
||||
}
|
||||
|
||||
structuredData := ""
|
||||
if sd, ok := doc["structured_data"].(string); ok {
|
||||
structuredData = sd
|
||||
}
|
||||
|
||||
cssHash := ""
|
||||
if ch, ok := doc["css_hash"].(string); ok {
|
||||
cssHash = ch
|
||||
}
|
||||
|
||||
pageviewNonce := ""
|
||||
if pn, ok := doc["pageview_nonce"].(string); ok {
|
||||
pageviewNonce = pn
|
||||
}
|
||||
|
||||
themeMode := "dark"
|
||||
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||
themeMode = tm
|
||||
}
|
||||
|
||||
return PageData{
|
||||
Title: title,
|
||||
Slots: slots,
|
||||
ThemeMode: themeMode,
|
||||
ThemeCSS: themeCSS,
|
||||
SiteSettings: bn.ParseSiteSettings(doc),
|
||||
PageMeta: bn.ParsePageMeta(doc),
|
||||
StructuredData: structuredData,
|
||||
CSSHash: cssHash,
|
||||
PageviewNonce: pageviewNonce,
|
||||
EngagementConfig: bn.ParseEngagementConfig(doc),
|
||||
}
|
||||
}
|
||||
|
||||
// htmlClassForMode returns the html-element class hint that flips dark mode.
|
||||
func htmlClassForMode(mode string) string {
|
||||
if mode == "dark" {
|
||||
return "dark"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ArtDecoDefault — centered symmetric layout with gold rule header and footer.
|
||||
func ArtDecoDefault(data PageData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!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, "Cormorant Garamond", "Cormorant", 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, "Cormorant Garamond", "Cormorant", 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, "Cormorant Garamond", "Cormorant", 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, "Cormorant Garamond", "Cormorant", 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
|
||||
@ -1,16 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,18 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,23 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,22 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,13 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,15 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,14 +0,0 @@
|
||||
<!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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,64 +0,0 @@
|
||||
<!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: &#39;Cormorant Garamond&#39;, &#39;Cormorant&#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: &#39;Italiana&#39;, &#39;Cinzel&#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: &#39;Italiana&#39;, &#39;Cinzel&#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 & 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>
|
||||
@ -1,21 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,16 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,33 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,31 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,22 +0,0 @@
|
||||
{# 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, "Cinzel", "Italiana", 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, "Cormorant Garamond", "Cormorant", 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 %}
|
||||
@ -1,14 +0,0 @@
|
||||
{# 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, "Cinzel", "Italiana", 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, "Cormorant Garamond", "Cormorant", Georgia, serif)">{{ post.author_bio }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
@ -1,24 +0,0 @@
|
||||
{# 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, "Cinzel", "Italiana", Georgia, serif)">{{ title }}</h1>{% endif %}
|
||||
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, "Cormorant Garamond", "Cormorant", 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, "Cinzel", "Italiana", 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, "Cormorant Garamond", "Cormorant", 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>
|
||||
@ -1,15 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,10 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,4 +0,0 @@
|
||||
{# 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 %}
|
||||