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 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-06-06 14:11:46 +08:00
commit 49f9c90589
73 changed files with 5465 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.so
*.test
tmp/
.idea/
.vscode/

237
BUILD_REPORT.md Normal file
View File

@ -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 59 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, <fallback-stack>)`. 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.72.8 (`make rebuild`, log scrape),
§5.7 (no console errors at the rendered URL), §6.16.7 (Lighthouse /
computed-style assertions on a running site), §7.17.6 (responsive
viewport checks), §8.* (rendering each block in three states in the
browser), §9.7 (admin-replace round-trip), §10.210.4 (Litmus / Gmail /
Apple Mail / Outlook 365 previews), §11.411.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
`<canvas data-y2k-waveform>` 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 `<html>` 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)
```

27
Makefile Normal file
View File

@ -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"

50
RECOMMENDED_FONTS.md Normal file
View File

@ -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.

0
assets/.gitkeep Normal file
View File

5
assets/placeholder.txt Normal file
View File

@ -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).

28
button_override.go Normal file
View File

@ -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
}

9
button_override.templ Normal file
View File

@ -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) {
<a href={ templ.SafeURL(data.URL) } class={ "y2k-button", data.Class } data-variant={ data.Variant }>
{ data.Text }
</a>
}

99
button_override_templ.go Normal file
View File

@ -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, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 6, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" data-variant=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Variant)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 6, Col: 99}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

37
chrome_navbar.go Normal file
View File

@ -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
}

38
chrome_navbar.templ Normal file
View File

@ -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) {
<nav class="y2k-chrome-bg y2k-bevel relative w-full" data-block-key="y2k:chrome_navbar" data-menu={ data.MenuName }>
<div class="max-w-6xl mx-auto flex items-center justify-between gap-4 px-4 py-3">
<a href="/" class="flex items-center gap-2 text-foreground no-underline">
if data.LogoImage != "" {
<img src={ data.LogoImage } alt={ data.LogoText } class="h-8 w-auto y2k-image-frame"/>
} else {
<span class="y2k-heading text-xl uppercase">{ defaultLogoText(data.LogoText) }</span>
}
</a>
<button
type="button"
class="md:hidden y2k-button text-sm"
aria-controls={ "y2k-nav-" + data.MenuName }
aria-expanded="false"
data-y2k-nav-toggle
>
Menu
</button>
<div id={ "y2k-nav-" + data.MenuName } class="hidden md:flex items-center gap-4" data-menu-target={ data.MenuName }>
<span class="text-muted-foreground text-sm uppercase tracking-wider">{ "menu:" + data.MenuName }</span>
</div>
</div>
<span class="y2k-sparkle absolute right-3 top-1 text-accent">*</span>
</nav>
}
func defaultLogoText(s string) string {
if s == "" {
return "y2k.fm"
}
return s
}

169
chrome_navbar_templ.go Normal file
View File

@ -0,0 +1,169 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<nav class=\"y2k-chrome-bg y2k-bevel relative w-full\" data-block-key=\"y2k:chrome_navbar\" data-menu=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 7, Col: 114}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><div class=\"max-w-6xl mx-auto flex items-center justify-between gap-4 px-4 py-3\"><a href=\"/\" class=\"flex items-center gap-2 text-foreground no-underline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.LogoImage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.LogoImage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 11, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.LogoText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 11, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"h-8 w-auto y2k-image-frame\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"y2k-heading text-xl uppercase\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(defaultLogoText(data.LogoText))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 13, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a> <button type=\"button\" class=\"md:hidden y2k-button text-sm\" aria-controls=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("y2k-nav-" + data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 19, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" aria-expanded=\"false\" data-y2k-nav-toggle>Menu</button><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("y2k-nav-" + data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 25, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"hidden md:flex items-center gap-4\" data-menu-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 25, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"><span class=\"text-muted-foreground text-sm uppercase tracking-wider\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("menu:" + data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `chrome_navbar.templ`, Line: 26, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></div></div><span class=\"y2k-sparkle absolute right-3 top-1 text-accent\">*</span></nav>")
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

228
css.go Normal file
View File

