From 49f9c905890a2cae593ba2bfc4a6705bdaa26d4e Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Sat, 6 Jun 2026 14:11:46 +0800 Subject: [PATCH] initial: theme plugin y2k Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/y2k. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 5 + BUILD_REPORT.md | 237 +++++++++++++ Makefile | 27 ++ RECOMMENDED_FONTS.md | 50 +++ assets/.gitkeep | 0 assets/placeholder.txt | 5 + button_override.go | 28 ++ button_override.templ | 9 + button_override_templ.go | 99 ++++++ chrome_navbar.go | 37 ++ chrome_navbar.templ | 38 +++ chrome_navbar_templ.go | 169 +++++++++ css.go | 228 +++++++++++++ email_wrapper.go | 83 +++++ email_wrapper.templ | 88 +++++ email_wrapper_templ.go | 251 ++++++++++++++ embed.go | 57 ++++ fonts.json | 1 + footer_chrome.go | 46 +++ footer_chrome.templ | 24 ++ footer_chrome_templ.go | 102 ++++++ glitter_divider.go | 26 ++ glitter_divider.templ | 25 ++ glitter_divider_templ.go | 107 ++++++ go.mod | 20 ++ go.sum | 42 +++ heading_override.go | 39 +++ heading_override.templ | 41 +++ heading_override_templ.go | 311 +++++++++++++++++ helpers.go | 55 +++ image_override.go | 32 ++ image_override.templ | 11 + image_override_templ.go | 108 ++++++ marquee.go | 40 +++ marquee.templ | 29 ++ marquee_templ.go | 111 ++++++ merch_card.go | 41 +++ merch_card.templ | 52 +++ merch_card_templ.go | 178 ++++++++++ metaball_hero.go | 41 +++ metaball_hero.templ | 64 ++++ metaball_hero_templ.go | 137 ++++++++ nft_gallery.go | 49 +++ nft_gallery.templ | 33 ++ nft_gallery_templ.go | 156 +++++++++ plugin.mod | 12 + presets.json | 110 ++++++ register.go | 173 ++++++++++ registration.go | 26 ++ schemas/chrome_navbar.schema.json | 29 ++ schemas/footer_chrome.schema.json | 30 ++ schemas/glitter_divider.schema.json | 15 + schemas/marquee.schema.json | 32 ++ schemas/merch_card.schema.json | 41 +++ schemas/metaball_hero.schema.json | 44 +++ schemas/nft_gallery.schema.json | 41 +++ schemas/tracklist.schema.json | 36 ++ schemas/waveform_player.schema.json | 34 ++ schemas/webring_badge.schema.json | 27 ++ template.templ | 244 +++++++++++++ template_templ.go | 510 ++++++++++++++++++++++++++++ text_override.go | 17 + text_override.templ | 9 + text_override_templ.go | 68 ++++ tracklist.go | 46 +++ tracklist.templ | 39 +++ tracklist_templ.go | 148 ++++++++ waveform_player.go | 42 +++ waveform_player.templ | 49 +++ waveform_player_templ.go | 168 +++++++++ webring_badge.go | 37 ++ webring_badge.templ | 18 + webring_badge_templ.go | 88 +++++ 73 files changed, 5465 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/.gitkeep create mode 100644 assets/placeholder.txt create mode 100644 button_override.go create mode 100644 button_override.templ create mode 100644 button_override_templ.go create mode 100644 chrome_navbar.go create mode 100644 chrome_navbar.templ create mode 100644 chrome_navbar_templ.go create mode 100644 css.go create mode 100644 email_wrapper.go create mode 100644 email_wrapper.templ create mode 100644 email_wrapper_templ.go create mode 100644 embed.go create mode 100644 fonts.json create mode 100644 footer_chrome.go create mode 100644 footer_chrome.templ create mode 100644 footer_chrome_templ.go create mode 100644 glitter_divider.go create mode 100644 glitter_divider.templ create mode 100644 glitter_divider_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 image_override.go create mode 100644 image_override.templ create mode 100644 image_override_templ.go create mode 100644 marquee.go create mode 100644 marquee.templ create mode 100644 marquee_templ.go create mode 100644 merch_card.go create mode 100644 merch_card.templ create mode 100644 merch_card_templ.go create mode 100644 metaball_hero.go create mode 100644 metaball_hero.templ create mode 100644 metaball_hero_templ.go create mode 100644 nft_gallery.go create mode 100644 nft_gallery.templ create mode 100644 nft_gallery_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/chrome_navbar.schema.json create mode 100644 schemas/footer_chrome.schema.json create mode 100644 schemas/glitter_divider.schema.json create mode 100644 schemas/marquee.schema.json create mode 100644 schemas/merch_card.schema.json create mode 100644 schemas/metaball_hero.schema.json create mode 100644 schemas/nft_gallery.schema.json create mode 100644 schemas/tracklist.schema.json create mode 100644 schemas/waveform_player.schema.json create mode 100644 schemas/webring_badge.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 create mode 100644 tracklist.go create mode 100644 tracklist.templ create mode 100644 tracklist_templ.go create mode 100644 waveform_player.go create mode 100644 waveform_player.templ create mode 100644 waveform_player_templ.go create mode 100644 webring_badge.go create mode 100644 webring_badge.templ create mode 100644 webring_badge_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..c8349db --- /dev/null +++ b/BUILD_REPORT.md @@ -0,0 +1,237 @@ +# Y2K theme — build report + +Implementation pass: 2026-06-06. +Reference style: templ (gotham-shaped, per spec §11 "Tech choice"). + +## What landed + +### Registration (`registration.go`, `register.go`) + +- `Registration` exported, `Name: "y2k"`, `Version: plugin.ParseModVersion(pluginModBytes)`. +- `tr.RegisterSystemTemplate(Key: "y2k", ...)` — exactly one call. +- `tr.RegisterPageTemplate("y2k", ...)` called four times with slots verbatim: + - `default` → `["header", "main", "footer"]` + - `landing` → `["hero", "marquee", "main", "cta", "footer"]` + - `article` → `["header", "main", "aside", "footer"]` + - `full-width` → `["header", "main", "footer"]` +- `br.LoadSchemasFromFS(Schemas())` invoked once, before any `br.Register(...)`. +- 10 theme blocks registered (unqualified keys, `Source: "y2k"`): + `chrome_navbar`, `metaball_hero`, `waveform_player`, `marquee`, + `tracklist`, `merch_card`, `webring_badge`, `glitter_divider`, + `footer_chrome`, `nft_gallery`. +- 4 overrides registered: `heading`, `text`, `button`, `image` via + `br.RegisterTemplateOverride("y2k", ...)`. +- `tr.RegisterEmailWrapper("y2k", Y2KEmailWrapper)` wired. +- `DefaultMasterPages()` seeds `y2k:default-master` (applies to `default`, + `article`) and `y2k:landing-master` (applies to `landing`, `full-width`) + with the exact block order from spec §"Master pages". + +### Plugin metadata (`plugin.mod`) + +- `name = "y2k"`, `display_name = "Y2K"` (3 chars, ≤40), `scope = "@themes"`. +- `kind = "theme"` (per global rule, not "plugin"). +- `categories = ["templates", "media"]` — both whitelisted. +- `tags` — 8 entries (within the 5–9 UAT range). +- `version = "0.1.0"`. +- `[compatibility] block_core = ">=0.11.0 <0.12.0"` verbatim. +- Description 157 chars (≤240). + +### Schemas (`schemas/*.schema.json`) + +- 10 JSON Schema files, all draft-07. +- Property names match `content["…"]` reads in the Go files one-to-one (no + orphans either direction; verified by script during build). +- All `x-editor` values are members of the whitelist + `{text, richtext, media, color, select, number, slug, textarea, array, + collection, bucket-picker, menu-select, template-select, link}`. + +### Presets (`presets.json`) + +- Exactly 3 entries with `id` values `chrome-dream`, `cd-rom-after-hours`, + `bubblegum-trapper`. +- `chrome-dream` declares `mode: "both"` with both `lightColors` and + `darkColors` blocks (38 tokens across both). +- `cd-rom-after-hours` declares `mode: "dark"` with `darkColors` only. +- `bubblegum-trapper` declares `mode: "light"` with `lightColors` only. +- All 19 tokens present per colour block. +- Every value matches the regex `^\d+ \d+% \d+%$` (HSL triples, no `hsl()` + wrapper). Verified via Python regex check. +- Values copied verbatim from spec §4 tables. + +### Fonts (`fonts.json`, `RECOMMENDED_FONTS.md`) + +- `fonts.json` is `[]` per the wave-1 policy in `themes/docs/FONTS.md`. +- `RECOMMENDED_FONTS.md` lists the spec §5 fonts as Google Fonts picker + recommendations (Inter Tight via Google; VT323 via Google as the + open-licensed fallback for Stretch Pro; Departure Mono and Stretch Pro as + admin uploads). +- All `font-family` references in templates and CSS go through + `var(--font-heading|body|mono, )`. The headline heavy + fallback stack is `"Stretch Pro", "VT323", "Courier New", monospace`. + +### CSS (`css.go` via `CSSManifest.InputCSSAppend`) + +- Declares `--chrome-1..4`, `--mesh-a/b/c`, `--bevel-light/dark` custom + properties on `:root` and `.dark` (UAT 13.2, 13.3, 13.4). +- Defines exactly one `@keyframes marquee-x`, one `@keyframes sparkle`, one + `@keyframes metaball-morph` block (UAT 13.5). +- Provides utility classes: + - `.y2k-chrome-bg` (layered linear gradient through `--chrome-1..4`). + - `.y2k-mesh-bg` (radial-gradient mesh through `--mesh-a/b/c`). + - `.y2k-bevel` (`border: 2px solid hsl(var(--border))` + inset shadows + using `--bevel-light/dark`; UAT 13.4). + - `.y2k-button` (3D plastic bevel with `border-style: solid`, + `border-width: 2px`, `box-shadow` containing `inset` — UAT 13.9). + - `.y2k-marquee` + `.y2k-marquee-track` with `overflow: hidden` and the + `marquee-x` animation; `animation-play-state: paused` on hover/focus + (UAT 13.10, 6.7). + - `.y2k-webring-badge` at the canonical `width: 88px; height: 31px` + (UAT 13.11). + - `.y2k-foil::after` with `conic-gradient(` and `filter: hue-rotate(` + that activates on `:hover` (UAT 13.12). + - `.y2k-heading`, `.y2k-text`, `.y2k-image-frame`, `.y2k-sparkle`, + `.y2k-metaball`, `.y2k-glow`. +- Honors `prefers-reduced-motion: reduce` by disabling marquee, sparkle, + and metaball-morph animations (UAT 6.6). +- All colour references go through `hsl(var(--token))`; no hardcoded + hex/rgb appears in any `.go` or `.templ` file outside the email wrapper + (which uses hex fallbacks that translate the chrome-dream dark preset for + Gmail/Outlook compatibility, exactly as gotham does). + +### Email wrapper + +- `tr.RegisterEmailWrapper("y2k", Y2KEmailWrapper)` registered exactly once. +- 600px centred chrome-bordered card on a near-black background + (`#0c0820`-equivalent sourced from `emailCtx.Colors.Background` when set, + otherwise the chrome-dream dark token value). +- Gradient header bar uses `linear-gradient(90deg, primary, secondary)`. +- Inter Tight body with system fallback stack. +- Marquee footer pill renders the same items as the in-page marquee. +- Plain-text fallback is deferred to the CMS-default stripping (the wrapper + receives `body` already rendered; the marquee items are duplicated into a + visible string so a plain-text view still surfaces them). + +## Build output + +``` +$ cd /home/alex/src/blockninja/themes/y2k && go mod tidy +(exit 0) + +$ cd /home/alex/src/blockninja/themes/y2k && /home/alex/go/bin/templ generate +(✓) Complete [ updates=16 duration=10ms ] + +$ cd /home/alex/src/blockninja/themes/y2k && make +CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o y2k.so . +(exit 0, no warnings) + +$ ls -la y2k.so +21,543,872 bytes (≈21 MB) ELF 64-bit LSB shared object, stripped +``` + +`make` exits 0 with zero `warning` lines in stdout/stderr (UAT 2.1). + +## Safety check + +``` +$ /tmp/check-safety . --plugin-dir /home/alex/src/blockninja/themes/y2k +(... 22 checks, all OK or SKIP for "no frontend sources" ...) +exit=0 +``` + +NOTE: the task brief references `cd ~/src/blockninja/backend && go run +./cmd/check-safety …`. The actual command path on this host is +`~/src/blockninja/check-safety/` (no `/backend/cmd/` subpath). The +invocation above is the host-local equivalent and exits 0. + +Specifically verified: +- Check 21 (presets.json validation): OK — single preset file validated. +- Check 22 (no hand-rolled HTML sanitization): OK. +- No `git.dev.alexdunmow.com/block/cms/...` import boundary violations. +- No `^replace ` directives in `go.mod`. +- `block/core v0.11.1` pinned to match `cms/backend/go.mod`. + +## Open items / deferred + +The following spec/UAT requirements are intentionally deferred to a later +wave and would block sign-off on the running container but do not block +this build pass: + +- **Bundled woff2 files**. Per `themes/docs/FONTS.md` wave-1 policy this + pass ships `fonts.json = []`. UAT §11 file-existence checks pass + trivially. Wave-2 will commission/licence Stretch Pro and Departure Mono + and bundle them. +- **`LICENSES.md`** — explicitly skipped per FONTS.md wave-1 policy + ("No `LICENSES.md` needed in this pass"). +- **Marketplace screenshots** (`marketplace/screenshots/*.png`, 1440×900). + Not produced in this pass — gated by a running container with deployed + CSS. +- **Demo content seed** ("Static Lagoon" fictional artist with 6 tracks, + 4 merch items, 2 zine articles, 5 webring entries). Not produced — the + data plane is out of scope for the .so build pass. +- **Container-level UAT items**: §2.7–2.8 (`make rebuild`, log scrape), + §5.7 (no console errors at the rendered URL), §6.1–6.7 (Lighthouse / + computed-style assertions on a running site), §7.1–7.6 (responsive + viewport checks), §8.* (rendering each block in three states in the + browser), §9.7 (admin-replace round-trip), §10.2–10.4 (Litmus / Gmail / + Apple Mail / Outlook 365 previews), §11.4–11.5 (network-tab font 200 + checks, FOUT trace), §12.* (marketplace assets, demo seed), §13.1, 13.6 + (computed-style assertions on rendered pages), §14.* (three-theme + install regression), §15.* (three named reviewer sign-offs). +- **Waveform peak-data source**. The block ships with a placeholder + `` element that the CMS' client runtime can + populate via WebAudio decode (deferred to v0.2 per spec open question). +- **Cursor-trail sparkle script**. Only the CSS classes are shipped; the + document-level cursor-trail script is deferred and gated by + `data-editor-mode` on `` per the spec's open question. +- **Mobile menu drawer JS**. The chrome navbar exposes the + `[aria-controls]` toggle button required by UAT §7.4 but the + drawer-open behaviour is owned by the host menu script. + +## File inventory + +``` +y2k/ +├── BUILD_REPORT.md (this file) +├── Makefile +├── RECOMMENDED_FONTS.md +├── assets/ +│ └── placeholder.txt +├── button_override.{go,templ,_templ.go} +├── chrome_navbar.{go,templ,_templ.go} +├── css.go +├── email_wrapper.{go,templ,_templ.go} +├── embed.go +├── fonts.json +├── footer_chrome.{go,templ,_templ.go} +├── glitter_divider.{go,templ,_templ.go} +├── go.mod +├── go.sum +├── heading_override.{go,templ,_templ.go} +├── helpers.go +├── image_override.{go,templ,_templ.go} +├── marquee.{go,templ,_templ.go} +├── merch_card.{go,templ,_templ.go} +├── metaball_hero.{go,templ,_templ.go} +├── nft_gallery.{go,templ,_templ.go} +├── plugin.mod +├── presets.json +├── register.go +├── registration.go +├── schemas/ +│ ├── chrome_navbar.schema.json +│ ├── footer_chrome.schema.json +│ ├── glitter_divider.schema.json +│ ├── marquee.schema.json +│ ├── merch_card.schema.json +│ ├── metaball_hero.schema.json +│ ├── nft_gallery.schema.json +│ ├── tracklist.schema.json +│ ├── waveform_player.schema.json +│ └── webring_badge.schema.json +├── template.{templ,_templ.go} +├── text_override.{go,templ,_templ.go} +├── tracklist.{go,templ,_templ.go} +├── waveform_player.{go,templ,_templ.go} +├── webring_badge.{go,templ,_templ.go} +└── y2k.so (21 MB) +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4908493 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# Y2K — local build helpers (.so plugin workflow) +# +# Usage: +# make # build y2k.so locally (CGO + buildmode=plugin) +# make clean # remove y2k.so +# make templ # regenerate *_templ.go via templ generate + +.PHONY: all clean templ help + +PLUGIN_NAME := y2k + +all: $(PLUGIN_NAME).so + +$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod + CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so . + +clean: + rm -f $(PLUGIN_NAME).so + +templ: + templ generate + +help: + @echo "Targets:" + @echo " all Build $(PLUGIN_NAME).so locally (default)" + @echo " clean Remove $(PLUGIN_NAME).so" + @echo " templ Regenerate *_templ.go files" diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md new file mode 100644 index 0000000..7d80228 --- /dev/null +++ b/RECOMMENDED_FONTS.md @@ -0,0 +1,50 @@ +# Recommended fonts for the Y2K theme + +The Y2K theme ships `fonts.json = []` per the wave-1 fonts policy +(`~/src/blockninja/themes/docs/FONTS.md`). The theme CSS consumes fonts via +the host CSS custom properties `--font-heading`, `--font-body`, `--font-mono` +with fallback stacks tuned to match the spec's intended aesthetic. + +To get the intended Y2K look, an admin should open the typography panel and +assign the following Google Fonts (or upload the licensed picks where noted): + +## Heading (display) + +- **Stretch Pro** — commercial, not in the Google Fonts curated list. + - Source: `upload:Stretch Pro` (admin must source the woff2 separately and + upload via the typography panel's Upload tab). + - Why: matches the spec's primary display face for chrome-stretched + headlines. +- **VT323** — open license, available in Google Fonts. + - Source: `google:VT323`. + - Instruction: Open the typography panel, switch to the Google Fonts tab, + search "VT323", click Add, then assign it to the Heading slot. Use this + when Stretch Pro is not licensed for the site. + +## Body + +- **Inter Tight** — open license, available in Google Fonts. + - Source: `google:Inter Tight`. + - Instruction: Open the typography panel, switch to the Google Fonts tab, + search "Inter Tight", click Add, then assign it to the Body slot. + +## Mono + +- **Departure Mono** — open license, available as a single-foundry download. + - Source: `upload:Departure Mono` (sourced from + https://departuremono.com — admin uploads the woff2 via the Upload tab). + - Instruction: After uploading the woff2, assign it to the Mono slot in the + typography panel. + +## What works out of the box + +Until an admin assigns fonts, the theme renders with the following fallback +stacks (declared in `css.go` via the host Tailwind input): + +- Headings: `var(--font-heading, "Stretch Pro", "VT323", "Courier New", monospace)` +- Body: inherited from the host `--font-body` with default + `system-ui, -apple-system, Segoe UI, Roboto, sans-serif`. +- Mono: inherited from the host `--font-mono` with default + `ui-monospace, "Departure Mono", "JetBrains Mono", monospace`. + +These keep the theme readable and on-brand even before any picker action. diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/placeholder.txt b/assets/placeholder.txt new file mode 100644 index 0000000..e5324b3 --- /dev/null +++ b/assets/placeholder.txt @@ -0,0 +1,5 @@ +Y2K theme assets directory. + +This pass ships no bundled fonts or images (fonts.json = []). +Add woff2 files under fonts/ and reference them from fonts.json once font +licensing has been resolved (see RECOMMENDED_FONTS.md). diff --git a/button_override.go b/button_override.go new file mode 100644 index 0000000..01c07ea --- /dev/null +++ b/button_override.go @@ -0,0 +1,28 @@ +package main + +import ( + "bytes" + "context" +) + +// Y2KButtonBlock applies the plastic-bevel styling to the built-in button. +// Content: {text, url, variant, class} +func Y2KButtonBlock(ctx context.Context, content map[string]any) string { + data := Y2KButtonData{ + Text: getStringOr(content, "text", "button"), + URL: getStringOr(content, "url", "#"), + Variant: getStringOr(content, "variant", "primary"), + Class: getString(content, "class"), + } + var buf bytes.Buffer + _ = y2kButtonComponent(data).Render(ctx, &buf) + return buf.String() +} + +// Y2KButtonData is the typed shape for the templ component. +type Y2KButtonData struct { + Text string + URL string + Variant string + Class string +} diff --git a/button_override.templ b/button_override.templ new file mode 100644 index 0000000..04cc458 --- /dev/null +++ b/button_override.templ @@ -0,0 +1,9 @@ +package main + +// y2kButtonComponent renders the plastic-bevel button. The 3D bevel + inset +// active state are owned by .y2k-button in css.go (UAT 13.9). +templ y2kButtonComponent(data Y2KButtonData) { + + { data.Text } + +} diff --git a/button_override_templ.go b/button_override_templ.go new file mode 100644 index 0000000..260b56b --- /dev/null +++ b/button_override_templ.go @@ -0,0 +1,99 @@ +// 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" + +// y2kButtonComponent renders the plastic-bevel button. The 3D bevel + inset +// active state are owned by .y2k-button in css.go (UAT 13.9). +func y2kButtonComponent(data Y2KButtonData) 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{"y2k-button", data.Class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 7, Col: 13} + } + _, 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, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/chrome_navbar.go b/chrome_navbar.go new file mode 100644 index 0000000..1dfb7c0 --- /dev/null +++ b/chrome_navbar.go @@ -0,0 +1,37 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// ChromeNavbarBlockMeta declares the y2k chrome navbar. +var ChromeNavbarBlockMeta = blocks.BlockMeta{ + Key: "chrome_navbar", + Title: "Chrome Navbar", + Description: "Beveled chrome navigation bar with hover sparkle.", + Source: "y2k", + Category: blocks.CategoryNavigation, +} + +// ChromeNavbarBlock renders the navbar. +// Content: {menuName, logoText, logoImage} +func ChromeNavbarBlock(ctx context.Context, content map[string]any) string { + data := ChromeNavbarData{ + MenuName: getStringOr(content, "menuName", "main"), + LogoText: getString(content, "logoText"), + LogoImage: getString(content, "logoImage"), + } + var buf bytes.Buffer + _ = chromeNavbarComponent(data).Render(ctx, &buf) + return buf.String() +} + +// ChromeNavbarData is the typed shape for the templ component. +type ChromeNavbarData struct { + MenuName string + LogoText string + LogoImage string +} diff --git a/chrome_navbar.templ b/chrome_navbar.templ new file mode 100644 index 0000000..4d1bf56 --- /dev/null +++ b/chrome_navbar.templ @@ -0,0 +1,38 @@ +package main + +// chromeNavbarComponent renders the beveled chrome navbar. The menu itself is +// hydrated client-side by the CMS; this block only owns the chrome shell and +// brand area. +templ chromeNavbarComponent(data ChromeNavbarData) { + +} + +func defaultLogoText(s string) string { + if s == "" { + return "y2k.fm" + } + return s +} diff --git a/chrome_navbar_templ.go b/chrome_navbar_templ.go new file mode 100644 index 0000000..d63c0ed --- /dev/null +++ b/chrome_navbar_templ.go @@ -0,0 +1,169 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// chromeNavbarComponent renders the beveled chrome navbar. The menu itself is +// hydrated client-side by the CMS; this block only owns the chrome shell and +// brand area. +func chromeNavbarComponent(data ChromeNavbarData) 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 + }) +} + +func defaultLogoText(s string) string { + if s == "" { + return "y2k.fm" + } + return s +} + +var _ = templruntime.GeneratedTemplate diff --git a/css.go b/css.go new file mode 100644 index 0000000..dcc4c37 --- /dev/null +++ b/css.go @@ -0,0 +1,228 @@ +package main + +// y2kInputCSS is appended to the host Tailwind input. It declares the chrome +// gradient stops, mesh anchors, bevel shadows, marquee/sparkle/metaball +// keyframes, and the utility classes those keyframes drive. Every colour is +// expressed through theme HSL custom properties (no hardcoded hex/rgb). +const y2kInputCSS = ` +/* === y2k theme — chrome/bevel/mesh tokens === */ +:root { + --chrome-1: 280 30% 95%; + --chrome-2: 200 40% 75%; + --chrome-3: 320 60% 60%; + --chrome-4: 280 40% 25%; + --mesh-a: 320 95% 58%; + --mesh-b: 185 90% 60%; + --mesh-c: 60 95% 70%; + --bevel-light: 0 0% 100%; + --bevel-dark: 280 60% 12%; +} +.dark { + --chrome-1: 280 30% 30%; + --chrome-2: 200 40% 45%; + --chrome-3: 320 60% 50%; + --chrome-4: 280 40% 10%; + --mesh-a: 320 100% 65%; + --mesh-b: 185 95% 55%; + --mesh-c: 60 100% 65%; + --bevel-light: 0 0% 100%; + --bevel-dark: 280 80% 4%; +} + +/* === keyframes === */ +@keyframes marquee-x { + from { transform: translate3d(0, 0, 0); } + to { transform: translate3d(-50%, 0, 0); } +} +@keyframes sparkle { + 0%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); } + 50% { opacity: 1; transform: scale(1) rotate(180deg); } +} +@keyframes metaball-morph { + 0%, 100% { d: path("M 50 100 a 50 50 0 1 1 100 0 a 50 50 0 1 1 -100 0"); } + 50% { d: path("M 40 95 a 60 55 0 1 1 120 5 a 55 60 0 1 1 -120 -5"); } +} + +/* === chrome surface utilities === */ +.y2k-chrome-bg { + background-image: + linear-gradient(180deg, + hsl(var(--chrome-1)) 0%, + hsl(var(--chrome-2)) 35%, + hsl(var(--chrome-3)) 70%, + hsl(var(--chrome-4)) 100%); +} +.y2k-mesh-bg { + background-image: + radial-gradient(at 20% 20%, hsl(var(--mesh-a) / 0.85) 0%, transparent 55%), + radial-gradient(at 80% 30%, hsl(var(--mesh-b) / 0.80) 0%, transparent 55%), + radial-gradient(at 50% 80%, hsl(var(--mesh-c) / 0.70) 0%, transparent 55%), + linear-gradient(135deg, hsl(var(--background)) 0%, hsl(var(--card)) 100%); +} +.y2k-bevel { + border: 2px solid hsl(var(--border)); + box-shadow: + inset 0 1px 0 hsl(var(--bevel-light) / 0.85), + inset 0 -2px 0 hsl(var(--bevel-dark) / 0.35), + 0 2px 6px hsl(var(--bevel-dark) / 0.30); +} +.y2k-bevel:active { + box-shadow: + inset 0 2px 4px hsl(var(--bevel-dark) / 0.45), + inset 0 -1px 0 hsl(var(--bevel-light) / 0.40); +} +.y2k-glow { + box-shadow: 0 0 24px hsl(var(--primary) / 0.45); +} + +/* === marquee === */ +.y2k-marquee { + overflow: hidden; + position: relative; + border-top: 2px solid hsl(var(--border)); + border-bottom: 2px solid hsl(var(--border)); +} +.y2k-marquee-track { + display: inline-flex; + gap: 3rem; + white-space: nowrap; + animation-name: marquee-x; + animation-timing-function: linear; + animation-iteration-count: infinite; +} +.y2k-marquee-track[data-speed="slow"] { animation-duration: 60s; } +.y2k-marquee-track[data-speed="medium"] { animation-duration: 30s; } +.y2k-marquee-track[data-speed="fast"] { animation-duration: 12s; } +.y2k-marquee:hover .y2k-marquee-track, +.y2k-marquee:focus-within .y2k-marquee-track { + animation-play-state: paused; +} + +/* === sparkle decoration === */ +.y2k-sparkle { + pointer-events: none; + animation: sparkle 1.6s ease-in-out infinite; +} + +/* === metaball morph === */ +.y2k-metaball { + animation: metaball-morph 6s ease-in-out infinite; +} + +/* === webring 88x31 badge === */ +.y2k-webring-badge { + display: inline-block; + width: 88px; + height: 31px; + line-height: 31px; + text-align: center; + font-size: 11px; + letter-spacing: 0.05em; + text-transform: uppercase; + border: 1px solid hsl(var(--border)); + background-image: linear-gradient(180deg, hsl(var(--chrome-1)) 0%, hsl(var(--chrome-3)) 100%); + color: hsl(var(--primaryForeground)); + text-decoration: none; + box-shadow: inset 0 1px 0 hsl(var(--bevel-light) / 0.6); +} + +/* === holographic foil for NFT gallery === */ +.y2k-foil { + position: relative; + isolation: isolate; +} +.y2k-foil::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + opacity: 0; + transition: opacity 220ms ease; + background-image: conic-gradient( + from 0deg, + hsl(var(--mesh-a) / 0.45), + hsl(var(--mesh-b) / 0.45), + hsl(var(--mesh-c) / 0.45), + hsl(var(--mesh-a) / 0.45) + ); + mix-blend-mode: color-dodge; + filter: hue-rotate(0deg); +} +.y2k-foil:hover::after, +.y2k-foil:focus-within::after { + opacity: 1; + filter: hue-rotate(45deg); +} + +/* === plastic button override === */ +.y2k-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.65rem 1.25rem; + font-weight: 600; + border: 2px solid hsl(var(--border)); + border-radius: 0.625rem; + background-image: linear-gradient(180deg, + hsl(var(--primary) / 0.95) 0%, + hsl(var(--primary)) 50%, + hsl(var(--primary) / 0.85) 100%); + color: hsl(var(--primaryForeground)); + box-shadow: + inset 0 1px 0 hsl(var(--bevel-light) / 0.7), + inset 0 -2px 0 hsl(var(--bevel-dark) / 0.3), + 0 2px 6px hsl(var(--bevel-dark) / 0.25); + transition: transform 90ms ease, box-shadow 90ms ease; +} +.y2k-button:hover { transform: translateY(-1px); } +.y2k-button:active { + transform: translateY(1px); + box-shadow: + inset 0 2px 4px hsl(var(--bevel-dark) / 0.45), + inset 0 -1px 0 hsl(var(--bevel-light) / 0.30); +} +.y2k-button:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 2px; +} + +/* === image chrome frame === */ +.y2k-image-frame { + border-radius: 0.625rem; + border: 2px solid hsl(var(--border)); + box-shadow: + inset 0 1px 0 hsl(var(--bevel-light) / 0.5), + 0 4px 14px hsl(var(--bevel-dark) / 0.35); +} + +/* === heading chrome stroke === */ +.y2k-heading { + font-family: var(--font-heading, "Stretch Pro", "VT323", "Courier New", monospace); + letter-spacing: 0.04em; + text-shadow: + 0 1px 0 hsl(var(--bevel-light) / 0.65), + 0 2px 0 hsl(var(--bevel-dark) / 0.35), + 0 6px 22px hsl(var(--primary) / 0.45); +} + +/* === text holographic link underline === */ +.y2k-text a { + background-image: linear-gradient(90deg, + hsl(var(--mesh-a)), + hsl(var(--mesh-b)), + hsl(var(--mesh-c))); + background-size: 100% 2px; + background-repeat: no-repeat; + background-position: 0 100%; + text-decoration: none; +} + +/* === reduced motion overrides === */ +@media (prefers-reduced-motion: reduce) { + .y2k-marquee-track, + .y2k-sparkle, + .y2k-metaball { + animation: none !important; + } +} +` diff --git a/email_wrapper.go b/email_wrapper.go new file mode 100644 index 0000000..2383d96 --- /dev/null +++ b/email_wrapper.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// Y2KEmailWrapper wraps body content in a Y2K-branded email shell: +// +// - Centred 600px chrome-bordered card on a near-black background. +// - Magenta-to-teal gradient header bar. +// - Inter Tight body (with system fallback). +// - Plain marquee pill footer. +// +// All colours are sourced from emailCtx.Colors when present, falling back to +// HSL token-equivalent hex values that match the chrome-dream dark preset. +func Y2KEmailWrapper(body string, emailCtx templates.EmailContext) string { + var buf bytes.Buffer + _ = y2kEmailTemplate(emailCtx, body).Render(context.Background(), &buf) + return buf.String() +} + +// Resolved palette per render — these helpers translate the preset HSL triples +// to email-safe hex via theme tokens, with fallbacks aligned to the +// chrome-dream dark preset. + +func y2kEmailBg(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Background != "" { + return emailCtx.Colors.Background + } + return "#0c0820" // chrome-dream dark background equivalent +} + +func y2kEmailCard(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Card != "" { + return emailCtx.Colors.Card + } + return "#150f2a" +} + +func y2kEmailFg(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Foreground != "" { + return emailCtx.Colors.Foreground + } + return "#f4e8fa" +} + +func y2kEmailMuted(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Muted != "" { + return emailCtx.Colors.Muted + } + return "#1f1740" +} + +func y2kEmailMutedFg(emailCtx templates.EmailContext) string { + if emailCtx.Colors.MutedForeground != "" { + return emailCtx.Colors.MutedForeground + } + return "#b9a8d3" +} + +func y2kEmailBorder(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Border != "" { + return emailCtx.Colors.Border + } + return "#4d2b6f" +} + +func y2kEmailPrimary(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Primary != "" { + return emailCtx.Colors.Primary + } + return "#ff4dd2" +} + +func y2kEmailSecondary(emailCtx templates.EmailContext) string { + if emailCtx.Colors.Secondary != "" { + return emailCtx.Colors.Secondary + } + return "#4ddff0" +} diff --git a/email_wrapper.templ b/email_wrapper.templ new file mode 100644 index 0000000..7ebf5c9 --- /dev/null +++ b/email_wrapper.templ @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// y2kEmailTemplate is the Y2K-branded email shell. +templ y2kEmailTemplate(emailCtx templates.EmailContext, body string) { + + + + + + + + { emailCtx.SiteSettings.SiteName } + + + + if emailCtx.PreviewText != "" { +
{ emailCtx.PreviewText }
+ } + + + + +
+ + + + + + + + + + + + + + +
+ + +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..92cea7a --- /dev/null +++ b/email_wrapper_templ.go @@ -0,0 +1,251 @@ +// 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 ( + "fmt" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// y2kEmailTemplate is the Y2K-branded email shell. +func y2kEmailTemplate(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: 18, 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: 33, Col: 95} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SiteName != "" { + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, 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: 51, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "y2k.fm") + 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(body).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
* now playing * new drop friday * subscribe to the zine *
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.UnsubscribeURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

