From 11c6c8c63e3d7f7cd72b156bc13d5f6c6e6e875a Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Sat, 6 Jun 2026 14:11:22 +0800 Subject: [PATCH] initial: theme plugin coffee Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/coffee. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 5 + BUILD_REPORT.md | 238 ++++++++++++++ Makefile | 195 +++++++++++ RECOMMENDED_FONTS.md | 49 +++ assets/style.css | 151 +++++++++ assets/textures/.gitkeep | 0 assets/textures/README.txt | 7 + button_override.go | 18 + button_override.templ | 23 ++ button_override_templ.go | 136 ++++++++ doodle_divider.go | 32 ++ doodle_divider.templ | 47 +++ doodle_divider_templ.go | 82 +++++ email_wrapper.templ | 204 ++++++++++++ email_wrapper_templ.go | 432 ++++++++++++++++++++++++ embed.go | 66 ++++ featured_pour.go | 39 +++ featured_pour.templ | 36 ++ featured_pour_templ.go | 143 ++++++++ fonts.json | 1 + footer.go | 35 ++ footer.templ | 37 +++ footer_templ.go | 79 +++++ go.mod | 20 ++ go.sum | 42 +++ heading_override.go | 40 +++ heading_override.templ | 48 +++ heading_override_templ.go | 312 ++++++++++++++++++ helpers.go | 91 ++++++ hours_strip.go | 73 +++++ hours_strip.templ | 35 ++ hours_strip_templ.go | 152 +++++++++ image_override.go | 19 ++ image_override.templ | 20 ++ image_override_templ.go | 106 ++++++ location_card.go | 37 +++ location_card.templ | 35 ++ location_card_templ.go | 116 +++++++ master_pages.go | 95 ++++++ menu_board.go | 82 +++++ menu_board.templ | 54 +++ menu_board_templ.go | 220 +++++++++++++ plugin.mod | 12 + presets.json | 152 +++++++++ register.go | 89 +++++ registration.go | 25 ++ schemas/doodle_divider.schema.json | 16 + schemas/featured_pour.schema.json | 32 ++ schemas/footer.schema.json | 22 ++ schemas/hours_strip.schema.json | 47 +++ schemas/location_card.schema.json | 26 ++ schemas/menu_board.schema.json | 70 ++++ template.templ | 257 +++++++++++++++ template_templ.go | 506 +++++++++++++++++++++++++++++ text_override.go | 17 + text_override.templ | 8 + text_override_templ.go | 67 ++++ 57 files changed, 4998 insertions(+) create mode 100644 .gitignore create mode 100644 BUILD_REPORT.md create mode 100644 Makefile create mode 100644 RECOMMENDED_FONTS.md create mode 100644 assets/style.css create mode 100644 assets/textures/.gitkeep create mode 100644 assets/textures/README.txt create mode 100644 button_override.go create mode 100644 button_override.templ create mode 100644 button_override_templ.go create mode 100644 doodle_divider.go create mode 100644 doodle_divider.templ create mode 100644 doodle_divider_templ.go create mode 100644 email_wrapper.templ create mode 100644 email_wrapper_templ.go create mode 100644 embed.go create mode 100644 featured_pour.go create mode 100644 featured_pour.templ create mode 100644 featured_pour_templ.go create mode 100644 fonts.json create mode 100644 footer.go create mode 100644 footer.templ create mode 100644 footer_templ.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 heading_override.go create mode 100644 heading_override.templ create mode 100644 heading_override_templ.go create mode 100644 helpers.go create mode 100644 hours_strip.go create mode 100644 hours_strip.templ create mode 100644 hours_strip_templ.go create mode 100644 image_override.go create mode 100644 image_override.templ create mode 100644 image_override_templ.go create mode 100644 location_card.go create mode 100644 location_card.templ create mode 100644 location_card_templ.go create mode 100644 master_pages.go create mode 100644 menu_board.go create mode 100644 menu_board.templ create mode 100644 menu_board_templ.go create mode 100644 plugin.mod create mode 100644 presets.json create mode 100644 register.go create mode 100644 registration.go create mode 100644 schemas/doodle_divider.schema.json create mode 100644 schemas/featured_pour.schema.json create mode 100644 schemas/footer.schema.json create mode 100644 schemas/hours_strip.schema.json create mode 100644 schemas/location_card.schema.json create mode 100644 schemas/menu_board.schema.json create mode 100644 template.templ create mode 100644 template_templ.go create mode 100644 text_override.go create mode 100644 text_override.templ create mode 100644 text_override_templ.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f780e6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.so +*.test +tmp/ +.idea/ +.vscode/ diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..b46a398 --- /dev/null +++ b/BUILD_REPORT.md @@ -0,0 +1,238 @@ +# Coffee — Build Report (v0.1.0 pass) + +## What landed + +### Plugin shell +- `plugin.mod` — `name=coffee`, `kind=theme`, `scope=@themes`, `version=0.1.0`, + `categories=["templates"]`, `tags=["warm","hospitality","cafe","bakery","food","handcraft","organic","artisan","menu"]`, + `[compatibility].block_core = ">=0.11.0 <0.12.0"`. +- `go.mod` — module `git.dev.alexdunmow.com/block/themes/coffee`, + `go 1.26.4`, pinned `block/core v0.11.1`, no `replace` directives. +- `Makefile` — local-only `make` (CGO `go build -buildmode=plugin`), + `make clean`, `make templ`, plus the full container-deploy targets copied + from gotham (rebuild / backend / build-so / sync-migrations / etc.). +- `embed.go` — embeds `assets/*`, `schemas/*`, `presets.json`, `fonts.json`, + `plugin.mod`. Exposes `Assets`, `Schemas`, `AssetsHandler`, `ThemePresets`, + `BundledFonts`, `ThemeCSSManifest`. +- `registration.go` — `var Registration plugin.PluginRegistration` with + `CSSManifest` wired (theme needs paper texture, torn-edge, kraft-tag CSS + injected into the host Tailwind input). +- `register.go` — entry point. Order: system template → 4 page templates → + `br.LoadSchemasFromFS` → 6 theme block registrations → 4 built-in + overrides → email wrapper. +- `master_pages.go` — `DefaultMasterPages()` returns `coffee:default-master` + and `coffee:landing-master` per spec §7. +- `helpers.go` — content-map readers (`getString`, `getBool`, `getSlice`) + and weekday helpers (`shortDayName`, `normaliseDay`) for the + server-side today-row detection in `hours_strip`. + +### System + page templates +- System template `coffee` registered exactly once. +- Page templates: `default` (header/main/footer), `landing` + (hero/menu/story/cta/footer), `article` (header/main/footer), + `full-width` (header/main/footer) — `templ` components in + `template.templ`, each rendered via `Render*` adapter wrapped with `wrap()`. + +### Theme blocks (Source: `coffee`) +| Key | File pair | Schema | Notes | +|---|---|---|---| +| `menu_board` | `menu_board.{go,templ}` | `menu_board.schema.json` | Sectioned kraft-paper card; collection of items with price, note, allergens. | +| `hours_strip` | `hours_strip.{go,templ}` | `hours_strip.schema.json` | Today row flagged server-side via `time.Now().Weekday()` → `is-today today` classes. | +| `location_card` | `location_card.{go,templ}` | `location_card.schema.json` | Doodled SVG pin overlay; falls back to placeholder when map image absent. | +| `featured_pour` | `featured_pour.{go,templ}` | `featured_pour.schema.json` | Hero card with "Featured" badge, rich-text tasting notes, mono price. | +| `footer` | `footer.{go,templ}` | `footer.schema.json` | Torn-edge top border via `.coffee-torn-top` mask; newsletter caption. | +| `doodle_divider` | `doodle_divider.{go,templ}` | `doodle_divider.schema.json` | Inline SVG; four motifs (beans/croissant/cup/leaf), each visibly distinct. | + +All 6 schemas use draft-07 and an allowed `x-editor` from +`{text, richtext, media, color, select, number, slug, textarea, array, collection, bucket-picker, menu-select, template-select, link}`. +Schema property names match each block's `content["..."]` reads. + +### Built-in block overrides (active only when Coffee is the system template) +| Built-in | Coffee replacement | Aesthetic | +|---|---|---| +| `heading` | `CoffeeHeadingBlock` (`heading_override.{go,templ}`) | Fraunces display via `var(--font-heading)`, doodle underline on h2/h3. | +| `text` | `CoffeeTextBlock` (`text_override.{go,templ}`) | `.coffee-dropcap` first-letter floats large in `--primary`. | +| `button` | `CoffeeButtonBlock` (`button_override.{go,templ}`) | `.kraft-tag` class with `rotate(-1.2deg)` on hover, `:focus-visible` ring at 2px. | +| `image` | `CoffeeImageBlock` (`image_override.{go,templ}`) | Torn-edge bottom frame, handwritten italic caption. | + +### Email wrapper +- `coffee/email_wrapper.templ` renders a single-column 560px-wide table + layout, cream `Background`, espresso `Foreground`, inline-SVG doodle + divider beneath the body. Hex fallbacks live in `coffeeBgColor` etc.; + the resolved `EmailContext.Colors` overrides them per email. +- Registered via `tr.RegisterEmailWrapper("coffee", CoffeeEmailWrapper)`. + +### Presets (`presets.json`) +Three presets, all 19 tokens populated for both `lightColors` and `darkColors`: +- `morning-pour` (mode: light) — cream paper, espresso ink, terracotta accent. +- `dark-roast` (mode: dark) — espresso base, copper primary, terracotta accent. +- `kraft-cream` (mode: light) — warmer ivory paper, deeper terracotta. + +All colour values are HSL triple strings (no `hsl()` wrapper). Token values +copied from spec §4 verbatim where given. Two derived values were tuned +for accessibility (noted under "Open items"). + +### CSS Manifest +`assets/style.css` is injected into the host Tailwind input via +`CSSManifest.InputCSSAppend`. Provides: +- `--coffee-{heading,body,mono}-fallback` font stacks consumed via + `var(--font-heading, var(--coffee-heading-fallback))` etc. +- `.coffee-paper` body background (inline SVG noise, palette-neutral). +- `.coffee-display`, `.coffee-body`, `.coffee-mono` font-family hooks. +- `.coffee-doodle-underline` SVG underline applied to h2/h3. +- `.coffee-dropcap` for article drop-cap and text override. +- `.kraft-tag` button styling with `:hover { transform: rotate(-1.2deg); }` + and `:focus-visible` outline. +- `.coffee-torn-top` and `.coffee-torn-bottom` `mask-image` SVG paths. +- `.coffee-card`, `.coffee-frame`, `.coffee-pencil-rule` surfaces. +- `.coffee-hours-today` today-row highlight (border-left accent). +- `.coffee-price` mono numerals. + +### Fonts policy (per `docs/FONTS.md` wave-1) +- `fonts.json` is literal `[]`. +- No woff2 bundled in this pass. +- All `font-family` usage goes through the CSS variables. +- `RECOMMENDED_FONTS.md` documents Fraunces / Inter / JetBrains Mono as + Google Fonts picker recommendations. + +## Build output + +``` +$ cd /home/alex/src/blockninja/themes/coffee +$ go mod tidy +(no output) + +$ /home/alex/go/bin/templ generate +(✓) Complete [ updates=12 duration=8.143585ms ] + +$ make +CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o coffee.so . +$ ls -la coffee.so +-rw-rw-r-- 1 alex alex 21520096 ./coffee.so (≈ 20.5 MiB) +``` + +Zero compiler warnings, zero `WARN` lines. The .so is in the same size +range as the gotham reference (`gotham.so` is 20.2 MiB). + +## Safety check + +The user-supplied invocation references `/home/alex/src/blockninja/backend` +which does not exist on this host. The two available check-safety +locations are: + +1. Standalone module at `/home/alex/src/blockninja/check-safety` — the + canonical clean run. +2. CMS backend worktree at + `/home/alex/src/blockninja/cms/.worktrees/orchestrator-plugin-builds/backend/cmd/check-safety` — + also reports coffee-specific checks as `OK` but has pre-existing + failures in the CMS backend itself unrelated to this plugin + (`templ.ResolveAttributeValue`, Tailwind v4 frontend lints). + +Used invocation: + +``` +$ cd /home/alex/src/blockninja/check-safety +$ go run . /home/alex/src/blockninja/themes/coffee \ + --plugin-dir /home/alex/src/blockninja/themes/coffee +... 22 checks ... +=== Check 22: No hand-rolled HTML sanitization (use bluemonday) === + OK: No hand-rolled HTML sanitization detected +exit=0 +``` + +All 22 checks PASS or SKIP. Notable: +- Check 2c (Standalone plugin SDK import boundaries) — `coffee` stays on + the SDK boundary v0.11.1. +- Check 3 (Go lint pipeline) — clean for the single coffee module + after dropping the unused `getInt` helper. +- Check 6 (No hardcoded colours in `.templ` / `.ninjatpl`) — passes; the + hex fallbacks in `email_wrapper.templ` (`coffeeBgColor` and siblings) + are the email-client compatibility fallbacks the check tolerates, + matching the same pattern gotham ships. +- Check 21 (preset validation) — all three presets parse against the + theme.Theme schema. + +## Open items / deferred + +1. **No bundled woff2s.** Per wave-1 `docs/FONTS.md`. `fonts.json` is `[]`, + `RECOMMENDED_FONTS.md` documents the Google Fonts picks (Fraunces / + Inter / JetBrains Mono). Wave-2 follow-up will bundle and add + `LICENSES.md`. +2. **No bitmap paper-grain texture.** The `.coffee-paper` body background + uses an inline SVG noise data URI. The UAT §13.1 expects + `assets/textures/paper-grain.` at 8–80 KB. Deferred to a screenshot + / marketplace pass that ships an actual PNG. +3. **`mutedForeground` accessibility tuning.** Spec §4 sets the + `morning-pour` light `mutedForeground` at `25 20% 40%`, which fails + the UAT §6 contrast gate against `muted` at 4.5:1. Bumped to + `25 25% 32%` to clear AA. Same treatment applied to `kraft-cream` + and `dark-roast` light fallbacks. The HSL family stays in the + spec-described "soft brown over cream" range. +4. **Marketplace assets (UAT §12).** Screenshots, demo seed JSON, and + launch-copy.txt are not produced in this pass. +5. **Live URL & container deploy (UAT §2 final, §5 final, §6 etc.).** + `make rebuild` and the `instance-coffee` container are out of scope + here; the build pass only runs `make` locally + safety. +6. **Email wrapper hex fallbacks.** `coffeeBgColor` and siblings carry + hard-coded hex for the case where `EmailContext.Colors` arrives + empty. This mirrors the gotham reference pattern; email clients + cannot consume CSS variables, so the resolved palette is delivered + per-email and the hex strings only ever land in the no-colour + fallback path. Visual gate UAT §5 last bullet treats this as a + deliberate exception. +7. **`EmailColors.Accent` does not exist.** The spec mentions a + terracotta accent for the email body divider. The SDK's + `templates.EmailColors` exposes Primary / Secondary / Background / + Foreground / Muted / MutedForeground / Border / Card / CardForeground + only — no `Accent`. `coffeeAccentColor` therefore reuses Primary + when present, with a terracotta hex fallback. +8. **`hours_strip` "today" detection uses the server's local time zone.** + Spec §15 flagged this as an open question; resolved here by using + `time.Now()`. A future pass could thread a clock through context to + make this testable without monkey-patching. +9. **`menu_board` allergen field is free-text.** Per spec §15. Structured + allergen taxonomy deferred. + +## File map + +``` +coffee/ +├── BUILD_REPORT.md ← this file +├── Makefile +├── RECOMMENDED_FONTS.md +├── assets/ +│ ├── style.css ← injected via CSSManifest +│ └── textures/ +│ └── README.txt ← placeholder; paper-grain inlined in CSS for v0.1.0 +├── coffee.so ← 20.5 MiB build artefact +├── embed.go +├── plugin.mod +├── go.mod +├── go.sum +├── presets.json ← morning-pour, dark-roast, kraft-cream +├── fonts.json ← [] (wave-1 policy) +├── registration.go ← exports var Registration +├── register.go ← Register(tr, br) wires everything +├── master_pages.go ← DefaultMasterPages() +├── helpers.go ← getString/getBool/getSlice + day helpers +├── template.templ ← 4 page templates (Coffee, CoffeeLanding, CoffeeArticle, CoffeeFullWidth) +├── menu_board.{go,templ} +├── hours_strip.{go,templ} +├── location_card.{go,templ} +├── featured_pour.{go,templ} +├── footer.{go,templ} +├── doodle_divider.{go,templ} +├── heading_override.{go,templ} +├── text_override.{go,templ} +├── button_override.{go,templ} +├── image_override.{go,templ} +├── email_wrapper.templ +├── *_templ.go ← templ-generated companions (committed) +└── schemas/ + ├── menu_board.schema.json + ├── hours_strip.schema.json + ├── location_card.schema.json + ├── featured_pour.schema.json + ├── footer.schema.json + └── doodle_divider.schema.json +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5eaa1d8 --- /dev/null +++ b/Makefile @@ -0,0 +1,195 @@ +# Coffee — build & deploy helpers (.so plugin workflow) +# +# The plugin compiles to a .so shared object loaded by the CMS at runtime. +# `make rebuild` copies source to the container, builds the .so, and restarts. +# +# Usage: +# make rebuild # Full rebuild: frontend + .so + CSS + migrations, restart +# make backend # Build .so + migrations, restart +# make build-css # Rebuild Tailwind CSS +# make logs # Tail instance logs +# make status # Show instance container status + +.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 + +# Paths +BLOCKNINJA_DIR := $(HOME)/src/blockninja +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) + +# Default target: build the .so locally for development. +all: $(PLUGIN_NAME).so + +# Local plugin build (no container). Useful for CI / quick checks. +$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod + CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so . + +# Clean local build artifacts. +clean: + 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: + @echo "Targets:" + @echo " all Build $(PLUGIN_NAME).so locally (default)" + @echo " clean Remove $(PLUGIN_NAME).so" + @echo " templ Regenerate templ Go files locally" + @echo " spinup Start blockninja core services + instance container if stopped" + @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" diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md new file mode 100644 index 0000000..f992e0c --- /dev/null +++ b/RECOMMENDED_FONTS.md @@ -0,0 +1,49 @@ +# Coffee — Recommended fonts + +The Coffee theme ships `fonts.json = []` per the wave-1 +[FONTS.md](../docs/FONTS.md) policy. No woff2s are bundled in this pass. +Templates resolve font families through the BlockNinja CSS variables +`--font-heading`, `--font-body`, and `--font-mono` with the fallback stacks +defined in `assets/style.css`. The site admin assigns fonts in the +typography panel; the picks below are the spec-aligned defaults that match +the visual identity. + +## How to apply + +1. Open the admin → Theme → Typography panel. +2. Switch to the **Google Fonts** tab. +3. Search and pick each family below. +4. Assign to the matching slot (Heading / Body / Mono). +5. Save. The picks are persisted as `google:` and rendered as + `@import` URLs by `theme.GenerateCSS()`. + +## 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`. | + +All three are already in the curated Google Fonts list in the picker and +have permissive SIL OFL 1.1 licences. + +## Fallback stacks (used before admin picks fonts) + +Defined in `assets/style.css`: + +- `--coffee-heading-fallback`: `"Fraunces", "Playfair Display", Georgia, "Times New Roman", serif` +- `--coffee-body-fallback`: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` +- `--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. diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..01b7ede --- /dev/null +++ b/assets/style.css @@ -0,0 +1,151 @@ +/* Coffee theme styles + * + * Uses the 19 shadcn-style HSL token CSS variables (--background, + * --foreground, --primary, --accent, --border, --muted, --card, ...) via + * `hsl(var(--token))`. Font families are resolved through the BlockNinja font + * variables (--font-heading, --font-body, --font-mono) with fallback stacks + * 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 { + --coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif; + --coffee-body-fallback: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; +} + +/* --- 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 { + 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; +} + +/* --- Headings ----------------------------------------------------------- */ +.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)); +} + +/* --- Doodle underline (heading override) -------------------------------- */ +.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 (text override / article body) ---------------------------- */ +.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 --------------------------------------------------- */ +.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 1px 0 hsl(var(--border)); + transition: transform 120ms ease, box-shadow 120ms ease; + cursor: pointer; +} + +.kraft-tag:hover { + transform: rotate(-1.2deg); + box-shadow: 0 2px 0 hsl(var(--border)); +} + +.kraft-tag:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 3px; +} + +/* --- Torn-edge utility -------------------------------------------------- */ +.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"); + 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-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.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 { + 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; +} + +/* --- Today highlight ---------------------------------------------------- */ +.coffee-hours-today { + background-color: hsl(var(--accent) / 0.15); + color: hsl(var(--accent-foreground)); + border-left: 3px solid hsl(var(--accent)); + padding-left: 0.75rem; +} + +/* --- Price typography --------------------------------------------------- */ +.coffee-price { + font-family: var(--font-mono, var(--coffee-mono-fallback)); + font-variant-numeric: tabular-nums; + color: hsl(var(--accent)); +} + +/* --- Doodle pin overlay for location card ------------------------------- */ +.coffee-pin { + color: hsl(var(--accent)); +} diff --git a/assets/textures/.gitkeep b/assets/textures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/textures/README.txt b/assets/textures/README.txt new file mode 100644 index 0000000..1bc278a --- /dev/null +++ b/assets/textures/README.txt @@ -0,0 +1,7 @@ +Texture assets placeholder. + +The Coffee theme inlines its paper-grain texture as a base64 SVG data URI +in assets/style.css (.coffee-paper background-image). No bitmap textures +are bundled in this pass. Drop paper-grain.png here in a follow-up wave +and switch the CSS to reference it via the assets handler at +/templates/coffee/textures/paper-grain.png. diff --git a/button_override.go b/button_override.go new file mode 100644 index 0000000..79e1a70 --- /dev/null +++ b/button_override.go @@ -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() +} diff --git a/button_override.templ b/button_override.templ new file mode 100644 index 0000000..989d0ee --- /dev/null +++ b/button_override.templ @@ -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 != "" { + { text } + } else { + + } +} diff --git a/button_override_templ.go b/button_override_templ.go new file mode 100644 index 0000000..f35945b --- /dev/null +++ b/button_override_templ.go @@ -0,0 +1,136 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// 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, "") + 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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/doodle_divider.go b/doodle_divider.go new file mode 100644 index 0000000..68c91d4 --- /dev/null +++ b/doodle_divider.go @@ -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() +} diff --git a/doodle_divider.templ b/doodle_divider.templ new file mode 100644 index 0000000..80b64a1 --- /dev/null +++ b/doodle_divider.templ @@ -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) { +
+ switch motif { + case "croissant": + + case "cup": + + case "leaf": + + default: + + } +
+} diff --git a/doodle_divider_templ.go b/doodle_divider_templ.go new file mode 100644 index 0000000..0f35547 --- /dev/null +++ b/doodle_divider_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + switch motif { + case "croissant": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "cup": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "leaf": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/email_wrapper.templ b/email_wrapper.templ new file mode 100644 index 0000000..cc578d3 --- /dev/null +++ b/email_wrapper.templ @@ -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) { + + + + + + + + { emailCtx.SiteSettings.SiteName } + + + + if emailCtx.PreviewText != "" { +
+ { emailCtx.PreviewText } +
+ } + + + + +
+ + + + + + + + + + + + + + +
+ + +} + +// 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" +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..a53abbb --- /dev/null +++ b/email_wrapper_templ.go @@ -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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.PreviewText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.LogoURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if emailCtx.SiteSettings.SiteName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SiteName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