@ -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;
}
}
`

83
email_wrapper.go Normal file
View File

@ -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"
}

88
email_wrapper.templ Normal file
View File

@ -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) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.y2k-email-container { width: 100% !important; max-width: 100% !important; }
.y2k-email-content { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter Tight','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;", y2kEmailBg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">{ emailCtx.PreviewText }</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 36px 12px; background-color: %s;", y2kEmailBg(emailCtx)) }>
<table
role="presentation"
class="y2k-email-container"
width="600"
cellspacing="0"
cellpadding="0"
border="0"
style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 2px solid %s; border-radius: 12px; overflow: hidden;", y2kEmailCard(emailCtx), y2kEmailBorder(emailCtx)) }
>
<!-- gradient header bar -->
<tr>
<td style={ fmt.Sprintf("padding: 18px 28px; background-image: linear-gradient(90deg, %s 0%%, %s 100%%); color: %s; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;", y2kEmailPrimary(emailCtx), y2kEmailSecondary(emailCtx), y2kEmailFg(emailCtx)) }>
if emailCtx.SiteSettings.SiteName != "" {
{ emailCtx.SiteSettings.SiteName }
} else {
y2k.fm
}
</td>
</tr>
<!-- body content -->
<tr>
<td class="y2k-email-content" style={ fmt.Sprintf("padding: 32px 40px; color: %s; font-size: 16px; line-height: 1.7;", y2kEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<!-- marquee pill footer -->
<tr>
<td align="center" style={ fmt.Sprintf("padding: 18px 28px; background-color: %s;", y2kEmailMuted(emailCtx)) }>
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style={ fmt.Sprintf("padding: 8px 18px; border-radius: 999px; background-image: linear-gradient(90deg, %s 0%%, %s 100%%); color: %s; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;", y2kEmailPrimary(emailCtx), y2kEmailSecondary(emailCtx), y2kEmailFg(emailCtx)) }>
* now playing * new drop friday * subscribe to the zine *
</td>
</tr>
</table>
if emailCtx.UnsubscribeURL != "" {
<p style={ fmt.Sprintf("margin: 12px 0 0; font-size: 11px; color: %s;", y2kEmailMutedFg(emailCtx)) }>
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", y2kEmailMutedFg(emailCtx)) }>
unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

251
email_wrapper_templ.go Normal file
View File

@ -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, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "</title><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }\n\t\t\t\ttable, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }\n\t\t\t\timg { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\ta[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.y2k-email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.y2k-email-content { padding-left: 20px !important; padding-right: 20px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter Tight','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;", y2kEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 31, Col: 195}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display:none;max-height:0;overflow:hidden;mso-hide:all;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 36px 12px; background-color: %s;", y2kEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 37, Col: 110}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><table role=\"presentation\" class=\"y2k-email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 2px solid %s; border-radius: 12px; overflow: hidden;", y2kEmailCard(emailCtx), y2kEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 45, Col: 180}
}
_, 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, "\"><!-- gradient header bar --><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 18px 28px; background-image: linear-gradient(90deg, %s 0%%, %s 100%%); color: %s; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;", y2kEmailPrimary(emailCtx), y2kEmailSecondary(emailCtx), y2kEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 49, Col: 271}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.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, "</td></tr><!-- body content --><tr><td class=\"y2k-email-content\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 40px; color: %s; font-size: 16px; line-height: 1.7;", y2kEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 59, Col: 148}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
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, "</td></tr><!-- marquee pill footer --><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 18px 28px; background-color: %s;", y2kEmailMuted(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 65, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"><table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 8px 18px; border-radius: 999px; background-image: linear-gradient(90deg, %s 0%%, %s 100%%); color: %s; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;", y2kEmailPrimary(emailCtx), y2kEmailSecondary(emailCtx), y2kEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 68, Col: 313}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">* now playing * new drop friday * subscribe to the zine *</td></tr></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 12px 0 0; font-size: 11px; color: %s;", y2kEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 74, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 75, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", y2kEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 75, Col: 150}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

57
embed.go Normal file
View File

@ -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,
}
}

1
fonts.json Normal file
View File

@ -0,0 +1 @@
[]

46
footer_chrome.go Normal file
View File

@ -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
}

24
footer_chrome.templ Normal file
View File

@ -0,0 +1,24 @@
package main
// footerChromeComponent renders the bottom chrome strip with optional webring.
templ footerChromeComponent(data FooterChromeData) {
<div class="y2k-chrome-bg y2k-bevel mt-8" data-block-key="y2k:footer_chrome">
<div class="max-w-6xl mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-3">
if data.MenuName != "" {
<nav class="flex gap-3 text-sm uppercase tracking-wider" data-menu-target={ data.MenuName }>
<span class="text-muted-foreground">{ "menu:" + data.MenuName }</span>
</nav>
}
if data.ShowWebring {
<div class="flex items-center gap-2">
<a href="#" class="y2k-webring-badge">prev</a>
<a href="#" class="y2k-webring-badge">ring</a>
<a href="#" class="y2k-webring-badge">next</a>
</div>
}
if data.Copyright != "" {
<span class="text-xs text-muted-foreground">{ data.Copyright }</span>
}
</div>
</div>
}

102
footer_chrome_templ.go Normal file
View File

@ -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, "<div class=\"y2k-chrome-bg y2k-bevel mt-8\" data-block-key=\"y2k:footer_chrome\"><div class=\"max-w-6xl mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.MenuName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<nav class=\"flex gap-3 text-sm uppercase tracking-wider\" data-menu-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer_chrome.templ`, Line: 8, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><span class=\"text-muted-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("menu:" + data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer_chrome.templ`, Line: 9, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowWebring {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"flex items-center gap-2\"><a href=\"#\" class=\"y2k-webring-badge\">prev</a> <a href=\"#\" class=\"y2k-webring-badge\">ring</a> <a href=\"#\" class=\"y2k-webring-badge\">next</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Copyright != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"text-xs text-muted-foreground\">")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