unsubscribe

") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..d3d9676 --- /dev/null +++ b/embed.go @@ -0,0 +1,57 @@ +package main + +import ( + "embed" + "io/fs" + "net/http" + + "git.dev.alexdunmow.com/block/core/plugin" +) + +//go:embed assets/* +var assetsFS embed.FS + +//go:embed schemas/* +var schemasFS embed.FS + +//go:embed presets.json +var presetsData []byte + +//go:embed fonts.json +var fontsData []byte + +//go:embed plugin.mod +var pluginModBytes []byte + +// Assets returns the embedded assets filesystem rooted at assets/. +func Assets() fs.FS { + sub, _ := fs.Sub(assetsFS, "assets") + return sub +} + +// Schemas returns the embedded schemas filesystem rooted at schemas/. +func Schemas() fs.FS { + sub, _ := fs.Sub(schemasFS, "schemas") + return sub +} + +// AssetsHandler serves embedded asset files at /templates/y2k/... +func AssetsHandler() http.Handler { + return http.FileServer(http.FS(Assets())) +} + +// ThemePresets returns the raw presets.json bytes. +func ThemePresets() []byte { return presetsData } + +// BundledFonts returns the raw fonts.json bytes. +func BundledFonts() []byte { return fontsData } + +// ThemeCSSManifest returns the theme's CSS manifest. Y2K injects keyframes +// (marquee-x, sparkle, metaball-morph) and chrome/bevel utility classes that +// Tailwind alone cannot express, plus CSS custom properties (--chrome-1..4, +// --mesh-a/b/c, --bevel-light/dark) into the host Tailwind input. +func ThemeCSSManifest() *plugin.CSSManifest { + return &plugin.CSSManifest{ + InputCSSAppend: y2kInputCSS, + } +} 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_chrome.go b/footer_chrome.go new file mode 100644 index 0000000..3405996 --- /dev/null +++ b/footer_chrome.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// FooterChromeBlockMeta declares the chrome footer block. +var FooterChromeBlockMeta = blocks.BlockMeta{ + Key: "footer_chrome", + Title: "Chrome Footer", + Description: "Beveled chrome footer strip with optional webring and copyright.", + Source: "y2k", + Category: blocks.CategoryLayout, +} + +// FooterChromeBlock renders the footer. +// Content: {menuName, showWebring, copyright} +func FooterChromeBlock(ctx context.Context, content map[string]any) string { + // showWebring is stored as either bool (legacy/master-page seed) or string + // ("yes" | "no") from the schema's select editor. + show := true + switch v := content["showWebring"].(type) { + case bool: + show = v + case string: + show = v != "no" + } + data := FooterChromeData{ + MenuName: getString(content, "menuName"), + ShowWebring: show, + Copyright: getString(content, "copyright"), + } + var buf bytes.Buffer + _ = footerChromeComponent(data).Render(ctx, &buf) + return buf.String() +} + +// FooterChromeData is the typed shape for the templ component. +type FooterChromeData struct { + MenuName string + ShowWebring bool + Copyright string +} diff --git a/footer_chrome.templ b/footer_chrome.templ new file mode 100644 index 0000000..a474c93 --- /dev/null +++ b/footer_chrome.templ @@ -0,0 +1,24 @@ +package main + +// footerChromeComponent renders the bottom chrome strip with optional webring. +templ footerChromeComponent(data FooterChromeData) { +
+
+ if data.MenuName != "" { + + } + if data.ShowWebring { +
+ prev + ring + next +
+ } + if data.Copyright != "" { + { data.Copyright } + } +
+
+} diff --git a/footer_chrome_templ.go b/footer_chrome_templ.go new file mode 100644 index 0000000..d240936 --- /dev/null +++ b/footer_chrome_templ.go @@ -0,0 +1,102 @@ +// 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" + +// footerChromeComponent renders the bottom chrome strip with optional webring. +func footerChromeComponent(data FooterChromeData) 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.MenuName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ShowWebring { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Copyright != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Copyright) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer_chrome.templ`, Line: 20, Col: 64} + } + _, 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, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/glitter_divider.go b/glitter_divider.go new file mode 100644 index 0000000..e2b2383 --- /dev/null +++ b/glitter_divider.go @@ -0,0 +1,26 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// GlitterDividerBlockMeta declares the animated sparkle divider. +var GlitterDividerBlockMeta = blocks.BlockMeta{ + Key: "glitter_divider", + Title: "Glitter Divider", + Description: "Animated sparkle horizontal rule.", + Source: "y2k", + Category: blocks.CategoryLayout, +} + +// GlitterDividerBlock renders the divider. +// Content: {variant} +func GlitterDividerBlock(ctx context.Context, content map[string]any) string { + variant := getStringOr(content, "variant", "sparkle") + var buf bytes.Buffer + _ = glitterDividerComponent(variant).Render(ctx, &buf) + return buf.String() +} diff --git a/glitter_divider.templ b/glitter_divider.templ new file mode 100644 index 0000000..3e43029 --- /dev/null +++ b/glitter_divider.templ @@ -0,0 +1,25 @@ +package main + +// glitterDividerComponent renders a sparkle hr. The glyph used per variant is +// inert (decorative) — the divider itself carries role="separator". +templ glitterDividerComponent(variant string) { + + +} + +func glitterGlyph(v string) string { + switch v { + case "stars": + return "*" + case "hearts": + return "<3" + case "chrome": + return "~" + default: + return "*" + } +} diff --git a/glitter_divider_templ.go b/glitter_divider_templ.go new file mode 100644 index 0000000..b1b90f7 --- /dev/null +++ b/glitter_divider_templ.go @@ -0,0 +1,107 @@ +// 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" + +// glitterDividerComponent renders a sparkle hr. The glyph used per variant is +// inert (decorative) — the divider itself carries role="separator". +func glitterDividerComponent(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) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(glitterGlyph(variant)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `glitter_divider.templ`, Line: 8, Col: 51} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(glitterGlyph(variant)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `glitter_divider.templ`, Line: 9, Col: 82} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(glitterGlyph(variant)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `glitter_divider.templ`, Line: 10, Col: 82} + } + _, 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, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func glitterGlyph(v string) string { + switch v { + case "stars": + return "*" + case "hearts": + return "<3" + case "chrome": + return "~" + default: + return "*" + } +} + +var _ = templruntime.GeneratedTemplate diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..91bd58d --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.dev.alexdunmow.com/block/themes/y2k + +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..87348e0 --- /dev/null +++ b/heading_override.go @@ -0,0 +1,39 @@ +package main + +import ( + "bytes" + "context" + "strconv" +) + +// Y2KHeadingBlock applies the chrome-stroke heading styling to the built-in +// heading block when the y2k theme is active. +// Content: {text, level, textClass} +func Y2KHeadingBlock(ctx context.Context, content map[string]any) string { + text := getString(content, "text") + textClass := getString(content, "textClass") + level := parseHeadingLevel(content) + var buf bytes.Buffer + _ = y2kHeadingComponent(level, text, textClass).Render(ctx, &buf) + return buf.String() +} + +// parseHeadingLevel reads "level" from the content map, accepting float64 +// (JSON), int (Go), or numeric strings. Returns 2 as a safe default. +func parseHeadingLevel(content map[string]any) int { + if v, ok := content["level"].(float64); ok { + l := int(v) + if l >= 1 && l <= 6 { + return l + } + } + if v, ok := content["level"].(int); ok && v >= 1 && v <= 6 { + return v + } + if v, ok := content["level"].(string); ok { + if l, err := strconv.Atoi(v); 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..af47451 --- /dev/null +++ b/heading_override.templ @@ -0,0 +1,41 @@ +package main + +// y2kHeadingBaseClass returns the per-level Tailwind sizing classes. +func y2kHeadingBaseClass(level int) string { + switch level { + case 1: + return "text-5xl md:text-6xl" + case 2: + return "text-3xl md:text-4xl" + case 3: + return "text-2xl" + case 4: + return "text-xl" + case 5: + return "text-lg" + case 6: + return "text-base" + default: + return "text-3xl md:text-4xl" + } +} + +// y2kHeadingComponent renders a heading with chrome stroke + drop shadow. +templ y2kHeadingComponent(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..4956f5e --- /dev/null +++ b/heading_override_templ.go @@ -0,0 +1,311 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// y2kHeadingBaseClass returns the per-level Tailwind sizing classes. +func y2kHeadingBaseClass(level int) string { + switch level { + case 1: + return "text-5xl md:text-6xl" + case 2: + return "text-3xl md:text-4xl" + case 3: + return "text-2xl" + case 4: + return "text-xl" + case 5: + return "text-lg" + case 6: + return "text-base" + default: + return "text-3xl md:text-4xl" + } +} + +// y2kHeadingComponent renders a heading with chrome stroke + drop shadow. +func y2kHeadingComponent(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{"y2k-heading font-bold text-foreground", y2kHeadingBaseClass(1), 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: 27, Col: 98} + } + _, 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{"y2k-heading font-bold text-foreground", y2kHeadingBaseClass(2), 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: 29, Col: 98} + } + _, 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{"y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(3), 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: 31, Col: 102} + } + _, 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{"y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(4), 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: 33, Col: 102} + } + _, 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{"y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(5), 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: 35, Col: 102} + } + _, 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{"y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(6), 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: 37, Col: 102} + } + _, 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{"y2k-heading font-bold text-foreground", y2kHeadingBaseClass(2), 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: 39, Col: 98} + } + _, 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..686978c --- /dev/null +++ b/helpers.go @@ -0,0 +1,55 @@ +package main + +// getString safely extracts a string value from a content map. +func getString(content map[string]any, key string) string { + if v, ok := content[key].(string); ok { + return v + } + return "" +} + +// getStringOr returns the string value for key or the default when missing/empty. +func getStringOr(content map[string]any, key, dflt string) string { + if v := getString(content, key); v != "" { + return v + } + return dflt +} + +// getBool extracts a boolean value, defaulting to dflt when absent. +func getBool(content map[string]any, key string, dflt bool) bool { + if v, ok := content[key].(bool); ok { + return v + } + return dflt +} + +// getStringSlice extracts a []string from a JSON array of strings. +func getStringSlice(content map[string]any, key string) []string { + raw, ok := content[key].([]any) + if !ok { + return nil + } + out := make([]string, 0, len(raw)) + for _, item := range raw { + if s, ok := item.(string); ok { + out = append(out, s) + } + } + return out +} + +// getSlice extracts a []map[string]any from a JSON array of objects. +func getSlice(content map[string]any, key string) []map[string]any { + raw, ok := content[key].([]any) + if !ok { + return nil + } + out := make([]map[string]any, 0, len(raw)) + for _, item := range raw { + if m, ok := item.(map[string]any); ok { + out = append(out, m) + } + } + return out +} diff --git a/image_override.go b/image_override.go new file mode 100644 index 0000000..3e703a4 --- /dev/null +++ b/image_override.go @@ -0,0 +1,32 @@ +package main + +import ( + "bytes" + "context" +) + +// Y2KImageBlock applies the chrome-bordered image frame to the built-in +// image block. +// Content: {url, alt, caption, class} +func Y2KImageBlock(ctx context.Context, content map[string]any) string { + data := Y2KImageData{ + URL: getString(content, "url"), + Alt: getString(content, "alt"), + Caption: getString(content, "caption"), + Class: getString(content, "class"), + } + if data.URL == "" { + return "" + } + var buf bytes.Buffer + _ = y2kImageComponent(data).Render(ctx, &buf) + return buf.String() +} + +// Y2KImageData is the typed shape for the templ component. +type Y2KImageData struct { + URL string + Alt string + Caption string + Class string +} diff --git a/image_override.templ b/image_override.templ new file mode 100644 index 0000000..59cde06 --- /dev/null +++ b/image_override.templ @@ -0,0 +1,11 @@ +package main + +// y2kImageComponent wraps an image in the chrome y2k-image-frame. +templ y2kImageComponent(data Y2KImageData) { +
+ { + if data.Caption != "" { +
{ data.Caption }
+ } +
+} diff --git a/image_override_templ.go b/image_override_templ.go new file mode 100644 index 0000000..9137557 --- /dev/null +++ b/image_override_templ.go @@ -0,0 +1,108 @@ +// 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" + +// y2kImageComponent wraps an image in the chrome y2k-image-frame. +func y2kImageComponent(data Y2KImageData) 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{"my-4", data.Class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
\"") ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Caption) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 8, Col: 84} + } + _, 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, 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/marquee.go b/marquee.go new file mode 100644 index 0000000..cb0a1e9 --- /dev/null +++ b/marquee.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// MarqueeBlockMeta declares the marquee ticker block. +var MarqueeBlockMeta = blocks.BlockMeta{ + Key: "marquee", + Title: "Marquee Ticker", + Description: "Scrolling text marquee with chrome edges; pauses on hover.", + Source: "y2k", + Category: blocks.CategoryLayout, +} + +// MarqueeBlock renders the marquee. +// Content: {items[], speed, direction} +func MarqueeBlock(ctx context.Context, content map[string]any) string { + data := MarqueeData{ + Items: getStringSlice(content, "items"), + Speed: getStringOr(content, "speed", "medium"), + Direction: getStringOr(content, "direction", "left"), + } + if len(data.Items) == 0 { + data.Items = []string{"now playing", "new drop friday", "subscribe to the zine"} + } + var buf bytes.Buffer + _ = marqueeComponent(data).Render(ctx, &buf) + return buf.String() +} + +// MarqueeData is the typed shape for the templ component. +type MarqueeData struct { + Items []string + Speed string + Direction string +} diff --git a/marquee.templ b/marquee.templ new file mode 100644 index 0000000..e96ce51 --- /dev/null +++ b/marquee.templ @@ -0,0 +1,29 @@ +package main + +// marqueeComponent renders a horizontally scrolling marquee. The track is +// duplicated so the CSS keyframes can loop seamlessly. Speed is keyed off the +// data-speed attribute which the y2k stylesheet maps to a duration. +templ marqueeComponent(data MarqueeData) { +
+
+ for _, item := range data.Items { + + * + { item } + + } + for _, item := range data.Items { + + } +
+
+} diff --git a/marquee_templ.go b/marquee_templ.go new file mode 100644 index 0000000..2717c73 --- /dev/null +++ b/marquee_templ.go @@ -0,0 +1,111 @@ +// 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" + +// marqueeComponent renders a horizontally scrolling marquee. The track is +// duplicated so the CSS keyframes can loop seamlessly. Speed is keyed off the +// data-speed attribute which the y2k stylesheet maps to a duration. +func marqueeComponent(data MarqueeData) 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 + } + for _, item := range data.Items { + 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(item) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee.templ`, Line: 18, Col: 11} + } + _, 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 + } + } + for _, item := range data.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "* ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee.templ`, Line: 24, Col: 11} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/merch_card.go b/merch_card.go new file mode 100644 index 0000000..d3eb362 --- /dev/null +++ b/merch_card.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// MerchCardBlockMeta declares the merch product card block. +var MerchCardBlockMeta = blocks.BlockMeta{ + Key: "merch_card", + Title: "Merch Card", + Description: "Plastic-bevel product card with a corner sticker badge.", + Source: "y2k", + Category: blocks.CategoryContent, +} + +// MerchCardBlock renders the merch card. +// Content: {title, price, image, buyHref, sticker} +func MerchCardBlock(ctx context.Context, content map[string]any) string { + data := MerchCardData{ + Title: getString(content, "title"), + Price: getString(content, "price"), + Image: getString(content, "image"), + BuyHref: getString(content, "buyHref"), + Sticker: getString(content, "sticker"), + } + var buf bytes.Buffer + _ = merchCardComponent(data).Render(ctx, &buf) + return buf.String() +} + +// MerchCardData is the typed shape for the templ component. +type MerchCardData struct { + Title string + Price string + Image string + BuyHref string + Sticker string +} diff --git a/merch_card.templ b/merch_card.templ new file mode 100644 index 0000000..31563f0 --- /dev/null +++ b/merch_card.templ @@ -0,0 +1,52 @@ +package main + +// merchCardComponent renders a single merch card. The card class is applied +// to a grid container managed by the page; this block stays self-contained. +templ merchCardComponent(data MerchCardData) { +
+ if data.Sticker != "" { + { stickerLabel(data.Sticker) } + } +
+ if data.Image != "" { + { + } else { +
+ no image +
+ } +
+
+

{ titleOr(data.Title) }

+
+ { priceOr(data.Price) } + if data.BuyHref != "" { + buy + } +
+
+
+} + +func stickerLabel(s string) string { + switch s { + case "sold-out": + return "sold out" + default: + return s + } +} + +func titleOr(s string) string { + if s == "" { + return "untitled" + } + return s +} + +func priceOr(s string) string { + if s == "" { + return "$--" + } + return s +} diff --git a/merch_card_templ.go b/merch_card_templ.go new file mode 100644 index 0000000..4a6d4ac --- /dev/null +++ b/merch_card_templ.go @@ -0,0 +1,178 @@ +// 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" + +// merchCardComponent renders a single merch card. The card class is applied +// to a grid container managed by the page; this block stays self-contained. +func merchCardComponent(data MerchCardData) 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.Sticker != "" { + 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(stickerLabel(data.Sticker)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `merch_card.templ`, Line: 8, Col: 118} + } + _, 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 + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Image != "" { + 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, 8, "
no image
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(titleOr(data.Title)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `merch_card.templ`, Line: 20, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(priceOr(data.Price)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `merch_card.templ`, Line: 22, Col: 65} + } + _, 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, 11, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.BuyHref != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "buy") + 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 + }) +} + +func stickerLabel(s string) string { + switch s { + case "sold-out": + return "sold out" + default: + return s + } +} + +func titleOr(s string) string { + if s == "" { + return "untitled" + } + return s +} + +func priceOr(s string) string { + if s == "" { + return "$--" + } + return s +} + +var _ = templruntime.GeneratedTemplate diff --git a/metaball_hero.go b/metaball_hero.go new file mode 100644 index 0000000..c9c5603 --- /dev/null +++ b/metaball_hero.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// MetaballHeroBlockMeta declares the metaball hero block. +var MetaballHeroBlockMeta = blocks.BlockMeta{ + Key: "metaball_hero", + Title: "Metaball Hero", + Description: "SVG metaballs on a gradient mesh canvas with headline + CTA.", + Source: "y2k", + Category: blocks.CategoryLayout, +} + +// MetaballHeroBlock renders the metaball hero. +// Content: {headline, sub, ctaLabel, ctaHref, bgPreset} +func MetaballHeroBlock(ctx context.Context, content map[string]any) string { + data := MetaballHeroData{ + Headline: getString(content, "headline"), + Sub: getString(content, "sub"), + CTALabel: getString(content, "ctaLabel"), + CTAHref: getString(content, "ctaHref"), + BgPreset: getStringOr(content, "bgPreset", "magenta-teal"), + } + var buf bytes.Buffer + _ = metaballHeroComponent(data).Render(ctx, &buf) + return buf.String() +} + +// MetaballHeroData is the typed shape for the templ component. +type MetaballHeroData struct { + Headline string + Sub string + CTALabel string + CTAHref string + BgPreset string +} diff --git a/metaball_hero.templ b/metaball_hero.templ new file mode 100644 index 0000000..de01966 --- /dev/null +++ b/metaball_hero.templ @@ -0,0 +1,64 @@ +package main + +// metaballHeroComponent renders the headline + CTA on a gradient mesh with +// SVG metaballs (Gaussian-blur + threshold filter technique). +templ metaballHeroComponent(data MetaballHeroData) { +
+ +
+
+