") + 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, "

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

Pull up a seat. Pastries from 7, coffee until late.

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

") + 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, "

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

Unsubscribe

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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 diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..d160f4e --- /dev/null +++ b/embed.go @@ -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), + } +} diff --git a/featured_pour.go b/featured_pour.go new file mode 100644 index 0000000..2d8a701 --- /dev/null +++ b/featured_pour.go @@ -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 +} diff --git a/featured_pour.templ b/featured_pour.templ new file mode 100644 index 0000000..107edc5 --- /dev/null +++ b/featured_pour.templ @@ -0,0 +1,36 @@ +package main + +// featuredPourComponent renders the featured pour hero card. +templ featuredPourComponent(data FeaturedPourData) { +
+
+ + Featured + +
+ if data.Image != "" { + { + } else { +
+ Add a hero image +
+ } +
+
+ if data.Name != "" { +

{ data.Name }

+ } else { +

Featured pour

+ } + if data.Tasting != "" { +
+ @templ.Raw(data.Tasting) +
+ } + if data.Price != "" { +
{ data.Price }
+ } +
+
+
+} diff --git a/featured_pour_templ.go b/featured_pour_templ.go new file mode 100644 index 0000000..57b00b1 --- /dev/null +++ b/featured_pour_templ.go @@ -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, "
Featured
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Image != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
Add a hero image
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Name != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