26
glitter_divider.go Normal file
View File

@ -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()
}

25
glitter_divider.templ Normal file
View File

@ -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) {
<hr class="border-0 my-6" role="separator" data-block-key="y2k:glitter_divider" data-variant={ variant }/>
<div class="flex items-center justify-center gap-2 -mt-4 mb-4 text-accent" aria-hidden="true">
<span class="y2k-sparkle">{ glitterGlyph(variant) }</span>
<span class="y2k-sparkle" style="animation-delay: 200ms">{ glitterGlyph(variant) }</span>
<span class="y2k-sparkle" style="animation-delay: 400ms">{ glitterGlyph(variant) }</span>
</div>
}
func glitterGlyph(v string) string {
switch v {
case "stars":
return "*"
case "hearts":
return "<3"
case "chrome":
return "~"
default:
return "*"
}
}

107
glitter_divider_templ.go Normal file
View File

@ -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, "<hr class=\"border-0 my-6\" role=\"separator\" data-block-key=\"y2k:glitter_divider\" data-variant=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(variant)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `glitter_divider.templ`, Line: 6, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><div class=\"flex items-center justify-center gap-2 -mt-4 mb-4 text-accent\" aria-hidden=\"true\"><span class=\"y2k-sparkle\">")
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, "</span> <span class=\"y2k-sparkle\" style=\"animation-delay: 200ms\">")
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, "</span> <span class=\"y2k-sparkle\" style=\"animation-delay: 400ms\">")
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, "</span></div>")
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

20
go.mod Normal file
View File

@ -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
)

42
go.sum Normal file
View File

@ -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=

39
heading_override.go Normal file
View File

@ -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
}

41
heading_override.templ Normal file
View File

@ -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:
<h1 class={ "y2k-heading font-bold text-foreground", y2kHeadingBaseClass(1), textClass }>{ text }</h1>
case 2:
<h2 class={ "y2k-heading font-bold text-foreground", y2kHeadingBaseClass(2), textClass }>{ text }</h2>
case 3:
<h3 class={ "y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(3), textClass }>{ text }</h3>
case 4:
<h4 class={ "y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(4), textClass }>{ text }</h4>
case 5:
<h5 class={ "y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(5), textClass }>{ text }</h5>
case 6:
<h6 class={ "y2k-heading font-semibold text-foreground", y2kHeadingBaseClass(6), textClass }>{ text }</h6>
default:
<h2 class={ "y2k-heading font-bold text-foreground", y2kHeadingBaseClass(2), textClass }>{ text }</h2>
}
}

311
heading_override_templ.go Normal file
View File

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

55
helpers.go Normal file
View File

@ -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
}

32
image_override.go Normal file
View File

@ -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
}

11
image_override.templ Normal file
View File