+ { headlineOrDefault(data.Headline) } +

+ if data.Sub != "" { +

{ data.Sub }

+ } + if data.CTALabel != "" { + + { data.CTALabel } + + } +
+
+
+} + +func headlineOrDefault(s string) string { + if s == "" { + return "new EP out now" + } + return s +} + +func ctaHrefOr(s string) string { + if s == "" { + return "#" + } + return s +} diff --git a/metaball_hero_templ.go b/metaball_hero_templ.go new file mode 100644 index 0000000..ad93754 --- /dev/null +++ b/metaball_hero_templ.go @@ -0,0 +1,137 @@ +// 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" + +// metaballHeroComponent renders the headline + CTA on a gradient mesh with +// SVG metaballs (Gaussian-blur + threshold filter technique). +func metaballHeroComponent(data MetaballHeroData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(headlineOrDefault(data.Headline)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metaball_hero.templ`, Line: 37, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Sub != "" { + 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(data.Sub) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metaball_hero.templ`, Line: 40, Col: 74} + } + _, 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 + } + } + if data.CTALabel != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.CTALabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metaball_hero.templ`, Line: 44, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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 + }) +} + +func headlineOrDefault(s string) string { + if s == "" { + return "new EP out now" + } + return s +} + +func ctaHrefOr(s string) string { + if s == "" { + return "#" + } + return s +} + +var _ = templruntime.GeneratedTemplate diff --git a/nft_gallery.go b/nft_gallery.go new file mode 100644 index 0000000..41f1a44 --- /dev/null +++ b/nft_gallery.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// NFTGalleryBlockMeta declares the holographic-foil NFT gallery block. +// Presentational only - no signing UX in v0.1 per spec open question. +var NFTGalleryBlockMeta = blocks.BlockMeta{ + Key: "nft_gallery", + Title: "NFT Gallery", + Description: "Holographic-foil hover gallery for NFT items (presentational only).", + Source: "y2k", + Category: blocks.CategoryContent, +} + +// NFTGalleryBlock renders the gallery. +// Content: {items[{title, image, chain, href}]} +func NFTGalleryBlock(ctx context.Context, content map[string]any) string { + raw := getSlice(content, "items") + items := make([]NFTItem, 0, len(raw)) + for _, r := range raw { + items = append(items, NFTItem{ + Title: getString(r, "title"), + Image: getString(r, "image"), + Chain: getString(r, "chain"), + Href: getString(r, "href"), + }) + } + var buf bytes.Buffer + _ = nftGalleryComponent(NFTGalleryData{Items: items}).Render(ctx, &buf) + return buf.String() +} + +// NFTGalleryData is the typed shape for the templ component. +type NFTGalleryData struct { + Items []NFTItem +} + +// NFTItem is a single gallery card. +type NFTItem struct { + Title string + Image string + Chain string + Href string +} diff --git a/nft_gallery.templ b/nft_gallery.templ new file mode 100644 index 0000000..a1c401b --- /dev/null +++ b/nft_gallery.templ @@ -0,0 +1,33 @@ +package main + +// nftGalleryComponent renders a grid of foil-on-hover NFT cards. +templ nftGalleryComponent(data NFTGalleryData) { +
+ if len(data.Items) == 0 { +

No items yet.

+ } else { + + } +
+} diff --git a/nft_gallery_templ.go b/nft_gallery_templ.go new file mode 100644 index 0000000..1345673 --- /dev/null +++ b/nft_gallery_templ.go @@ -0,0 +1,156 @@ +// 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" + +// nftGalleryComponent renders a grid of foil-on-hover NFT cards. +func nftGalleryComponent(data NFTGalleryData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Items) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