Featured pour

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Tasting != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Price != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/fonts.json @@ -0,0 +1 @@ +[] diff --git a/footer.go b/footer.go new file mode 100644 index 0000000..2053f54 --- /dev/null +++ b/footer.go @@ -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 +} diff --git a/footer.templ b/footer.templ new file mode 100644 index 0000000..59c42cf --- /dev/null +++ b/footer.templ @@ -0,0 +1,37 @@ +package main + +// footerComponent renders the kraft-paper footer with a torn top edge. +templ footerComponent(data FooterData) { +
+
+
+

Hello there

+

Pull up a seat. Pastries from 7, coffee until late.

+
+ if data.ShowLocation { +
+

Visit

+
42 Roastery Lane +City, State 9000
+
+ } +
+

Stay in touch

+ if data.NewsletterText != "" { +

{ data.NewsletterText }

+ } else { +

Slow notes from the bar. Brew tips, seasonal pours, no spam.

+ } +
+ + + +
+
+
+
+ © Coffee theme — kraft paper edition. + Hand-drawn with care. +
+
+} diff --git a/footer_templ.go b/footer_templ.go new file mode 100644 index 0000000..d052dc2 --- /dev/null +++ b/footer_templ.go @@ -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, "

Hello there

Pull up a seat. Pastries from 7, coffee until late.

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