@ -0,0 +1,11 @@
package main
// y2kImageComponent wraps an image in the chrome y2k-image-frame.
templ y2kImageComponent(data Y2KImageData) {
<figure class={ "my-4", data.Class }>
<img src={ data.URL } alt={ data.Alt } class="y2k-image-frame w-full h-auto"/>
if data.Caption != "" {
<figcaption class="mt-2 text-sm text-muted-foreground text-center">{ data.Caption }</figcaption>
}
</figure>
}

108
image_override_templ.go Normal file
View File

@ -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, "<figure class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.URL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"y2k-image-frame w-full h-auto\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figcaption class=\"mt-2 text-sm text-muted-foreground text-center\">")
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, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

40
marquee.go Normal file
View File

@ -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
}

29
marquee.templ Normal file
View File

@ -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) {
<div
class="y2k-marquee y2k-chrome-bg"
data-block-key="y2k:marquee"
data-direction={ data.Direction }
tabindex="0"
aria-label="ticker"
>
<div class="y2k-marquee-track py-2 px-4 text-sm uppercase tracking-wider text-foreground" data-speed={ data.Speed }>
for _, item := range data.Items {
<span class="inline-flex items-center gap-2">
<span class="text-accent">*</span>
{ item }
</span>
}
for _, item := range data.Items {
<span class="inline-flex items-center gap-2" aria-hidden="true">
<span class="text-accent">*</span>
{ item }
</span>
}
</div>
</div>
}

111
marquee_templ.go Normal file
View File

@ -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, "<div class=\"y2k-marquee y2k-chrome-bg\" data-block-key=\"y2k:marquee\" data-direction=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Direction)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee.templ`, Line: 10, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" tabindex=\"0\" aria-label=\"ticker\"><div class=\"y2k-marquee-track py-2 px-4 text-sm uppercase tracking-wider text-foreground\" data-speed=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Speed)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marquee.templ`, Line: 14, Col: 115}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"inline-flex items-center gap-2\"><span class=\"text-accent\">*</span> ")
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, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, item := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"inline-flex items-center gap-2\" aria-hidden=\"true\"><span class=\"text-accent\">*</span> ")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

41
merch_card.go Normal file
View File

@ -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
}

52
merch_card.templ Normal file
View File

@ -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) {
<article class="relative y2k-bevel rounded-2xl overflow-hidden bg-card flex flex-col" data-block-key="y2k:merch_card">
if data.Sticker != "" {
<span class="absolute top-3 right-3 z-10 y2k-button text-xs uppercase tracking-widest">{ stickerLabel(data.Sticker) }</span>
}
<div class="aspect-square bg-muted overflow-hidden y2k-image-frame border-0 rounded-none">
if data.Image != "" {
<img src={ data.Image } alt={ data.Title } class="w-full h-full object-cover"/>
} else {
<div class="w-full h-full flex items-center justify-center text-muted-foreground text-xs uppercase tracking-widest">
no image
</div>
}
</div>
<div class="p-4 flex flex-col gap-2 flex-1">
<h3 class="y2k-heading text-foreground text-lg">{ titleOr(data.Title) }</h3>
<div class="flex items-center justify-between gap-3 mt-auto">
<span class="text-foreground font-bold">{ priceOr(data.Price) }</span>
if data.BuyHref != "" {
<a href={ templ.SafeURL(data.BuyHref) } class="y2k-button text-sm">buy</a>
}
</div>
</div>
</article>
}
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
}

178
merch_card_templ.go Normal file
View File

@ -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, "<article class=\"relative y2k-bevel rounded-2xl overflow-hidden bg-card flex flex-col\" data-block-key=\"y2k:merch_card\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Sticker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"absolute top-3 right-3 z-10 y2k-button text-xs uppercase tracking-widest\">")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"aspect-square bg-muted overflow-hidden y2k-image-frame border-0 rounded-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `merch_card.templ`, Line: 12, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `merch_card.templ`, Line: 12, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"w-full h-full object-cover\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"w-full h-full flex items-center justify-center text-muted-foreground text-xs uppercase tracking-widest\">no image</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"p-4 flex flex-col gap-2 flex-1\"><h3 class=\"y2k-heading text-foreground text-lg\">")
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, "</h3><div class=\"flex items-center justify-between gap-3 mt-auto\"><span class=\"text-foreground font-bold\">")
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, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.BuyHref != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.BuyHref))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `merch_card.templ`, Line: 24, Col: 42}
}
_, 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, 13, "\" class=\"y2k-button text-sm\">buy</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div></article>")
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