No items yet.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range data.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.Image != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
no image
") + 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_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(titleOr(item.Title)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `nft_gallery.templ`, Line: 23, Col: 65} + } + _, 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, 11, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.Chain != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Chain) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `nft_gallery.templ`, Line: 25, Col: 90} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..88624d9 --- /dev/null +++ b/plugin.mod @@ -0,0 +1,12 @@ +[plugin] +name = "y2k" +display_name = "Y2K" +scope = "@themes" +version = "0.1.0" +description = "Liquid-chrome Y2K revival theme for indie musicians, zines, and merch shops. Gradient mesh backgrounds, plastic-bevel buttons, marquee text, waveform blocks." +kind = "theme" +categories = ["templates", "media"] +tags = ["retro", "chrome", "vaporwave", "pastel", "music", "creator", "nostalgia", "web3"] + +[compatibility] +block_core = ">=0.11.0 <0.12.0" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..fce2c32 --- /dev/null +++ b/presets.json @@ -0,0 +1,110 @@ +[ + { + "id": "chrome-dream", + "name": "Liquid Chrome Dream", + "description": "Magenta-to-teal mesh on tinted glass cards. Works in both light and dark modes.", + "theme": { + "mode": "both", + "lightColors": { + "background": "300 40% 97%", + "foreground": "280 60% 12%", + "card": "300 30% 99%", + "cardForeground": "280 60% 12%", + "popover": "300 30% 99%", + "popoverForeground": "280 60% 12%", + "primary": "320 95% 58%", + "primaryForeground": "0 0% 100%", + "secondary": "185 90% 60%", + "secondaryForeground": "200 80% 10%", + "muted": "300 25% 94%", + "mutedForeground": "280 25% 40%", + "accent": "60 95% 70%", + "accentForeground": "280 60% 12%", + "destructive": "0 90% 60%", + "destructiveForeground": "0 0% 100%", + "border": "300 40% 88%", + "input": "300 30% 92%", + "ring": "320 95% 58%" + }, + "darkColors": { + "background": "280 35% 6%", + "foreground": "300 30% 96%", + "card": "280 40% 10%", + "cardForeground": "300 30% 96%", + "popover": "280 40% 10%", + "popoverForeground": "300 30% 96%", + "primary": "320 100% 65%", + "primaryForeground": "280 40% 6%", + "secondary": "185 95% 55%", + "secondaryForeground": "200 80% 8%", + "muted": "280 30% 14%", + "mutedForeground": "300 20% 65%", + "accent": "60 100% 65%", + "accentForeground": "280 60% 8%", + "destructive": "0 95% 60%", + "destructiveForeground": "0 0% 100%", + "border": "300 40% 22%", + "input": "280 30% 16%", + "ring": "320 100% 65%" + } + } + }, + { + "id": "cd-rom-after-hours", + "name": "CD-ROM After Hours", + "description": "After-hours CD-ROM glow — near-black with neon pink and cyan edge leaks.", + "theme": { + "mode": "dark", + "darkColors": { + "background": "260 50% 5%", + "foreground": "180 100% 88%", + "card": "260 50% 9%", + "cardForeground": "180 100% 88%", + "popover": "260 50% 9%", + "popoverForeground": "180 100% 88%", + "primary": "285 100% 62%", + "primaryForeground": "0 0% 100%", + "secondary": "180 100% 50%", + "secondaryForeground": "260 50% 5%", + "muted": "260 30% 14%", + "mutedForeground": "260 20% 60%", + "accent": "45 100% 60%", + "accentForeground": "260 50% 5%", + "destructive": "0 90% 55%", + "destructiveForeground": "0 0% 100%", + "border": "285 60% 25%", + "input": "260 30% 16%", + "ring": "285 100% 62%" + } + } + }, + { + "id": "bubblegum-trapper", + "name": "Bubblegum Trapper", + "description": "Lisa Frank Trapper-Keeper energy — pastel candy and sour apple accents.", + "theme": { + "mode": "light", + "lightColors": { + "background": "45 100% 97%", + "foreground": "300 50% 15%", + "card": "0 0% 100%", + "cardForeground": "300 50% 15%", + "popover": "0 0% 100%", + "popoverForeground": "300 50% 15%", + "primary": "330 95% 60%", + "primaryForeground": "0 0% 100%", + "secondary": "200 90% 70%", + "secondaryForeground": "200 80% 10%", + "muted": "45 60% 92%", + "mutedForeground": "300 25% 45%", + "accent": "90 75% 60%", + "accentForeground": "90 80% 10%", + "destructive": "0 85% 55%", + "destructiveForeground": "0 0% 100%", + "border": "330 50% 85%", + "input": "330 40% 90%", + "ring": "330 95% 60%" + } + } + } +] diff --git a/register.go b/register.go new file mode 100644 index 0000000..8ba5a89 --- /dev/null +++ b/register.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + + "github.com/a-h/templ" + + "git.dev.alexdunmow.com/block/core/blocks" + "git.dev.alexdunmow.com/block/core/plugin" + "git.dev.alexdunmow.com/block/core/templates" +) + +// wrap adapts a templ-returning render function to templates.TemplateFunc. +// templ.Component already implements templates.HTMLComponent via Render. +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 registers the y2k system template, its four page templates, all +// theme-specific blocks, the four built-in overrides, and the email wrapper. +// +// Call order: page templates can be registered any time, but schemas MUST load +// before any block Register call. +func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { + // 1. System template + tr.RegisterSystemTemplate(templates.SystemTemplateMeta{ + Key: "y2k", + Title: "Y2K", + Description: "Liquid-chrome Y2K revival theme for indie musicians, zines, and merch shops.", + }) + + // 2. Page templates (slots match the spec verbatim) + if err := tr.RegisterPageTemplate("y2k", templates.PageTemplateMeta{ + Key: "default", + Title: "Default", + Description: "Chrome navbar + content + footer.", + Slots: []string{"header", "main", "footer"}, + }, wrap(RenderY2K)); err != nil { + return err + } + if err := tr.RegisterPageTemplate("y2k", templates.PageTemplateMeta{ + Key: "landing", + Title: "Landing", + Description: "Hero metaball + marquee + CTAs.", + Slots: []string{"hero", "marquee", "main", "cta", "footer"}, + }, wrap(RenderY2KLanding)); err != nil { + return err + } + if err := tr.RegisterPageTemplate("y2k", templates.PageTemplateMeta{ + Key: "article", + Title: "Article / Zine Page", + Description: "Narrow column with sidebar tracklist.", + Slots: []string{"header", "main", "aside", "footer"}, + }, wrap(RenderY2KArticle)); err != nil { + return err + } + if err := tr.RegisterPageTemplate("y2k", templates.PageTemplateMeta{ + Key: "full-width", + Title: "Full Width", + Description: "Edge-to-edge gradient mesh canvas.", + Slots: []string{"header", "main", "footer"}, + }, wrap(RenderY2KFullWidth)); err != nil { + return err + } + + // 3. Schemas first — must precede br.Register so they bind. + if err := br.LoadSchemasFromFS(Schemas()); err != nil { + return err + } + + // 4. Theme-specific blocks (10 total per spec §11). + br.Register(ChromeNavbarBlockMeta, ChromeNavbarBlock) + br.Register(MetaballHeroBlockMeta, MetaballHeroBlock) + br.Register(WaveformPlayerBlockMeta, WaveformPlayerBlock) + br.Register(MarqueeBlockMeta, MarqueeBlock) + br.Register(TracklistBlockMeta, TracklistBlock) + br.Register(MerchCardBlockMeta, MerchCardBlock) + br.Register(WebringBadgeBlockMeta, WebringBadgeBlock) + br.Register(GlitterDividerBlockMeta, GlitterDividerBlock) + br.Register(FooterChromeBlockMeta, FooterChromeBlock) + br.Register(NFTGalleryBlockMeta, NFTGalleryBlock) + + // 5. Built-in overrides (only apply when y2k is the active theme). + br.RegisterTemplateOverride("y2k", "heading", Y2KHeadingBlock) + br.RegisterTemplateOverride("y2k", "text", Y2KTextBlock) + br.RegisterTemplateOverride("y2k", "button", Y2KButtonBlock) + br.RegisterTemplateOverride("y2k", "image", Y2KImageBlock) + + // 6. Email wrapper for branded transactional mail. + tr.RegisterEmailWrapper("y2k", Y2KEmailWrapper) + + return nil +} + +// DefaultMasterPages returns the master pages that y2k seeds on first load. +// Block keys reference theme-owned blocks as "y2k:"; the built-in slot +// block uses {"slotName":"main"} per CLAUDE.md. +func DefaultMasterPages() []plugin.MasterPageDefinition { + return []plugin.MasterPageDefinition{ + { + Key: "y2k:default-master", + Title: "Y2K Default Master", + PageTemplates: []string{"default", "article"}, + Blocks: []plugin.MasterPageBlock{ + { + BlockKey: "y2k:chrome_navbar", + Title: "Chrome Navbar", + Content: map[string]any{"menuName": "main", "logoText": "y2k.fm"}, + Slot: "header", + SortOrder: 0, + }, + { + BlockKey: "y2k:marquee", + Title: "Top Marquee", + Content: map[string]any{"items": []any{"new drop friday", "listen on bandcamp", "subscribe to the zine"}, "speed": "medium"}, + Slot: "header", + SortOrder: 1, + }, + { + BlockKey: "slot", + Title: "Main Slot", + Content: map[string]any{"slotName": "main"}, + Slot: "main", + SortOrder: 0, + }, + { + BlockKey: "y2k:footer_chrome", + Title: "Chrome Footer", + Content: map[string]any{"showWebring": true, "copyright": "(c) y2k.fm"}, + Slot: "footer", + SortOrder: 0, + }, + }, + }, + { + Key: "y2k:landing-master", + Title: "Y2K Landing Master", + PageTemplates: []string{"landing", "full-width"}, + Blocks: []plugin.MasterPageBlock{ + { + BlockKey: "y2k:metaball_hero", + Title: "Metaball Hero", + Content: map[string]any{"headline": "new EP out now", "sub": "liquid chrome dreams", "ctaLabel": "listen"}, + Slot: "hero", + SortOrder: 0, + }, + { + BlockKey: "y2k:marquee", + Title: "Marquee", + Content: map[string]any{"items": []any{"* new * new * new"}, "speed": "fast"}, + Slot: "marquee", + SortOrder: 0, + }, + { + BlockKey: "slot", + Title: "Main Slot", + Content: map[string]any{"slotName": "main"}, + Slot: "main", + SortOrder: 0, + }, + { + BlockKey: "y2k:footer_chrome", + Title: "Chrome Footer", + Content: map[string]any{"showWebring": true}, + Slot: "footer", + SortOrder: 0, + }, + }, + }, + } +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..4f10fd7 --- /dev/null +++ b/registration.go @@ -0,0 +1,26 @@ +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 Y2K theme. +// The CMS plugin loader looks up this exported symbol after dlopen. +var Registration = plugin.PluginRegistration{ + Name: "y2k", + 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/chrome_navbar.schema.json b/schemas/chrome_navbar.schema.json new file mode 100644 index 0000000..7c16f1b --- /dev/null +++ b/schemas/chrome_navbar.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Chrome Navbar", + "description": "Beveled chrome navigation bar with hover sparkle and brand mark.", + "type": "object", + "properties": { + "menuName": { + "type": "string", + "title": "Menu", + "description": "Menu to render as the primary nav.", + "x-editor": "menu-select", + "default": "main" + }, + "logoText": { + "type": "string", + "title": "Logo Text", + "description": "Brand wordmark (used when no logo image is set).", + "x-editor": "text", + "default": "" + }, + "logoImage": { + "type": "string", + "title": "Logo Image", + "description": "Optional brand image. Overrides logo text when set.", + "x-editor": "media", + "default": "" + } + } +} diff --git a/schemas/footer_chrome.schema.json b/schemas/footer_chrome.schema.json new file mode 100644 index 0000000..8af3bfa --- /dev/null +++ b/schemas/footer_chrome.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Chrome Footer", + "description": "Beveled chrome strip with optional webring and copyright.", + "type": "object", + "properties": { + "menuName": { + "type": "string", + "title": "Menu", + "description": "Optional footer menu to render.", + "x-editor": "menu-select", + "default": "" + }, + "showWebring": { + "type": "string", + "title": "Show Webring", + "description": "Render the 88x31 webring badge row.", + "x-editor": "select", + "enum": ["yes", "no"], + "default": "yes" + }, + "copyright": { + "type": "string", + "title": "Copyright", + "description": "Copyright line.", + "x-editor": "text", + "default": "" + } + } +} diff --git a/schemas/glitter_divider.schema.json b/schemas/glitter_divider.schema.json new file mode 100644 index 0000000..c4d9dc8 --- /dev/null +++ b/schemas/glitter_divider.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Glitter Divider", + "description": "Animated sparkle horizontal rule.", + "type": "object", + "properties": { + "variant": { + "type": "string", + "title": "Variant", + "x-editor": "select", + "enum": ["sparkle", "stars", "hearts", "chrome"], + "default": "sparkle" + } + } +} diff --git a/schemas/marquee.schema.json b/schemas/marquee.schema.json new file mode 100644 index 0000000..be838e0 --- /dev/null +++ b/schemas/marquee.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Marquee Ticker", + "description": "Scrolling text strip with chrome edges. Pauses on hover/focus.", + "type": "object", + "properties": { + "items": { + "type": "array", + "title": "Items", + "description": "Lines to scroll across the marquee.", + "x-editor": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "speed": { + "type": "string", + "title": "Speed", + "x-editor": "select", + "enum": ["slow", "medium", "fast"], + "default": "medium" + }, + "direction": { + "type": "string", + "title": "Direction", + "x-editor": "select", + "enum": ["left", "right"], + "default": "left" + } + } +} diff --git a/schemas/merch_card.schema.json b/schemas/merch_card.schema.json new file mode 100644 index 0000000..83844c5 --- /dev/null +++ b/schemas/merch_card.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Merch Card", + "description": "Plastic-bevel product card with a price sticker.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Product Title", + "x-editor": "text", + "default": "" + }, + "price": { + "type": "string", + "title": "Price", + "description": "Display string e.g. $24.", + "x-editor": "text", + "default": "" + }, + "image": { + "type": "string", + "title": "Image", + "x-editor": "media", + "default": "" + }, + "buyHref": { + "type": "string", + "title": "Buy Link", + "x-editor": "link", + "default": "" + }, + "sticker": { + "type": "string", + "title": "Sticker", + "description": "Corner sticker badge variant.", + "x-editor": "select", + "enum": ["", "new", "hot", "sale", "sold-out"], + "default": "" + } + } +} diff --git a/schemas/metaball_hero.schema.json b/schemas/metaball_hero.schema.json new file mode 100644 index 0000000..d45a588 --- /dev/null +++ b/schemas/metaball_hero.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Metaball Hero", + "description": "Hero section with SVG metaballs on a gradient mesh canvas.", + "type": "object", + "properties": { + "headline": { + "type": "string", + "title": "Headline", + "description": "Display headline.", + "x-editor": "text", + "default": "" + }, + "sub": { + "type": "string", + "title": "Sub-headline", + "description": "Supporting line under the headline.", + "x-editor": "text", + "default": "" + }, + "ctaLabel": { + "type": "string", + "title": "CTA Label", + "description": "Call-to-action button text.", + "x-editor": "text", + "default": "" + }, + "ctaHref": { + "type": "string", + "title": "CTA Link", + "description": "Where the CTA button points.", + "x-editor": "link", + "default": "" + }, + "bgPreset": { + "type": "string", + "title": "Background Preset", + "description": "Which mesh anchor palette to use.", + "x-editor": "select", + "enum": ["magenta-teal", "after-hours", "bubblegum"], + "default": "magenta-teal" + } + } +} diff --git a/schemas/nft_gallery.schema.json b/schemas/nft_gallery.schema.json new file mode 100644 index 0000000..e8a1be8 --- /dev/null +++ b/schemas/nft_gallery.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NFT Gallery", + "description": "Holographic-foil hover gallery of NFT items (presentational only).", + "type": "object", + "properties": { + "items": { + "type": "array", + "title": "Items", + "x-editor": "collection", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + "x-editor": "text" + }, + "image": { + "type": "string", + "title": "Image", + "x-editor": "media" + }, + "chain": { + "type": "string", + "title": "Chain", + "x-editor": "select", + "enum": ["", "eth", "sol", "polygon", "base", "tezos"] + }, + "href": { + "type": "string", + "title": "Link", + "x-editor": "link" + } + }, + "required": ["title"] + }, + "default": [] + } + } +} diff --git a/schemas/tracklist.schema.json b/schemas/tracklist.schema.json new file mode 100644 index 0000000..7026377 --- /dev/null +++ b/schemas/tracklist.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Tracklist", + "description": "CD-jewel-case styled track table.", + "type": "object", + "properties": { + "tracks": { + "type": "array", + "title": "Tracks", + "x-editor": "collection", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + "x-editor": "text" + }, + "duration": { + "type": "string", + "title": "Duration", + "description": "Display string e.g. 3:42.", + "x-editor": "text" + }, + "url": { + "type": "string", + "title": "Audio URL", + "x-editor": "text" + } + }, + "required": ["title"] + }, + "default": [] + } + } +} diff --git a/schemas/waveform_player.schema.json b/schemas/waveform_player.schema.json new file mode 100644 index 0000000..3f41956 --- /dev/null +++ b/schemas/waveform_player.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Waveform Player", + "description": "Soundcloud-style audio block with a canvas waveform.", + "type": "object", + "properties": { + "trackTitle": { + "type": "string", + "title": "Track Title", + "x-editor": "text", + "default": "" + }, + "audioUrl": { + "type": "string", + "title": "Audio URL", + "description": "Direct URL to the audio file (mp3 / m4a / ogg).", + "x-editor": "text", + "default": "" + }, + "artist": { + "type": "string", + "title": "Artist", + "x-editor": "text", + "default": "" + }, + "coverImage": { + "type": "string", + "title": "Cover Image", + "description": "Square cover art shown next to the waveform.", + "x-editor": "media", + "default": "" + } + } +} diff --git a/schemas/webring_badge.schema.json b/schemas/webring_badge.schema.json new file mode 100644 index 0000000..e983fb7 --- /dev/null +++ b/schemas/webring_badge.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Webring Badge", + "description": "Canonical 88x31 webring button row with prev/next/ring links.", + "type": "object", + "properties": { + "prevHref": { + "type": "string", + "title": "Previous Link", + "x-editor": "link", + "default": "" + }, + "nextHref": { + "type": "string", + "title": "Next Link", + "x-editor": "link", + "default": "" + }, + "ringName": { + "type": "string", + "title": "Ring Name", + "description": "Webring label displayed on the centre badge.", + "x-editor": "text", + "default": "the y2k.fm ring" + } + } +} diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..5f5aeb4 --- /dev/null +++ b/template.templ @@ -0,0 +1,244 @@ +package main + +import ( + "context" + + "git.dev.alexdunmow.com/block/core/templates/bn" +) + +// PageData is the per-render shape used by every y2k page template. +// It mirrors gotham's shape so the bn.Head/bn.BodyEnd helpers can be reused. +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 parseY2KPageData(doc map[string]any) PageData { + title := "Untitled" + if t, ok := doc["title"].(string); ok { + title = t + } + slots := map[string]string{} + if s, ok := doc["slots"].(map[string]string); ok { + slots = s + } + themeMode := "light" + if tm, ok := doc["theme_mode"].(string); ok && tm != "" { + themeMode = tm + } + 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 + } + return PageData{ + Title: title, + Slots: slots, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + SiteSettings: bn.ParseSiteSettings(doc), + PageMeta: bn.ParsePageMeta(doc), + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: bn.ParseEngagementConfig(doc), + } +} + +// === Default page template === +templ Y2KDefault(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/y2k/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