Visit

42 Roastery Lane City, State 9000
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Stay in touch

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

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Slow notes from the bar. Brew tips, seasonal pours, no spam.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
© Coffee theme — kraft paper edition. Hand-drawn with care.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b47ea48 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.dev.alexdunmow.com/block/themes/coffee + +go 1.26.4 + +require ( + git.dev.alexdunmow.com/block/core v0.11.1 + github.com/a-h/templ v0.3.1020 +) + +require ( + connectrpc.com/connect v1.20.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/text v0.36.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26aea2c --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +git.dev.alexdunmow.com/block/core v0.11.1 h1:5b3Ps9CLor2FGyxw/Qovt27AGZKR5Xi1JZGi/TfliTA= +git.dev.alexdunmow.com/block/core v0.11.1/go.mod h1:ZwzEOxRDLDfrhQGqo6hLw01/C1z/aS4Dm9ljQMl0Bg4= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= +github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/heading_override.go b/heading_override.go new file mode 100644 index 0000000..f7155b4 --- /dev/null +++ b/heading_override.go @@ -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 +} diff --git a/heading_override.templ b/heading_override.templ new file mode 100644 index 0000000..fd171f5 --- /dev/null +++ b/heading_override.templ @@ -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: +

{ text }

+ case 2: +

+ { text } +

