Compare commits
No commits in common. "main" and "v0.1.1" have entirely different histories.
1
.gitignore
vendored
@ -3,4 +3,3 @@
|
|||||||
tmp/
|
tmp/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.bnp
|
|
||||||
|
|||||||
226
Makefile
@ -1,55 +1,195 @@
|
|||||||
# coffee — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
# Coffee — build & deploy helpers (.so plugin workflow)
|
||||||
#
|
#
|
||||||
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
|
||||||
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
# `make rebuild` copies source to the container, builds the .so, and restarts.
|
||||||
# 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:
|
# Usage:
|
||||||
# make build # pack coffee-<version>.bnp (codeless — no wasm)
|
# make rebuild # Full rebuild: frontend + .so + CSS + migrations, restart
|
||||||
# make verify # validate the built .bnp (loader-identical checks)
|
# make backend # Build .so + migrations, restart
|
||||||
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
# make build-css # Rebuild Tailwind CSS
|
||||||
# make publish # ninja plugin publish --bnp (to the active registry)
|
# make logs # Tail instance logs
|
||||||
# make clean # remove build artefacts
|
# make status # Show instance container status
|
||||||
|
|
||||||
.PHONY: build verify archive-check publish clean help
|
.PHONY: rebuild backend build-frontend build-base-binary build-so copy-plugin-source sync-migrations build-css deploy-css logs status help spinup templ bump-patch bump-minor bump-major sync-version clean
|
||||||
|
|
||||||
PLUGIN_NAME := coffee
|
# Paths
|
||||||
NINJA := ninja
|
BLOCKNINJA_DIR := $(HOME)/src/blockninja
|
||||||
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
PLUGIN_SRC := $(CURDIR)
|
||||||
|
PLUGIN_NAME := coffee
|
||||||
|
MIGRATIONS_SRC := $(BLOCKNINJA_DIR)/cms/backend/sql/migrations
|
||||||
|
GO_BUILDER := localhost/blockninja-go-builder:latest
|
||||||
|
CONTAINER := instance-coffee
|
||||||
|
ACCOUNT_SLUG := blockninja
|
||||||
|
INSTANCE_SLUG := coffee
|
||||||
|
STYLES_DIR := /var/lib/blockninja/$(ACCOUNT_SLUG)/$(INSTANCE_SLUG)/styles
|
||||||
|
PLUGIN_DEST := /app/data/plugins/src/$(PLUGIN_NAME)
|
||||||
|
|
||||||
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
# Default target: build the .so locally for development.
|
||||||
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
all: $(PLUGIN_NAME).so
|
||||||
build:
|
|
||||||
$(NINJA) plugin build --dir .
|
|
||||||
|
|
||||||
# Validate the built .bnp exactly as the registry/loader will.
|
# Local plugin build (no container). Useful for CI / quick checks.
|
||||||
verify:
|
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
|
||||||
$(NINJA) plugin verify $(BNP)
|
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.
|
# Clean local build artifacts.
|
||||||
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)
|
|
||||||
|
|
||||||
# Remove build artefacts.
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
rm -f $(PLUGIN_NAME).so
|
||||||
|
|
||||||
|
# Regenerate templ Go files locally (for development).
|
||||||
|
templ:
|
||||||
|
cd $(PLUGIN_SRC) && templ generate
|
||||||
|
|
||||||
|
# Ensure blockninja core services and the instance container are running.
|
||||||
|
spinup:
|
||||||
|
$(MAKE) -C $(BLOCKNINJA_DIR) spinup
|
||||||
|
|
||||||
|
# Full rebuild: frontend + .so plugin + CSS + migrations, restart.
|
||||||
|
rebuild: spinup
|
||||||
|
$(MAKE) build-frontend
|
||||||
|
$(MAKE) build-base-binary
|
||||||
|
$(MAKE) copy-plugin-source
|
||||||
|
$(MAKE) build-so
|
||||||
|
$(MAKE) build-css
|
||||||
|
$(MAKE) sync-migrations
|
||||||
|
podman restart $(CONTAINER)
|
||||||
|
@sleep 2
|
||||||
|
$(MAKE) deploy-css
|
||||||
|
@echo ""
|
||||||
|
@echo "Done. https://$(INSTANCE_SLUG).localdev.blockninjacms.com/"
|
||||||
|
|
||||||
|
# Backend-only rebuild: .so plugin + migrations, restart.
|
||||||
|
backend: spinup
|
||||||
|
$(MAKE) build-base-binary
|
||||||
|
$(MAKE) copy-plugin-source
|
||||||
|
$(MAKE) build-so
|
||||||
|
$(MAKE) sync-migrations
|
||||||
|
podman restart $(CONTAINER)
|
||||||
|
@echo "Backend updated."
|
||||||
|
|
||||||
|
# Build host admin UI and deploy to container.
|
||||||
|
build-frontend:
|
||||||
|
@echo "==> Building @block-ninja/ui ..."
|
||||||
|
cd $(BLOCKNINJA_DIR)/cms/packages/ui && pnpm run build
|
||||||
|
@echo "==> Building host admin UI ..."
|
||||||
|
cd $(BLOCKNINJA_DIR)/cms/web && pnpm run build
|
||||||
|
@echo "==> Deploying frontend to container ..."
|
||||||
|
podman exec $(CONTAINER) rm -rf /app/web/dist
|
||||||
|
podman cp $(BLOCKNINJA_DIR)/cms/web/dist $(CONTAINER):/app/web/dist
|
||||||
|
@echo "Frontend deployed."
|
||||||
|
|
||||||
|
# Build the base CMS binary (without external plugins) and copy to container.
|
||||||
|
build-base-binary:
|
||||||
|
@echo "==> Building base CMS binary ..."
|
||||||
|
podman run --rm \
|
||||||
|
-v $(BLOCKNINJA_DIR)/cms/backend:/src/backend:ro \
|
||||||
|
-v blockninja_go_cache:/go/pkg/mod \
|
||||||
|
-v /tmp:/out \
|
||||||
|
-w /src/backend \
|
||||||
|
$(GO_BUILDER) \
|
||||||
|
go build -o /out/blockninja-server ./cmd/server
|
||||||
|
podman cp /tmp/blockninja-server $(CONTAINER):/app/server
|
||||||
|
rm -f /tmp/blockninja-server
|
||||||
|
|
||||||
|
# Copy plugin source into the container's plugin source directory.
|
||||||
|
copy-plugin-source:
|
||||||
|
@echo "==> Copying $(PLUGIN_NAME) source to container ..."
|
||||||
|
podman exec $(CONTAINER) rm -rf $(PLUGIN_DEST)
|
||||||
|
podman exec $(CONTAINER) mkdir -p $(PLUGIN_DEST)
|
||||||
|
podman cp $(PLUGIN_SRC)/. $(CONTAINER):$(PLUGIN_DEST)/
|
||||||
|
podman exec $(CONTAINER) rm -rf $(PLUGIN_DEST)/.git $(PLUGIN_DEST)/Makefile
|
||||||
|
@echo "Plugin source copied."
|
||||||
|
|
||||||
|
# Build the .so using the go-builder container (same toolchain as CMS binary).
|
||||||
|
build-so:
|
||||||
|
@echo "==> Building $(PLUGIN_NAME).so ..."
|
||||||
|
podman run --rm \
|
||||||
|
-v $(PLUGIN_SRC):/src/plugin:ro \
|
||||||
|
-v blockninja_go_cache:/go/pkg/mod \
|
||||||
|
-v /tmp:/out \
|
||||||
|
-w /src/plugin \
|
||||||
|
-e CGO_ENABLED=1 \
|
||||||
|
$(GO_BUILDER) \
|
||||||
|
go build -buildmode=plugin -ldflags="-s -w" -o /out/$(PLUGIN_NAME).so .
|
||||||
|
podman exec $(CONTAINER) mkdir -p /app/data/plugins/so
|
||||||
|
podman cp /tmp/$(PLUGIN_NAME).so $(CONTAINER):/app/data/plugins/so/$(PLUGIN_NAME).so
|
||||||
|
rm -f /tmp/$(PLUGIN_NAME).so
|
||||||
|
@echo "$(PLUGIN_NAME).so built."
|
||||||
|
|
||||||
|
# Sync base blockninja migration files from host to container.
|
||||||
|
sync-migrations:
|
||||||
|
@echo "==> Syncing migrations ..."
|
||||||
|
@podman unshare bash -c ' \
|
||||||
|
M=$$(podman mount $(CONTAINER)) && \
|
||||||
|
rm -rf "$$M/app/migrations" && \
|
||||||
|
mkdir -p "$$M/app/migrations" && \
|
||||||
|
podman umount $(CONTAINER)'
|
||||||
|
@podman cp $(MIGRATIONS_SRC)/. $(CONTAINER):/app/migrations/
|
||||||
|
@echo "Migrations synced."
|
||||||
|
|
||||||
|
# Rebuild Tailwind CSS.
|
||||||
|
build-css:
|
||||||
|
@echo "==> Building CSS ..."
|
||||||
|
cd $(BLOCKNINJA_DIR) && make css
|
||||||
|
|
||||||
|
# Copy built CSS to instance styles dir and container.
|
||||||
|
deploy-css:
|
||||||
|
@mkdir -p $(STYLES_DIR)
|
||||||
|
cp $(BLOCKNINJA_DIR)/cms/data/styles/styles.css $(STYLES_DIR)/styles.css
|
||||||
|
podman cp $(BLOCKNINJA_DIR)/cms/data/styles/styles.css $(CONTAINER):/app/data/styles/styles.css
|
||||||
|
podman cp $(BLOCKNINJA_DIR)/cms/styles/input.base.css $(CONTAINER):/app/styles/input.base.css
|
||||||
|
@echo "CSS deployed."
|
||||||
|
|
||||||
|
# Tail instance logs.
|
||||||
|
logs:
|
||||||
|
podman logs -f $(CONTAINER)
|
||||||
|
|
||||||
|
# Show instance container status.
|
||||||
|
status:
|
||||||
|
@podman inspect $(CONTAINER) --format \
|
||||||
|
'Name: {{.Name}}\nImage: {{.Config.Image}}\nStatus: {{.State.Status}}\nHealth: {{.State.Health.Status}}\nStarted: {{.State.StartedAt}}' \
|
||||||
|
2>/dev/null || echo "Container $(CONTAINER) not found."
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " build Pack the codeless .bnp (no wasm)"
|
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
||||||
@echo " verify Validate the built .bnp (loader-identical checks)"
|
@echo " clean Remove $(PLUGIN_NAME).so"
|
||||||
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
@echo " templ Regenerate templ Go files locally"
|
||||||
@echo " publish Publish the .bnp to the active registry"
|
@echo " spinup Start blockninja core services + instance container if stopped"
|
||||||
@echo " clean Remove build artefacts"
|
@echo " rebuild Full rebuild: frontend + .so + CSS + migrations, restart"
|
||||||
|
@echo " backend Build .so + migrations, restart"
|
||||||
|
@echo " build-frontend Build host admin UI, deploy to container"
|
||||||
|
@echo " build-base-binary Build base CMS binary, copy to container"
|
||||||
|
@echo " copy-plugin-source Copy plugin source into container"
|
||||||
|
@echo " build-so Build .so inside container"
|
||||||
|
@echo " sync-migrations Copy migration files from host to container"
|
||||||
|
@echo " build-css Rebuild Tailwind CSS"
|
||||||
|
@echo " deploy-css Copy CSS to instance styles dir"
|
||||||
|
@echo " logs Tail instance container logs"
|
||||||
|
@echo " status Show instance container status"
|
||||||
|
|
||||||
|
# --- Version bump targets ---
|
||||||
|
CURRENT_VERSION := $(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
|
||||||
|
bump-patch:
|
||||||
|
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.%d", $$1, $$2, $$3+1}'); \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
|
||||||
|
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
|
||||||
|
echo "Bumped to $$NEW and tagged v$$NEW"
|
||||||
|
|
||||||
|
bump-minor:
|
||||||
|
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.0", $$1, $$2+1}'); \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
|
||||||
|
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
|
||||||
|
echo "Bumped to $$NEW and tagged v$$NEW"
|
||||||
|
|
||||||
|
bump-major:
|
||||||
|
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.0.0", $$1+1}'); \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
|
||||||
|
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
|
||||||
|
echo "Bumped to $$NEW and tagged v$$NEW"
|
||||||
|
|
||||||
|
sync-version:
|
||||||
|
@TAG=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \
|
||||||
|
if [ -z "$$TAG" ]; then echo "No tags found"; exit 1; fi; \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$TAG'"/' plugin.mod; \
|
||||||
|
echo "Synced plugin.mod to $$TAG"
|
||||||
|
|||||||
24
README.md
@ -1,24 +0,0 @@
|
|||||||
# Coffee
|
|
||||||
|
|
||||||
A warm, hand-made theme in roasted browns over kraft paper. Hand-drawn doodles, chalk-menu lettering, and stamped badges give it the feel of a neighbourhood café that does everything by hand. It reads friendly and tactile. Nothing about it looks mass-produced.
|
|
||||||
|
|
||||||
## Good for
|
|
||||||
|
|
||||||
We built Coffee for cafés, roasteries, and slow-craft makers. Treat these as starting points, not limits. If the mood fits your brand, use it:
|
|
||||||
|
|
||||||
- Coffee shops and roasteries
|
|
||||||
- Bakeries and pastry counters
|
|
||||||
- Small-batch food and drink brands
|
|
||||||
- Farmers' market and artisan stalls
|
|
||||||
|
|
||||||
## What you get
|
|
||||||
|
|
||||||
- Five color presets, each with a tuned light mode and a tuned dark mode: Morning Pour, Dark Roast, Kraft Cream, Chalkboard, and Matcha and Oat.
|
|
||||||
- Fraunces for headings, Work Sans for body, and Caveat for hand-written touches, all bundled. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
|
||||||
- Four persona blocks: a featured pour to spotlight a drink, a roast profile with tasting notes, a stamp-style loyalty card, and a step-by-step brew guide.
|
|
||||||
- A demo site that installs when you activate the theme, styled around a roastery: a home page, an about page, a short blog, 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 built starting point.
|
|
||||||
@ -1,50 +1,49 @@
|
|||||||
# Coffee — Recommended fonts
|
# Coffee — Recommended fonts
|
||||||
|
|
||||||
The Coffee theme **bundles** its type as woff2 in `assets/fonts/` (latin
|
The Coffee theme ships `fonts.json = []` per the wave-1
|
||||||
subsets, SIL Open Font License 1.1 — see `assets/fonts/OFL.txt`). The three
|
[FONTS.md](../docs/FONTS.md) policy. No woff2s are bundled in this pass.
|
||||||
families become `source=template` rows in the fonts table and are selectable
|
Templates resolve font families through the BlockNinja CSS variables
|
||||||
in the admin typography picker. Templates always resolve font families through
|
`--font-heading`, `--font-body`, and `--font-mono` with the fallback stacks
|
||||||
the BlockNinja CSS variables `--font-heading`, `--font-body`, `--font-mono`
|
defined in `assets/style.css`. The site admin assigns fonts in the
|
||||||
with the fallback stacks defined in the theme CSS — no family is hardcoded.
|
typography panel; the picks below are the spec-aligned defaults that match
|
||||||
|
the visual identity.
|
||||||
|
|
||||||
## Bundled families
|
## How to apply
|
||||||
|
|
||||||
| Slot | Family | Weights | Role |
|
1. Open the admin → Theme → Typography panel.
|
||||||
|-----------|------------|-----------|------|
|
2. Switch to the **Google Fonts** tab.
|
||||||
| Heading | Fraunces | 400, 600 | Ligature-rich display serif for headings and prices. Warm, hand-cut character. |
|
3. Search and pick each family below.
|
||||||
| Body | Work Sans | 400, 600 | Relaxed grotesque for long copy at 17px+. Reads well on cream and espresso backgrounds. |
|
4. Assign to the matching slot (Heading / Body / Mono).
|
||||||
| Accent | Caveat | 400, 700 | Hand-drawn script for badges, stamps, and doodle captions. |
|
5. Save. The picks are persisted as `google:<Family>` and rendered as
|
||||||
|
`@import` URLs by `theme.GenerateCSS()`.
|
||||||
|
|
||||||
Mono is intentionally **not** bundled: prices and tabular figures fall back to
|
## Picks
|
||||||
`"JetBrains Mono", ..., monospace` via `--font-mono`. Assign a mono family from
|
|
||||||
the Google Fonts tab if you want a specific face.
|
|
||||||
|
|
||||||
## Default behaviour (no admin picks)
|
| Slot | Source | Family | Why |
|
||||||
|
|----------|------------------|------------------|-----|
|
||||||
|
| Heading | `google:Fraunces` | Fraunces | Ligature-rich display serif. Italic stylistic alternates land on h2/h3 headings; pairs with the spec's "Quentin Blake ink lines" aesthetic. |
|
||||||
|
| Body | `google:Inter` | Inter | Relaxed body sans for long copy at 17px+. Excellent screen rendering across cream and espresso backgrounds. |
|
||||||
|
| Mono | `google:JetBrains Mono` | JetBrains Mono | Tabular-numerals mono for prices in `menu_board` and `featured_pour`, and hours in `hours_strip`. |
|
||||||
|
|
||||||
On a fresh install the admin has not assigned any slot, so `--font-heading` /
|
All three are already in the curated Google Fonts list in the picker and
|
||||||
`--font-body` / `--font-mono` are unset and the CSS fallbacks apply. Because the
|
have permissive SIL OFL 1.1 licences.
|
||||||
bundled families emit `@font-face` from the fonts table, the fallbacks resolve
|
|
||||||
to the bundled woff2:
|
|
||||||
|
|
||||||
- Headings render in **Fraunces**.
|
## Fallback stacks (used before admin picks fonts)
|
||||||
- Body renders in **Work Sans**.
|
|
||||||
- Hand-drawn accents (`.coffee-hand`) render in **Caveat**.
|
|
||||||
|
|
||||||
## How to change
|
Defined in `assets/style.css`:
|
||||||
|
|
||||||
1. Open admin → Theme → Typography.
|
|
||||||
2. Pick a family per slot (bundled families appear as **Template** fonts; the
|
|
||||||
Google Fonts tab offers ~50 more).
|
|
||||||
3. Assign to Heading / Body / Mono and save.
|
|
||||||
|
|
||||||
Note: the platform exposes three controllable slots. The hand-drawn accent
|
|
||||||
utility resolves through `--font-heading` (falling back to Caveat), so if you
|
|
||||||
deliberately change the heading font the badge accents follow it — that is the
|
|
||||||
three-slot model working as intended.
|
|
||||||
|
|
||||||
## Fallback stacks (theme CSS)
|
|
||||||
|
|
||||||
- `--coffee-heading-fallback`: `"Fraunces", "Playfair Display", Georgia, "Times New Roman", serif`
|
- `--coffee-heading-fallback`: `"Fraunces", "Playfair Display", Georgia, "Times New Roman", serif`
|
||||||
- `--coffee-body-fallback`: `"Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
|
- `--coffee-body-fallback`: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
|
||||||
- `--coffee-hand-fallback`: `"Caveat", "Segoe Print", "Bradley Hand", cursive`
|
|
||||||
- `--coffee-mono-fallback`: `"JetBrains Mono", "Fira Code", Menlo, Consolas, monospace`
|
- `--coffee-mono-fallback`: `"JetBrains Mono", "Fira Code", Menlo, Consolas, monospace`
|
||||||
|
|
||||||
|
Templates always consume the variable form, e.g.
|
||||||
|
`font-family: var(--font-heading, var(--coffee-heading-fallback))`.
|
||||||
|
|
||||||
|
## Wave-2 follow-up
|
||||||
|
|
||||||
|
If/when bundling becomes necessary (offline-first deployments, brand
|
||||||
|
exclusivity), commission/license the spec's exact Fraunces weights
|
||||||
|
(Regular 400, SemiBold 600, BoldItalic 700), Inter (Regular 400, Medium
|
||||||
|
500), and JetBrains Mono (Regular 400), and re-populate `fonts.json` per
|
||||||
|
[FONTS.md §"fonts.json schema"](../docs/FONTS.md). A `LICENSES.md` at the
|
||||||
|
theme root must also land in that pass.
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
# Coffee — Recommended icon packs
|
|
||||||
|
|
||||||
The Coffee theme renders icons through the CMS sprite system only
|
|
||||||
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`). It relies on two
|
|
||||||
bundled packs, declared in `plugin.mod` `required_icon_packs`:
|
|
||||||
|
|
||||||
| Pack | Used for |
|
|
||||||
|----------------|----------|
|
|
||||||
| `lucide` | UI glyphs in builtin overrides (arrows, chevrons, check, map-pin, navigation, star, play, download, x). Bundled with the CMS — no install needed. |
|
|
||||||
| `simple-icons` | Brand marks in author-bio-hero and social-links (x, linkedin, github, instagram, facebook). Bundled with the CMS — no install needed. |
|
|
||||||
|
|
||||||
Both packs ship with the CMS, so the theme renders with no manual setup.
|
|
||||||
|
|
||||||
## If the loader does not auto-install
|
|
||||||
|
|
||||||
The `required_icon_packs` field is forward-declared. Until the loader honors
|
|
||||||
it, confirm the packs are present:
|
|
||||||
|
|
||||||
1. Open `/admin/icons`.
|
|
||||||
2. Confirm **Lucide** and **Simple Icons** are installed and enabled (both are
|
|
||||||
bundled defaults).
|
|
||||||
3. No downloads are required.
|
|
||||||
|
|
||||||
Block content stores icon references as `"pack:name"` strings (e.g.
|
|
||||||
`"lucide:coffee"`, `"simple-icons:instagram"`). The theme never inlines SVG
|
|
||||||
paths.
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
Bundled fonts — SIL Open Font License 1.1
|
|
||||||
=========================================
|
|
||||||
|
|
||||||
This directory bundles latin-subset woff2 files for three families, all
|
|
||||||
licensed under the SIL Open Font License, Version 1.1 (OFL-1.1).
|
|
||||||
|
|
||||||
Copyright notices
|
|
||||||
-----------------
|
|
||||||
Fraunces: Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces)
|
|
||||||
Work Sans: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)
|
|
||||||
Caveat: Copyright 2019 The Caveat Project Authors (https://github.com/googlefonts/caveat)
|
|
||||||
|
|
||||||
Files
|
|
||||||
-----
|
|
||||||
fraunces-latin-400.woff2, fraunces-latin-600.woff2 — Fraunces (heading)
|
|
||||||
worksans-latin-400.woff2, worksans-latin-600.woff2 — Work Sans (body)
|
|
||||||
caveat-latin-400.woff2, caveat-latin-700.woff2 — Caveat (hand-drawn accents)
|
|
||||||
|
|
||||||
Full license text (SIL OFL 1.1)
|
|
||||||
-------------------------------
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at:
|
|
||||||
http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font creation
|
|
||||||
efforts of academic and linguistic communities, and to provide a free and
|
|
||||||
open framework in which fonts may be shared and improved in partnership
|
|
||||||
with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply
|
|
||||||
to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as
|
|
||||||
distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
||||||
or substituting -- in part or in whole -- any of the components of the
|
|
||||||
Original Version, by changing formats or by porting the Font Software to a
|
|
||||||
new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
||||||
redistribute, and sell modified and unmodified copies of the Font
|
|
||||||
Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components,
|
|
||||||
in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the corresponding
|
|
||||||
Copyright Holder. This restriction only applies to the primary font name as
|
|
||||||
presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created
|
|
||||||
using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
||||||
253
assets/style.css
@ -1,49 +1,51 @@
|
|||||||
/* Coffee theme styles
|
/* Coffee theme styles
|
||||||
*
|
*
|
||||||
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
|
* Uses the 19 shadcn-style HSL token CSS variables (--background,
|
||||||
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
|
* --foreground, --primary, --accent, --border, --muted, --card, ...) via
|
||||||
* as hsl(var(--token)). No literal colors. Font families resolve ONLY through
|
* `hsl(var(--token))`. Font families are resolved through the BlockNinja font
|
||||||
* the BlockNinja font variables (--font-heading, --font-body, --font-mono) with
|
* variables (--font-heading, --font-body, --font-mono) with fallback stacks
|
||||||
* the fallback stacks below — never hardcoded, so the admin font picker works.
|
* derived from the spec §3 typography list.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* --- Font-family fallbacks ---------------------------------------------
|
||||||
|
*
|
||||||
|
* Templates use the variable form; the second argument is the fallback the
|
||||||
|
* site shows before the admin assigns fonts via the typography picker.
|
||||||
|
*/
|
||||||
:root {
|
:root {
|
||||||
--coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
--coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
||||||
--coffee-body-fallback: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
--coffee-body-fallback: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
--coffee-hand-fallback: "Caveat", "Segoe Print", "Bradley Hand", cursive;
|
|
||||||
--coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
--coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Paper grain background --------------------------------------------- */
|
/* --- Paper grain background --------------------------------------------
|
||||||
|
*
|
||||||
|
* Inline SVG noise overlay applied as the body background. Keeps file size
|
||||||
|
* tiny and palette-neutral so it works with all three presets.
|
||||||
|
*/
|
||||||
body.coffee-paper {
|
body.coffee-paper {
|
||||||
background-color: hsl(var(--background));
|
background-color: hsl(var(--background));
|
||||||
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='paper-grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.18 0 0 0 0 0.13 0 0 0 0 0.09 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23paper-grain)'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='paper-grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.18 0 0 0 0 0.13 0 0 0 0 0.09 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23paper-grain)'/%3E%3C/svg%3E");
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Type roles --------------------------------------------------------- */
|
/* --- Headings ----------------------------------------------------------- */
|
||||||
.coffee-display {
|
.coffee-display {
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
||||||
font-feature-settings: "liga" 1, "dlig" 1;
|
font-feature-settings: "liga" 1, "dlig" 1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-body {
|
.coffee-body {
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
font-family: var(--font-body, var(--coffee-body-fallback));
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-mono {
|
.coffee-mono {
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
||||||
}
|
}
|
||||||
/* Hand-drawn accents (badges, stamps, captions). Routed through --font-heading
|
|
||||||
* with a Caveat fallback: unassigned it renders Caveat; if the admin sets a
|
|
||||||
* heading font the accents follow it (the 3-slot model working as intended). */
|
|
||||||
.coffee-hand {
|
|
||||||
font-family: var(--font-heading, var(--coffee-hand-fallback));
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
line-height: 1.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Doodle underline --------------------------------------------------- */
|
/* --- Doodle underline (heading override) -------------------------------- */
|
||||||
.coffee-doodle-underline {
|
.coffee-doodle-underline {
|
||||||
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 20 2 40 7 T 80 6 T 118 7' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' opacity='0.55'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 20 2 40 7 T 80 6 T 118 7' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' opacity='0.55'/%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@ -52,7 +54,7 @@ body.coffee-paper {
|
|||||||
padding-bottom: 0.18em;
|
padding-bottom: 0.18em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Drop-cap ----------------------------------------------------------- */
|
/* --- Drop-cap (text override / article body) ---------------------------- */
|
||||||
.coffee-dropcap > p:first-of-type::first-letter {
|
.coffee-dropcap > p:first-of-type::first-letter {
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
@ -62,7 +64,7 @@ body.coffee-paper {
|
|||||||
color: hsl(var(--primary));
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Kraft-tag button (stamp press on hover) ---------------------------- */
|
/* --- Kraft-tag button --------------------------------------------------- */
|
||||||
.kraft-tag {
|
.kraft-tag {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -76,223 +78,74 @@ body.coffee-paper {
|
|||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
font-family: var(--font-body, var(--coffee-body-fallback));
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
box-shadow: 0 2px 0 hsl(var(--border));
|
box-shadow: 0 1px 0 hsl(var(--border));
|
||||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kraft-tag:hover {
|
.kraft-tag:hover {
|
||||||
transform: translateY(1px) rotate(-1.2deg);
|
transform: rotate(-1.2deg);
|
||||||
box-shadow: 0 1px 0 hsl(var(--border));
|
box-shadow: 0 2px 0 hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.kraft-tag:focus-visible {
|
.kraft-tag:focus-visible {
|
||||||
outline: 2px solid hsl(var(--ring));
|
outline: 2px solid hsl(var(--ring));
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
.kraft-tag--ghost {
|
|
||||||
background-color: transparent;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
box-shadow: none;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
.kraft-tag--ghost:hover {
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Torn-paper edges ---------------------------------------------------
|
/* --- Torn-edge utility -------------------------------------------------- */
|
||||||
* The deckled edge is a FIXED ~13px band at the named edge; the rest of the
|
|
||||||
* element is masked solid. (The old `mask-size:100% 100%` stretched the 12px
|
|
||||||
* torn SVG across the whole element, eating ~40% of tall cards as a transparent
|
|
||||||
* wavy cut-out — the source of the hero/featured-pour/footer clipping.) */
|
|
||||||
.coffee-torn-top {
|
.coffee-torn-top {
|
||||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black);
|
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black);
|
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||||
-webkit-mask-position: top, bottom;
|
-webkit-mask-repeat: no-repeat;
|
||||||
mask-position: top, bottom;
|
mask-repeat: no-repeat;
|
||||||
-webkit-mask-size: 100% 13px, 100% calc(100% - 12px);
|
-webkit-mask-size: 100% 100%;
|
||||||
mask-size: 100% 13px, 100% calc(100% - 12px);
|
mask-size: 100% 100%;
|
||||||
-webkit-mask-repeat: no-repeat, no-repeat;
|
|
||||||
mask-repeat: no-repeat, no-repeat;
|
|
||||||
}
|
|
||||||
.coffee-torn-bottom {
|
|
||||||
-webkit-mask-image: linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
mask-image: linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
-webkit-mask-position: top, bottom;
|
|
||||||
mask-position: top, bottom;
|
|
||||||
-webkit-mask-size: 100% calc(100% - 12px), 100% 13px;
|
|
||||||
mask-size: 100% calc(100% - 12px), 100% 13px;
|
|
||||||
-webkit-mask-repeat: no-repeat, no-repeat;
|
|
||||||
mask-repeat: no-repeat, no-repeat;
|
|
||||||
}
|
|
||||||
.coffee-torn-top.coffee-torn-bottom {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
-webkit-mask-position: top, center, bottom;
|
|
||||||
mask-position: top, center, bottom;
|
|
||||||
-webkit-mask-size: 100% 13px, 100% calc(100% - 24px), 100% 13px;
|
|
||||||
mask-size: 100% 13px, 100% calc(100% - 24px), 100% 13px;
|
|
||||||
-webkit-mask-repeat: no-repeat, no-repeat, no-repeat;
|
|
||||||
mask-repeat: no-repeat, no-repeat, no-repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Surfaces ----------------------------------------------------------- */
|
.coffee-torn-bottom {
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
||||||
|
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-size: 100% 100%;
|
||||||
|
mask-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Coffee surfaces ---------------------------------------------------- */
|
||||||
.coffee-card {
|
.coffee-card {
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
color: hsl(var(--card-foreground));
|
color: hsl(var(--card-foreground));
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-frame {
|
.coffee-frame {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-pencil-rule {
|
.coffee-pencil-rule {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Chalkboard menu panel (inverts foreground/background for a hung board;
|
/* --- Today highlight ---------------------------------------------------- */
|
||||||
* light mode = dark board + cream chalk, dark mode = light menu paper) --- */
|
.coffee-hours-today {
|
||||||
.coffee-chalkboard {
|
background-color: hsl(var(--accent) / 0.15);
|
||||||
position: relative;
|
color: hsl(var(--accent-foreground));
|
||||||
background-color: hsl(var(--foreground));
|
border-left: 3px solid hsl(var(--accent));
|
||||||
color: hsl(var(--background));
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: inset 0 0 0 3px hsl(var(--background) / 0.12), inset 0 0 0 4px hsl(var(--foreground));
|
|
||||||
}
|
|
||||||
.coffee-chalk-rule {
|
|
||||||
border-color: hsl(var(--background) / 0.28);
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
.coffee-chalk-muted {
|
|
||||||
color: hsl(var(--background) / 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Hours: today highlight ---------------------------------------------
|
|
||||||
* Applied to the whole <tr> so both cells share one readable band. (Was on the
|
|
||||||
* <th> only with accent-foreground text over a 15%-accent tint — light-on-light,
|
|
||||||
* unreadable.) accent text over a 12% tint of the same accent reads cleanly. */
|
|
||||||
.coffee-hours-today > th,
|
|
||||||
.coffee-hours-today > td {
|
|
||||||
background-color: hsl(var(--accent) / 0.12);
|
|
||||||
}
|
|
||||||
.coffee-hours-today > th {
|
|
||||||
box-shadow: inset 3px 0 0 hsl(var(--accent));
|
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
color: hsl(var(--accent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Layout helpers ------------------------------------------------------
|
/* --- Price typography --------------------------------------------------- */
|
||||||
* coffee-pour-grid keeps an explicit md 2fr:3fr split — a non-standard grid
|
|
||||||
* ratio with no plain Tailwind utility. Single-column base collapses on
|
|
||||||
* mobile. */
|
|
||||||
.coffee-pour-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.coffee-pour-grid { grid-template-columns: 2fr 3fr; align-items: center; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Price + spec chips ------------------------------------------------- */
|
|
||||||
.coffee-price {
|
.coffee-price {
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
color: hsl(var(--accent));
|
color: hsl(var(--accent));
|
||||||
}
|
}
|
||||||
.coffee-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.3rem 0.7rem;
|
|
||||||
border: 1px dashed hsl(var(--border));
|
|
||||||
border-radius: 9999px;
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Circular stamp / badge (press on hover) ---------------------------- */
|
/* --- Doodle pin overlay for location card ------------------------------- */
|
||||||
.coffee-stamp {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 4.25rem;
|
|
||||||
height: 4.25rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background-color: hsl(var(--accent));
|
|
||||||
color: hsl(var(--accent-foreground));
|
|
||||||
border: 2px solid hsl(var(--accent-foreground) / 0.35);
|
|
||||||
box-shadow: inset 0 0 0 3px hsl(var(--accent));
|
|
||||||
font-family: var(--font-heading, var(--coffee-hand-fallback));
|
|
||||||
font-size: 0.95rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.05;
|
|
||||||
transform: rotate(-6deg);
|
|
||||||
transition: transform 140ms ease;
|
|
||||||
}
|
|
||||||
.coffee-stamp:hover {
|
|
||||||
transform: rotate(-6deg) scale(0.94);
|
|
||||||
}
|
|
||||||
.coffee-stamp-sm {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
transform: rotate(-4deg);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Loyalty punch stamps ----------------------------------------------- */
|
|
||||||
.coffee-punch {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 2px dashed hsl(var(--border));
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
}
|
|
||||||
.coffee-punch-on {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: hsl(var(--accent));
|
|
||||||
background-color: hsl(var(--accent) / 0.15);
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
transform: rotate(-5deg);
|
|
||||||
}
|
|
||||||
.coffee-punch-reward {
|
|
||||||
border-color: hsl(var(--primary));
|
|
||||||
background-color: hsl(var(--primary) / 0.12);
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Location pin ------------------------------------------------------- */
|
|
||||||
.coffee-pin {
|
.coffee-pin {
|
||||||
color: hsl(var(--accent));
|
color: hsl(var(--accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Doodle divider draw-in --------------------------------------------- */
|
|
||||||
.coffee-doodle-draw path,
|
|
||||||
.coffee-doodle-draw ellipse,
|
|
||||||
.coffee-doodle-draw circle {
|
|
||||||
stroke-dasharray: 240;
|
|
||||||
stroke-dashoffset: 240;
|
|
||||||
animation: coffee-draw 1.4s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes coffee-draw {
|
|
||||||
to { stroke-dashoffset: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Motion safety: static fallback ------------------------------------- */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.kraft-tag,
|
|
||||||
.coffee-stamp,
|
|
||||||
.coffee-punch-on { transition: none; }
|
|
||||||
.kraft-tag:hover,
|
|
||||||
.coffee-stamp:hover { transform: none; }
|
|
||||||
.coffee-doodle-draw path,
|
|
||||||
.coffee-doodle-draw ellipse,
|
|
||||||
.coffee-doodle-draw circle {
|
|
||||||
stroke-dasharray: none;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
# Coffee theme block definitions (codeless .bnp).
|
|
||||||
# Only true persona furniture stays a theme block. Everything that duplicated a
|
|
||||||
# builtin (menu, hours, location, footer, divider) is now a template override of
|
|
||||||
# the builtin key under templates/overrides/coffee/ — not a block here.
|
|
||||||
#
|
|
||||||
# Keys are fully qualified (coffee:<key>): the codeless loader registers
|
|
||||||
# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path).
|
|
||||||
blocks:
|
|
||||||
- key: coffee:featured_pour
|
|
||||||
title: Featured Pour
|
|
||||||
description: Single-origin spotlight card with hand-drawn badge, tasting notes and price.
|
|
||||||
category: content
|
|
||||||
schema: featured_pour.schema.json
|
|
||||||
template: featured_pour.ninjatpl
|
|
||||||
- key: coffee:roast_profile
|
|
||||||
title: Roast Profile
|
|
||||||
description: Roastery spec card — origin, process, roast level and tasting notes with a circular roast stamp.
|
|
||||||
category: content
|
|
||||||
schema: roast_profile.schema.json
|
|
||||||
template: roast_profile.ninjatpl
|
|
||||||
- key: coffee:loyalty_card
|
|
||||||
title: Loyalty Card
|
|
||||||
description: Ten-stamp coffee punch card with a reward on the tenth stamp.
|
|
||||||
category: content
|
|
||||||
schema: loyalty_card.schema.json
|
|
||||||
template: loyalty_card.ninjatpl
|
|
||||||
- key: coffee:brew_guide
|
|
||||||
title: Brew Guide
|
|
||||||
description: Step-by-step brew method card with a ratio/grind/water/temp/time strip and numbered steps.
|
|
||||||
category: content
|
|
||||||
schema: brew_guide.schema.json
|
|
||||||
template: brew_guide.ninjatpl
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<section data-block="coffee:brew_guide" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-3xl px-4">
|
|
||||||
<div class="coffee-card p-6 md:p-8">
|
|
||||||
{% if kicker %}<p class="coffee-hand text-2xl text-accent">{{ kicker }}</p>{% endif %}
|
|
||||||
{% if method %}<h3 class="coffee-display text-3xl text-primary"><span class="coffee-doodle-underline">{{ method }}</span></h3>{% endif %}
|
|
||||||
{% if intro %}<p class="coffee-body mt-3 text-muted-foreground">{{ intro }}</p>{% endif %}
|
|
||||||
<div class="mt-6 flex flex-wrap gap-2">
|
|
||||||
{% if ratio %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Ratio</span> <span class="coffee-mono">{{ ratio }}</span></span>{% endif %}
|
|
||||||
{% if grind %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Grind</span> {{ grind }}</span>{% endif %}
|
|
||||||
{% if water %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Water</span> <span class="coffee-mono">{{ water }}</span></span>{% endif %}
|
|
||||||
{% if temperature %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Temp</span> <span class="coffee-mono">{{ temperature }}</span></span>{% endif %}
|
|
||||||
{% if time %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Time</span> <span class="coffee-mono">{{ time }}</span></span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if steps %}
|
|
||||||
<ol class="mt-8 space-y-5">
|
|
||||||
{% for step in steps %}
|
|
||||||
<li class="flex gap-4">
|
|
||||||
<span class="coffee-stamp coffee-stamp-sm shrink-0">{{ forloop.Counter }}</span>
|
|
||||||
<div class="pt-1">
|
|
||||||
{% if step.title %}<p class="coffee-display text-lg text-primary">{{ step.title }}</p>{% endif %}
|
|
||||||
{% if step.text %}<p class="coffee-body text-foreground">{{ step.text }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"title": "Brew Guide",
|
|
||||||
"description": "Step-by-step brew method card with a ratio/grind/water/temp/time spec strip and numbered steps",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"kicker": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Kicker (hand-drawn)",
|
|
||||||
"default": "How we brew it",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Method",
|
|
||||||
"description": "e.g. \"V60 Pour-Over\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Intro",
|
|
||||||
"x-editor": "textarea"
|
|
||||||
},
|
|
||||||
"ratio": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Ratio",
|
|
||||||
"description": "e.g. \"1:16\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"grind": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Grind",
|
|
||||||
"description": "e.g. \"Medium-fine\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"water": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Water",
|
|
||||||
"description": "e.g. \"300g\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"temperature": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Temperature",
|
|
||||||
"description": "e.g. \"94C\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Total Time",
|
|
||||||
"description": "e.g. \"3:00\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"steps": {
|
|
||||||
"type": "array",
|
|
||||||
"title": "Steps",
|
|
||||||
"x-editor": "collection",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"title": { "type": "string", "title": "Step Title", "x-editor": "text" },
|
|
||||||
"text": { "type": "string", "title": "Step Text", "x-editor": "textarea" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<section data-block="coffee:featured_pour" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="coffee-card coffee-torn-bottom coffee-pour-grid relative mx-auto max-w-4xl p-6">
|
|
||||||
<span class="coffee-hand absolute -top-4 left-6 inline-flex rotate-[-3deg] items-center gap-1 rounded-full bg-accent px-4 py-1 text-lg text-accent-foreground shadow-sm">{{ label|default:"Featured pour" }}</span>
|
|
||||||
<div class="aspect-square overflow-hidden rounded-sm bg-secondary">
|
|
||||||
{% if image %}{% img image alt=name|default:"Featured pour" class="h-full w-full object-cover" %}{% else %}<div class="flex h-full w-full items-center justify-center bg-secondary text-primary/20"><svg width="96" height="96" class="h-24 w-24" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg></div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
{% if origin %}<p class="coffee-hand text-xl text-accent">{{ origin }}</p>{% endif %}
|
|
||||||
{% if name %}<h3 class="coffee-display text-3xl text-primary"><span class="coffee-doodle-underline">{{ name }}</span></h3>{% else %}<h3 class="coffee-display text-3xl italic text-muted-foreground">Featured pour</h3>{% endif %}
|
|
||||||
{% if tasting %}<div class="coffee-body max-w-none text-foreground [&_strong]:text-primary">{{ tasting|safe }}</div>{% endif %}
|
|
||||||
{% if price %}<div class="coffee-price mt-1 text-2xl">{{ currency|default:"$" }}{{ price }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
{% with filled=filled|default:0 %}
|
|
||||||
<section data-block="coffee:loyalty_card" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-md px-4">
|
|
||||||
<div class="coffee-card coffee-torn-top coffee-torn-bottom relative p-6 text-center">
|
|
||||||
{% if title %}<h3 class="coffee-display text-2xl text-primary">{{ title }}</h3>{% endif %}
|
|
||||||
{% if subtitle %}<p class="coffee-hand mt-1 text-xl text-accent">{{ subtitle }}</p>{% endif %}
|
|
||||||
<div class="mt-6 grid grid-cols-5 gap-3">
|
|
||||||
<span class="coffee-punch{% if filled >= 1 %} coffee-punch-on{% endif %}">{% if filled >= 1 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 2 %} coffee-punch-on{% endif %}">{% if filled >= 2 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 3 %} coffee-punch-on{% endif %}">{% if filled >= 3 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 4 %} coffee-punch-on{% endif %}">{% if filled >= 4 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 5 %} coffee-punch-on{% endif %}">{% if filled >= 5 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 6 %} coffee-punch-on{% endif %}">{% if filled >= 6 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 7 %} coffee-punch-on{% endif %}">{% if filled >= 7 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 8 %} coffee-punch-on{% endif %}">{% if filled >= 8 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 9 %} coffee-punch-on{% endif %}">{% if filled >= 9 %}<svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
|
||||||
<span class="coffee-punch{% if filled >= 10 %} coffee-punch-on coffee-punch-reward{% endif %}"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#gift"></use></svg></span>
|
|
||||||
</div>
|
|
||||||
{% if reward %}<p class="coffee-body mt-6 text-sm text-muted-foreground">{{ reward }}</p>{% endif %}
|
|
||||||
{% if footnote %}<p class="coffee-hand mt-2 text-lg text-accent">{{ footnote }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endwith %}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"title": "Loyalty Card",
|
|
||||||
"description": "Ten-stamp coffee punch card. The tenth stamp is the reward. Set how many stamps are filled for the demo state.",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"title": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Title",
|
|
||||||
"default": "Buy nine, tenth on us",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"subtitle": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Subtitle (hand-drawn)",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"filled": {
|
|
||||||
"type": "number",
|
|
||||||
"title": "Stamps Filled",
|
|
||||||
"description": "How many of the ten stamps are punched (0-10)",
|
|
||||||
"default": 0,
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 10,
|
|
||||||
"x-editor": "number"
|
|
||||||
},
|
|
||||||
"reward": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Reward Line",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"footnote": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Footnote (hand-drawn)",
|
|
||||||
"x-editor": "text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<section data-block="coffee:roast_profile" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-4xl px-4">
|
|
||||||
<div class="coffee-card grid gap-8 p-6 md:grid-cols-[3fr_2fr] md:p-8">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
{% if origin %}<p class="coffee-hand text-2xl text-accent">{{ origin }}</p>{% endif %}
|
|
||||||
{% if name %}<h3 class="coffee-display text-3xl text-primary">{{ name }}</h3>{% endif %}
|
|
||||||
{% if producer %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ producer }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if roastLevel %}<span class="coffee-stamp shrink-0">{{ roastLevel }}</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if notes %}<div class="coffee-body text-foreground">{{ notes|safe }}</div>{% endif %}
|
|
||||||
<dl class="mt-2 grid grid-cols-2 gap-x-6 gap-y-3 border-t border-dashed border-border pt-4 text-sm">
|
|
||||||
{% if process %}<div><dt class="coffee-hand text-base text-accent">Process</dt><dd class="coffee-body font-medium">{{ process }}</dd></div>{% endif %}
|
|
||||||
{% if varietal %}<div><dt class="coffee-hand text-base text-accent">Varietal</dt><dd class="coffee-body font-medium">{{ varietal }}</dd></div>{% endif %}
|
|
||||||
{% if altitude %}<div><dt class="coffee-hand text-base text-accent">Altitude</dt><dd class="coffee-body font-medium">{{ altitude }}</dd></div>{% endif %}
|
|
||||||
{% if harvest %}<div><dt class="coffee-hand text-base text-accent">Harvest</dt><dd class="coffee-body font-medium">{{ harvest }}</dd></div>{% endif %}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-hidden rounded-sm bg-secondary coffee-torn-top">
|
|
||||||
{% if image %}{% img image alt=name|default:"Roast profile" class="h-full w-full object-cover" %}{% else %}<div class="flex h-full min-h-[12rem] items-center justify-center coffee-hand text-2xl text-muted-foreground">Bean photo</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"title": "Roast Profile",
|
|
||||||
"description": "Single-origin roastery spec card: origin, process, roast level and tasting notes with a circular roast stamp",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"origin": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Origin / Kicker",
|
|
||||||
"description": "Hand-drawn line above the name (e.g. \"Single origin\")",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Coffee Name",
|
|
||||||
"description": "e.g. \"Guji Highlands\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"producer": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Producer / Farm",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"roastLevel": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Roast Level (stamp)",
|
|
||||||
"description": "Short label shown in the circular stamp (e.g. \"Medium\")",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Tasting Notes",
|
|
||||||
"description": "Rich text tasting description",
|
|
||||||
"x-editor": "richtext"
|
|
||||||
},
|
|
||||||
"process": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Process",
|
|
||||||
"description": "e.g. Washed, Natural, Honey",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"varietal": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Varietal",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"altitude": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Altitude",
|
|
||||||
"description": "e.g. \"1900-2100 masl\"",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"harvest": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Harvest",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Image",
|
|
||||||
"x-editor": "media"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
button_override.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoffeeButtonBlock renders a button with the kraft-tag override styling.
|
||||||
|
// Content shape: {"text": "Click me", "url": "...", "variant": "primary|secondary"}
|
||||||
|
func CoffeeButtonBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
text := getString(content, "text")
|
||||||
|
url := getString(content, "url")
|
||||||
|
variant := getString(content, "variant")
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = coffeeButtonComponent(text, url, variant).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
23
button_override.templ
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// coffeeButtonVariantClass appends a variant-specific tone to the kraft-tag.
|
||||||
|
func coffeeButtonVariantClass(variant string) string {
|
||||||
|
switch variant {
|
||||||
|
case "secondary":
|
||||||
|
return "bg-secondary text-secondary-foreground"
|
||||||
|
case "destructive":
|
||||||
|
return "bg-destructive text-destructive-foreground"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffeeButtonComponent renders a button or link styled like a kraft tag with
|
||||||
|
// a slight rotation on hover.
|
||||||
|
templ coffeeButtonComponent(text, url, variant string) {
|
||||||
|
if url != "" {
|
||||||
|
<a href={ templ.SafeURL(url) } class={ "kraft-tag", coffeeButtonVariantClass(variant) }>{ text }</a>
|
||||||
|
} else {
|
||||||
|
<button type="button" class={ "kraft-tag", coffeeButtonVariantClass(variant) }>{ text }</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
136
button_override_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"
|
||||||
|
|
||||||
|
// coffeeButtonVariantClass appends a variant-specific tone to the kraft-tag.
|
||||||
|
func coffeeButtonVariantClass(variant string) string {
|
||||||
|
switch variant {
|
||||||
|
case "secondary":
|
||||||
|
return "bg-secondary text-secondary-foreground"
|
||||||
|
case "destructive":
|
||||||
|
return "bg-destructive text-destructive-foreground"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffeeButtonComponent renders a button or link styled like a kraft tag with
|
||||||
|
// a slight rotation on hover.
|
||||||
|
func coffeeButtonComponent(text, url, variant 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)
|
||||||
|
if url != "" {
|
||||||
|
var templ_7745c5c3_Var2 = []any{"kraft-tag", coffeeButtonVariantClass(variant)}
|
||||||
|
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, "<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(url))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 19, Col: 30}
|
||||||
|
}
|
||||||
|
_, 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, 2, "\" 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_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.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, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 19, Col: 96}
|
||||||
|
}
|
||||||
|
_, 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, 4, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var6 = []any{"kraft-tag", coffeeButtonVariantClass(variant)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"button\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 21, Col: 87}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
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
|
||||||
32
doodle_divider.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoodleDividerBlockMeta defines metadata for the doodle_divider block.
|
||||||
|
var DoodleDividerBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "doodle_divider",
|
||||||
|
Title: "Doodle Divider",
|
||||||
|
Description: "Hand-drawn divider in one of four motifs (beans, croissant, cup, leaf)",
|
||||||
|
Source: "coffee",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoodleDividerBlock renders an inline SVG divider based on the motif.
|
||||||
|
// Content shape: {"motif": "beans"} where motif is one of beans/croissant/cup/leaf.
|
||||||
|
func DoodleDividerBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
motif := getString(content, "motif")
|
||||||
|
switch motif {
|
||||||
|
case "beans", "croissant", "cup", "leaf":
|
||||||
|
// allowed
|
||||||
|
default:
|
||||||
|
motif = "beans"
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = doodleDividerComponent(motif).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
47
doodle_divider.templ
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// doodleDividerComponent renders an inline SVG divider whose motif is
|
||||||
|
// selected by the editor. Each motif draws a distinct hand-drawn shape so
|
||||||
|
// they look visibly different even at thumbnail size.
|
||||||
|
templ doodleDividerComponent(motif string) {
|
||||||
|
<div data-block="coffee:doodle_divider" data-motif={ motif } class="my-8 flex items-center justify-center text-accent">
|
||||||
|
switch motif {
|
||||||
|
case "croissant":
|
||||||
|
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 18 L70 18"></path>
|
||||||
|
<path d="M130 18 L196 18"></path>
|
||||||
|
<path d="M85 22 Q 90 8 100 8 Q 110 8 115 22"></path>
|
||||||
|
<path d="M88 20 L92 14"></path>
|
||||||
|
<path d="M96 18 L100 10"></path>
|
||||||
|
<path d="M104 18 L108 10"></path>
|
||||||
|
<path d="M112 20 L108 14"></path>
|
||||||
|
</svg>
|
||||||
|
case "cup":
|
||||||
|
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 18 L75 18"></path>
|
||||||
|
<path d="M125 18 L196 18"></path>
|
||||||
|
<path d="M88 8 L112 8 L110 24 L90 24 Z"></path>
|
||||||
|
<path d="M112 12 Q 120 12 120 16 Q 120 20 112 20"></path>
|
||||||
|
<path d="M93 4 Q 95 1 97 4"></path>
|
||||||
|
<path d="M99 4 Q 101 1 103 4"></path>
|
||||||
|
<path d="M105 4 Q 107 1 109 4"></path>
|
||||||
|
</svg>
|
||||||
|
case "leaf":
|
||||||
|
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 18 L80 18"></path>
|
||||||
|
<path d="M120 18 L196 18"></path>
|
||||||
|
<path d="M90 20 Q 100 4 110 20 Q 100 24 90 20 Z"></path>
|
||||||
|
<path d="M93 19 L107 19"></path>
|
||||||
|
</svg>
|
||||||
|
default:
|
||||||
|
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 18 L80 18"></path>
|
||||||
|
<path d="M120 18 L196 18"></path>
|
||||||
|
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
|
||||||
|
<path d="M95 9 L95 27"></path>
|
||||||
|
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
|
||||||
|
<path d="M108 9 L108 27"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
82
doodle_divider_templ.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// doodleDividerComponent renders an inline SVG divider whose motif is
|
||||||
|
// selected by the editor. Each motif draws a distinct hand-drawn shape so
|
||||||
|
// they look visibly different even at thumbnail size.
|
||||||
|
func doodleDividerComponent(motif 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, "<div data-block=\"coffee:doodle_divider\" data-motif=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(motif)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `doodle_divider.templ`, Line: 7, Col: 59}
|
||||||
|
}
|
||||||
|
_, 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=\"my-8 flex items-center justify-center text-accent\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
switch motif {
|
||||||
|
case "croissant":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg viewBox=\"0 0 200 32\" class=\"w-48 h-8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 18 L70 18\"></path> <path d=\"M130 18 L196 18\"></path> <path d=\"M85 22 Q 90 8 100 8 Q 110 8 115 22\"></path> <path d=\"M88 20 L92 14\"></path> <path d=\"M96 18 L100 10\"></path> <path d=\"M104 18 L108 10\"></path> <path d=\"M112 20 L108 14\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "cup":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<svg viewBox=\"0 0 200 32\" class=\"w-48 h-8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 18 L75 18\"></path> <path d=\"M125 18 L196 18\"></path> <path d=\"M88 8 L112 8 L110 24 L90 24 Z\"></path> <path d=\"M112 12 Q 120 12 120 16 Q 120 20 112 20\"></path> <path d=\"M93 4 Q 95 1 97 4\"></path> <path d=\"M99 4 Q 101 1 103 4\"></path> <path d=\"M105 4 Q 107 1 109 4\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "leaf":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg viewBox=\"0 0 200 32\" class=\"w-48 h-8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 18 L80 18\"></path> <path d=\"M120 18 L196 18\"></path> <path d=\"M90 20 Q 100 4 110 20 Q 100 24 90 20 Z\"></path> <path d=\"M93 19 L107 19\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<svg viewBox=\"0 0 200 32\" class=\"w-48 h-8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 18 L80 18\"></path> <path d=\"M120 18 L196 18\"></path> <ellipse cx=\"95\" cy=\"18\" rx=\"6\" ry=\"9\"></ellipse> <path d=\"M95 9 L95 27\"></path> <ellipse cx=\"108\" cy=\"18\" rx=\"6\" ry=\"9\"></ellipse> <path d=\"M108 9 L108 27\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
204
email_wrapper.templ
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoffeeEmailWrapper renders a single-column 560px cream-and-espresso wrapper
|
||||||
|
// for transactional and newsletter emails. Table-only layout so it survives
|
||||||
|
// Outlook; doodle divider rendered inline as SVG so it survives email
|
||||||
|
// clients that strip styles.
|
||||||
|
func CoffeeEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = coffeeEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
templ coffeeEmailTemplate(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"/>
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
.coffee-email-container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
.coffee-email-padding {
|
||||||
|
padding-left: 24px !important;
|
||||||
|
padding-right: 24px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", coffeeBgColor(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: 40px 10px; background-color: %s;", coffeeBgColor(emailCtx)) }>
|
||||||
|
<table role="presentation" class="coffee-email-container" width="560" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 560px; background-color: %s; border: 1px solid %s; border-radius: 4px;", coffeeCardColor(emailCtx), coffeeBorderColor(emailCtx)) }>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style={ fmt.Sprintf("padding: 32px 40px 16px; border-bottom: 1px dashed %s;", coffeeBorderColor(emailCtx)) }>
|
||||||
|
if emailCtx.SiteSettings.LogoURL != "" {
|
||||||
|
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 48px; width: auto; display: block;"/>
|
||||||
|
} else if emailCtx.SiteSettings.SiteName != "" {
|
||||||
|
<h1 style={ fmt.Sprintf("margin: 0; font-size: 24px; letter-spacing: -0.01em; color: %s;", coffeePrimaryColor(emailCtx)) }>
|
||||||
|
{ emailCtx.SiteSettings.SiteName }
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="coffee-email-padding" style={ fmt.Sprintf("padding: 32px 40px; color: %s; font-size: 16px; line-height: 1.65;", coffeeFgColor(emailCtx)) }>
|
||||||
|
@templ.Raw(body)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 16px 40px 8px;">
|
||||||
|
<!-- inline SVG doodle divider, survives Outlook -->
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0"><tr><td>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="32" viewBox="0 0 200 32" fill="none" stroke={ coffeeAccentColor(emailCtx) } stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 18 L80 18"></path>
|
||||||
|
<path d="M120 18 L196 18"></path>
|
||||||
|
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
|
||||||
|
<path d="M95 9 L95 27"></path>
|
||||||
|
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
|
||||||
|
<path d="M108 9 L108 27"></path>
|
||||||
|
</svg>
|
||||||
|
</td></tr></table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style={ fmt.Sprintf("padding: 16px 40px 32px; color: %s; font-size: 12px; line-height: 1.6;", coffeeMutedFgColor(emailCtx)) }>
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
if emailCtx.SiteSettings.SiteName != "" {
|
||||||
|
<p style={ fmt.Sprintf("margin: 0 0 6px; font-size: 13px; color: %s;", coffeeFgColor(emailCtx)) }>
|
||||||
|
{ emailCtx.SiteSettings.SiteName }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<p style="margin: 0 0 6px;">Pull up a seat. Pastries from 7, coffee until late.</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 dashed %s;", coffeeAccentColor(emailCtx), coffeeAccentColor(emailCtx)) }>
|
||||||
|
{ emailCtx.SiteSettings.SiteURL }
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
if emailCtx.UnsubscribeURL != "" {
|
||||||
|
<p style="margin: 0; font-size: 11px;">
|
||||||
|
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", coffeeMutedFgColor(emailCtx)) }>
|
||||||
|
Unsubscribe
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coffee email color helpers — cream paper background, espresso ink, terracotta accent.
|
||||||
|
// EmailColors carries the resolved theme palette; we use Primary for the
|
||||||
|
// accent fallback because EmailColors has no dedicated Accent field.
|
||||||
|
func coffeeBgColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Background != "" {
|
||||||
|
return emailCtx.Colors.Background
|
||||||
|
}
|
||||||
|
return "#f4ece1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeCardColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Card != "" {
|
||||||
|
return emailCtx.Colors.Card
|
||||||
|
}
|
||||||
|
return "#ece1d0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeFgColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Foreground != "" {
|
||||||
|
return emailCtx.Colors.Foreground
|
||||||
|
}
|
||||||
|
return "#3d2a1a"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeePrimaryColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Primary != "" {
|
||||||
|
return emailCtx.Colors.Primary
|
||||||
|
}
|
||||||
|
return "#8a4a23"
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffeeAccentColor reuses Primary when no dedicated accent is in scope —
|
||||||
|
// EmailColors does not expose Accent; the resolved palette still applies via
|
||||||
|
// Primary which the CMS sets per-email.
|
||||||
|
func coffeeAccentColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Primary != "" {
|
||||||
|
return emailCtx.Colors.Primary
|
||||||
|
}
|
||||||
|
return "#c95b2f"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeMutedFgColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.MutedForeground != "" {
|
||||||
|
return emailCtx.Colors.MutedForeground
|
||||||
|
}
|
||||||
|
return "#6b5440"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeBorderColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Border != "" {
|
||||||
|
return emailCtx.Colors.Border
|
||||||
|
}
|
||||||
|
return "#c9b69e"
|
||||||
|
}
|
||||||
432
email_wrapper_templ.go
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoffeeEmailWrapper renders a single-column 560px cream-and-espresso wrapper
|
||||||
|
// for transactional and newsletter emails. Table-only layout so it survives
|
||||||
|
// Outlook; doodle divider rendered inline as SVG so it survives email
|
||||||
|
// clients that strip styles.
|
||||||
|
func CoffeeEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = coffeeEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeEmailTemplate(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\"><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: 29, 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 {\n\t\t\t\t\tmso-table-lspace: 0pt;\n\t\t\t\t\tmso-table-rspace: 0pt;\n\t\t\t\t}\n\t\t\t\timg {\n\t\t\t\t\t-ms-interpolation-mode: bicubic;\n\t\t\t\t\tborder: 0;\n\t\t\t\t\theight: auto;\n\t\t\t\t\tline-height: 100%;\n\t\t\t\t\toutline: none;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t}\n\t\t\t\tbody {\n\t\t\t\t\tmargin: 0 !important;\n\t\t\t\t\tpadding: 0 !important;\n\t\t\t\t\twidth: 100% !important;\n\t\t\t\t}\n\t\t\t\ta[x-apple-data-detectors] {\n\t\t\t\t\tcolor: inherit !important;\n\t\t\t\t\ttext-decoration: none !important;\n\t\t\t\t}\n\t\t\t\th1, h2, h3, h4, h5, h6 {\n\t\t\t\t\tfont-family: \"Fraunces\", \"Playfair Display\", Georgia, \"Times New Roman\", serif;\n\t\t\t\t\tfont-weight: 600;\n\t\t\t\t}\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.coffee-email-container {\n\t\t\t\t\t\twidth: 100% !important;\n\t\t\t\t\t\tmax-width: 100% !important;\n\t\t\t\t\t}\n\t\t\t\t\t.coffee-email-padding {\n\t\t\t\t\t\tpadding-left: 24px !important;\n\t\t\t\t\t\tpadding-right: 24px !important;\n\t\t\t\t\t}\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: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", coffeeBgColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 72, Col: 189}
|
||||||
|
}
|
||||||
|
_, 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: 75, 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: 40px 10px; background-color: %s;", coffeeBgColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 80, Col: 113}
|
||||||
|
}
|
||||||
|
_, 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=\"coffee-email-container\" width=\"560\" 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: 560px; background-color: %s; border: 1px solid %s; border-radius: 4px;", coffeeCardColor(emailCtx), coffeeBorderColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 81, Col: 279}
|
||||||
|
}
|
||||||
|
_, 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: 32px 40px 16px; border-bottom: 1px dashed %s;", coffeeBorderColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 83, Col: 133}
|
||||||
|
}
|
||||||
|
_, 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, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if emailCtx.SiteSettings.LogoURL != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<img src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 85, Col: 50}
|
||||||
|
}
|
||||||
|
_, 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, 11, "\" alt=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 85, Col: 89}
|
||||||
|
}
|
||||||
|
_, 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, 12, "\" style=\"max-height: 48px; width: auto; display: block;\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else if emailCtx.SiteSettings.SiteName != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<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; font-size: 24px; letter-spacing: -0.01em; color: %s;", coffeePrimaryColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 87, Col: 130}
|
||||||
|
}
|
||||||
|
_, 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, 14, "\">")
|
||||||
|
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: 88, 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, 15, "</h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td></tr><tr><td class=\"coffee-email-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: 32px 40px; color: %s; font-size: 16px; line-height: 1.65;", coffeeFgColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 94, Col: 155}
|
||||||
|
}
|
||||||
|
_, 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, 17, "\">")
|
||||||
|
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, 18, "</td></tr><tr><td align=\"center\" style=\"padding: 16px 40px 8px;\"><!-- inline SVG doodle divider, survives Outlook --><table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"32\" viewBox=\"0 0 200 32\" fill=\"none\" stroke=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(coffeeAccentColor(emailCtx))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 102, Col: 143}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 18 L80 18\"></path> <path d=\"M120 18 L196 18\"></path> <ellipse cx=\"95\" cy=\"18\" rx=\"6\" ry=\"9\"></ellipse> <path d=\"M95 9 L95 27\"></path> <ellipse cx=\"108\" cy=\"18\" rx=\"6\" ry=\"9\"></ellipse> <path d=\"M108 9 L108 27\"></path></svg></td></tr></table></td></tr><tr><td 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("padding: 16px 40px 32px; color: %s; font-size: 12px; line-height: 1.6;", coffeeMutedFgColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 114, Col: 135}
|
||||||
|
}
|
||||||
|
_, 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, 20, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if emailCtx.SiteSettings.SiteName != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p 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("margin: 0 0 6px; font-size: 13px; color: %s;", coffeeFgColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 119, Col: 108}
|
||||||
|
}
|
||||||
|
_, 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, 22, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, 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: 120, Col: 46}
|
||||||
|
}
|
||||||
|
_, 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, 23, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<p style=\"margin: 0 0 6px;\">Pull up a seat. Pastries from 7, coffee until late.</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if emailCtx.SiteSettings.SiteURL != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<p style=\"margin: 0 0 8px;\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var17, 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: 126, Col: 68}
|
||||||
|
}
|
||||||
|
_, 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, 26, "\" style=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px dashed %s;", coffeeAccentColor(emailCtx), coffeeAccentColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 126, Col: 215}
|
||||||
|
}
|
||||||
|
_, 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, 27, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, 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: 127, Col: 46}
|
||||||
|
}
|
||||||
|
_, 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, 28, "</a></p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if emailCtx.UnsubscribeURL != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<p style=\"margin: 0; font-size: 11px;\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var20, 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: 133, Col: 62}
|
||||||
|
}
|
||||||
|
_, 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, 30, "\" 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("color: %s; text-decoration: none;", coffeeMutedFgColor(emailCtx)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 133, Col: 151}
|
||||||
|
}
|
||||||
|
_, 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, 31, "\">Unsubscribe</a></p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td></tr></table></td></tr></table></td></tr></table></body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coffee email color helpers — cream paper background, espresso ink, terracotta accent.
|
||||||
|
// EmailColors carries the resolved theme palette; we use Primary for the
|
||||||
|
// accent fallback because EmailColors has no dedicated Accent field.
|
||||||
|
func coffeeBgColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Background != "" {
|
||||||
|
return emailCtx.Colors.Background
|
||||||
|
}
|
||||||
|
return "#f4ece1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeCardColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Card != "" {
|
||||||
|
return emailCtx.Colors.Card
|
||||||
|
}
|
||||||
|
return "#ece1d0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeFgColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Foreground != "" {
|
||||||
|
return emailCtx.Colors.Foreground
|
||||||
|
}
|
||||||
|
return "#3d2a1a"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeePrimaryColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Primary != "" {
|
||||||
|
return emailCtx.Colors.Primary
|
||||||
|
}
|
||||||
|
return "#8a4a23"
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffeeAccentColor reuses Primary when no dedicated accent is in scope —
|
||||||
|
// EmailColors does not expose Accent; the resolved palette still applies via
|
||||||
|
// Primary which the CMS sets per-email.
|
||||||
|
func coffeeAccentColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Primary != "" {
|
||||||
|
return emailCtx.Colors.Primary
|
||||||
|
}
|
||||||
|
return "#c95b2f"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeMutedFgColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.MutedForeground != "" {
|
||||||
|
return emailCtx.Colors.MutedForeground
|
||||||
|
}
|
||||||
|
return "#6b5440"
|
||||||
|
}
|
||||||
|
|
||||||
|
func coffeeBorderColor(emailCtx templates.EmailContext) string {
|
||||||
|
if emailCtx.Colors.Border != "" {
|
||||||
|
return emailCtx.Colors.Border
|
||||||
|
}
|
||||||
|
return "#c9b69e"
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
66
embed.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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.
|
||||||
|
func Assets() fs.FS {
|
||||||
|
sub, _ := fs.Sub(assetsFS, "assets")
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schemas returns the embedded schemas filesystem.
|
||||||
|
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.
|
||||||
|
// Coffee ships fonts.json = [] per FONTS.md wave-1 policy; recommended fonts
|
||||||
|
// (Fraunces, Inter, JetBrains Mono) are documented in RECOMMENDED_FONTS.md.
|
||||||
|
func BundledFonts() []byte {
|
||||||
|
return fontsData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThemeCSSManifest returns the additional CSS that Tailwind should include
|
||||||
|
// when this theme is active (paper texture, torn-edge mask, kraft-tag button,
|
||||||
|
// drop-cap, font-family variable fallbacks).
|
||||||
|
func ThemeCSSManifest() *plugin.CSSManifest {
|
||||||
|
css, err := assetsFS.ReadFile("assets/style.css")
|
||||||
|
if err != nil {
|
||||||
|
return &plugin.CSSManifest{}
|
||||||
|
}
|
||||||
|
return &plugin.CSSManifest{
|
||||||
|
InputCSSAppend: string(css),
|
||||||
|
}
|
||||||
|
}
|
||||||
39
featured_pour.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeaturedPourBlockMeta defines metadata for the featured_pour block.
|
||||||
|
var FeaturedPourBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "featured_pour",
|
||||||
|
Title: "Featured Pour",
|
||||||
|
Description: "Hero card for a featured coffee, tea or pastry with tasting notes and price",
|
||||||
|
Source: "coffee",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeaturedPourBlock renders a featured pour card.
|
||||||
|
// Content shape: {"name": "...", "tasting": "...rich text...", "image": "...", "price": "..."}
|
||||||
|
func FeaturedPourBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
data := FeaturedPourData{
|
||||||
|
Name: getString(content, "name"),
|
||||||
|
Tasting: getString(content, "tasting"),
|
||||||
|
Image: getString(content, "image"),
|
||||||
|
Price: getString(content, "price"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = featuredPourComponent(data).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeaturedPourData holds the data for the component.
|
||||||
|
type FeaturedPourData struct {
|
||||||
|
Name string
|
||||||
|
Tasting string
|
||||||
|
Image string
|
||||||
|
Price string
|
||||||
|
}
|
||||||
36
featured_pour.templ
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// featuredPourComponent renders the featured pour hero card.
|
||||||
|
templ featuredPourComponent(data FeaturedPourData) {
|
||||||
|
<section data-block="coffee:featured_pour" class="my-10">
|
||||||
|
<div class="coffee-card max-w-4xl mx-auto p-6 grid gap-6 md:grid-cols-[2fr_3fr] items-center relative">
|
||||||
|
<span class="absolute -top-3 left-6 inline-flex items-center gap-1 px-3 py-1 text-xs uppercase tracking-wider rounded-sm bg-accent text-accent-foreground coffee-body">
|
||||||
|
Featured
|
||||||
|
</span>
|
||||||
|
<div class="aspect-square overflow-hidden rounded-sm bg-secondary">
|
||||||
|
if data.Image != "" {
|
||||||
|
<img src={ data.Image } alt={ data.Name } class="w-full h-full object-cover" loading="lazy"/>
|
||||||
|
} else {
|
||||||
|
<div class="w-full h-full flex items-center justify-center coffee-body text-sm text-muted-foreground italic">
|
||||||
|
Add a hero image
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
if data.Name != "" {
|
||||||
|
<h3 class="coffee-display text-3xl text-primary">{ data.Name }</h3>
|
||||||
|
} else {
|
||||||
|
<h3 class="coffee-display text-3xl text-muted-foreground italic">Featured pour</h3>
|
||||||
|
}
|
||||||
|
if data.Tasting != "" {
|
||||||
|
<div class="coffee-body text-foreground prose-sm max-w-none">
|
||||||
|
@templ.Raw(data.Tasting)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if data.Price != "" {
|
||||||
|
<div class="coffee-price price text-xl">{ data.Price }</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
143
featured_pour_templ.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// featuredPourComponent renders the featured pour hero card.
|
||||||
|
func featuredPourComponent(data FeaturedPourData) 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=\"coffee:featured_pour\" class=\"my-10\"><div class=\"coffee-card max-w-4xl mx-auto p-6 grid gap-6 md:grid-cols-[2fr_3fr] items-center relative\"><span class=\"absolute -top-3 left-6 inline-flex items-center gap-1 px-3 py-1 text-xs uppercase tracking-wider rounded-sm bg-accent text-accent-foreground coffee-body\">Featured</span><div class=\"aspect-square overflow-hidden rounded-sm bg-secondary\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Image != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<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: `featured_pour.templ`, Line: 12, Col: 26}
|
||||||
|
}
|
||||||
|
_, 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=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `featured_pour.templ`, Line: 12, Col: 44}
|
||||||
|
}
|
||||||
|
_, 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, 4, "\" class=\"w-full h-full object-cover\" loading=\"lazy\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"w-full h-full flex items-center justify-center coffee-body text-sm text-muted-foreground italic\">Add a hero image</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"flex flex-col gap-3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Name != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"coffee-display text-3xl text-primary\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `featured_pour.templ`, Line: 21, Col: 65}
|
||||||
|
}
|
||||||
|
_, 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, "</h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"coffee-display text-3xl text-muted-foreground italic\">Featured pour</h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.Tasting != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"coffee-body text-foreground prose-sm max-w-none\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.Raw(data.Tasting).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.Price != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"coffee-price price text-xl\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Price)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `featured_pour.templ`, Line: 31, Col: 57}
|
||||||
|
}
|
||||||
|
_, 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, 13, "</div>")
|
||||||
|
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
|
||||||
27
fonts.json
@ -1,26 +1 @@
|
|||||||
[
|
[]
|
||||||
{
|
|
||||||
"name": "Fraunces",
|
|
||||||
"family": "Fraunces",
|
|
||||||
"variants": [
|
|
||||||
{ "weight": "400", "style": "normal", "file": "fonts/fraunces-latin-400.woff2" },
|
|
||||||
{ "weight": "600", "style": "normal", "file": "fonts/fraunces-latin-600.woff2" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Work Sans",
|
|
||||||
"family": "Work Sans",
|
|
||||||
"variants": [
|
|
||||||
{ "weight": "400", "style": "normal", "file": "fonts/worksans-latin-400.woff2" },
|
|
||||||
{ "weight": "600", "style": "normal", "file": "fonts/worksans-latin-600.woff2" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Caveat",
|
|
||||||
"family": "Caveat",
|
|
||||||
"variants": [
|
|
||||||
{ "weight": "400", "style": "normal", "file": "fonts/caveat-latin-400.woff2" },
|
|
||||||
{ "weight": "700", "style": "normal", "file": "fonts/caveat-latin-700.woff2" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
35
footer.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FooterBlockMeta defines metadata for the Coffee footer block.
|
||||||
|
var FooterBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "footer",
|
||||||
|
Title: "Footer",
|
||||||
|
Description: "Torn-edge footer with optional location summary and newsletter caption",
|
||||||
|
Source: "coffee",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FooterBlock renders the coffee footer.
|
||||||
|
// Content shape: {"showLocation": "true", "newsletterText": "..."}
|
||||||
|
func FooterBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
data := FooterData{
|
||||||
|
ShowLocation: getBool(content, "showLocation", true),
|
||||||
|
NewsletterText: getString(content, "newsletterText"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = footerComponent(data).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FooterData holds the data for the footer component.
|
||||||
|
type FooterData struct {
|
||||||
|
ShowLocation bool
|
||||||
|
NewsletterText string
|
||||||
|
}
|
||||||
37
footer.templ
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// footerComponent renders the kraft-paper footer with a torn top edge.
|
||||||
|
templ footerComponent(data FooterData) {
|
||||||
|
<div data-block="coffee:footer" class="coffee-torn-top bg-card text-card-foreground pt-12 pb-8 mt-12">
|
||||||
|
<div class="max-w-5xl mx-auto px-6 grid gap-8 md:grid-cols-3 coffee-body">
|
||||||
|
<div>
|
||||||
|
<h4 class="coffee-display text-xl text-primary mb-2">Hello there</h4>
|
||||||
|
<p class="text-sm text-muted-foreground">Pull up a seat. Pastries from 7, coffee until late.</p>
|
||||||
|
</div>
|
||||||
|
if data.ShowLocation {
|
||||||
|
<div>
|
||||||
|
<h4 class="coffee-display text-xl text-primary mb-2">Visit</h4>
|
||||||
|
<address class="not-italic text-sm text-foreground whitespace-pre-line">42 Roastery Lane
|
||||||
|
City, State 9000</address>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<h4 class="coffee-display text-xl text-primary mb-2">Stay in touch</h4>
|
||||||
|
if data.NewsletterText != "" {
|
||||||
|
<p class="text-sm text-muted-foreground mb-3">{ data.NewsletterText }</p>
|
||||||
|
} else {
|
||||||
|
<p class="text-sm text-muted-foreground mb-3">Slow notes from the bar. Brew tips, seasonal pours, no spam.</p>
|
||||||
|
}
|
||||||
|
<form class="flex gap-2" action="#" method="post" onsubmit="event.preventDefault();">
|
||||||
|
<label class="sr-only" for="coffee-newsletter-email">Email</label>
|
||||||
|
<input id="coffee-newsletter-email" name="email" type="email" required placeholder="you@cafe.com" class="flex-1 px-3 py-2 text-sm coffee-body bg-input text-foreground border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-ring"/>
|
||||||
|
<button type="submit" class="kraft-tag">Subscribe</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-5xl mx-auto px-6 mt-10 pt-6 border-t coffee-pencil-rule text-xs coffee-body text-muted-foreground flex flex-wrap gap-2 justify-between">
|
||||||
|
<span>© Coffee theme — kraft paper edition.</span>
|
||||||
|
<span>Hand-drawn with care.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
79
footer_templ.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// footerComponent renders the kraft-paper footer with a torn top edge.
|
||||||
|
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, "<div data-block=\"coffee:footer\" class=\"coffee-torn-top bg-card text-card-foreground pt-12 pb-8 mt-12\"><div class=\"max-w-5xl mx-auto px-6 grid gap-8 md:grid-cols-3 coffee-body\"><div><h4 class=\"coffee-display text-xl text-primary mb-2\">Hello there</h4><p class=\"text-sm text-muted-foreground\">Pull up a seat. Pastries from 7, coffee until late.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.ShowLocation {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div><h4 class=\"coffee-display text-xl text-primary mb-2\">Visit</h4><address class=\"not-italic text-sm text-foreground whitespace-pre-line\">42 Roastery Lane City, State 9000</address></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div><h4 class=\"coffee-display text-xl text-primary mb-2\">Stay in touch</h4>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.NewsletterText != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"text-sm text-muted-foreground mb-3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.NewsletterText)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 21, Col: 72}
|
||||||
|
}
|
||||||
|
_, 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, 5, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"text-sm text-muted-foreground mb-3\">Slow notes from the bar. Brew tips, seasonal pours, no spam.</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form class=\"flex gap-2\" action=\"#\" method=\"post\" onsubmit=\"event.preventDefault();\"><label class=\"sr-only\" for=\"coffee-newsletter-email\">Email</label> <input id=\"coffee-newsletter-email\" name=\"email\" type=\"email\" required placeholder=\"you@cafe.com\" class=\"flex-1 px-3 py-2 text-sm coffee-body bg-input text-foreground border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-ring\"> <button type=\"submit\" class=\"kraft-tag\">Subscribe</button></form></div></div><div class=\"max-w-5xl mx-auto px-6 mt-10 pt-6 border-t coffee-pencil-rule text-xs coffee-body text-muted-foreground flex flex-wrap gap-2 justify-between\"><span>© Coffee theme — kraft paper edition.</span> <span>Hand-drawn with care.</span></div></div>")
|
||||||
|
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/coffee
|
||||||
|
|
||||||
|
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=
|
||||||
40
heading_override.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoffeeHeadingBlock renders a heading with Coffee styling.
|
||||||
|
// Content shape: {"text": "...", "level": 1-6, "textClass": "..."}
|
||||||
|
func CoffeeHeadingBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
text := getString(content, "text")
|
||||||
|
textClass := getString(content, "textClass")
|
||||||
|
level := parseHeadingLevel(content)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = coffeeHeadingComponent(level, text, textClass).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeadingLevel parses the heading level, defaulting to 2.
|
||||||
|
func parseHeadingLevel(content map[string]any) int {
|
||||||
|
if level, ok := content["level"].(float64); ok {
|
||||||
|
l := int(level)
|
||||||
|
if l >= 1 && l <= 6 {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if level, ok := content["level"].(int); ok {
|
||||||
|
if level >= 1 && level <= 6 {
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if level, ok := content["level"].(string); ok {
|
||||||
|
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
48
heading_override.templ
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// coffeeHeadingBaseClass returns base Tailwind classes for each heading level.
|
||||||
|
func coffeeHeadingBaseClass(level int) string {
|
||||||
|
switch level {
|
||||||
|
case 1:
|
||||||
|
return "coffee-display text-5xl leading-tight"
|
||||||
|
case 2:
|
||||||
|
return "coffee-display text-3xl italic"
|
||||||
|
case 3:
|
||||||
|
return "coffee-display text-2xl"
|
||||||
|
case 4:
|
||||||
|
return "coffee-display text-xl"
|
||||||
|
case 5:
|
||||||
|
return "coffee-display text-lg"
|
||||||
|
case 6:
|
||||||
|
return "coffee-display text-base"
|
||||||
|
default:
|
||||||
|
return "coffee-display text-3xl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffeeHeadingComponent renders a heading with Coffee display styling and an
|
||||||
|
// optional doodle underline for h2+ levels.
|
||||||
|
templ coffeeHeadingComponent(level int, text, textClass string) {
|
||||||
|
switch level {
|
||||||
|
case 1:
|
||||||
|
<h1 class={ coffeeHeadingBaseClass(1), "text-primary", textClass }>{ text }</h1>
|
||||||
|
case 2:
|
||||||
|
<h2 class={ coffeeHeadingBaseClass(2), "text-primary", textClass }>
|
||||||
|
<span class="coffee-doodle-underline">{ text }</span>
|
||||||
|
</h2>
|
||||||
|
case 3:
|
||||||
|
<h3 class={ coffeeHeadingBaseClass(3), "text-primary", textClass }>
|
||||||
|
<span class="coffee-doodle-underline">{ text }</span>
|
||||||
|
</h3>
|
||||||
|
case 4:
|
||||||
|
<h4 class={ coffeeHeadingBaseClass(4), "text-primary", textClass }>{ text }</h4>
|
||||||
|
case 5:
|
||||||
|
<h5 class={ coffeeHeadingBaseClass(5), "text-primary", textClass }>{ text }</h5>
|
||||||
|
case 6:
|
||||||
|
<h6 class={ coffeeHeadingBaseClass(6), "text-primary", textClass }>{ text }</h6>
|
||||||
|
default:
|
||||||
|
<h2 class={ coffeeHeadingBaseClass(2), "text-primary", textClass }>
|
||||||
|
<span class="coffee-doodle-underline">{ text }</span>
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
}
|
||||||
312
heading_override_templ.go
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// coffeeHeadingBaseClass returns base Tailwind classes for each heading level.
|
||||||
|
func coffeeHeadingBaseClass(level int) string {
|
||||||
|
switch level {
|
||||||
|
case 1:
|
||||||
|
return "coffee-display text-5xl leading-tight"
|
||||||
|
case 2:
|
||||||
|
return "coffee-display text-3xl italic"
|
||||||
|
case 3:
|
||||||
|
return "coffee-display text-2xl"
|
||||||
|
case 4:
|
||||||
|
return "coffee-display text-xl"
|
||||||
|
case 5:
|
||||||
|
return "coffee-display text-lg"
|
||||||
|
case 6:
|
||||||
|
return "coffee-display text-base"
|
||||||
|
default:
|
||||||
|
return "coffee-display text-3xl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coffeeHeadingComponent renders a heading with Coffee display styling and an
|
||||||
|
// optional doodle underline for h2+ levels.
|
||||||
|
func coffeeHeadingComponent(level int, text, textClass 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)
|
||||||
|
switch level {
|
||||||
|
case 1:
|
||||||
|
var templ_7745c5c3_Var2 = []any{coffeeHeadingBaseClass(1), "text-primary", textClass}
|
||||||
|
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 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, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 28, Col: 76}
|
||||||
|
}
|
||||||
|
_, 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{coffeeHeadingBaseClass(2), "text-primary", textClass}
|
||||||
|
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 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, "\"><span class=\"coffee-doodle-underline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 31, Col: 48}
|
||||||
|
}
|
||||||
|
_, 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, "</span></h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
var templ_7745c5c3_Var8 = []any{coffeeHeadingBaseClass(3), "text-primary", textClass}
|
||||||
|
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 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, "\"><span class=\"coffee-doodle-underline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 35, Col: 48}
|
||||||
|
}
|
||||||
|
_, 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, "</span></h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
var templ_7745c5c3_Var11 = []any{coffeeHeadingBaseClass(4), "text-primary", textClass}
|
||||||
|
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 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, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 38, Col: 76}
|
||||||
|
}
|
||||||
|
_, 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{coffeeHeadingBaseClass(5), "text-primary", textClass}
|
||||||
|
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 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, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 40, Col: 76}
|
||||||
|
}
|
||||||
|
_, 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{coffeeHeadingBaseClass(6), "text-primary", textClass}
|
||||||
|
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 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, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 42, Col: 76}
|
||||||
|
}
|
||||||
|
_, 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{coffeeHeadingBaseClass(2), "text-primary", textClass}
|
||||||
|
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 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, "\"><span class=\"coffee-doodle-underline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 45, Col: 48}
|
||||||
|
}
|
||||||
|
_, 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, "</span></h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
91
helpers.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getString extracts a string value from content map.
|
||||||
|
func getString(content map[string]any, key string) string {
|
||||||
|
if v, ok := content[key].(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBool extracts a bool value from content map (handles string forms).
|
||||||
|
func getBool(content map[string]any, key string, defaultVal bool) bool {
|
||||||
|
if v, ok := content[key].(bool); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if v, ok := content[key].(string); ok {
|
||||||
|
switch strings.ToLower(v) {
|
||||||
|
case "true", "yes", "1":
|
||||||
|
return true
|
||||||
|
case "false", "no", "0":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSlice extracts a slice of maps from content.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortDayName returns the three-letter weekday abbreviation for "today" in
|
||||||
|
// the server's local time zone. Used by hours_strip to flag today's row
|
||||||
|
// server-side (no JS).
|
||||||
|
func shortDayName(now time.Time) string {
|
||||||
|
switch now.Weekday() {
|
||||||
|
case time.Monday:
|
||||||
|
return "Mon"
|
||||||
|
case time.Tuesday:
|
||||||
|
return "Tue"
|
||||||
|
case time.Wednesday:
|
||||||
|
return "Wed"
|
||||||
|
case time.Thursday:
|
||||||
|
return "Thu"
|
||||||
|
case time.Friday:
|
||||||
|
return "Fri"
|
||||||
|
case time.Saturday:
|
||||||
|
return "Sat"
|
||||||
|
case time.Sunday:
|
||||||
|
return "Sun"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// normaliseDay accepts a free-form day string and returns the matching short
|
||||||
|
// name. This lets editors enter "Monday", "mon", or "Mon" and still get a
|
||||||
|
// reliable comparison against today.
|
||||||
|
func normaliseDay(day string) string {
|
||||||
|
d := strings.ToLower(strings.TrimSpace(day))
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(d, "mon"):
|
||||||
|
return "Mon"
|
||||||
|
case strings.HasPrefix(d, "tue"):
|
||||||
|
return "Tue"
|
||||||
|
case strings.HasPrefix(d, "wed"):
|
||||||
|
return "Wed"
|
||||||
|
case strings.HasPrefix(d, "thu"):
|
||||||
|
return "Thu"
|
||||||
|
case strings.HasPrefix(d, "fri"):
|
||||||
|
return "Fri"
|
||||||
|
case strings.HasPrefix(d, "sat"):
|
||||||
|
return "Sat"
|
||||||
|
case strings.HasPrefix(d, "sun"):
|
||||||
|
return "Sun"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(day)
|
||||||
|
}
|
||||||
73
hours_strip.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HoursStripBlockMeta defines metadata for the hours_strip block.
|
||||||
|
var HoursStripBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "hours_strip",
|
||||||
|
Title: "Hours Strip",
|
||||||
|
Description: "Weekly hours strip with today's row server-side highlighted",
|
||||||
|
Source: "coffee",
|
||||||
|
}
|
||||||
|
|
||||||
|
// HoursStripBlock renders a weekly hours strip.
|
||||||
|
// Content shape:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "todayLabel": "Today",
|
||||||
|
// "hours": [
|
||||||
|
// {"day": "Mon", "open": "7:00", "close": "15:00"}, ...
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The "today" row is detected server-side by comparing each row's day to the
|
||||||
|
// current local weekday — no JS required.
|
||||||
|
func HoursStripBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
todayLabel := getString(content, "todayLabel")
|
||||||
|
if todayLabel == "" {
|
||||||
|
todayLabel = "Today"
|
||||||
|
}
|
||||||
|
|
||||||
|
today := shortDayName(time.Now())
|
||||||
|
|
||||||
|
rawHours := getSlice(content, "hours")
|
||||||
|
var rows []HoursRow
|
||||||
|
for _, h := range rawHours {
|
||||||
|
day := normaliseDay(getString(h, "day"))
|
||||||
|
rows = append(rows, HoursRow{
|
||||||
|
Day: day,
|
||||||
|
Open: getString(h, "open"),
|
||||||
|
Close: getString(h, "close"),
|
||||||
|
IsToday: day != "" && day == today,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data := HoursStripData{
|
||||||
|
TodayLabel: todayLabel,
|
||||||
|
Rows: rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = hoursStripComponent(data).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HoursStripData contains data for the hours strip component.
|
||||||
|
type HoursStripData struct {
|
||||||
|
TodayLabel string
|
||||||
|
Rows []HoursRow
|
||||||
|
}
|
||||||
|
|
||||||
|
// HoursRow represents one weekday's hours.
|
||||||
|
type HoursRow struct {
|
||||||
|
Day string
|
||||||
|
Open string
|
||||||
|
Close string
|
||||||
|
IsToday bool
|
||||||
|
}
|
||||||
35
hours_strip.templ
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// hoursStripComponent renders the hours strip with the today row highlighted.
|
||||||
|
templ hoursStripComponent(data HoursStripData) {
|
||||||
|
<aside data-block="coffee:hours_strip" class="my-4">
|
||||||
|
<ul class="coffee-card grid grid-cols-1 sm:grid-cols-2 md:grid-cols-7 gap-1 p-2 max-w-5xl mx-auto">
|
||||||
|
if len(data.Rows) == 0 {
|
||||||
|
<li class="coffee-body text-sm text-muted-foreground italic px-3 py-2 col-span-full text-center">Add weekday hours to display the strip.</li>
|
||||||
|
}
|
||||||
|
for _, row := range data.Rows {
|
||||||
|
<li class={ "coffee-body px-2 py-1 flex items-baseline gap-2 text-sm rounded-sm", todayClass(row.IsToday) }>
|
||||||
|
<span class="coffee-display font-semibold text-foreground w-12 shrink-0">{ row.Day }</span>
|
||||||
|
if row.IsToday {
|
||||||
|
<span class="coffee-body text-[10px] uppercase tracking-wider text-accent">{ data.TodayLabel }</span>
|
||||||
|
}
|
||||||
|
<span class="coffee-mono text-foreground">
|
||||||
|
if row.Open != "" || row.Close != "" {
|
||||||
|
{ row.Open } – { row.Close }
|
||||||
|
} else {
|
||||||
|
Closed
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
}
|
||||||
|
|
||||||
|
// todayClass returns the highlight class when this row is today's.
|
||||||
|
func todayClass(isToday bool) string {
|
||||||
|
if isToday {
|
||||||
|
return "coffee-hours-today is-today today"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
152
hours_strip_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"
|
||||||
|
|
||||||
|
// hoursStripComponent renders the hours strip with the today row highlighted.
|
||||||
|
func hoursStripComponent(data HoursStripData) 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, "<aside data-block=\"coffee:hours_strip\" class=\"my-4\"><ul class=\"coffee-card grid grid-cols-1 sm:grid-cols-2 md:grid-cols-7 gap-1 p-2 max-w-5xl mx-auto\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(data.Rows) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<li class=\"coffee-body text-sm text-muted-foreground italic px-3 py-2 col-span-full text-center\">Add weekday hours to display the strip.</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range data.Rows {
|
||||||
|
var templ_7745c5c3_Var2 = []any{"coffee-body px-2 py-1 flex items-baseline gap-2 text-sm rounded-sm", todayClass(row.IsToday)}
|
||||||
|
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, 3, "<li 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: `hours_strip.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, 4, "\"><span class=\"coffee-display font-semibold text-foreground w-12 shrink-0\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(row.Day)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hours_strip.templ`, Line: 12, Col: 87}
|
||||||
|
}
|
||||||
|
_, 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, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if row.IsToday {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"coffee-body text-[10px] uppercase tracking-wider text-accent\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.TodayLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hours_strip.templ`, Line: 14, Col: 98}
|
||||||
|
}
|
||||||
|
_, 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, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"coffee-mono text-foreground\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if row.Open != "" || row.Close != "" {
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(row.Open)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hours_strip.templ`, Line: 18, Col: 17}
|
||||||
|
}
|
||||||
|
_, 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, 9, " – ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(row.Close)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hours_strip.templ`, Line: 18, Col: 35}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Closed")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</ul></aside>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// todayClass returns the highlight class when this row is today's.
|
||||||
|
func todayClass(isToday bool) string {
|
||||||
|
if isToday {
|
||||||
|
return "coffee-hours-today is-today today"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
19
image_override.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoffeeImageBlock renders an image with the torn-edge frame and optional
|
||||||
|
// handwritten caption.
|
||||||
|
// Content shape: {"src": "...", "alt": "...", "caption": "..."}
|
||||||
|
func CoffeeImageBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
src := getString(content, "src")
|
||||||
|
alt := getString(content, "alt")
|
||||||
|
caption := getString(content, "caption")
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = coffeeImageComponent(src, alt, caption).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
20
image_override.templ
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// coffeeImageComponent renders an image with the torn-edge frame and an
|
||||||
|
// optional caption styled like a handwritten note.
|
||||||
|
templ coffeeImageComponent(src, alt, caption string) {
|
||||||
|
<figure class="my-8 flex flex-col items-center coffee-body">
|
||||||
|
<div class="coffee-frame p-2 bg-card rounded-sm relative coffee-torn-bottom">
|
||||||
|
if src != "" {
|
||||||
|
<img src={ src } alt={ alt } class="block max-w-full h-auto" loading="lazy"/>
|
||||||
|
} else {
|
||||||
|
<div class="w-full h-48 flex items-center justify-center coffee-body text-sm text-muted-foreground italic px-6">
|
||||||
|
Add an image to render the torn-edge frame.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
if caption != "" {
|
||||||
|
<figcaption class="coffee-display italic text-center text-sm text-muted-foreground mt-3">{ caption }</figcaption>
|
||||||
|
}
|
||||||
|
</figure>
|
||||||
|
}
|
||||||
106
image_override_templ.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// coffeeImageComponent renders an image with the torn-edge frame and an
|
||||||
|
// optional caption styled like a handwritten note.
|
||||||
|
func coffeeImageComponent(src, alt, caption 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, "<figure class=\"my-8 flex flex-col items-center coffee-body\"><div class=\"coffee-frame p-2 bg-card rounded-sm relative coffee-torn-bottom\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if src != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<img src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 9, Col: 18}
|
||||||
|
}
|
||||||
|
_, 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=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 9, Col: 30}
|
||||||
|
}
|
||||||
|
_, 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, 4, "\" class=\"block max-w-full h-auto\" loading=\"lazy\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"w-full h-48 flex items-center justify-center coffee-body text-sm text-muted-foreground italic px-6\">Add an image to render the torn-edge frame.</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if caption != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<figcaption class=\"coffee-display italic text-center text-sm text-muted-foreground mt-3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(caption)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 17, Col: 101}
|
||||||
|
}
|
||||||
|
_, 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, "</figcaption>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</figure>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
37
location_card.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocationCardBlockMeta defines metadata for the location_card block.
|
||||||
|
var LocationCardBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "location_card",
|
||||||
|
Title: "Location Card",
|
||||||
|
Description: "Address card with optional static map image and doodled pin overlay",
|
||||||
|
Source: "coffee",
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocationCardBlock renders a location card.
|
||||||
|
// Content shape: {"address": "...", "mapImage": "...", "directionsUrl": "..."}
|
||||||
|
func LocationCardBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
data := LocationCardData{
|
||||||
|
Address: getString(content, "address"),
|
||||||
|
MapImage: getString(content, "mapImage"),
|
||||||
|
DirectionsURL: getString(content, "directionsUrl"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = locationCardComponent(data).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocationCardData holds the data the template needs.
|
||||||
|
type LocationCardData struct {
|
||||||
|
Address string
|
||||||
|
MapImage string
|
||||||
|
DirectionsURL string
|
||||||
|
}
|
||||||
35
location_card.templ
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// locationCardComponent renders a location card with doodled pin overlay.
|
||||||
|
templ locationCardComponent(data LocationCardData) {
|
||||||
|
<section data-block="coffee:location_card" class="my-8">
|
||||||
|
<div class="coffee-card max-w-3xl mx-auto p-6 grid gap-6 md:grid-cols-2">
|
||||||
|
<div class="relative aspect-[4/3] overflow-hidden rounded-sm bg-secondary">
|
||||||
|
if data.MapImage != "" {
|
||||||
|
<img src={ data.MapImage } alt="Map of our location" class="absolute inset-0 w-full h-full object-cover" loading="lazy"/>
|
||||||
|
} else {
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center coffee-body text-sm text-muted-foreground italic">
|
||||||
|
Map preview
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<svg class="coffee-pin absolute top-1/3 left-1/2 -translate-x-1/2 w-10 h-10" viewBox="0 0 40 56" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M20 4 Q 6 4 6 22 Q 6 36 20 52 Q 34 36 34 22 Q 34 4 20 4 Z"></path>
|
||||||
|
<circle cx="20" cy="22" r="6"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3 justify-center">
|
||||||
|
<h3 class="coffee-display text-2xl text-primary">
|
||||||
|
<span class="coffee-doodle-underline">Find us</span>
|
||||||
|
</h3>
|
||||||
|
if data.Address != "" {
|
||||||
|
<address class="coffee-body not-italic text-foreground whitespace-pre-line">{ data.Address }</address>
|
||||||
|
} else {
|
||||||
|
<p class="coffee-body text-sm text-muted-foreground italic">Add an address to render the location card.</p>
|
||||||
|
}
|
||||||
|
if data.DirectionsURL != "" {
|
||||||
|
<a href={ templ.SafeURL(data.DirectionsURL) } class="kraft-tag mt-2 self-start" target="_blank" rel="noopener noreferrer">Directions</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
116
location_card_templ.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// locationCardComponent renders a location card with doodled pin overlay.
|
||||||
|
func locationCardComponent(data LocationCardData) 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=\"coffee:location_card\" class=\"my-8\"><div class=\"coffee-card max-w-3xl mx-auto p-6 grid gap-6 md:grid-cols-2\"><div class=\"relative aspect-[4/3] overflow-hidden rounded-sm bg-secondary\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.MapImage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<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.MapImage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `location_card.templ`, Line: 9, 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, 3, "\" alt=\"Map of our location\" class=\"absolute inset-0 w-full h-full object-cover\" loading=\"lazy\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"absolute inset-0 flex items-center justify-center coffee-body text-sm text-muted-foreground italic\">Map preview</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg class=\"coffee-pin absolute top-1/3 left-1/2 -translate-x-1/2 w-10 h-10\" viewBox=\"0 0 40 56\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 4 Q 6 4 6 22 Q 6 36 20 52 Q 34 36 34 22 Q 34 4 20 4 Z\"></path> <circle cx=\"20\" cy=\"22\" r=\"6\"></circle></svg></div><div class=\"flex flex-col gap-3 justify-center\"><h3 class=\"coffee-display text-2xl text-primary\"><span class=\"coffee-doodle-underline\">Find us</span></h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Address != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<address class=\"coffee-body not-italic text-foreground whitespace-pre-line\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Address)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `location_card.templ`, Line: 25, Col: 95}
|
||||||
|
}
|
||||||
|
_, 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, "</address>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"coffee-body text-sm text-muted-foreground italic\">Add an address to render the location card.</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data.DirectionsURL != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.DirectionsURL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `location_card.templ`, Line: 30, Col: 48}
|
||||||
|
}
|
||||||
|
_, 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, 10, "\" class=\"kraft-tag mt-2 self-start\" target=\"_blank\" rel=\"noopener noreferrer\">Directions</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div></section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
422
manifest.yaml
@ -1,422 +0,0 @@
|
|||||||
# Coffee theme — codeless .bnp manifest (Wave A, WO-TF-013).
|
|
||||||
# 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: coffee
|
|
||||||
title: Coffee
|
|
||||||
description: "Warm, hand-crafted cafe and roastery theme — kraft paper, chalkboard menus, doodles and stamps."
|
|
||||||
|
|
||||||
page_templates:
|
|
||||||
- system: coffee
|
|
||||||
key: default
|
|
||||||
title: Default
|
|
||||||
description: "Header + main + footer over warm paper grain."
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: coffee
|
|
||||||
key: landing
|
|
||||||
title: Landing
|
|
||||||
description: "Hero pour, featured menu, story and invitation."
|
|
||||||
slots: [hero, menu, story, cta, footer]
|
|
||||||
- system: coffee
|
|
||||||
key: article
|
|
||||||
title: Article
|
|
||||||
description: "Narrow narrative column with drop-cap for journal and recipes."
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: coffee
|
|
||||||
key: full-width
|
|
||||||
title: Full Width
|
|
||||||
description: "Edge-to-edge gallery and interior shots."
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: coffee
|
|
||||||
key: blog-index
|
|
||||||
title: Blog Index
|
|
||||||
description: "Blog hero, featured posts and a post grid."
|
|
||||||
slots: [header, featured, main, footer]
|
|
||||||
- system: coffee
|
|
||||||
key: contact
|
|
||||||
title: Contact
|
|
||||||
description: "Contact form, hours and location."
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: coffee
|
|
||||||
key: auth
|
|
||||||
title: Auth
|
|
||||||
description: "Centered card for login, register and password reset."
|
|
||||||
slots: [main, footer]
|
|
||||||
|
|
||||||
email_wrappers:
|
|
||||||
- coffee
|
|
||||||
|
|
||||||
template_overrides:
|
|
||||||
- { template: coffee, block: announcement-bar }
|
|
||||||
- { template: coffee, block: audio-embed }
|
|
||||||
- { template: coffee, block: auth-form }
|
|
||||||
- { template: coffee, block: author-bio }
|
|
||||||
- { template: coffee, block: author-bio-hero }
|
|
||||||
- { template: coffee, block: auth-status }
|
|
||||||
- { template: coffee, block: blog-hero }
|
|
||||||
- { template: coffee, block: breadcrumbs }
|
|
||||||
- { template: coffee, block: button }
|
|
||||||
- { template: coffee, block: card }
|
|
||||||
- { template: coffee, block: category-list }
|
|
||||||
- { template: coffee, block: contact-card }
|
|
||||||
- { template: coffee, block: contact-form }
|
|
||||||
- { template: coffee, block: countdown }
|
|
||||||
- { template: coffee, block: cta }
|
|
||||||
- { template: coffee, block: divider }
|
|
||||||
- { template: coffee, block: event-list }
|
|
||||||
- { template: coffee, block: faq }
|
|
||||||
- { template: coffee, block: featured-posts }
|
|
||||||
- { template: coffee, block: feature-grid }
|
|
||||||
- { template: coffee, block: footer }
|
|
||||||
- { template: coffee, block: gallery }
|
|
||||||
- { template: coffee, block: heading }
|
|
||||||
- { template: coffee, block: hero }
|
|
||||||
- { template: coffee, block: hours-location }
|
|
||||||
- { template: coffee, block: image }
|
|
||||||
- { template: coffee, block: logo-strip }
|
|
||||||
- { template: coffee, block: menu-list }
|
|
||||||
- { template: coffee, block: navbar }
|
|
||||||
- { template: coffee, block: newsletter-signup }
|
|
||||||
- { template: coffee, block: page-suggestions }
|
|
||||||
- { template: coffee, block: password-reset-form }
|
|
||||||
- { template: coffee, block: popular-posts }
|
|
||||||
- { template: coffee, block: post-hero }
|
|
||||||
- { template: coffee, block: post-metadata }
|
|
||||||
- { template: coffee, block: pricing }
|
|
||||||
- { template: coffee, block: quote }
|
|
||||||
- { template: coffee, block: related-posts }
|
|
||||||
- { template: coffee, block: rich-section }
|
|
||||||
- { template: coffee, block: rich-text }
|
|
||||||
- { template: coffee, block: sidebar-nav }
|
|
||||||
- { template: coffee, block: social-links }
|
|
||||||
- { template: coffee, block: stats }
|
|
||||||
- { template: coffee, block: team }
|
|
||||||
- { template: coffee, block: testimonial }
|
|
||||||
- { template: coffee, block: text }
|
|
||||||
- { template: coffee, block: timeline }
|
|
||||||
- { template: coffee, block: utility-bar }
|
|
||||||
- { template: coffee, block: video-embed }
|
|
||||||
|
|
||||||
css:
|
|
||||||
input_css_append: |
|
|
||||||
/* Coffee theme styles
|
|
||||||
*
|
|
||||||
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
|
|
||||||
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
|
|
||||||
* as hsl(var(--token)). No literal colors. Font families resolve ONLY through
|
|
||||||
* the BlockNinja font variables (--font-heading, --font-body, --font-mono) with
|
|
||||||
* the fallback stacks below — never hardcoded, so the admin font picker works.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Bundled faces. The host does not yet emit @font-face for bundled_fonts
|
|
||||||
* (LoadedPlugin.BundledFonts is unconsumed — cms WO-BUNDLED-FONTS-DEAD-CODE),
|
|
||||||
* so the theme declares its own faces against the served artifact paths
|
|
||||||
* (/templates/coffee/fonts/...). Families/weights mirror fonts.json exactly.
|
|
||||||
* Presets set typography.fontHeading/body to template:coffee:<Family> so
|
|
||||||
* --font-heading/--font-body resolve to Fraunces/Work Sans; Caveat backs the
|
|
||||||
* hand-accent fallback stack. */
|
|
||||||
@font-face { font-family: 'Fraunces'; src: url('/templates/coffee/fonts/fraunces-latin-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'Fraunces'; src: url('/templates/coffee/fonts/fraunces-latin-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'Work Sans'; src: url('/templates/coffee/fonts/worksans-latin-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'Work Sans'; src: url('/templates/coffee/fonts/worksans-latin-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'Caveat'; src: url('/templates/coffee/fonts/caveat-latin-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'Caveat'; src: url('/templates/coffee/fonts/caveat-latin-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
|
||||||
--coffee-body-fallback: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
||||||
--coffee-hand-fallback: "Caveat", "Segoe Print", "Bradley Hand", cursive;
|
|
||||||
--coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Paper grain background --------------------------------------------- */
|
|
||||||
body.coffee-paper {
|
|
||||||
background-color: hsl(var(--background));
|
|
||||||
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='paper-grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.18 0 0 0 0 0.13 0 0 0 0 0.09 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23paper-grain)'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Type roles --------------------------------------------------------- */
|
|
||||||
.coffee-display {
|
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
|
||||||
font-feature-settings: "liga" 1, "dlig" 1;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
.coffee-body {
|
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
.coffee-mono {
|
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
|
||||||
}
|
|
||||||
/* Hand-drawn accents (badges, stamps, captions). Routed through --font-heading
|
|
||||||
* with a Caveat fallback: unassigned it renders Caveat; if the admin sets a
|
|
||||||
* heading font the accents follow it (the 3-slot model working as intended). */
|
|
||||||
.coffee-hand {
|
|
||||||
font-family: var(--font-heading, var(--coffee-hand-fallback));
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
line-height: 1.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Doodle underline --------------------------------------------------- */
|
|
||||||
.coffee-doodle-underline {
|
|
||||||
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 20 2 40 7 T 80 6 T 118 7' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' opacity='0.55'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 0 100%;
|
|
||||||
background-size: 100% 0.5em;
|
|
||||||
padding-bottom: 0.18em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Drop-cap ----------------------------------------------------------- */
|
|
||||||
.coffee-dropcap > p:first-of-type::first-letter {
|
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
|
||||||
font-size: 4em;
|
|
||||||
line-height: 0.85;
|
|
||||||
float: left;
|
|
||||||
padding: 0.05em 0.12em 0 0;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Kraft-tag button (stamp press on hover) ---------------------------- */
|
|
||||||
.kraft-tag {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.55rem 1.1rem;
|
|
||||||
background-color: hsl(var(--primary));
|
|
||||||
color: hsl(var(--primary-foreground));
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
box-shadow: 0 2px 0 hsl(var(--border));
|
|
||||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.kraft-tag:hover {
|
|
||||||
transform: translateY(1px) rotate(-1.2deg);
|
|
||||||
box-shadow: 0 1px 0 hsl(var(--border));
|
|
||||||
}
|
|
||||||
.kraft-tag:focus-visible {
|
|
||||||
outline: 2px solid hsl(var(--ring));
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
.kraft-tag--ghost {
|
|
||||||
background-color: transparent;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
box-shadow: none;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
.kraft-tag--ghost:hover {
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Torn-paper edges ---------------------------------------------------
|
|
||||||
* The deckled edge is a FIXED ~13px band at the named edge; the rest of the
|
|
||||||
* element is masked solid. (The old `mask-size:100% 100%` stretched the 12px
|
|
||||||
* torn SVG across the whole element, eating ~40% of tall cards as a transparent
|
|
||||||
* wavy cut-out — the source of the hero/featured-pour/footer clipping.) */
|
|
||||||
.coffee-torn-top {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black);
|
|
||||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black);
|
|
||||||
-webkit-mask-position: top, bottom;
|
|
||||||
mask-position: top, bottom;
|
|
||||||
-webkit-mask-size: 100% 13px, 100% calc(100% - 12px);
|
|
||||||
mask-size: 100% 13px, 100% calc(100% - 12px);
|
|
||||||
-webkit-mask-repeat: no-repeat, no-repeat;
|
|
||||||
mask-repeat: no-repeat, no-repeat;
|
|
||||||
}
|
|
||||||
.coffee-torn-bottom {
|
|
||||||
-webkit-mask-image: linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
mask-image: linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
-webkit-mask-position: top, bottom;
|
|
||||||
mask-position: top, bottom;
|
|
||||||
-webkit-mask-size: 100% calc(100% - 12px), 100% 13px;
|
|
||||||
mask-size: 100% calc(100% - 12px), 100% 13px;
|
|
||||||
-webkit-mask-repeat: no-repeat, no-repeat;
|
|
||||||
mask-repeat: no-repeat, no-repeat;
|
|
||||||
}
|
|
||||||
.coffee-torn-top.coffee-torn-bottom {
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"), linear-gradient(black, black), url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E");
|
|
||||||
-webkit-mask-position: top, center, bottom;
|
|
||||||
mask-position: top, center, bottom;
|
|
||||||
-webkit-mask-size: 100% 13px, 100% calc(100% - 24px), 100% 13px;
|
|
||||||
mask-size: 100% 13px, 100% calc(100% - 24px), 100% 13px;
|
|
||||||
-webkit-mask-repeat: no-repeat, no-repeat, no-repeat;
|
|
||||||
mask-repeat: no-repeat, no-repeat, no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Surfaces ----------------------------------------------------------- */
|
|
||||||
.coffee-card {
|
|
||||||
background-color: hsl(var(--card));
|
|
||||||
color: hsl(var(--card-foreground));
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
.coffee-frame {
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
background-color: hsl(var(--card));
|
|
||||||
}
|
|
||||||
.coffee-pencil-rule {
|
|
||||||
border-color: hsl(var(--border));
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Chalkboard menu panel (inverts foreground/background for a hung board;
|
|
||||||
* light mode = dark board + cream chalk, dark mode = light menu paper) --- */
|
|
||||||
.coffee-chalkboard {
|
|
||||||
position: relative;
|
|
||||||
background-color: hsl(var(--foreground));
|
|
||||||
color: hsl(var(--background));
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: inset 0 0 0 3px hsl(var(--background) / 0.12), inset 0 0 0 4px hsl(var(--foreground));
|
|
||||||
}
|
|
||||||
.coffee-chalk-rule {
|
|
||||||
border-color: hsl(var(--background) / 0.28);
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
.coffee-chalk-muted {
|
|
||||||
color: hsl(var(--background) / 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Hours: today highlight ---------------------------------------------
|
|
||||||
* Applied to the whole <tr> so both cells share one readable band. (Was on the
|
|
||||||
* <th> only with accent-foreground text over a 15%-accent tint — light-on-light,
|
|
||||||
* unreadable.) accent text over a 12% tint of the same accent reads cleanly. */
|
|
||||||
.coffee-hours-today > th,
|
|
||||||
.coffee-hours-today > td {
|
|
||||||
background-color: hsl(var(--accent) / 0.12);
|
|
||||||
}
|
|
||||||
.coffee-hours-today > th {
|
|
||||||
box-shadow: inset 3px 0 0 hsl(var(--accent));
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Layout helpers ------------------------------------------------------
|
|
||||||
* coffee-pour-grid keeps an explicit md 2fr:3fr split — a non-standard grid
|
|
||||||
* ratio with no plain Tailwind utility. Single-column base collapses on
|
|
||||||
* mobile. (The min-height + standard-column grid helpers that used to live
|
|
||||||
* here are now plain Tailwind utilities in the templates, since the host
|
|
||||||
* build scans theme .ninjatpl files.) */
|
|
||||||
.coffee-pour-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.coffee-pour-grid { grid-template-columns: 2fr 3fr; align-items: center; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Price + spec chips ------------------------------------------------- */
|
|
||||||
.coffee-price {
|
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
}
|
|
||||||
.coffee-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.3rem 0.7rem;
|
|
||||||
border: 1px dashed hsl(var(--border));
|
|
||||||
border-radius: 9999px;
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Circular stamp / badge (press on hover) ---------------------------- */
|
|
||||||
.coffee-stamp {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 4.25rem;
|
|
||||||
height: 4.25rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background-color: hsl(var(--accent));
|
|
||||||
color: hsl(var(--accent-foreground));
|
|
||||||
border: 2px solid hsl(var(--accent-foreground) / 0.35);
|
|
||||||
box-shadow: inset 0 0 0 3px hsl(var(--accent));
|
|
||||||
font-family: var(--font-heading, var(--coffee-hand-fallback));
|
|
||||||
font-size: 0.95rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.05;
|
|
||||||
transform: rotate(-6deg);
|
|
||||||
transition: transform 140ms ease;
|
|
||||||
}
|
|
||||||
.coffee-stamp:hover {
|
|
||||||
transform: rotate(-6deg) scale(0.94);
|
|
||||||
}
|
|
||||||
.coffee-stamp-sm {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
transform: rotate(-4deg);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Loyalty punch stamps ----------------------------------------------- */
|
|
||||||
.coffee-punch {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 2px dashed hsl(var(--border));
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
}
|
|
||||||
.coffee-punch-on {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: hsl(var(--accent));
|
|
||||||
background-color: hsl(var(--accent) / 0.15);
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
transform: rotate(-5deg);
|
|
||||||
}
|
|
||||||
.coffee-punch-reward {
|
|
||||||
border-color: hsl(var(--primary));
|
|
||||||
background-color: hsl(var(--primary) / 0.12);
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Location pin ------------------------------------------------------- */
|
|
||||||
.coffee-pin {
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Doodle divider draw-in --------------------------------------------- */
|
|
||||||
.coffee-doodle-draw path,
|
|
||||||
.coffee-doodle-draw ellipse,
|
|
||||||
.coffee-doodle-draw circle {
|
|
||||||
stroke-dasharray: 240;
|
|
||||||
stroke-dashoffset: 240;
|
|
||||||
animation: coffee-draw 1.4s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes coffee-draw {
|
|
||||||
to { stroke-dashoffset: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Motion safety: static fallback ------------------------------------- */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.kraft-tag,
|
|
||||||
.coffee-stamp,
|
|
||||||
.coffee-punch-on { transition: none; }
|
|
||||||
.kraft-tag:hover,
|
|
||||||
.coffee-stamp:hover { transform: none; }
|
|
||||||
.coffee-doodle-draw path,
|
|
||||||
.coffee-doodle-draw ellipse,
|
|
||||||
.coffee-doodle-draw circle {
|
|
||||||
stroke-dasharray: none;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
95
master_pages.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "git.dev.alexdunmow.com/block/core/plugin"
|
||||||
|
|
||||||
|
// DefaultMasterPages returns the default master pages the Coffee theme seeds.
|
||||||
|
// Spec §7. Two masters: default-master (default + article templates) and
|
||||||
|
// landing-master (landing template only).
|
||||||
|
func DefaultMasterPages() []plugin.MasterPageDefinition {
|
||||||
|
return []plugin.MasterPageDefinition{
|
||||||
|
{
|
||||||
|
Key: "coffee:default-master",
|
||||||
|
Title: "Coffee Default Master",
|
||||||
|
PageTemplates: []string{"default", "article"},
|
||||||
|
Blocks: []plugin.MasterPageBlock{
|
||||||
|
{
|
||||||
|
BlockKey: "navbar",
|
||||||
|
Title: "Main Navigation",
|
||||||
|
Content: map[string]any{"menuName": "main"},
|
||||||
|
Slot: "header",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "coffee:hours_strip",
|
||||||
|
Title: "Hours Strip",
|
||||||
|
Content: map[string]any{"todayLabel": "Today"},
|
||||||
|
Slot: "header",
|
||||||
|
SortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "slot",
|
||||||
|
Title: "Main Content",
|
||||||
|
Content: map[string]any{"slotName": "main", "placeholder": "Pour something in here"},
|
||||||
|
Slot: "main",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "coffee:footer",
|
||||||
|
Title: "Site Footer",
|
||||||
|
Content: map[string]any{"showLocation": true},
|
||||||
|
Slot: "footer",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "coffee:landing-master",
|
||||||
|
Title: "Coffee Landing Master",
|
||||||
|
PageTemplates: []string{"landing"},
|
||||||
|
Blocks: []plugin.MasterPageBlock{
|
||||||
|
{
|
||||||
|
BlockKey: "navbar",
|
||||||
|
Title: "Main Navigation",
|
||||||
|
Content: map[string]any{"menuName": "main"},
|
||||||
|
Slot: "header",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "slot",
|
||||||
|
Title: "Hero Slot",
|
||||||
|
Content: map[string]any{"slotName": "hero", "placeholder": "Drop a hero block"},
|
||||||
|
Slot: "hero",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "coffee:featured_pour",
|
||||||
|
Title: "Featured Pour",
|
||||||
|
Content: map[string]any{},
|
||||||
|
Slot: "menu",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "slot",
|
||||||
|
Title: "Story Slot",
|
||||||
|
Content: map[string]any{"slotName": "story", "placeholder": "Tell the story"},
|
||||||
|
Slot: "story",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "coffee:location_card",
|
||||||
|
Title: "Location Card",
|
||||||
|
Content: map[string]any{},
|
||||||
|
Slot: "cta",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlockKey: "coffee:footer",
|
||||||
|
Title: "Site Footer",
|
||||||
|
Content: map[string]any{"showLocation": true},
|
||||||
|
Slot: "footer",
|
||||||
|
SortOrder: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,45 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"key": "coffee:default-master",
|
|
||||||
"title": "Coffee Default Master",
|
|
||||||
"page_templates": ["default", "article", "full-width", "contact"],
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "header", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Pour something in here" }, "slot": "main", "sort_order": 0 },
|
|
||||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily." }, "slot": "footer", "sort_order": 0 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "coffee:landing-master",
|
|
||||||
"title": "Coffee Landing Master",
|
|
||||||
"page_templates": ["landing"],
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "hero", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Hero", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "slot": "hero", "sort_order": 1 },
|
|
||||||
{ "block_key": "slot", "title": "Menu", "content": { "slotName": "menu", "placeholder": "Feature the menu" }, "slot": "menu", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Story", "content": { "slotName": "story", "placeholder": "Tell the story" }, "slot": "story", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Call To Action", "content": { "slotName": "cta", "placeholder": "Invite them in" }, "slot": "cta", "sort_order": 0 },
|
|
||||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily." }, "slot": "footer", "sort_order": 0 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "coffee:blog-master",
|
|
||||||
"title": "Coffee Blog Master",
|
|
||||||
"page_templates": ["blog-index"],
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "header", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Featured", "content": { "slotName": "featured", "placeholder": "Featured posts" }, "slot": "featured", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Posts", "content": { "slotName": "main", "placeholder": "Post grid" }, "slot": "main", "sort_order": 0 },
|
|
||||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily." }, "slot": "footer", "sort_order": 0 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "coffee:auth-master",
|
|
||||||
"title": "Coffee Auth Master",
|
|
||||||
"page_templates": ["auth"],
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "slot", "title": "Auth Form", "content": { "slotName": "main", "placeholder": "Auth form" }, "slot": "main", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Footer Note", "content": { "slotName": "footer", "placeholder": "Small print" }, "slot": "footer", "sort_order": 0 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
82
menu_board.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MenuBoardBlockMeta defines metadata for the menu_board block.
|
||||||
|
var MenuBoardBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "menu_board",
|
||||||
|
Title: "Menu Board",
|
||||||
|
Description: "Kraft-paper menu with sections of items (espresso, filter, pastry, ...)",
|
||||||
|
Source: "coffee",
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuBoardBlock renders a sectioned menu card.
|
||||||
|
// Content shape:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "title": "Menu",
|
||||||
|
// "sections": [
|
||||||
|
// {"name": "Espresso", "items": [
|
||||||
|
// {"name": "Flat White", "price": "5.50", "note": "...", "allergens": "..."}
|
||||||
|
// ]}
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
func MenuBoardBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
title := getString(content, "title")
|
||||||
|
if title == "" {
|
||||||
|
title = "Menu"
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSections := getSlice(content, "sections")
|
||||||
|
var sections []MenuSection
|
||||||
|
for _, s := range rawSections {
|
||||||
|
rawItems := getSlice(s, "items")
|
||||||
|
var items []MenuItem
|
||||||
|
for _, it := range rawItems {
|
||||||
|
items = append(items, MenuItem{
|
||||||
|
Name: getString(it, "name"),
|
||||||
|
Price: getString(it, "price"),
|
||||||
|
Note: getString(it, "note"),
|
||||||
|
Allergens: getString(it, "allergens"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sections = append(sections, MenuSection{
|
||||||
|
Name: getString(s, "name"),
|
||||||
|
Items: items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data := MenuBoardData{
|
||||||
|
Title: title,
|
||||||
|
Sections: sections,
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = menuBoardComponent(data).Render(ctx, &buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuBoardData contains data for the menu board component.
|
||||||
|
type MenuBoardData struct {
|
||||||
|
Title string
|
||||||
|
Sections []MenuSection
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuSection groups a list of items under a heading.
|
||||||
|
type MenuSection struct {
|
||||||
|
Name string
|
||||||
|
Items []MenuItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuItem represents a single menu line.
|
||||||
|
type MenuItem struct {
|
||||||
|
Name string
|
||||||
|
Price string
|
||||||
|
Note string
|
||||||
|
Allergens string
|
||||||
|
}
|
||||||
54
menu_board.templ
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// menuBoardComponent renders a Coffee-styled menu board.
|
||||||
|
templ menuBoardComponent(data MenuBoardData) {
|
||||||
|
<section data-block="coffee:menu_board" class="my-10">
|
||||||
|
<div class="coffee-card max-w-3xl mx-auto px-6 py-8">
|
||||||
|
if data.Title != "" {
|
||||||
|
<h2 class="coffee-display text-3xl mb-6 text-center">
|
||||||
|
<span class="coffee-doodle-underline">{ data.Title }</span>
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
if len(data.Sections) == 0 {
|
||||||
|
<p class="coffee-body text-sm text-muted-foreground text-center italic">Add a section to get started.</p>
|
||||||
|
}
|
||||||
|
for sIdx, section := range data.Sections {
|
||||||
|
<div class={ menuSectionClass(sIdx) }>
|
||||||
|
if section.Name != "" {
|
||||||
|
<h3 class="coffee-display text-xl mb-3 text-primary">{ section.Name }</h3>
|
||||||
|
}
|
||||||
|
if len(section.Items) == 0 {
|
||||||
|
<p class="coffee-body text-sm text-muted-foreground italic">No items yet.</p>
|
||||||
|
}
|
||||||
|
<ul class="space-y-2">
|
||||||
|
for _, item := range section.Items {
|
||||||
|
<li class="flex items-baseline gap-3 py-1 border-b coffee-pencil-rule border-dashed">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="coffee-body font-medium text-foreground">{ item.Name }</div>
|
||||||
|
if item.Note != "" {
|
||||||
|
<div class="coffee-body text-sm text-muted-foreground">{ item.Note }</div>
|
||||||
|
}
|
||||||
|
if item.Allergens != "" {
|
||||||
|
<div class="coffee-body text-xs text-muted-foreground italic">Allergens: { item.Allergens }</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
if item.Price != "" {
|
||||||
|
<div class="coffee-price price text-base whitespace-nowrap">{ item.Price }</div>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
// menuSectionClass returns the CSS class for a section, leaving the first
|
||||||
|
// section without a top margin and adding spacing between later sections.
|
||||||
|
func menuSectionClass(idx int) string {
|
||||||
|
if idx == 0 {
|
||||||
|
return "mb-6"
|
||||||
|
}
|
||||||
|
return "mt-8 mb-6"
|
||||||
|
}
|
||||||
220
menu_board_templ.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// menuBoardComponent renders a Coffee-styled menu board.
|
||||||
|
func menuBoardComponent(data MenuBoardData) 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=\"coffee:menu_board\" class=\"my-10\"><div class=\"coffee-card max-w-3xl mx-auto px-6 py-8\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if data.Title != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"coffee-display text-3xl mb-6 text-center\"><span class=\"coffee-doodle-underline\">")
|
||||||
|
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_board.templ`, Line: 9, Col: 55}
|
||||||
|
}
|
||||||
|
_, 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, "</span></h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(data.Sections) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"coffee-body text-sm text-muted-foreground text-center italic\">Add a section to get started.</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for sIdx, section := range data.Sections {
|
||||||
|
var templ_7745c5c3_Var3 = []any{menuSectionClass(sIdx)}
|
||||||
|
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, 5, "<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: `menu_board.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, 6, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if section.Name != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"coffee-display text-xl mb-3 text-primary\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu_board.templ`, Line: 18, Col: 73}
|
||||||
|
}
|
||||||
|
_, 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, 8, "</h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(section.Items) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"coffee-body text-sm text-muted-foreground italic\">No items yet.</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<ul class=\"space-y-2\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, item := range section.Items {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<li class=\"flex items-baseline gap-3 py-1 border-b coffee-pencil-rule border-dashed\"><div class=\"flex-1 min-w-0\"><div class=\"coffee-body font-medium text-foreground\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu_board.templ`, Line: 27, Col: 73}
|
||||||
|
}
|
||||||
|
_, 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, 12, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if item.Note != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"coffee-body text-sm text-muted-foreground\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Note)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu_board.templ`, Line: 29, Col: 76}
|
||||||
|
}
|
||||||
|
_, 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, 14, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item.Allergens != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"coffee-body text-xs text-muted-foreground italic\">Allergens: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Allergens)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu_board.templ`, Line: 32, Col: 99}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if item.Price != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"coffee-price price text-base whitespace-nowrap\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Price)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `menu_board.templ`, Line: 36, Col: 81}
|
||||||
|
}
|
||||||
|
_, 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, 19, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</ul></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// menuSectionClass returns the CSS class for a section, leaving the first
|
||||||
|
// section without a top margin and adding spacing between later sections.
|
||||||
|
func menuSectionClass(idx int) string {
|
||||||
|
if idx == 0 {
|
||||||
|
return "mb-6"
|
||||||
|
}
|
||||||
|
return "mt-8 mb-6"
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@ -2,8 +2,11 @@
|
|||||||
name = "coffee"
|
name = "coffee"
|
||||||
display_name = "Coffee"
|
display_name = "Coffee"
|
||||||
scope = "@themes"
|
scope = "@themes"
|
||||||
version = "0.4.0"
|
version = "0.1.1"
|
||||||
description = "Warm, hand-crafted theme for cafes, bakeries, and slow-craft makers — kraft paper, doodle illustrations, and roasted browns."
|
description = "Warm, hand-crafted theme for cafes, bakeries, and slow-craft makers — kraft paper, doodle illustrations, and roasted browns."
|
||||||
kind = "theme"
|
kind = "theme"
|
||||||
categories = ["templates"]
|
categories = ["templates"]
|
||||||
tags = ["warm", "hospitality", "cafe", "bakery", "food", "handcraft", "organic", "artisan", "menu"]
|
tags = ["warm", "hospitality", "cafe", "bakery", "food", "handcraft", "organic", "artisan", "menu"]
|
||||||
|
|
||||||
|
[compatibility]
|
||||||
|
block_core = ">=0.11.0 <0.12.0"
|
||||||
|
|||||||
132
presets.json
@ -2,13 +2,9 @@
|
|||||||
{
|
{
|
||||||
"id": "morning-pour",
|
"id": "morning-pour",
|
||||||
"name": "Morning Pour",
|
"name": "Morning Pour",
|
||||||
"description": "Cream paper and espresso ink with a terracotta accent. Daylight cafe.",
|
"description": "Cream paper, espresso ink, terracotta accent",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "light",
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:coffee:Fraunces",
|
|
||||||
"fontBody": "template:coffee:Work Sans"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "36 30% 96%",
|
"background": "36 30% 96%",
|
||||||
"foreground": "25 35% 18%",
|
"foreground": "25 35% 18%",
|
||||||
@ -56,13 +52,9 @@
|
|||||||
{
|
{
|
||||||
"id": "dark-roast",
|
"id": "dark-roast",
|
||||||
"name": "Dark Roast",
|
"name": "Dark Roast",
|
||||||
"description": "Espresso-deep base with a copper accent. Evening roastery.",
|
"description": "Espresso-deep base with copper accent for evening cafes and roasteries",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "dark",
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:coffee:Fraunces",
|
|
||||||
"fontBody": "template:coffee:Work Sans"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "36 30% 96%",
|
"background": "36 30% 96%",
|
||||||
"foreground": "25 35% 18%",
|
"foreground": "25 35% 18%",
|
||||||
@ -110,13 +102,9 @@
|
|||||||
{
|
{
|
||||||
"id": "kraft-cream",
|
"id": "kraft-cream",
|
||||||
"name": "Kraft Cream",
|
"name": "Kraft Cream",
|
||||||
"description": "Warmer ivory paper with a deeper terracotta accent. Bakery counter.",
|
"description": "Warmer ivory paper with deeper terracotta accent",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "light",
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:coffee:Fraunces",
|
|
||||||
"fontBody": "template:coffee:Work Sans"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "32 35% 92%",
|
"background": "32 35% 92%",
|
||||||
"foreground": "22 40% 16%",
|
"foreground": "22 40% 16%",
|
||||||
@ -160,113 +148,5 @@
|
|||||||
"ring": "16 50% 55%"
|
"ring": "16 50% 55%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "chalkboard",
|
|
||||||
"name": "Chalkboard",
|
|
||||||
"description": "Slate-black menu board with chalk-white lettering and a chalk-cream accent.",
|
|
||||||
"theme": {
|
|
||||||
"mode": "both",
|
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:coffee:Fraunces",
|
|
||||||
"fontBody": "template:coffee:Work Sans"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "40 20% 94%",
|
|
||||||
"foreground": "200 12% 16%",
|
|
||||||
"card": "40 18% 90%",
|
|
||||||
"cardForeground": "200 12% 16%",
|
|
||||||
"popover": "40 18% 90%",
|
|
||||||
"popoverForeground": "200 12% 16%",
|
|
||||||
"primary": "195 22% 26%",
|
|
||||||
"primaryForeground": "40 30% 96%",
|
|
||||||
"secondary": "40 16% 86%",
|
|
||||||
"secondaryForeground": "200 12% 16%",
|
|
||||||
"muted": "40 14% 88%",
|
|
||||||
"mutedForeground": "200 10% 34%",
|
|
||||||
"accent": "28 55% 48%",
|
|
||||||
"accentForeground": "40 30% 96%",
|
|
||||||
"destructive": "0 68% 46%",
|
|
||||||
"destructiveForeground": "40 30% 96%",
|
|
||||||
"border": "200 10% 78%",
|
|
||||||
"input": "200 10% 78%",
|
|
||||||
"ring": "195 22% 26%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
|
||||||
"background": "195 18% 12%",
|
|
||||||
"foreground": "40 34% 92%",
|
|
||||||
"card": "195 16% 15%",
|
|
||||||
"cardForeground": "40 34% 92%",
|
|
||||||
"popover": "195 16% 15%",
|
|
||||||
"popoverForeground": "40 34% 92%",
|
|
||||||
"primary": "40 34% 90%",
|
|
||||||
"primaryForeground": "195 18% 12%",
|
|
||||||
"secondary": "195 14% 20%",
|
|
||||||
"secondaryForeground": "40 34% 92%",
|
|
||||||
"muted": "195 12% 18%",
|
|
||||||
"mutedForeground": "40 18% 72%",
|
|
||||||
"accent": "30 70% 62%",
|
|
||||||
"accentForeground": "195 18% 12%",
|
|
||||||
"destructive": "0 62% 55%",
|
|
||||||
"destructiveForeground": "40 34% 92%",
|
|
||||||
"border": "195 12% 24%",
|
|
||||||
"input": "195 12% 22%",
|
|
||||||
"ring": "40 34% 90%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "matcha-oat",
|
|
||||||
"name": "Matcha and Oat",
|
|
||||||
"description": "Soft sage green and oat cream for tea rooms and matcha bars.",
|
|
||||||
"theme": {
|
|
||||||
"mode": "both",
|
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:coffee:Fraunces",
|
|
||||||
"fontBody": "template:coffee:Work Sans"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "44 32% 94%",
|
|
||||||
"foreground": "120 15% 18%",
|
|
||||||
"card": "44 28% 91%",
|
|
||||||
"cardForeground": "120 15% 18%",
|
|
||||||
"popover": "44 28% 91%",
|
|
||||||
"popoverForeground": "120 15% 18%",
|
|
||||||
"primary": "132 26% 32%",
|
|
||||||
"primaryForeground": "44 32% 94%",
|
|
||||||
"secondary": "80 18% 84%",
|
|
||||||
"secondaryForeground": "120 15% 18%",
|
|
||||||
"muted": "80 16% 87%",
|
|
||||||
"mutedForeground": "120 12% 34%",
|
|
||||||
"accent": "36 60% 48%",
|
|
||||||
"accentForeground": "44 32% 94%",
|
|
||||||
"destructive": "0 68% 46%",
|
|
||||||
"destructiveForeground": "44 32% 94%",
|
|
||||||
"border": "90 14% 76%",
|
|
||||||
"input": "90 14% 76%",
|
|
||||||
"ring": "132 26% 32%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
|
||||||
"background": "130 16% 11%",
|
|
||||||
"foreground": "44 30% 91%",
|
|
||||||
"card": "130 14% 14%",
|
|
||||||
"cardForeground": "44 30% 91%",
|
|
||||||
"popover": "130 14% 14%",
|
|
||||||
"popoverForeground": "44 30% 91%",
|
|
||||||
"primary": "128 34% 60%",
|
|
||||||
"primaryForeground": "130 16% 11%",
|
|
||||||
"secondary": "130 12% 20%",
|
|
||||||
"secondaryForeground": "44 30% 91%",
|
|
||||||
"muted": "130 10% 18%",
|
|
||||||
"mutedForeground": "80 14% 68%",
|
|
||||||
"accent": "38 72% 60%",
|
|
||||||
"accentForeground": "130 16% 11%",
|
|
||||||
"destructive": "0 60% 55%",
|
|
||||||
"destructiveForeground": "44 30% 91%",
|
|
||||||
"border": "130 10% 24%",
|
|
||||||
"input": "130 10% 22%",
|
|
||||||
"ring": "128 34% 60%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
preview.png
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 328 KiB |
89
register.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
"git.dev.alexdunmow.com/block/core/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrap adapts a templ-returning render function to 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 is the plugin entry point that wires Coffee's system template,
|
||||||
|
// page templates, blocks, overrides, and email wrapper.
|
||||||
|
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||||
|
// 1. System template ----------------------------------------------------
|
||||||
|
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
|
||||||
|
Key: "coffee",
|
||||||
|
Title: "Coffee",
|
||||||
|
Description: "Warm, hand-crafted theme for cafes, bakeries, and slow-craft makers",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. Page templates -----------------------------------------------------
|
||||||
|
if err := tr.RegisterPageTemplate("coffee", templates.PageTemplateMeta{
|
||||||
|
Key: "default",
|
||||||
|
Title: "Default",
|
||||||
|
Description: "Header + main + footer with warm paper background",
|
||||||
|
Slots: []string{"header", "main", "footer"},
|
||||||
|
}, wrap(RenderCoffee)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tr.RegisterPageTemplate("coffee", templates.PageTemplateMeta{
|
||||||
|
Key: "landing",
|
||||||
|
Title: "Landing",
|
||||||
|
Description: "Hero pour + featured menu, big imagery",
|
||||||
|
Slots: []string{"hero", "menu", "story", "cta", "footer"},
|
||||||
|
}, wrap(RenderCoffeeLanding)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tr.RegisterPageTemplate("coffee", templates.PageTemplateMeta{
|
||||||
|
Key: "article",
|
||||||
|
Title: "Article",
|
||||||
|
Description: "Narrow narrative column for journal / recipes",
|
||||||
|
Slots: []string{"header", "main", "footer"},
|
||||||
|
}, wrap(RenderCoffeeArticle)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tr.RegisterPageTemplate("coffee", templates.PageTemplateMeta{
|
||||||
|
Key: "full-width",
|
||||||
|
Title: "Full Width",
|
||||||
|
Description: "Edge-to-edge gallery / interior shots",
|
||||||
|
Slots: []string{"header", "main", "footer"},
|
||||||
|
}, wrap(RenderCoffeeFullWidth)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Schemas — MUST be loaded BEFORE br.Register --------------------------
|
||||||
|
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Theme-specific blocks (registered unqualified; addressed as coffee:<key>)
|
||||||
|
br.Register(MenuBoardBlockMeta, MenuBoardBlock)
|
||||||
|
br.Register(HoursStripBlockMeta, HoursStripBlock)
|
||||||
|
br.Register(LocationCardBlockMeta, LocationCardBlock)
|
||||||
|
br.Register(FeaturedPourBlockMeta, FeaturedPourBlock)
|
||||||
|
br.Register(FooterBlockMeta, FooterBlock)
|
||||||
|
br.Register(DoodleDividerBlockMeta, DoodleDividerBlock)
|
||||||
|
|
||||||
|
// 5. Built-in block overrides — only active when Coffee is the system template
|
||||||
|
br.RegisterTemplateOverride("coffee", "heading", CoffeeHeadingBlock)
|
||||||
|
br.RegisterTemplateOverride("coffee", "text", CoffeeTextBlock)
|
||||||
|
br.RegisterTemplateOverride("coffee", "button", CoffeeButtonBlock)
|
||||||
|
br.RegisterTemplateOverride("coffee", "image", CoffeeImageBlock)
|
||||||
|
|
||||||
|
// 6. Email wrapper ------------------------------------------------------
|
||||||
|
tr.RegisterEmailWrapper("coffee", CoffeeEmailWrapper)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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 Coffee theme.
|
||||||
|
var Registration = plugin.PluginRegistration{
|
||||||
|
Name: "coffee",
|
||||||
|
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() },
|
||||||
|
}
|
||||||
16
schemas/doodle_divider.schema.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Doodle Divider",
|
||||||
|
"description": "Inline SVG divider in one of four hand-drawn motifs",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"motif": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Motif",
|
||||||
|
"description": "Which doodle to render",
|
||||||
|
"default": "beans",
|
||||||
|
"x-editor": "select",
|
||||||
|
"enum": ["beans", "croissant", "cup", "leaf"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,19 +4,6 @@
|
|||||||
"description": "Hero card for a featured coffee, tea, or pastry with image, tasting notes and price",
|
"description": "Hero card for a featured coffee, tea, or pastry with image, tasting notes and price",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"label": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Badge Label",
|
|
||||||
"description": "Hand-drawn badge text",
|
|
||||||
"default": "Featured pour",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"origin": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Origin / Kicker",
|
|
||||||
"description": "Small hand-drawn line above the name (e.g. \"Single origin\")",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Name",
|
"title": "Name",
|
||||||
@ -35,12 +22,6 @@
|
|||||||
"description": "Hero image",
|
"description": "Hero image",
|
||||||
"x-editor": "media"
|
"x-editor": "media"
|
||||||
},
|
},
|
||||||
"currency": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Currency Symbol",
|
|
||||||
"default": "$",
|
|
||||||
"x-editor": "text"
|
|
||||||
},
|
|
||||||
"price": {
|
"price": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Price",
|
"title": "Price",
|
||||||
22
schemas/footer.schema.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Coffee Footer",
|
||||||
|
"description": "Torn-edge footer with optional location block and newsletter caption",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"showLocation": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Show Location",
|
||||||
|
"description": "Whether to render the location/hours summary",
|
||||||
|
"default": "true",
|
||||||
|
"x-editor": "select",
|
||||||
|
"enum": ["true", "false"]
|
||||||
|
},
|
||||||
|
"newsletterText": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Newsletter Caption",
|
||||||
|
"description": "Short caption above the newsletter input",
|
||||||
|
"x-editor": "textarea"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
schemas/hours_strip.schema.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Hours Strip",
|
||||||
|
"description": "Horizontal opening hours strip with today's row server-side highlighted",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"todayLabel": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Today Label",
|
||||||
|
"description": "Label rendered next to today's row (e.g. \"Today\")",
|
||||||
|
"default": "Today",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"hours": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Weekly Hours",
|
||||||
|
"description": "One row per day. Days not listed render as closed.",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"day": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Day",
|
||||||
|
"description": "Weekday (Mon, Tue, Wed, Thu, Fri, Sat, Sun)",
|
||||||
|
"x-editor": "select",
|
||||||
|
"enum": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||||
|
},
|
||||||
|
"open": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Opens",
|
||||||
|
"description": "Opening time (e.g. \"7:00\")",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"close": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Closes",
|
||||||
|
"description": "Closing time (e.g. \"15:00\")",
|
||||||
|
"x-editor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["day"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
schemas/location_card.schema.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Location Card",
|
||||||
|
"description": "Address card with optional map image and directions link, doodle-pin overlay",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Address",
|
||||||
|
"description": "Street address as a multi-line block",
|
||||||
|
"x-editor": "textarea"
|
||||||
|
},
|
||||||
|
"mapImage": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Map Image",
|
||||||
|
"description": "Optional static map image",
|
||||||
|
"x-editor": "media"
|
||||||
|
},
|
||||||
|
"directionsUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Directions URL",
|
||||||
|
"description": "Link to Google Maps or similar",
|
||||||
|
"x-editor": "link"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
schemas/menu_board.schema.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Menu Board",
|
||||||
|
"description": "Kraft-paper menu card with sections of items (espresso, filter, pastry, etc.)",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Title",
|
||||||
|
"description": "Heading shown at the top of the menu (e.g. \"Menu\", \"Today's Pour\")",
|
||||||
|
"default": "Menu",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Sections",
|
||||||
|
"description": "Menu sections such as Espresso, Filter, Pastry",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Section Name",
|
||||||
|
"description": "Section heading (e.g. \"Espresso\")",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Items",
|
||||||
|
"description": "Items in this section",
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Name",
|
||||||
|
"description": "Item name (e.g. \"Flat White\")",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Price",
|
||||||
|
"description": "Price (e.g. \"5.50\")",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Note",
|
||||||
|
"description": "Optional descriptive line (e.g. tasting notes)",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"allergens": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Allergens",
|
||||||
|
"description": "Free-text allergen list (e.g. \"contains dairy, gluten\")",
|
||||||
|
"x-editor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 899 KiB |
|
Before Width: | Height: | Size: 808 KiB |
177
seed/seed.json
@ -1,177 +0,0 @@
|
|||||||
{
|
|
||||||
"demo": true,
|
|
||||||
"settings": {
|
|
||||||
"merge": {
|
|
||||||
"site_title": "Ritual Coffee",
|
|
||||||
"site_description": "A neighbourhood cafe and small-batch roastery."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data_tables": [
|
|
||||||
{
|
|
||||||
"key": "contact_submissions",
|
|
||||||
"name": "Contact Submissions",
|
|
||||||
"description": "Messages sent from the cafe contact form.",
|
|
||||||
"primary_key": "id",
|
|
||||||
"columns": [
|
|
||||||
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
|
||||||
{ "key": "email", "label": "Email", "type": "email", "required": true },
|
|
||||||
{ "key": "reason", "label": "Reason", "type": "text", "required": false },
|
|
||||||
{ "key": "message", "label": "Message", "type": "text", "required": true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"menu_items": [
|
|
||||||
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 0 },
|
|
||||||
{ "menu": "main", "label": "Menu", "url": "/#menu", "sort_order": 1 },
|
|
||||||
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
|
|
||||||
{ "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 },
|
|
||||||
{ "menu": "main", "label": "Visit", "page_slug": "/contact", "sort_order": 4 }
|
|
||||||
],
|
|
||||||
"pages": [
|
|
||||||
{
|
|
||||||
"slug": "/",
|
|
||||||
"title": "Ritual Coffee",
|
|
||||||
"template_key": "default",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Open from 7", "headline": "Coffee worth slowing down for", "subheadline": "We roast in small batches and pull every shot to order.", "ctaPrimary": { "text": "See the menu", "url": "/#menu" }, "ctaSecondary": { "text": "Find us", "url": "/contact" }, "minHeight": "50vh" } },
|
|
||||||
{ "block_key": "coffee:featured_pour", "title": "Featured Pour", "slot": "main", "sort_order": 1, "content": { "label": "This week", "origin": "Single origin", "name": "Guji Highlands", "tasting": "<p>Bright and floral. Notes of peach, jasmine and brown sugar.</p>", "price": "6.00", "currency": "$" } },
|
|
||||||
{ "block_key": "menu-list", "title": "Menu", "slot": "main", "sort_order": 2, "content": { "heading": "On the board", "intro": "Prices are for takeaway. Add fifty cents to dine in.", "currency": "$", "columns": 2, "sections": [
|
|
||||||
{ "title": "Espresso", "items": [
|
|
||||||
{ "name": "Espresso", "description": "Double shot", "price": "4.00" },
|
|
||||||
{ "name": "Flat white", "price": "5.00" },
|
|
||||||
{ "name": "Latte", "price": "5.50" },
|
|
||||||
{ "name": "Batch filter", "description": "Rotating single origin", "price": "5.00", "badges": [{ "label": "Filter" }] }
|
|
||||||
] },
|
|
||||||
{ "title": "Kitchen", "items": [
|
|
||||||
{ "name": "Butter croissant", "price": "5.50" },
|
|
||||||
{ "name": "Sourdough toast", "description": "With jam or honey", "price": "6.00" },
|
|
||||||
{ "name": "Bacon and egg roll", "price": "12.00" },
|
|
||||||
{ "name": "Granola bowl", "description": "Oats, yoghurt, seasonal fruit", "price": "11.00", "badges": [{ "label": "V" }] }
|
|
||||||
] }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 3, "content": { "heading": "Come and sit", "address": "12 Baker Lane\nNorthbridge", "phone": "08 1234 5678", "hours": [
|
|
||||||
{ "day": "Monday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Tuesday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Wednesday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Thursday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Friday", "hours": "7:00 - 16:00" },
|
|
||||||
{ "day": "Saturday", "hours": "8:00 - 16:00" },
|
|
||||||
{ "day": "Sunday", "closed": true }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "address": "12 Baker Lane, Northbridge", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/about",
|
|
||||||
"title": "About",
|
|
||||||
"template_key": "default",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Our story", "headline": "One roaster, one room", "subheadline": "We started on a single machine and never grew out of caring about it.", "minHeight": "auto" } },
|
|
||||||
{ "block_key": "rich-section", "title": "Story", "slot": "main", "sort_order": 1, "content": { "eyebrow": "Why we roast", "heading": "Fresh, not fast", "body": "<p>We roast twice a week and rest every batch before it hits the hopper. That means the coffee you drink was roasted days ago, not months.</p><p>Everything on the board is made in-house. The croissants are laminated overnight. The jam is cooked in small pots on the back bench.</p>", "mediaPosition": "right" } },
|
|
||||||
{ "block_key": "coffee:brew_guide", "title": "Brew Guide", "slot": "main", "sort_order": 2, "content": { "kicker": "Make it at home", "method": "V60 pour-over", "intro": "This is how we brew filter on the bar. Scale it up or down by the ratio.", "ratio": "1:16", "grind": "Medium-fine", "water": "300g", "temperature": "94C", "time": "3:00", "steps": [
|
|
||||||
{ "title": "Rinse and dose", "text": "Rinse the paper. Add 19g of coffee and level the bed." },
|
|
||||||
{ "title": "Bloom", "text": "Pour 45g of water. Wait 40 seconds." },
|
|
||||||
{ "title": "Pour in stages", "text": "Pour in slow circles to 300g by two minutes." },
|
|
||||||
{ "title": "Draw down", "text": "Let it finish by three minutes. Swirl and serve." }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "team", "title": "Team", "slot": "main", "sort_order": 3, "content": { "heading": "Behind the bar", "members": [
|
|
||||||
{ "name": "Mara", "role": "Head roaster", "bio": "Runs the roaster and tastes every batch." },
|
|
||||||
{ "name": "Theo", "role": "Baker", "bio": "In before the birds. Owns the croissants." },
|
|
||||||
{ "name": "Priya", "role": "Barista", "bio": "Knows your order before you say it." }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog",
|
|
||||||
"title": "Journal",
|
|
||||||
"template_key": "blog-index",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "blog-hero", "title": "Journal Hero", "slot": "featured", "sort_order": 0, "content": { "title": "The Journal", "subtitle": "Notes from the roaster and the bar." } },
|
|
||||||
{ "block_key": "card", "title": "Post: Guji", "slot": "main", "sort_order": 0, "content": { "title": "Meet the Guji Highlands", "text": "Our new single origin is bright, floral and easy to love.", "link": "/blog/meet-the-guji-highlands", "linkLabel": "Read on" } },
|
|
||||||
{ "block_key": "card", "title": "Post: Roasting week", "slot": "main", "sort_order": 1, "content": { "title": "A week on the roaster", "text": "How we plan a roasting week and why we rest every batch.", "link": "/blog/a-week-on-the-roaster", "linkLabel": "Read on" } },
|
|
||||||
{ "block_key": "card", "title": "Post: Filter", "slot": "main", "sort_order": 2, "content": { "title": "How we brew filter", "text": "The recipe we use on the bar, step by step.", "link": "/blog/how-we-brew-filter", "linkLabel": "Read on" } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog/meet-the-guji-highlands",
|
|
||||||
"parent_slug": "/blog",
|
|
||||||
"title": "Meet the Guji Highlands",
|
|
||||||
"template_key": "article",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "Meet the Guji Highlands" } },
|
|
||||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>Our new single origin comes from the Guji zone in southern Ethiopia. It is washed, which keeps it clean and bright.</p><p>We taste peach and jasmine up front, then a soft brown sugar finish. It works as espresso and shines as filter.</p><p>Come in this week and try it on the bar.</p>" } },
|
|
||||||
{ "block_key": "coffee:roast_profile", "title": "Roast Profile", "slot": "main", "sort_order": 2, "content": { "origin": "Single origin", "name": "Guji Highlands", "producer": "Smallholders, Guji zone", "roastLevel": "Medium", "notes": "<p>Peach, jasmine, brown sugar.</p>", "process": "Washed", "varietal": "Heirloom", "altitude": "1900-2100 masl" } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog/a-week-on-the-roaster",
|
|
||||||
"parent_slug": "/blog",
|
|
||||||
"title": "A week on the roaster",
|
|
||||||
"template_key": "article",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "A week on the roaster" } },
|
|
||||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>We roast on Tuesdays and Fridays. Each batch is small, and we rest it before it goes on the bar.</p><p>Resting lets the coffee settle after roasting. Pulled too soon, it tastes sharp and gassy. Given a few days, it opens up.</p><p>That is why the date on the bag matters more than the date on the door.</p>" } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog/how-we-brew-filter",
|
|
||||||
"parent_slug": "/blog",
|
|
||||||
"title": "How we brew filter",
|
|
||||||
"template_key": "article",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "How we brew filter" } },
|
|
||||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>Filter is the clearest way to taste a coffee. We keep the recipe simple so you can repeat it at home.</p><p>Weigh the coffee and the water. Keep the grind even. Pour in slow circles and give it room to draw down.</p>" } },
|
|
||||||
{ "block_key": "coffee:brew_guide", "title": "Brew Guide", "slot": "main", "sort_order": 2, "content": { "kicker": "The recipe", "method": "V60 pour-over", "ratio": "1:16", "grind": "Medium-fine", "water": "300g", "temperature": "94C", "time": "3:00", "steps": [
|
|
||||||
{ "title": "Rinse and dose", "text": "Rinse the paper. Add 19g and level the bed." },
|
|
||||||
{ "title": "Bloom", "text": "Pour 45g. Wait 40 seconds." },
|
|
||||||
{ "title": "Pour", "text": "Slow circles to 300g by two minutes." },
|
|
||||||
{ "title": "Finish", "text": "Draw down by three minutes. Serve." }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/contact",
|
|
||||||
"title": "Visit",
|
|
||||||
"template_key": "contact",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } },
|
|
||||||
{ "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 0, "content": { "heading": "Find us", "address": "12 Baker Lane\nNorthbridge", "phone": "08 1234 5678", "hours": [
|
|
||||||
{ "day": "Monday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Tuesday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Wednesday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Thursday", "hours": "7:00 - 15:00" },
|
|
||||||
{ "day": "Friday", "hours": "7:00 - 16:00" },
|
|
||||||
{ "day": "Saturday", "hours": "8:00 - 16:00" },
|
|
||||||
{ "day": "Sunday", "closed": true }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "contact-form", "title": "Contact Form", "slot": "main", "sort_order": 1, "content": { "heading": "Say hello", "description": "Booking a table for six or more? Send us a note and we will sort it.", "submitLabel": "Send message", "successMessage": "Thanks. We will get back to you soon.", "formConfig": { "targetTable": "contact_submissions" }, "fields": [
|
|
||||||
{ "name": "name", "label": "Name", "type": "text", "required": true, "width": "half" },
|
|
||||||
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
|
||||||
{ "name": "reason", "label": "Reason", "type": "text", "required": false, "width": "full" },
|
|
||||||
{ "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" }
|
|
||||||
] } },
|
|
||||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/login",
|
|
||||||
"title": "Login",
|
|
||||||
"template_key": "auth",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "auth-form", "title": "Auth Form", "slot": "main", "sort_order": 0, "content": { "default_tab": "login", "show_social": false } },
|
|
||||||
{ "block_key": "text", "title": "Footer Note", "slot": "footer", "sort_order": 0, "content": { "text": "<p>Members get first pick of new roasts.</p>" } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"workflows": [
|
|
||||||
{
|
|
||||||
"key": "notify_admins_on_contact",
|
|
||||||
"name": "Notify admins on contact",
|
|
||||||
"description": "Email site admins when someone submits the cafe contact form.",
|
|
||||||
"enabled": true,
|
|
||||||
"trigger": {
|
|
||||||
"type": "row_inserted",
|
|
||||||
"table": "contact_submissions"
|
|
||||||
},
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"type": "email",
|
|
||||||
"name": "Email admins",
|
|
||||||
"template": "Form Submission Notification",
|
|
||||||
"recipients": {
|
|
||||||
"mode": "admins",
|
|
||||||
"scope": "all"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
257
template.templ
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageData carries everything the Coffee page templates need to render.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCoffeePageData(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 := "light"
|
||||||
|
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||||
|
themeMode = tm
|
||||||
|
}
|
||||||
|
|
||||||
|
siteSettings := bn.ParseSiteSettings(doc)
|
||||||
|
pageMeta := bn.ParsePageMeta(doc)
|
||||||
|
engagementConfig := bn.ParseEngagementConfig(doc)
|
||||||
|
|
||||||
|
return PageData{
|
||||||
|
Title: title,
|
||||||
|
Slots: slots,
|
||||||
|
ThemeMode: themeMode,
|
||||||
|
ThemeCSS: themeCSS,
|
||||||
|
SiteSettings: siteSettings,
|
||||||
|
PageMeta: pageMeta,
|
||||||
|
StructuredData: structuredData,
|
||||||
|
CSSHash: cssHash,
|
||||||
|
PageviewNonce: pageviewNonce,
|
||||||
|
EngagementConfig: engagementConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default page template (header / main / footer)
|
||||||
|
templ Coffee(data PageData) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@bn.Head(bn.HeadData{
|
||||||
|
Title: data.Title,
|
||||||
|
Settings: data.SiteSettings,
|
||||||
|
PageMeta: data.PageMeta,
|
||||||
|
ThemeMode: data.ThemeMode,
|
||||||
|
ThemeCSS: data.ThemeCSS,
|
||||||
|
PluginStyles: []string{"/templates/coffee/style.css"},
|
||||||
|
StructuredData: data.StructuredData,
|
||||||
|
CSSHash: data.CSSHash,
|
||||||
|
PageviewNonce: data.PageviewNonce,
|
||||||
|
EngagementConfig: data.EngagementConfig,
|
||||||
|
})
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
@bn.AdminBypassBanner(data.SiteSettings)
|
||||||
|
<header class="w-full">
|
||||||
|
@templ.Raw(data.Slots["header"])
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow max-w-5xl mx-auto w-full px-4 py-8">
|
||||||
|
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||||
|
@templ.Raw(main)
|
||||||
|
} else {
|
||||||
|
<div class="py-20 text-center coffee-body text-muted-foreground italic">
|
||||||
|
<p>Pour something in here.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">
|
||||||
|
@templ.Raw(data.Slots["footer"])
|
||||||
|
</footer>
|
||||||
|
@bn.BodyEnd(data.SiteSettings)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landing page template with hero / menu / story / cta / footer slots.
|
||||||
|
templ CoffeeLanding(data PageData) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@bn.Head(bn.HeadData{
|
||||||
|
Title: data.Title,
|
||||||
|
Settings: data.SiteSettings,
|
||||||
|
PageMeta: data.PageMeta,
|
||||||
|
ThemeMode: data.ThemeMode,
|
||||||
|
ThemeCSS: data.ThemeCSS,
|
||||||
|
PluginStyles: []string{"/templates/coffee/style.css"},
|
||||||
|
StructuredData: data.StructuredData,
|
||||||
|
CSSHash: data.CSSHash,
|
||||||
|
PageviewNonce: data.PageviewNonce,
|
||||||
|
EngagementConfig: data.EngagementConfig,
|
||||||
|
})
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
@bn.AdminBypassBanner(data.SiteSettings)
|
||||||
|
<section class="w-full">
|
||||||
|
@templ.Raw(data.Slots["hero"])
|
||||||
|
</section>
|
||||||
|
<section class="w-full">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">
|
||||||
|
@templ.Raw(data.Slots["menu"])
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="w-full">
|
||||||
|
<div class="max-w-3xl mx-auto px-4 py-12">
|
||||||
|
@templ.Raw(data.Slots["story"])
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="w-full">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">
|
||||||
|
@templ.Raw(data.Slots["cta"])
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer class="w-full mt-auto">
|
||||||
|
@templ.Raw(data.Slots["footer"])
|
||||||
|
</footer>
|
||||||
|
@bn.BodyEnd(data.SiteSettings)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Article page template — narrow narrative column for journal / recipes.
|
||||||
|
templ CoffeeArticle(data PageData) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@bn.Head(bn.HeadData{
|
||||||
|
Title: data.Title,
|
||||||
|
Settings: data.SiteSettings,
|
||||||
|
PageMeta: data.PageMeta,
|
||||||
|
ThemeMode: data.ThemeMode,
|
||||||
|
ThemeCSS: data.ThemeCSS,
|
||||||
|
PluginStyles: []string{"/templates/coffee/style.css"},
|
||||||
|
StructuredData: data.StructuredData,
|
||||||
|
CSSHash: data.CSSHash,
|
||||||
|
PageviewNonce: data.PageviewNonce,
|
||||||
|
EngagementConfig: data.EngagementConfig,
|
||||||
|
})
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
@bn.AdminBypassBanner(data.SiteSettings)
|
||||||
|
<header class="w-full">
|
||||||
|
<div class="max-w-3xl mx-auto px-4">
|
||||||
|
@templ.Raw(data.Slots["header"])
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow max-w-2xl mx-auto w-full px-4 py-12 coffee-dropcap">
|
||||||
|
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||||
|
<article class="prose prose-lg max-w-none">
|
||||||
|
@templ.Raw(main)
|
||||||
|
</article>
|
||||||
|
} else {
|
||||||
|
<div class="py-20 text-center coffee-body text-muted-foreground italic">
|
||||||
|
<p>Write something honest here.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">
|
||||||
|
@templ.Raw(data.Slots["footer"])
|
||||||
|
</footer>
|
||||||
|
@bn.BodyEnd(data.SiteSettings)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-width page template — edge-to-edge gallery / interior shots.
|
||||||
|
templ CoffeeFullWidth(data PageData) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@bn.Head(bn.HeadData{
|
||||||
|
Title: data.Title,
|
||||||
|
Settings: data.SiteSettings,
|
||||||
|
PageMeta: data.PageMeta,
|
||||||
|
ThemeMode: data.ThemeMode,
|
||||||
|
ThemeCSS: data.ThemeCSS,
|
||||||
|
PluginStyles: []string{"/templates/coffee/style.css"},
|
||||||
|
StructuredData: data.StructuredData,
|
||||||
|
CSSHash: data.CSSHash,
|
||||||
|
PageviewNonce: data.PageviewNonce,
|
||||||
|
EngagementConfig: data.EngagementConfig,
|
||||||
|
})
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
@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="max-w-3xl mx-auto py-20 px-4 text-center coffee-body text-muted-foreground italic">
|
||||||
|
<p>Add a gallery, a wide photo, a confession.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">
|
||||||
|
@templ.Raw(data.Slots["footer"])
|
||||||
|
</footer>
|
||||||
|
@bn.BodyEnd(data.SiteSettings)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffee is the default page renderer.
|
||||||
|
func RenderCoffee(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return Coffee(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffeeLanding renders the landing page template.
|
||||||
|
func RenderCoffeeLanding(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return CoffeeLanding(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffeeArticle renders the article page template.
|
||||||
|
func RenderCoffeeArticle(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return CoffeeArticle(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffeeFullWidth renders the full-width page template.
|
||||||
|
func RenderCoffeeFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return CoffeeFullWidth(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
506
template_templ.go
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
// 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 carries everything the Coffee page templates need to render.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCoffeePageData(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 := "light"
|
||||||
|
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||||
|
themeMode = tm
|
||||||
|
}
|
||||||
|
|
||||||
|
siteSettings := bn.ParseSiteSettings(doc)
|
||||||
|
pageMeta := bn.ParsePageMeta(doc)
|
||||||
|
engagementConfig := bn.ParseEngagementConfig(doc)
|
||||||
|
|
||||||
|
return PageData{
|
||||||
|
Title: title,
|
||||||
|
Slots: slots,
|
||||||
|
ThemeMode: themeMode,
|
||||||
|
ThemeCSS: themeCSS,
|
||||||
|
SiteSettings: siteSettings,
|
||||||
|
PageMeta: pageMeta,
|
||||||
|
StructuredData: structuredData,
|
||||||
|
CSSHash: cssHash,
|
||||||
|
PageviewNonce: pageviewNonce,
|
||||||
|
EngagementConfig: engagementConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default page template (header / main / footer)
|
||||||
|
func Coffee(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><html lang=\"en\">")
|
||||||
|
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/coffee/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, 2, "<body class=\"coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col\">")
|
||||||
|
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, 3, "<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, 4, "</header><main class=\"flex-grow max-w-5xl mx-auto w-full px-4 py-8\">")
|
||||||
|
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, 5, "<div class=\"py-20 text-center coffee-body text-muted-foreground italic\"><p>Pour something in here.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer class=\"w-full mt-auto\">")
|
||||||
|
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, 7, "</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, 8, "</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landing page template with hero / menu / story / cta / footer slots.
|
||||||
|
func CoffeeLanding(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_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\">")
|
||||||
|
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/coffee/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, 10, "<body class=\"coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col\">")
|
||||||
|
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, 11, "<section class=\"w-full\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section><section 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["menu"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></section><section class=\"w-full\"><div class=\"max-w-3xl mx-auto px-4 py-12\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.Raw(data.Slots["story"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></section><section 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["cta"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></section><footer class=\"w-full mt-auto\">")
|
||||||
|
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, 16, "</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, 17, "</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Article page template — narrow narrative column for journal / recipes.
|
||||||
|
func CoffeeArticle(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_Var3 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var3 == nil {
|
||||||
|
templ_7745c5c3_Var3 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<!doctype html><html lang=\"en\">")
|
||||||
|
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/coffee/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, 19, "<body class=\"coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col\">")
|
||||||
|
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, 20, "<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, 21, "</div></header><main class=\"flex-grow max-w-2xl mx-auto w-full px-4 py-12 coffee-dropcap\">")
|
||||||
|
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, 22, "<article class=\"prose prose-lg max-w-none\">")
|
||||||
|
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, 23, "</article>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<div class=\"py-20 text-center coffee-body text-muted-foreground italic\"><p>Write something honest here.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</main><footer class=\"w-full mt-auto\">")
|
||||||
|
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, 26, "</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, 27, "</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-width page template — edge-to-edge gallery / interior shots.
|
||||||
|
func CoffeeFullWidth(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, 28, "<!doctype html><html lang=\"en\">")
|
||||||
|
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/coffee/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, 29, "<body class=\"coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col\">")
|
||||||
|
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, 30, "<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, 31, "</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, 32, "<div class=\"max-w-3xl mx-auto py-20 px-4 text-center coffee-body text-muted-foreground italic\"><p>Add a gallery, a wide photo, a confession.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</main><footer class=\"w-full mt-auto\">")
|
||||||
|
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, "</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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffee is the default page renderer.
|
||||||
|
func RenderCoffee(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return Coffee(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffeeLanding renders the landing page template.
|
||||||
|
func RenderCoffeeLanding(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return CoffeeLanding(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffeeArticle renders the article page template.
|
||||||
|
func RenderCoffeeArticle(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return CoffeeArticle(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCoffeeFullWidth renders the full-width page template.
|
||||||
|
func RenderCoffeeFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
||||||
|
return CoffeeFullWidth(parseCoffeePageData(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
|
||||||
{{ head_html|safe }}
|
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ admin_banner_html|safe }}
|
|
||||||
<header class="w-full">
|
|
||||||
<div class="max-w-3xl mx-auto px-4">{{ slots.header|safe }}</div>
|
|
||||||
</header>
|
|
||||||
<main class="flex-grow max-w-2xl mx-auto w-full px-4 py-12 coffee-dropcap">
|
|
||||||
{% if slots.main %}<article class="prose prose-lg max-w-none">{{ slots.main|safe }}</article>{% else %}<div class="py-20 text-center coffee-body text-muted-foreground italic"><p>Write something honest here.</p></div>{% endif %}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
|
||||||
{{ body_end_html|safe }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
|
||||||
{{ head_html|safe }}
|
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ admin_banner_html|safe }}
|
|
||||||
<main class="flex flex-grow w-full flex-col items-center justify-center px-4 py-12">
|
|
||||||
<div class="w-full max-w-md">
|
|
||||||
<a href="/" class="mb-6 flex items-center justify-center" aria-label="Home">
|
|
||||||
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt|default:site_settings.Title }}" class="h-10 w-auto">{% else %}<span class="coffee-display text-3xl text-primary">{{ site_settings.Title|default:"Coffee" }}</span>{% endif %}
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="coffee-card p-6 text-center coffee-body text-muted-foreground"><p>No form assigned to this page.</p></div>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if slots.footer %}<div class="coffee-body mt-6 text-center text-sm text-muted-foreground">{{ slots.footer|safe }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{{ 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="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ admin_banner_html|safe }}
|
|
||||||
<header class="w-full">{{ slots.header|safe }}</header>
|
|
||||||
<main class="flex-grow w-full">
|
|
||||||
{% if title %}<div class="mx-auto max-w-6xl px-4 pt-12"><h1 class="coffee-display text-4xl text-primary md:text-5xl"><span class="coffee-doodle-underline">{{ title }}</span></h1>{% if page_meta.MetaDescription %}<p class="coffee-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
|
|
||||||
{% if slots.featured %}<section class="w-full">{{ slots.featured|safe }}</section>{% endif %}
|
|
||||||
{% if slots.main %}<section class="w-full">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center coffee-hand text-2xl text-muted-foreground"><p>No stories brewed yet.</p></div>{% endif %}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
|
||||||
{{ 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="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ 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 coffee-hand text-2xl text-muted-foreground"><p>Add a contact form and your hours.</p></div>{% endif %}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
|
||||||
{{ 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="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ 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 coffee-hand text-2xl text-muted-foreground"><p>Pour something in here.</p></div>{% endif %}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
|
||||||
{{ 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="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ 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="max-w-3xl mx-auto py-20 px-4 text-center coffee-body text-muted-foreground italic"><p>Add a gallery, a wide photo, a confession.</p></div>{% endif %}
|
|
||||||
</main>
|
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
|
||||||
{{ body_end_html|safe }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
|
||||||
{{ head_html|safe }}
|
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
|
||||||
{{ admin_banner_html|safe }}
|
|
||||||
<section class="w-full">{{ slots.hero|safe }}</section>
|
|
||||||
<section class="w-full">
|
|
||||||
<div class="max-w-5xl mx-auto px-4">{{ slots.menu|safe }}</div>
|
|
||||||
</section>
|
|
||||||
<section class="w-full">
|
|
||||||
<div class="max-w-3xl mx-auto px-4 py-12">{{ slots.story|safe }}</div>
|
|
||||||
</section>
|
|
||||||
<section class="w-full">
|
|
||||||
<div class="max-w-5xl mx-auto px-4">{{ slots.cta|safe }}</div>
|
|
||||||
</section>
|
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
|
||||||
{{ body_end_html|safe }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,99 +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">
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-family: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 620px) {
|
|
||||||
.coffee-email-container {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
.coffee-email-padding {
|
|
||||||
padding-left: 24px !important;
|
|
||||||
padding-right: 24px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body style="background-color: {{ colors.background|default:"#f4ece1" }}; margin: 0; padding: 0; font-family: &#39;Inter&#39;, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-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: 40px 10px; background-color: {{ colors.background|default:"#f4ece1" }};">
|
|
||||||
<table role="presentation" class="coffee-email-container" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; background-color: {{ colors.card|default:"#ece1d0" }}; border: 1px solid {{ colors.border|default:"#c9b69e" }}; border-radius: 4px;">
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding: 32px 40px 16px; border-bottom: 1px dashed {{ colors.border|default:"#c9b69e" }};">
|
|
||||||
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 24px; letter-spacing: -0.01em; color: {{ colors.primary|default:"#8a4a23" }};">{{ site_name }}</h1>{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="coffee-email-padding" style="padding: 32px 40px; color: {{ colors.foreground|default:"#3d2a1a" }}; font-size: 16px; line-height: 1.65;">{{ body|safe }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding: 16px 40px 8px;">
|
|
||||||
<!-- inline SVG doodle divider, survives Outlook -->
|
|
||||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0"><tr><td>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="32" viewBox="0 0 200 32" fill="none" stroke="{{ colors.primary|default:"#c95b2f" }}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M4 18 L80 18"></path>
|
|
||||||
<path d="M120 18 L196 18"></path>
|
|
||||||
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
|
|
||||||
<path d="M95 9 L95 27"></path>
|
|
||||||
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
|
|
||||||
<path d="M108 9 L108 27"></path>
|
|
||||||
</svg>
|
|
||||||
</td></tr></table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 16px 40px 32px; color: {{ colors.mutedForeground|default:"#6b5440" }}; font-size: 12px; line-height: 1.6;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
{% if site_name %}<p style="margin: 0 0 6px; font-size: 13px; color: {{ colors.foreground|default:"#3d2a1a" }};">{{ site_name }}</p>{% endif %}
|
|
||||||
<p style="margin: 0 0 6px;">Pull up a seat. Pastries from 7, coffee until late.</p>
|
|
||||||
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#c95b2f" }}; text-decoration: none; border-bottom: 1px dashed {{ colors.primary|default:"#c95b2f" }};">{{ site_url }}</a></p>{% endif %}
|
|
||||||
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#6b5440" }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{# coffee announcement-bar — dismissable strip; dismissal persists in localStorage keyed by variant+message hash. #}
|
|
||||||
{% set anncls = "bg-accent/15 text-foreground" %}
|
|
||||||
{% if variant == "promo" %}{% set anncls = "bg-accent text-accent-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
|
|
||||||
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
|
|
||||||
<div class="relative mx-auto flex max-w-7xl items-center justify-center gap-2 px-10 py-2 text-center text-sm">
|
|
||||||
<p class="coffee-body font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline decoration-dashed 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 inline-flex -translate-y-1/2 items-center justify-center rounded p-1 transition-colors hover:bg-black/10"><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,15 +0,0 @@
|
|||||||
<figure class="bn-audio coffee-card my-8 overflow-hidden{% 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="h-16 w-16 shrink-0 overflow-hidden rounded-sm bg-secondary">{% 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="coffee-display truncate text-lg text-primary">{{ title }}</p>{% endif %}{% if artist %}<p class="coffee-hand truncate text-lg text-accent">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="kraft-tag text-xs" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="coffee-body inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> 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,32 +0,0 @@
|
|||||||
{# coffee auth-form — combined login/register in kraft chrome. Endpoints and panel ids match the platform auth flow (/api/auth/login, /api/auth/register, #auth-message, #login-panel/#register-panel). Auth state via the auth_state provider (auth.logged_in / auth.display_name). Captcha, when enabled, is emitted by the platform auth path. #}
|
|
||||||
{% if auth.logged_in %}
|
|
||||||
<div class="auth-form-logged-in coffee-card mx-auto max-w-md p-6 text-center">
|
|
||||||
<p class="coffee-body text-muted-foreground">You are logged in as <strong class="text-primary">{{ auth.display_name }}</strong></p>
|
|
||||||
<a href="/account" class="kraft-tag mt-4">My account</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% set defaultTab = default_tab|default:"login" %}
|
|
||||||
<div class="auth-form coffee-card mx-auto max-w-md p-6">
|
|
||||||
<div id="auth-message"></div>
|
|
||||||
<div class="mb-5 flex border-b border-dashed border-border">
|
|
||||||
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.className='coffee-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.nextElementSibling.className='coffee-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="coffee-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-transparent text-muted-foreground{% else %}border-primary text-primary{% endif %}">Login</button>
|
|
||||||
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.className='coffee-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.previousElementSibling.className='coffee-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="coffee-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-primary text-primary{% else %}border-transparent text-muted-foreground{% endif %}">Register</button>
|
|
||||||
</div>
|
|
||||||
<div id="login-panel" class="{% if defaultTab == "register" %}hidden{% endif %}">
|
|
||||||
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
|
||||||
<div class="mb-3"><label class="coffee-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
|
||||||
<div class="mb-4"><label class="coffee-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
|
||||||
<button type="submit" class="kraft-tag w-full justify-center" hx-disabled-elt="this">Login</button>
|
|
||||||
<p class="coffee-body mt-3 text-center text-sm"><a href="/forgot-password" class="text-accent underline decoration-dashed">Forgot password?</a></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="register-panel" class="{% if defaultTab == "register" %}{% else %}hidden{% endif %}">
|
|
||||||
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
|
||||||
<div class="mb-3"><label class="coffee-hand mb-1 block text-lg text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
|
||||||
<div class="mb-3"><label class="coffee-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
|
||||||
<div class="mb-4"><label class="coffee-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required minlength="8" class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
|
||||||
<button type="submit" class="kraft-tag w-full justify-center" hx-disabled-elt="this">Create account</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{# coffee auth-status — header login state. Auth state via the auth_state provider; endpoints match the platform (/login, /login?tab=register, /api/auth/logout, /api/auth/resend-verification). #}
|
|
||||||
{% if not auth.logged_in %}
|
|
||||||
<div class="auth-status flex items-center gap-2">
|
|
||||||
<a href="/login" class="coffee-body rounded-lg px-3 py-1.5 text-sm font-medium opacity-80 transition-colors hover:bg-muted hover:opacity-100">Login</a>
|
|
||||||
<a href="/login?tab=register" class="kraft-tag text-sm">Sign up</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% if not auth.email_verified %}<div class="fixed inset-x-0 top-16 z-40 bg-accent px-4 py-2 text-center text-sm text-accent-foreground">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="font-medium underline">Resend verification email</button></div>{% endif %}
|
|
||||||
<div class="auth-status relative">
|
|
||||||
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="coffee-body flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-muted hover:opacity-100">
|
|
||||||
<span class="coffee-hand flex h-7 w-7 items-center justify-center rounded-full bg-accent text-sm text-accent-foreground">{{ auth.display_name|first|upper }}</span>
|
|
||||||
<span>{{ auth.display_name }}</span>
|
|
||||||
<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
|
||||||
</button>
|
|
||||||
<div id="auth-dropdown-menu" class="coffee-card absolute right-0 top-full z-50 mt-1 hidden w-48 py-1 shadow-lg">
|
|
||||||
<a href="/account" class="coffee-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My profile</a>
|
|
||||||
<a href="/account/reviews" class="coffee-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My reviews</a>
|
|
||||||
<hr class="my-1 border-dashed border-border">
|
|
||||||
<button hx-post="/api/auth/logout" hx-swap="none" class="coffee-body block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-muted">Logout</button>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{% if author %}
|
|
||||||
<section class="border-b border-dashed border-border 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-5 h-24 w-24 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
|
||||||
<h1 class="coffee-display text-3xl text-primary md:text-4xl">{{ author.display_name }}</h1>
|
|
||||||
{% if author.job_title or author.company %}<p class="coffee-hand mt-1 text-xl text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
|
||||||
{% if author.bio %}<p class="coffee-body mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
|
|
||||||
<div class="coffee-body mt-5 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-5 flex items-center justify-center gap-3">
|
|
||||||
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" 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" class="text-muted-foreground transition-colors hover:text-accent" 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" class="text-muted-foreground transition-colors hover:text-accent" 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" class="text-muted-foreground transition-colors hover:text-accent" 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,12 +0,0 @@
|
|||||||
{% if post.author_name %}
|
|
||||||
<aside class="coffee-card p-6{% if class %} {{ class }}{% endif %}">
|
|
||||||
{% if heading %}<p class="coffee-hand mb-4 text-xl text-accent">{{ 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-secondary ring-2 ring-border">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
|
||||||
<div class="min-w-0">
|
|
||||||
<h3 class="coffee-display text-base text-primary">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
|
||||||
{% if post.author_bio %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
<section 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="max-w-2xl">
|
|
||||||
{% if title %}<h1 class="coffee-display text-4xl text-primary md:text-5xl"><span class="coffee-doodle-underline">{{ title }}</span></h1>{% endif %}
|
|
||||||
{% if subtitle %}<p class="coffee-body mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if posts %}
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-6 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-6{% endif %}">
|
|
||||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="coffee-frame mb-3 block overflow-hidden rounded-sm p-1.5{% 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 rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
|
||||||
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
|
|
||||||
<h2 class="coffee-display {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
|
|
||||||
{% if showExcerpt and post.excerpt %}<p class="coffee-body mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
|
||||||
<div class="coffee-hand mt-3 flex flex-wrap items-center gap-x-3 text-lg text-accent">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
{% set arrows = template == "arrows" %}
|
|
||||||
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
|
|
||||||
<ol class="coffee-body flex flex-wrap items-center gap-1 text-sm">
|
|
||||||
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-accent">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
|
|
||||||
{% for item in breadcrumbs %}
|
|
||||||
<li class="flex items-center">
|
|
||||||
{% if showHome or not forloop.First %}{% if arrows %}<svg width="16" height="16" class="mx-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-accent">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
|
||||||
{% if item.is_current %}{% if showCurrent %}<span class="coffee-display text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{% with align=align|default:"left" style=style|default:"primary" %}
|
|
||||||
<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
|
|
||||||
{% if link %}<a href="{{ link }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="kraft-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}kraft-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a>{% else %}<button type="button" class="kraft-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}kraft-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</button>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
{% with layout=layout|default:"vertical" %}
|
|
||||||
<article class="bn-card coffee-card overflow-hidden transition-shadow hover:shadow-md {% 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-secondary {% 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="coffee-display text-xl text-primary">{{ title }}</h3>{% endif %}
|
|
||||||
{% if text %}<div class="coffee-body text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}
|
|
||||||
{% if linkLabel and link %}<span class="coffee-hand mt-2 inline-flex items-center gap-1 text-lg text-accent">{{ linkLabel }} <svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if link %}</a>{% endif %}
|
|
||||||
</article>
|
|
||||||
{% endwith %}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{% set pills = style == "pills" %}
|
|
||||||
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
{% if heading %}<h2 class="coffee-hand mb-4 text-2xl text-accent">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if pills %}
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{% for cat in categories %}
|
|
||||||
<a href="{{ cat.url }}" class="coffee-body inline-flex items-center gap-1.5 rounded-full border border-dashed border-border bg-card px-3 py-1 text-sm text-card-foreground transition-colors hover:border-accent hover:text-accent">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<ul class="space-y-1">
|
|
||||||
{% for cat in categories %}
|
|
||||||
<li><a href="{{ cat.url }}" class="coffee-body flex items-center justify-between rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-4xl px-4">
|
|
||||||
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
|
||||||
{% if description %}<p class="coffee-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-4 {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
|
|
||||||
{% for channel in channels %}
|
|
||||||
<div class="coffee-card flex items-start gap-4 p-5">
|
|
||||||
{% if channel.icon %}<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
|
|
||||||
<div class="min-w-0">
|
|
||||||
{% if channel.label %}<div class="coffee-hand text-lg text-accent">{{ channel.label }}</div>{% endif %}
|
|
||||||
{% if channel.value %}<div class="coffee-body mt-0.5 break-words font-medium">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-accent">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{% with bid=_block_id|default:context.blockId %}
|
|
||||||
<section id="contact-form-{{ bid }}" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-2xl px-4">
|
|
||||||
{% if form.submitted %}
|
|
||||||
<div class="coffee-card coffee-torn-top coffee-torn-bottom p-8 text-center">
|
|
||||||
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-accent/15 text-accent"><svg width="24" height="24" class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
|
|
||||||
<p class="coffee-display text-xl text-primary">{{ form.message|default:successMessage|default:"Thanks. Your message is on its way." }}</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
|
||||||
{% if description %}<p class="coffee-body mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
|
||||||
{% if form.errors._form %}<div class="coffee-body mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% endif %}
|
|
||||||
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
|
|
||||||
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %}
|
|
||||||
{% for field in fields %}
|
|
||||||
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
|
|
||||||
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="coffee-hand mb-1.5 block text-lg text-accent">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
|
|
||||||
{% if field.type == "textarea" %}
|
|
||||||
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"></textarea>
|
|
||||||
{% elif field.type == "select" %}
|
|
||||||
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
|
||||||
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
|
|
||||||
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring">
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
|
||||||
<div class="sm:col-span-2">
|
|
||||||
<button type="submit" class="kraft-tag">{{ submitLabel|default:"Send message" }}</button>
|
|
||||||
</div>
|
|
||||||
{% endform %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endwith %}
|
|
||||||