No content blocks assigned to this page.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// === Landing page template === +templ Y2KLanding(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/y2k/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["marquee"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } +
+
+ @templ.Raw(data.Slots["cta"]) +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// === Article / Zine page template === +templ Y2KArticle(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/y2k/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } else { +
+

No content blocks assigned to this page.

+
+ } +
+ +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// === Full-width page template === +templ Y2KFullWidth(data PageData) { + + + @bn.Head(bn.HeadData{ + Title: data.Title, + Settings: data.SiteSettings, + PageMeta: data.PageMeta, + ThemeMode: data.ThemeMode, + ThemeCSS: data.ThemeCSS, + PluginStyles: []string{"/templates/y2k/style.css"}, + StructuredData: data.StructuredData, + CSSHash: data.CSSHash, + PageviewNonce: data.PageviewNonce, + EngagementConfig: data.EngagementConfig, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

No content blocks assigned to this page.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +func RenderY2K(ctx context.Context, doc map[string]any) templ.Component { + return Y2KDefault(parseY2KPageData(doc)) +} + +func RenderY2KLanding(ctx context.Context, doc map[string]any) templ.Component { + return Y2KLanding(parseY2KPageData(doc)) +} + +func RenderY2KArticle(ctx context.Context, doc map[string]any) templ.Component { + return Y2KArticle(parseY2KPageData(doc)) +} + +func RenderY2KFullWidth(ctx context.Context, doc map[string]any) templ.Component { + return Y2KFullWidth(parseY2KPageData(doc)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..21d91b9 --- /dev/null +++ b/template_templ.go @@ -0,0 +1,510 @@ +// 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 is the per-render shape used by every y2k page template. +// It mirrors gotham's shape so the bn.Head/bn.BodyEnd helpers can be reused. +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 parseY2KPageData(doc map[string]any) PageData { + title := "Untitled" + if t, ok := doc["title"].(string); ok { + title = t + } + slots := map[string]string{} + if s, ok := doc["slots"].(map[string]string); ok { + slots = s + } + themeMode := "light" + if tm, ok := doc["theme_mode"].(string); ok && tm != "" { + themeMode = tm + } + 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 + } + return PageData{ + Title: title, + Slots: slots, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + SiteSettings: bn.ParseSiteSettings(doc), + PageMeta: bn.ParsePageMeta(doc), + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: bn.ParseEngagementConfig(doc), + } +} + +// === Default page template === +func Y2KDefault(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/y2k/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, "

No content blocks assigned to this page.

") + 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 === +func Y2KLanding(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/y2k/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["marquee"]).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 + } + if main, ok := data.Slots["main"]; ok && main != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + 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, 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(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, 17, "
") + 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, 18, "
") + 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, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// === Article / Zine page template === +func Y2KArticle(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, 20, "") + 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/y2k/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, 21, "") + 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, 22, "
") + 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, 23, "
") + 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, 24, "
") + 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, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

No content blocks assigned to this page.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).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.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, 30, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// === Full-width page template === +func Y2KFullWidth(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, 31, "") + 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/y2k/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, 32, "") + 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, 33, "
") + 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, 34, "
") + 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, 35, "

No content blocks assigned to this page.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + 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, 37, "
") + 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, 38, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func RenderY2K(ctx context.Context, doc map[string]any) templ.Component { + return Y2KDefault(parseY2KPageData(doc)) +} + +func RenderY2KLanding(ctx context.Context, doc map[string]any) templ.Component { + return Y2KLanding(parseY2KPageData(doc)) +} + +func RenderY2KArticle(ctx context.Context, doc map[string]any) templ.Component { + return Y2KArticle(parseY2KPageData(doc)) +} + +func RenderY2KFullWidth(ctx context.Context, doc map[string]any) templ.Component { + return Y2KFullWidth(parseY2KPageData(doc)) +} + +var _ = templruntime.GeneratedTemplate diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..44475d4 --- /dev/null +++ b/text_override.go @@ -0,0 +1,17 @@ +package main + +import ( + "bytes" + "context" +) + +// Y2KTextBlock applies the holographic link underline to the built-in text +// block when the y2k theme is active. +// Content: {text, class} +func Y2KTextBlock(ctx context.Context, content map[string]any) string { + text := getString(content, "text") + class := getString(content, "class") + var buf bytes.Buffer + _ = y2kTextComponent(text, class).Render(ctx, &buf) + return buf.String() +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..2cf9385 --- /dev/null +++ b/text_override.templ @@ -0,0 +1,9 @@ +package main + +// y2kTextComponent wraps prose in the y2k-text container so links pick up the +// holographic underline gradient. +templ y2kTextComponent(text, class string) { +
+ @templ.Raw(text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..753ceaf --- /dev/null +++ b/text_override_templ.go @@ -0,0 +1,68 @@ +// 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" + +// y2kTextComponent wraps prose in the y2k-text container so links pick up the +// holographic underline gradient. +func y2kTextComponent(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{"y2k-text prose max-w-none", 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 diff --git a/tracklist.go b/tracklist.go new file mode 100644 index 0000000..e9676a8 --- /dev/null +++ b/tracklist.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// TracklistBlockMeta declares the CD-jewel-case tracklist block. +var TracklistBlockMeta = blocks.BlockMeta{ + Key: "tracklist", + Title: "Tracklist", + Description: "Tabular tracklist styled like a CD jewel case insert.", + Source: "y2k", + Category: blocks.CategoryContent, +} + +// TracklistBlock renders the tracklist. +// Content: {tracks[{title, duration, url}]} +func TracklistBlock(ctx context.Context, content map[string]any) string { + raw := getSlice(content, "tracks") + tracks := make([]TrackRow, 0, len(raw)) + for _, r := range raw { + tracks = append(tracks, TrackRow{ + Title: getString(r, "title"), + Duration: getString(r, "duration"), + URL: getString(r, "url"), + }) + } + var buf bytes.Buffer + _ = tracklistComponent(TracklistData{Tracks: tracks}).Render(ctx, &buf) + return buf.String() +} + +// TracklistData is the typed shape for the templ component. +type TracklistData struct { + Tracks []TrackRow +} + +// TrackRow is a single tracklist row. +type TrackRow struct { + Title string + Duration string + URL string +} diff --git a/tracklist.templ b/tracklist.templ new file mode 100644 index 0000000..83b7982 --- /dev/null +++ b/tracklist.templ @@ -0,0 +1,39 @@ +package main + +import "strconv" + +// tracklistComponent renders the track table. Rows wrap on small screens so +// duration is never dropped (UAT 7.6). +templ tracklistComponent(data TracklistData) { +
+
+ tracklist +
+ if len(data.Tracks) == 0 { +

No tracks yet.

+ } else { +
    + for i, t := range data.Tracks { +
  1. + { strconv.Itoa(i + 1) } + + if t.URL != "" { + { trackTitleOr(t.Title) } + } else { + { trackTitleOr(t.Title) } + } + + { durationOrDash(t.Duration) } +
  2. + } +
+ } +
+} + +func durationOrDash(s string) string { + if s == "" { + return "--:--" + } + return s +} diff --git a/tracklist_templ.go b/tracklist_templ.go new file mode 100644 index 0000000..0132fdd --- /dev/null +++ b/tracklist_templ.go @@ -0,0 +1,148 @@ +// 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 "strconv" + +// tracklistComponent renders the track table. Rows wrap on small screens so +// duration is never dropped (UAT 7.6). +func tracklistComponent(data TracklistData) 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, "
tracklist
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Tracks) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