+ case 3: +

+ { text } +

+ case 4: +

{ text }

+ case 5: +
{ text }
+ case 6: +
{ text }
+ default: +

+ { text } +

+ } +} diff --git a/heading_override_templ.go b/heading_override_templ.go new file mode 100644 index 0000000..aaea7e1 --- /dev/null +++ b/heading_override_templ.go @@ -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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..d936603 --- /dev/null +++ b/helpers.go @@ -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) +} diff --git a/hours_strip.go b/hours_strip.go new file mode 100644 index 0000000..c0c68ea --- /dev/null +++ b/hours_strip.go @@ -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 +} diff --git a/hours_strip.templ b/hours_strip.templ new file mode 100644 index 0000000..4a8407f --- /dev/null +++ b/hours_strip.templ @@ -0,0 +1,35 @@ +package main + +// hoursStripComponent renders the hours strip with the today row highlighted. +templ hoursStripComponent(data HoursStripData) { + +} + +// 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 "" +} diff --git a/hours_strip_templ.go b/hours_strip_templ.go new file mode 100644 index 0000000..48b2814 --- /dev/null +++ b/hours_strip_templ.go @@ -0,0 +1,152 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// 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, "") + 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 diff --git a/image_override.go b/image_override.go new file mode 100644 index 0000000..0cd5c22 --- /dev/null +++ b/image_override.go @@ -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() +} diff --git a/image_override.templ b/image_override.templ new file mode 100644 index 0000000..0190fd8 --- /dev/null +++ b/image_override.templ @@ -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) { +
+
+ if src != "" { + { + } else { +
+ Add an image to render the torn-edge frame. +
+ } +
+ if caption != "" { +
{ caption }
+ } +
+} diff --git a/image_override_templ.go b/image_override_templ.go new file mode 100644 index 0000000..7577b45 --- /dev/null +++ b/image_override_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if src != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
Add an image to render the torn-edge frame.
") + 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 caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(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, "
") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/location_card.go b/location_card.go new file mode 100644 index 0000000..d6e11a9 --- /dev/null +++ b/location_card.go @@ -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 +} diff --git a/location_card.templ b/location_card.templ new file mode 100644 index 0000000..b4b8254 --- /dev/null +++ b/location_card.templ @@ -0,0 +1,35 @@ +package main + +// locationCardComponent renders a location card with doodled pin overlay. +templ locationCardComponent(data LocationCardData) { +
+
+
+ if data.MapImage != "" { + Map of our location + } else { +
+ Map preview +
+ } + +
+
+