41
metaball_hero.go Normal file
View File

@ -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
}

64
metaball_hero.templ Normal file
View File

@ -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) {
<section
class="relative overflow-hidden y2k-mesh-bg py-24 md:py-32"
data-block-key="y2k:metaball_hero"
data-bg-preset={ data.BgPreset }
>
<svg class="absolute inset-0 w-full h-full opacity-80" viewBox="0 0 600 400" preserveAspectRatio="none" aria-hidden="true">
<defs>
<filter id="y2k-metaball" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceGraphic" stdDeviation="18" result="b1"/>
<feGaussianBlur in="b1" stdDeviation="8" result="b2"/>
<feColorMatrix in="b2" type="matrix" values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 22 -10" result="meta"/>
<feComposite in="meta" in2="SourceGraphic" operator="atop"/>
</filter>
<linearGradient id="y2k-mb-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="currentColor"/>
<stop offset="100%" stop-color="currentColor" stop-opacity="0.7"/>
</linearGradient>
</defs>
<g filter="url(#y2k-metaball)" fill="url(#y2k-mb-grad)" class="text-primary">
<circle class="y2k-metaball" cx="180" cy="200" r="80"/>
<circle class="y2k-metaball" cx="320" cy="160" r="70"/>
<circle class="y2k-metaball" cx="430" cy="240" r="90"/>
</g>
</svg>
<div class="relative z-10 max-w-4xl mx-auto px-4 text-center">
<div class="inline-block px-6 py-4 rounded-2xl bg-card/70 backdrop-blur-md y2k-bevel">
<h1 class="y2k-heading text-[4rem] md:text-[5rem] leading-tight font-bold text-foreground">
{ headlineOrDefault(data.Headline) }
</h1>
if data.Sub != "" {
<p class="text-lg md:text-xl text-foreground/80 mt-3 mb-6">{ data.Sub }</p>
}
if data.CTALabel != "" {
<a href={ templ.SafeURL(ctaHrefOr(data.CTAHref)) } class="y2k-button text-lg">
{ data.CTALabel }
</a>
}
</div>
</div>
</section>
}
func headlineOrDefault(s string) string {
if s == "" {
return "new EP out now"
}
return s
}
func ctaHrefOr(s string) string {
if s == "" {
return "#"
}
return s
}

137
metaball_hero_templ.go Normal file
View File