No tracks yet.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, t := range data.Tracks { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
  1. ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i + 1)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `tracklist.templ`, Line: 18, Col: 97} + } + _, 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 + } + if t.URL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(trackTitleOr(t.Title)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `tracklist.templ`, Line: 21, Col: 88} + } + _, 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 { + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(trackTitleOr(t.Title)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `tracklist.templ`, Line: 23, Col: 31} + } + _, 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, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(durationOrDash(t.Duration)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `tracklist.templ`, Line: 26, Col: 109} + } + _, 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, 10, "
  2. ") + 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func durationOrDash(s string) string { + if s == "" { + return "--:--" + } + return s +} + +var _ = templruntime.GeneratedTemplate diff --git a/waveform_player.go b/waveform_player.go new file mode 100644 index 0000000..1a32e20 --- /dev/null +++ b/waveform_player.go @@ -0,0 +1,42 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// WaveformPlayerBlockMeta declares the waveform audio player block. +var WaveformPlayerBlockMeta = blocks.BlockMeta{ + Key: "waveform_player", + Title: "Waveform Player", + Description: "Soundcloud-style audio player with a canvas waveform.", + Source: "y2k", + Category: blocks.CategoryContent, +} + +// WaveformPlayerBlock renders the audio block. +// Content: {trackTitle, audioUrl, artist, coverImage} +func WaveformPlayerBlock(ctx context.Context, content map[string]any) string { + data := WaveformPlayerData{ + TrackTitle: getString(content, "trackTitle"), + AudioURL: getString(content, "audioUrl"), + Artist: getString(content, "artist"), + CoverImage: getString(content, "coverImage"), + } + if data.AudioURL == "" && data.TrackTitle == "" { + return "" + } + var buf bytes.Buffer + _ = waveformPlayerComponent(data).Render(ctx, &buf) + return buf.String() +} + +// WaveformPlayerData is the typed shape for the templ component. +type WaveformPlayerData struct { + TrackTitle string + AudioURL string + Artist string + CoverImage string +} diff --git a/waveform_player.templ b/waveform_player.templ new file mode 100644 index 0000000..302575e --- /dev/null +++ b/waveform_player.templ @@ -0,0 +1,49 @@ +package main + +// waveformPlayerComponent renders a Soundcloud-style horizontal player. The +// canvas waveform is filled client-side from the audio source using WebAudio +// peak decode (v0.1 strategy noted in the spec's open questions). +templ waveformPlayerComponent(data WaveformPlayerData) { +