+ Find us +

+ if data.Address != "" { +
{ data.Address }
+ } else { +

Add an address to render the location card.

+ } + if data.DirectionsURL != "" { + Directions + } +
+
+
+} diff --git a/location_card_templ.go b/location_card_templ.go new file mode 100644 index 0000000..dfdbdae --- /dev/null +++ b/location_card_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.MapImage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"Map ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Map preview
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Find us

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Address != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Add an address to render the location card.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.DirectionsURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Directions") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/master_pages.go b/master_pages.go new file mode 100644 index 0000000..da02aae --- /dev/null +++ b/master_pages.go @@ -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, + }, + }, + }, + } +} diff --git a/menu_board.go b/menu_board.go new file mode 100644 index 0000000..eda2458 --- /dev/null +++ b/menu_board.go @@ -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 +} diff --git a/menu_board.templ b/menu_board.templ new file mode 100644 index 0000000..9a373c6 --- /dev/null +++ b/menu_board.templ @@ -0,0 +1,54 @@ +package main + +// menuBoardComponent renders a Coffee-styled menu board. +templ menuBoardComponent(data MenuBoardData) { +
+
+ if data.Title != "" { +

+ { data.Title } +

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

Add a section to get started.

+ } + for sIdx, section := range data.Sections { +
+ if section.Name != "" { +

{ section.Name }

+ } + if len(section.Items) == 0 { +

No items yet.

+ } +
    + for _, item := range section.Items { +
  • +
    +
    { item.Name }
    + if item.Note != "" { +
    { item.Note }
    + } + if item.Allergens != "" { +
    Allergens: { item.Allergens }
    + } +
    + if item.Price != "" { +
    { item.Price }
    + } +
  • + } +
+
+ } +
+
+} + +// 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" +} diff --git a/menu_board_templ.go b/menu_board_templ.go new file mode 100644 index 0000000..7d9ec4b --- /dev/null +++ b/menu_board_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

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

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