@ -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, "<section class=\"relative overflow-hidden y2k-mesh-bg py-24 md:py-32\" data-block-key=\"y2k:metaball_hero\" data-bg-preset=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.BgPreset)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metaball_hero.templ`, Line: 9, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><svg class=\"absolute inset-0 w-full h-full opacity-80\" viewBox=\"0 0 600 400\" preserveAspectRatio=\"none\" aria-hidden=\"true\"><defs><filter id=\"y2k-metaball\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"18\" result=\"b1\"></feGaussianBlur> <feGaussianBlur in=\"b1\" stdDeviation=\"8\" result=\"b2\"></feGaussianBlur> <feColorMatrix in=\"b2\" type=\"matrix\" values=\"\n\t\t\t\t\t\t1 0 0 0 0\n\t\t\t\t\t\t0 1 0 0 0\n\t\t\t\t\t\t0 0 1 0 0\n\t\t\t\t\t\t0 0 0 22 -10\" result=\"meta\"></feColorMatrix> <feComposite in=\"meta\" in2=\"SourceGraphic\" operator=\"atop\"></feComposite></filter> <linearGradient id=\"y2k-mb-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"currentColor\"></stop> <stop offset=\"100%\" stop-color=\"currentColor\" stop-opacity=\"0.7\"></stop></linearGradient></defs> <g filter=\"url(#y2k-metaball)\" fill=\"url(#y2k-mb-grad)\" class=\"text-primary\"><circle class=\"y2k-metaball\" cx=\"180\" cy=\"200\" r=\"80\"></circle> <circle class=\"y2k-metaball\" cx=\"320\" cy=\"160\" r=\"70\"></circle> <circle class=\"y2k-metaball\" cx=\"430\" cy=\"240\" r=\"90\"></circle></g></svg><div class=\"relative z-10 max-w-4xl mx-auto px-4 text-center\"><div class=\"inline-block px-6 py-4 rounded-2xl bg-card/70 backdrop-blur-md y2k-bevel\"><h1 class=\"y2k-heading text-[4rem] md:text-[5rem] leading-tight font-bold text-foreground\">")
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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Sub != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"text-lg md:text-xl text-foreground/80 mt-3 mb-6\">")
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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.CTALabel != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(ctaHrefOr(data.CTAHref)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metaball_hero.templ`, Line: 43, Col: 53}
}
_, 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, "\" class=\"y2k-button text-lg\">")
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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div></section>")
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

49
nft_gallery.go Normal file
View File

@ -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
}

33
nft_gallery.templ Normal file
View File

@ -0,0 +1,33 @@
package main
// nftGalleryComponent renders a grid of foil-on-hover NFT cards.
templ nftGalleryComponent(data NFTGalleryData) {
<section class="my-6" data-block-key="y2k:nft_gallery">
if len(data.Items) == 0 {
<p class="text-center text-muted-foreground py-12">No items yet.</p>
} else {
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
for _, item := range data.Items {
<a
href={ templ.SafeURL(orHash(item.Href)) }
class="y2k-foil y2k-bevel block bg-card rounded-xl overflow-hidden no-underline text-foreground"
>
<div class="aspect-square bg-muted overflow-hidden">
if item.Image != "" {
<img src={ item.Image } alt={ item.Title } class="w-full h-full object-cover"/>
} else {
<div class="w-full h-full flex items-center justify-center text-muted-foreground text-xs uppercase tracking-widest">no image</div>
}
</div>
<div class="p-3 flex items-center justify-between gap-2">
<span class="font-semibold truncate">{ titleOr(item.Title) }</span>
if item.Chain != "" {
<span class="text-xs uppercase tracking-widest text-muted-foreground">{ item.Chain }</span>
}
</div>
</a>
}
</div>
}
</section>
}

156
nft_gallery_templ.go Normal file
View File

@ -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, "<section class=\"my-6\" data-block-key=\"y2k:nft_gallery\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Items) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"text-center text-muted-foreground py-12\">No items yet.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(orHash(item.Href)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `nft_gallery.templ`, Line: 12, Col: 45}
}
_, 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, "\" class=\"y2k-foil y2k-bevel block bg-card rounded-xl overflow-hidden no-underline text-foreground\"><div class=\"aspect-square bg-muted overflow-hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `nft_gallery.templ`, Line: 17, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `nft_gallery.templ`, Line: 17, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"w-full h-full object-cover\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"w-full h-full flex items-center justify-center text-muted-foreground text-xs uppercase tracking-widest\">no image</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div class=\"p-3 flex items-center justify-between gap-2\"><span class=\"font-semibold truncate\">")
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, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Chain != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"text-xs uppercase tracking-widest text-muted-foreground\">")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

12
plugin.mod Normal file
View File

@ -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"

110
presets.json Normal file
View File

@ -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%"
}
}
}
]

173
register.go Normal file
View File

@ -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:<key>"; 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,
},
},
},
}
}

26
registration.go Normal file
View File

@ -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() },
}

View File

@ -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": ""
}
}
}

View File

@ -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": ""
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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": ""
}
}
}

View File

@ -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"
}
}
}

View File

@ -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": []
}
}
}

View File

@ -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": []
}
}
}

View File

@ -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": ""
}
}
}

View File

@ -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"
}
}
}

244
template.templ Normal file
View File

@ -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) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/y2k/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow max-w-4xl mx-auto w-full px-4 py-8">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// === Landing page template ===
templ Y2KLanding(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/y2k/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
@templ.Raw(data.Slots["hero"])
</section>
<section class="w-full">
@templ.Raw(data.Slots["marquee"])
</section>
<main class="flex-grow">
if main, ok := data.Slots["main"]; ok && main != "" {
<div class="max-w-6xl mx-auto px-4 py-16">
@templ.Raw(main)
</div>
}
</main>
<section class="w-full">
@templ.Raw(data.Slots["cta"])
</section>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// === Article / Zine page template ===
templ Y2KArticle(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/y2k/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<div class="max-w-6xl mx-auto w-full px-4 py-10 flex-grow grid gap-8 md:grid-cols-[1fr_280px]">
<main class="min-w-0">
if main, ok := data.Slots["main"]; ok && main != "" {
<article class="y2k-text prose max-w-none">
@templ.Raw(main)
</article>
} else {
<div class="py-20 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<aside class="min-w-0">
@templ.Raw(data.Slots["aside"])
</aside>
</div>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// === Full-width page template ===
templ Y2KFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/y2k/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col y2k-mesh-bg">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="max-w-4xl mx-auto py-20 px-4 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
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))
}

510
template_templ.go Normal file
View File

@ -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, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</header><main class=\"flex-grow max-w-4xl mx-auto w-full px-4 py-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"py-20 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// === Landing page template ===
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, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section><section class=\"w-full\">")
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, "</section><main class=\"flex-grow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"max-w-6xl mx-auto px-4 py-16\">")
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</main><section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</section><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</body></html>")
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, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</header><div class=\"max-w-6xl mx-auto w-full px-4 py-10 flex-grow grid gap-8 md:grid-cols-[1fr_280px]\"><main class=\"min-w-0\">")
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, "<article class=\"y2k-text prose max-w-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"py-20 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</main><aside class=\"min-w-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["aside"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</aside></div><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</body></html>")
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, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/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, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col y2k-mesh-bg\">")
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, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"max-w-4xl mx-auto py-20 px-4 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</body></html>")
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

17
text_override.go Normal file
View File

@ -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()
}

9
text_override.templ Normal file
View File

@ -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) {
<div class={ "y2k-text prose max-w-none", class }>
@templ.Raw(text)
</div>
}

68
text_override_templ.go Normal file
View File

@ -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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

46
tracklist.go Normal file
View File

@ -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
}

39
tracklist.templ Normal file
View File

@ -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) {
<section class="y2k-bevel rounded-xl overflow-hidden bg-card my-6" data-block-key="y2k:tracklist">
<header class="y2k-chrome-bg px-4 py-2 text-foreground uppercase tracking-wider text-xs">
tracklist
</header>
if len(data.Tracks) == 0 {
<p class="px-4 py-6 text-center text-muted-foreground">No tracks yet.</p>
} else {
<ol class="divide-y divide-border">
for i, t := range data.Tracks {
<li class="flex flex-wrap items-center gap-3 px-4 py-3">
<span class="w-6 shrink-0 text-muted-foreground text-sm tabular-nums">{ strconv.Itoa(i + 1) }</span>
<span class="flex-1 min-w-0 truncate text-foreground">
if t.URL != "" {
<a href={ templ.SafeURL(t.URL) } class="hover:underline">{ trackTitleOr(t.Title) }</a>
} else {
{ trackTitleOr(t.Title) }
}
</span>
<span class="text-muted-foreground text-sm tabular-nums whitespace-nowrap">{ durationOrDash(t.Duration) }</span>
</li>
}
</ol>
}
</section>
}
func durationOrDash(s string) string {
if s == "" {
return "--:--"
}
return s
}

148
tracklist_templ.go Normal file
View File

@ -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, "<section class=\"y2k-bevel rounded-xl overflow-hidden bg-card my-6\" data-block-key=\"y2k:tracklist\"><header class=\"y2k-chrome-bg px-4 py-2 text-foreground uppercase tracking-wider text-xs\">tracklist</header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Tracks) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"px-4 py-6 text-center text-muted-foreground\">No tracks yet.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ol class=\"divide-y divide-border\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i, t := range data.Tracks {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<li class=\"flex flex-wrap items-center gap-3 px-4 py-3\"><span class=\"w-6 shrink-0 text-muted-foreground text-sm tabular-nums\">")
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, "</span> <span class=\"flex-1 min-w-0 truncate text-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if t.URL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(t.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tracklist.templ`, Line: 21, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"hover:underline\">")
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, "</a>")
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, "</span> <span class=\"text-muted-foreground text-sm tabular-nums whitespace-nowrap\">")
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, "</span></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</ol>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func durationOrDash(s string) string {
if s == "" {
return "--:--"
}
return s
}
var _ = templruntime.GeneratedTemplate