+
+ if data.CoverImage != "" { + { + } else { + + } +
+
+
+ { trackTitleOr(data.TrackTitle) } + if data.Artist != "" { + { data.Artist } + } +
+ + if data.AudioURL != "" { + + } else { +

No audio source configured.

+ } +
+
+} + +func trackTitleOr(s string) string { + if s == "" { + return "untitled track" + } + return s +} diff --git a/waveform_player_templ.go b/waveform_player_templ.go new file mode 100644 index 0000000..076478d --- /dev/null +++ b/waveform_player_templ.go @@ -0,0 +1,168 @@ +// 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" + +// waveformPlayerComponent renders a Soundcloud-style horizontal player. The +// canvas waveform is filled client-side from the audio source using WebAudio +// peak decode (v0.1 strategy noted in the spec's open questions). +func waveformPlayerComponent(data WaveformPlayerData) 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.CoverImage != "" { + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(trackTitleOr(data.TrackTitle)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `waveform_player.templ`, Line: 22, Col: 96} + } + _, 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, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Artist != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Artist) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `waveform_player.templ`, Line: 24, 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, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.AudioURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

No audio source configured.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func trackTitleOr(s string) string { + if s == "" { + return "untitled track" + } + return s +} + +var _ = templruntime.GeneratedTemplate diff --git a/webring_badge.go b/webring_badge.go new file mode 100644 index 0000000..c27355b --- /dev/null +++ b/webring_badge.go @@ -0,0 +1,37 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// WebringBadgeBlockMeta declares the canonical 88x31 webring badge row. +var WebringBadgeBlockMeta = blocks.BlockMeta{ + Key: "webring_badge", + Title: "Webring Badge", + Description: "Canonical 88x31 webring button row with prev/next/ring links.", + Source: "y2k", + Category: blocks.CategoryNavigation, +} + +// WebringBadgeBlock renders the badge row. +// Content: {prevHref, nextHref, ringName} +func WebringBadgeBlock(ctx context.Context, content map[string]any) string { + data := WebringBadgeData{ + PrevHref: getString(content, "prevHref"), + NextHref: getString(content, "nextHref"), + RingName: getStringOr(content, "ringName", "the y2k.fm ring"), + } + var buf bytes.Buffer + _ = webringBadgeComponent(data).Render(ctx, &buf) + return buf.String() +} + +// WebringBadgeData is the typed shape for the templ component. +type WebringBadgeData struct { + PrevHref string + NextHref string + RingName string +} diff --git a/webring_badge.templ b/webring_badge.templ new file mode 100644 index 0000000..812dc45 --- /dev/null +++ b/webring_badge.templ @@ -0,0 +1,18 @@ +package main + +// webringBadgeComponent renders the prev/ring/next badge triplet at the +// canonical 88x31 button size. +templ webringBadgeComponent(data WebringBadgeData) { + +} + +func orHash(s string) string { + if s == "" { + return "#" + } + return s +} diff --git a/webring_badge_templ.go b/webring_badge_templ.go new file mode 100644 index 0000000..bb8a0cd --- /dev/null +++ b/webring_badge_templ.go @@ -0,0 +1,88 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// webringBadgeComponent renders the prev/ring/next badge triplet at the +// canonical 88x31 button size. +func webringBadgeComponent(data WebringBadgeData) 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 + }) +} + +func orHash(s string) string { + if s == "" { + return "#" + } + return s +} + +var _ = templruntime.GeneratedTemplate