Add a section to get started.

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

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(section.Items) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

No items yet.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range section.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(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, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.Note != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
    ") + 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, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if item.Allergens != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
    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, "
    ") + 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 + } + if item.Price != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
    ") + 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, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// 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 diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..a97fd75 --- /dev/null +++ b/plugin.mod @@ -0,0 +1,12 @@ +[plugin] +name = "coffee" +display_name = "Coffee" +scope = "@themes" +version = "0.1.0" +description = "Warm, hand-crafted theme for cafes, bakeries, and slow-craft makers — kraft paper, doodle illustrations, and roasted browns." +kind = "theme" +categories = ["templates"] +tags = ["warm", "hospitality", "cafe", "bakery", "food", "handcraft", "organic", "artisan", "menu"] + +[compatibility] +block_core = ">=0.11.0 <0.12.0" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..9a12e66 --- /dev/null +++ b/presets.json @@ -0,0 +1,152 @@ +[ + { + "id": "morning-pour", + "name": "Morning Pour", + "description": "Cream paper, espresso ink, terracotta accent", + "theme": { + "mode": "light", + "lightColors": { + "background": "36 30% 96%", + "foreground": "25 35% 18%", + "card": "36 28% 93%", + "cardForeground": "25 35% 18%", + "popover": "36 28% 93%", + "popoverForeground": "25 35% 18%", + "primary": "20 55% 35%", + "primaryForeground": "36 30% 96%", + "secondary": "32 25% 88%", + "secondaryForeground": "25 35% 18%", + "muted": "32 20% 90%", + "mutedForeground": "25 25% 32%", + "accent": "14 65% 50%", + "accentForeground": "36 30% 96%", + "destructive": "0 70% 45%", + "destructiveForeground": "36 30% 96%", + "border": "30 20% 80%", + "input": "30 20% 80%", + "ring": "20 55% 35%" + }, + "darkColors": { + "background": "25 25% 12%", + "foreground": "36 30% 92%", + "card": "25 22% 16%", + "cardForeground": "36 30% 92%", + "popover": "25 22% 16%", + "popoverForeground": "36 30% 92%", + "primary": "20 50% 55%", + "primaryForeground": "25 25% 12%", + "secondary": "25 20% 22%", + "secondaryForeground": "36 30% 92%", + "muted": "25 18% 20%", + "mutedForeground": "32 18% 65%", + "accent": "14 70% 60%", + "accentForeground": "25 25% 12%", + "destructive": "0 65% 50%", + "destructiveForeground": "36 30% 92%", + "border": "25 18% 26%", + "input": "25 18% 24%", + "ring": "20 50% 55%" + } + } + }, + { + "id": "dark-roast", + "name": "Dark Roast", + "description": "Espresso-deep base with copper accent for evening cafes and roasteries", + "theme": { + "mode": "dark", + "lightColors": { + "background": "36 30% 96%", + "foreground": "25 35% 18%", + "card": "36 28% 93%", + "cardForeground": "25 35% 18%", + "popover": "36 28% 93%", + "popoverForeground": "25 35% 18%", + "primary": "25 60% 30%", + "primaryForeground": "36 30% 96%", + "secondary": "32 25% 88%", + "secondaryForeground": "25 35% 18%", + "muted": "32 20% 90%", + "mutedForeground": "25 25% 32%", + "accent": "14 80% 55%", + "accentForeground": "36 30% 96%", + "destructive": "0 70% 45%", + "destructiveForeground": "36 30% 96%", + "border": "30 20% 80%", + "input": "30 20% 80%", + "ring": "25 60% 30%" + }, + "darkColors": { + "background": "20 18% 8%", + "foreground": "36 30% 92%", + "card": "20 18% 12%", + "cardForeground": "36 30% 92%", + "popover": "20 18% 12%", + "popoverForeground": "36 30% 92%", + "primary": "30 75% 55%", + "primaryForeground": "20 18% 8%", + "secondary": "20 18% 18%", + "secondaryForeground": "36 30% 92%", + "muted": "20 15% 16%", + "mutedForeground": "32 18% 70%", + "accent": "14 80% 55%", + "accentForeground": "20 18% 8%", + "destructive": "0 65% 50%", + "destructiveForeground": "36 30% 92%", + "border": "20 18% 22%", + "input": "20 18% 20%", + "ring": "30 75% 55%" + } + } + }, + { + "id": "kraft-cream", + "name": "Kraft Cream", + "description": "Warmer ivory paper with deeper terracotta accent", + "theme": { + "mode": "light", + "lightColors": { + "background": "32 35% 92%", + "foreground": "22 40% 16%", + "card": "32 30% 89%", + "cardForeground": "22 40% 16%", + "popover": "32 30% 89%", + "popoverForeground": "22 40% 16%", + "primary": "16 60% 40%", + "primaryForeground": "32 35% 92%", + "secondary": "30 25% 84%", + "secondaryForeground": "22 40% 16%", + "muted": "30 20% 86%", + "mutedForeground": "22 25% 30%", + "accent": "8 65% 50%", + "accentForeground": "32 35% 92%", + "destructive": "0 70% 45%", + "destructiveForeground": "32 35% 92%", + "border": "28 22% 76%", + "input": "28 22% 76%", + "ring": "16 60% 40%" + }, + "darkColors": { + "background": "24 22% 14%", + "foreground": "32 28% 90%", + "card": "24 20% 18%", + "cardForeground": "32 28% 90%", + "popover": "24 20% 18%", + "popoverForeground": "32 28% 90%", + "primary": "16 50% 55%", + "primaryForeground": "24 22% 14%", + "secondary": "24 18% 24%", + "secondaryForeground": "32 28% 90%", + "muted": "24 16% 22%", + "mutedForeground": "30 16% 65%", + "accent": "8 65% 60%", + "accentForeground": "24 22% 14%", + "destructive": "0 65% 50%", + "destructiveForeground": "32 28% 90%", + "border": "24 16% 28%", + "input": "24 16% 26%", + "ring": "16 50% 55%" + } + } + } +] diff --git a/register.go b/register.go new file mode 100644 index 0000000..7519d30 --- /dev/null +++ b/register.go @@ -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:) + 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 +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..4e4067a --- /dev/null +++ b/registration.go @@ -0,0 +1,25 @@ +package main + +import ( + "io/fs" + "net/http" + + "git.dev.alexdunmow.com/block/core/blocks" + "git.dev.alexdunmow.com/block/core/plugin" + "git.dev.alexdunmow.com/block/core/templates" +) + +// Registration is the compile-time plugin registration for the 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() }, +} diff --git a/schemas/doodle_divider.schema.json b/schemas/doodle_divider.schema.json new file mode 100644 index 0000000..3505381 --- /dev/null +++ b/schemas/doodle_divider.schema.json @@ -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"] + } + } +} diff --git a/schemas/featured_pour.schema.json b/schemas/featured_pour.schema.json new file mode 100644 index 0000000..f7020f9 --- /dev/null +++ b/schemas/featured_pour.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Featured Pour", + "description": "Hero card for a featured coffee, tea, or pastry with image, tasting notes and price", + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Item name (e.g. \"Ethiopia Yirgacheffe\")", + "x-editor": "text" + }, + "tasting": { + "type": "string", + "title": "Tasting Notes", + "description": "Rich text tasting description", + "x-editor": "richtext" + }, + "image": { + "type": "string", + "title": "Image", + "description": "Hero image", + "x-editor": "media" + }, + "price": { + "type": "string", + "title": "Price", + "description": "Price displayed in mono type (e.g. \"6.50\")", + "x-editor": "text" + } + } +} diff --git a/schemas/footer.schema.json b/schemas/footer.schema.json new file mode 100644 index 0000000..8c74c49 --- /dev/null +++ b/schemas/footer.schema.json @@ -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" + } + } +} diff --git a/schemas/hours_strip.schema.json b/schemas/hours_strip.schema.json new file mode 100644 index 0000000..f9a1c42 --- /dev/null +++ b/schemas/hours_strip.schema.json @@ -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"] + } + } + } +} diff --git a/schemas/location_card.schema.json b/schemas/location_card.schema.json new file mode 100644 index 0000000..334f5e4 --- /dev/null +++ b/schemas/location_card.schema.json @@ -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" + } + } +} diff --git a/schemas/menu_board.schema.json b/schemas/menu_board.schema.json new file mode 100644 index 0000000..0e9bfaa --- /dev/null +++ b/schemas/menu_board.schema.json @@ -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"] + } + } + } +} diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..8770a12 --- /dev/null +++ b/template.templ @@ -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) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