42
waveform_player.go Normal file
View File

@ -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
}

49
waveform_player.templ Normal file
View File

@ -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) {
<figure
class="y2k-bevel y2k-chrome-bg rounded-xl overflow-hidden flex items-stretch gap-4 p-3 my-4"
data-block-key="y2k:waveform_player"
>
<div class="w-20 h-20 shrink-0 y2k-image-frame overflow-hidden bg-muted">
if data.CoverImage != "" {
<img src={ data.CoverImage } alt={ data.TrackTitle } class="w-full h-full object-cover"/>
} else {
<svg viewBox="0 0 24 24" class="w-full h-full text-muted-foreground p-4" fill="currentColor" aria-hidden="true">
<path d="M9 19V6l11-3v13"/>
</svg>
}
</div>
<div class="flex-1 min-w-0 flex flex-col justify-between gap-2">
<figcaption class="flex items-baseline justify-between gap-3 min-w-0">
<span class="y2k-heading text-base truncate text-foreground">{ trackTitleOr(data.TrackTitle) }</span>
if data.Artist != "" {
<span class="text-xs uppercase tracking-wider text-muted-foreground shrink-0">{ data.Artist }</span>
}
</figcaption>
<canvas
class="w-full h-12 bg-muted/40 rounded"
data-y2k-waveform
data-audio-url={ data.AudioURL }
aria-label="audio waveform"
></canvas>
if data.AudioURL != "" {
<audio controls preload="none" class="w-full" src={ data.AudioURL }>
Your browser does not support the audio element.
</audio>
} else {
<p class="text-xs text-muted-foreground">No audio source configured.</p>
}
</div>
</figure>
}
func trackTitleOr(s string) string {
if s == "" {
return "untitled track"
}
return s
}

168
waveform_player_templ.go Normal file
View File

@ -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, "<figure class=\"y2k-bevel y2k-chrome-bg rounded-xl overflow-hidden flex items-stretch gap-4 p-3 my-4\" data-block-key=\"y2k:waveform_player\"><div class=\"w-20 h-20 shrink-0 y2k-image-frame overflow-hidden bg-muted\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.CoverImage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.CoverImage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `waveform_player.templ`, Line: 13, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.TrackTitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `waveform_player.templ`, Line: 13, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"w-full h-full object-cover\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg viewBox=\"0 0 24 24\" class=\"w-full h-full text-muted-foreground p-4\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M9 19V6l11-3v13\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"flex-1 min-w-0 flex flex-col justify-between gap-2\"><figcaption class=\"flex items-baseline justify-between gap-3 min-w-0\"><span class=\"y2k-heading text-base truncate text-foreground\">")
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, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Artist != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"text-xs uppercase tracking-wider text-muted-foreground shrink-0\">")
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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</figcaption><canvas class=\"w-full h-12 bg-muted/40 rounded\" data-y2k-waveform data-audio-url=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.AudioURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `waveform_player.templ`, Line: 30, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" aria-label=\"audio waveform\"></canvas>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.AudioURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<audio controls preload=\"none\" class=\"w-full\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.AudioURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `waveform_player.templ`, Line: 34, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">Your browser does not support the audio element.</audio>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"text-xs text-muted-foreground\">No audio source configured.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></figure>")
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

37
webring_badge.go Normal file
View File

@ -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
}

18
webring_badge.templ Normal file
View File

@ -0,0 +1,18 @@
package main
// webringBadgeComponent renders the prev/ring/next badge triplet at the
// canonical 88x31 button size.
templ webringBadgeComponent(data WebringBadgeData) {
<nav class="flex items-center gap-2 my-4" data-block-key="y2k:webring_badge" aria-label="webring">
<a href={ templ.SafeURL(orHash(data.PrevHref)) } class="y2k-webring-badge">prev</a>
<span class="y2k-webring-badge">{ data.RingName }</span>
<a href={ templ.SafeURL(orHash(data.NextHref)) } class="y2k-webring-badge">next</a>
</nav>
}
func orHash(s string) string {
if s == "" {
return "#"
}
return s
}

88
webring_badge_templ.go Normal file
View File

@ -0,0 +1,88 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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, "<nav class=\"flex items-center gap-2 my-4\" data-block-key=\"y2k:webring_badge\" aria-label=\"webring\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(orHash(data.PrevHref)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `webring_badge.templ`, Line: 7, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"y2k-webring-badge\">prev</a> <span class=\"y2k-webring-badge\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.RingName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `webring_badge.templ`, Line: 8, Col: 49}
}
_, 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, "</span> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(orHash(data.NextHref)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `webring_badge.templ`, Line: 9, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"y2k-webring-badge\">next</a></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func orHash(s string) string {
if s == "" {
return "#"
}
return s
}
var _ = templruntime.GeneratedTemplate