Pour something in here.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Landing page template with hero / menu / story / cta / footer slots. +templ CoffeeLanding(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["hero"]) +
+
+
+ @templ.Raw(data.Slots["menu"]) +
+
+
+
+ @templ.Raw(data.Slots["story"]) +
+
+
+
+ @templ.Raw(data.Slots["cta"]) +
+
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Article page template — narrow narrative column for journal / recipes. +templ CoffeeArticle(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+
+ @templ.Raw(data.Slots["header"]) +
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } else { +
+

Write something honest here.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Full-width page template — edge-to-edge gallery / interior shots. +templ CoffeeFullWidth(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

Add a gallery, a wide photo, a confession.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// 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)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..2059289 --- /dev/null +++ b/template_templ.go @@ -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, "") + 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, "") + 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, "
") + 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, "
") + 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, "

Pour something in here.

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

Write something honest here.

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

Add a gallery, a wide photo, a confession.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// 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 diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..78dde08 --- /dev/null +++ b/text_override.go @@ -0,0 +1,17 @@ +package main + +import ( + "bytes" + "context" +) + +// CoffeeTextBlock renders rich text with Coffee body styling and the optional +// drop-cap utility on the first paragraph. +func CoffeeTextBlock(ctx context.Context, content map[string]any) string { + text := getString(content, "text") + class := getString(content, "class") + + var buf bytes.Buffer + _ = coffeeTextComponent(text, class).Render(ctx, &buf) + return buf.String() +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..5ec33cb --- /dev/null +++ b/text_override.templ @@ -0,0 +1,8 @@ +package main + +// coffeeTextComponent renders text with a longer measure and drop-cap utility. +templ coffeeTextComponent(text, class string) { +
+ @templ.Raw(text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..bce7539 --- /dev/null +++ b/text_override_templ.go @@ -0,0 +1,67 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// coffeeTextComponent renders text with a longer measure and drop-cap utility. +func coffeeTextComponent(text, class 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) + var templ_7745c5c3_Var2 = []any{"coffee-body coffee-dropcap prose max-w-prose text-foreground", class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate