Compare commits

..

14 Commits
v0.1.1 ... main

Author SHA1 Message Date
Alex Dunmow
47776c0b6f feat(y2k): remediation — fix hero, light-mode contrast, seed media, fonts, article
- hero: read styled-text fields as `field|get:"text"|default:field` so the
  home hero renders (bare-string seed values were hard-erroring the block to
  empty output — the "weird af header" root cause; page jumped navbar→audio).
- light-mode: add a background-token radial scrim behind the hero headline
  (lightens the centre in light, darkens in dark) for legible text over the mesh.
- seed media: ship 6 licensed stock images; wire 4 as Records cover art
  (chrome-liquid-metal/holographic-foil/abstract-neon/cassette) + the waveform
  now-playing cover (neon-rave) + a chrome image on About.
- waveform: replace the "No audio source set yet." placeholder with a deliberate
  "SIDE A // live to tape" mono transport readout.
- fonts: @font-face Orbitron/Inter/Share Tech Mono from /templates/y2k/fonts/*
  + typography refs in all presets (bundled fonts were inert dead code).
- article: expand tracking-to-tape to 10 band-journal paragraphs (kills the
  ~350px blog dead space).
- SVG geometry guard: explicit width/height on every sprite <use> icon +
  replace ::icon:: shortcodes with sized <svg> (Tailwind JIT balloon guard).
- capture Wave B screenshots (home/about/article × light+dark, 2880×1800);
  preview.png = dark home (dark-canonical theme).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:51 +08:00
Alex Dunmow
4a77500bf0 bump to 0.3.1 2026-07-06 20:19:10 +08:00
Alex Dunmow
ea61a282ce bump to 0.3.0 2026-07-06 10:07:20 +08:00
Alex Dunmow
d6a9422633 feat: Wave B gallery screenshots (home/about/article × light+dark) 2026-07-06 10:07:00 +08:00
Alex Dunmow
925b1e5349 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
43a2e3dce4 refactor: replace hand-rolled utility CSS with Tailwind classes (host build now scans theme templates)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:54:30 +08:00
Alex Dunmow
b05ecf145d fix(y2k): remediate home whitespace + mobile responsiveness
- discography: kill the ballooning empty-media box — compact fixed-height
  chrome tile placeholder instead of a full aspect-square gray void, and
  a real CSS media-query grid (.y2k-grid-records: 1/2/4 cols) so column
  count no longer depends on the host Tailwind having scanned this persona
  template (it stuck at 2 cols at 1440px, making each empty square huge).
- landing: header absolute top-0 -> sticky top-0 (matches every other
  page template) so the audio bar no longer collides with / underlaps
  the navbar.
- waveform_player: constrain to a centered max-w-4xl column with section
  padding (was edge-to-edge full-bleed) and stack on phones.
- headings: remove the wide primary text-shadow halo that read as a stray
  render artifact behind every heading; tone the chrome shimmer to a faint
  glint. All colours stay semantic hsl(var(--token)).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:16:40 +08:00
Alex Dunmow
0d1e7164fe bump to 0.2.1 2026-07-05 16:44:49 +08:00
Alex Dunmow
7c178b90ea docs: publishing README 2026-07-05 13:44:05 +08:00
Alex Dunmow
ed85f673f4 feat(y2k): Wave A theme-fleet build — 49 overrides, 7 templates, band persona (WO-TF-026)
Rebuild the y2k theme to the OVERRIDE-CONTRACT (frozen cms a0f07e2ae) with the
locked musician/band persona: liquid chrome, frosted-plastic bevels, gradient
mesh, marquees, glitter, a webring, and a dependency-free canvas metaball hero.

- All 49 mandatory builtin overrides in templates/overrides/y2k/ + manifest
  template_overrides; every content/provider field preserved from the frozen
  builtin default, only the chrome re-skinned (legacy button/card field reads
  already reconciled to link/style/label + text/media).
- 7 page templates: default, full-width, landing, article, blog-index, contact,
  auth — chrome header, glitter backdrop, chrome footer.
- Hero override ships a GO-BIG canvas liquid-chrome metaball showpiece
  (hero/landing only): lazy-init on view, prefers-reduced-motion honored, static
  chrome-mesh no-JS fallback, dependency-free.
- Persona blocks (5): waveform_player, tracklist, tour_dates, discography,
  webring_badge. Duplicating theme blocks became overrides; nft_gallery DELETED
  per WO.
- 5 dual-mode presets (frosted iMac plastic light + rave blacklight dark), all
  19 tokens, mode both, tuned dark.
- Bundled OFL fonts (Orbitron / Inter / Share Tech Mono) with licenses; all
  font-family via var(--font-heading|body|mono, fallback). Updated
  RECOMMENDED_FONTS/ICONS; required_icon_packs declared.
- Themed email-safe wrapper (tables, inline styles, chrome header, marquee
  footer). Master pages + demo seed (home/about/blog x3/contact/login) in band
  voice; contact form wired to seeded contact_submissions + row_inserted
  email-admins workflow.

Known platform gaps (OVERRIDE-CONTRACT §11/§12, unchanged): auth trio +
page-suggestions don't dispatch overrides yet; provider-backed overrides render
empty live lists until the codeless loader registers template sources. Files
authored faithful to builtin field/provider names; they light up when the cms
fix lands. Visual quality UNVERIFIED until Wave B.

Gates: `make` exit 0 (codeless y2k-0.2.0.bnp), `ninja plugin verify` OK,
check-safety exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:43 +08:00
Alex Dunmow
a86a54639a feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Convert the Y2K theme from a Go+templ wasm plugin to a fully codeless .bnp:
blocks/blocks.yaml + .ninjatpl, page/system templates, template overrides,
email wrapper, master pages and CSS declared in manifest.yaml. All *.go, *.templ,
*_templ.go, go.mod and go.sum deleted; schemas moved schemas/ -> blocks/.

Static parity proven 48/48 identical against the templ baseline (10 blocks x
full+empty+variant branches, 4 overrides incl. heading L1-6, email full+empty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:10:52 +08:00
Alex Dunmow
93001afe44 chore: repo hygiene for the wasm era (WO-WZ-015 fix round)
Replace the retired .so Makefile with the wasm/.bnp convention (build /
verify / archive-check / publish via ninja plugin build), gitignore *.so
and *.bnp artifacts, and refresh the stale [compatibility] block_core
constraint to the v0.15 era.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:15:59 +08:00
Alex Dunmow
0e8a203680 wasm port (WO-WZ-015): reactor-mode wasip1 guest, core v0.15.2
Add wasmguest.Serve(Registration) boilerplate and bump block/core to v0.15.2
for the .so->wasm migration. Built + published as .bnp to the dev registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:11:12 +08:00
Alex Dunmow
6157aa76db bump to 0.1.2 2026-07-03 22:10:41 +08:00
161 changed files with 3097 additions and 5080 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
tmp/
.idea/
.vscode/
*.bnp

View File

@ -1,27 +1,55 @@
# Y2K — local build helpers (.so plugin workflow)
# y2k — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
#
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
#
# Usage:
# make # build y2k.so locally (CGO + buildmode=plugin)
# make clean # remove y2k.so
# make templ # regenerate *_templ.go via templ generate
# make build # pack y2k-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry)
# make clean # remove build artefacts
.PHONY: all clean templ help
.PHONY: build verify archive-check publish clean help
PLUGIN_NAME := y2k
NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
all: $(PLUGIN_NAME).so
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build:
$(NINJA) plugin build --dir .
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
# Validate the built .bnp exactly as the registry/loader will.
verify:
$(NINJA) plugin verify $(BNP)
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK"
# Publish the prebuilt .bnp to the active registry.
# Pass --host https://my.blockninja.dev for the dev orchestrator.
publish: build
$(NINJA) plugin publish --bnp $(BNP)
# Remove build artefacts.
clean:
rm -f $(PLUGIN_NAME).so
templ:
templ generate
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " clean Remove $(PLUGIN_NAME).so"
@echo " templ Regenerate *_templ.go files"
@echo " build Pack the codeless .bnp (no wasm)"
@echo " verify Validate the built .bnp (loader-identical checks)"
@echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry"
@echo " clean Remove build artefacts"

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# Y2K
A millennium-web theme built for musicians and bands. Liquid-chrome bevels, frosted plastic, gradient mesh, marquees, and a little glitter bring back the turn-of-the-2000s music site. Frosted iMac plastic in light mode, rave blacklight in dark.
## Good for
We built Y2K for artists who want a page with personality. Treat these as starting points, not limits. If the mood fits your act, use it:
- Musicians and bands
- Record and tour pages
- Indie labels and collectives
- Creators leaning into nostalgia
## What you get
- Five color presets, each with a tuned light mode and a tuned dark mode: Chrome Candy, Bubblegum, Lime Fizz, Graphite, and Tangerine.
- Orbitron display type, Inter body, and Share Tech Mono, bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Five persona blocks: a discography, tour dates, a tracklist, a waveform player, and a webring badge.
- A liquid-chrome hero that renders metaballs on a canvas at the top of the page. It respects reduced-motion and falls back to a static chrome backdrop with JavaScript off.
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.

View File

@ -1,50 +1,36 @@
# 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.
The Y2K theme **bundles** three OFL families as latin-subset woff2 (see
`assets/fonts/LICENSES.md`). They register as `source=template` rows and are
selectable in the typography picker. The theme CSS consumes fonts only through
the host CSS custom properties `--font-heading`, `--font-body`, `--font-mono`,
with fallback stacks that lead with the bundled families — so the intended look
survives before an admin assigns anything.
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):
## Bundled (works out of the box)
## Heading (display)
| Slot | Family | Notes |
|---|---|---|
| Heading / display | **Orbitron** (OFL) | Geometric techno face for chrome-stretched headlines. |
| Body | **Inter** (OFL) | Clean neutral body. |
| Mono | **Share Tech Mono** (OFL) | Eyebrows, kickers, tabular data, the webring badge. |
- **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.
Assign each to its slot in the typography panel (they appear under the bundled /
template source). Until then, the fallback stacks below apply automatically.
## Body
## Fallback stacks (declared in `manifest.yaml` CSS)
- **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.
- Heading: `var(--font-heading, "Orbitron", "Michroma", system-ui, sans-serif)`
- Body: `var(--font-body, "Inter", system-ui, -apple-system, "Segoe UI", sans-serif)`
- Mono: `var(--font-mono, "Share Tech Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, monospace)`
## Mono
## Alternatives from the Google Fonts tab
- **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.
- **Michroma** (OFL) — wider techno display; a strong Orbitron alternative for the
Heading slot. Open the typography panel, Google Fonts tab, search "Michroma",
Add, assign to Heading.
- **Inter Tight** (OFL) — tighter body option; assign to Body the same way.
## 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.
The bundled subsets cover English (Latin + Latin-1 punctuation). Sites needing
extended Latin, Cyrillic, Greek, or Vietnamese should assign the full family from
the Google Fonts tab instead of relying on the bundled subset.

15
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,15 @@
# Recommended icon packs for the Y2K theme
The theme renders icons only as sprite references
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`) and declares its packs in
`plugin.mod` `required_icon_packs`. The loader does not yet auto-install packs,
so install these before activation:
| Pack | Why | Install |
|---|---|---|
| **lucide** | Default UI glyphs used across overrides and the persona blocks (music, disc, arrow, mail, user, etc.). Bundled with the CMS. | Open `/admin/icons`, confirm Lucide is installed and enabled (it ships with the CMS). |
| **simple-icons** | Brand marks for social links and OAuth buttons (github, instagram, youtube, spotify, bandcamp, google, facebook). Bundled with the CMS. | Open `/admin/icons`, confirm Simple Icons is installed and enabled. |
Both packs ship bundled with the CMS, so no download is normally required — just
confirm they are enabled under `/admin/icons`. Block schemas default their icon
fields to Lucide names, so the theme renders correctly out of the box.

25
assets/fonts/LICENSES.md Normal file
View File

@ -0,0 +1,25 @@
# Bundled font licenses — Y2K theme
All three families are bundled as latin-subset `.woff2` and are licensed under the
SIL Open Font License 1.1 (OFL). Redistribution inside this theme artifact is permitted.
| File | Family | License | Source |
|---|---|---|---|
| `orbitron-latin.woff2` | Orbitron (variable, wght 500900) | OFL 1.1 | Google Fonts (fonts.gstatic.com, v35), latin subset |
| `share-tech-mono-latin.woff2` | Share Tech Mono (400) | OFL 1.1 | Google Fonts (fonts.gstatic.com, v16), latin subset |
| `inter-latin.woff2` | Inter (variable, wght 400700) | OFL 1.1 | Google Fonts (fonts.gstatic.com, v20), latin subset |
OFL 1.1 full text: https://openfontlicense.org
- Orbitron (c) The Orbitron Project Authors (Matt McInerney).
- Share Tech Mono (c) Carrois Apostrophe.
- Inter (c) The Inter Project Authors (Rasmus Andersson).
These are the Google-hosted latin subsets (Latin + Latin-1 punctuation), chosen to keep
the bundle small (~73 KB total). They cover English content; sites needing extended Latin,
Cyrillic, Greek, or Vietnamese glyphs should assign the full family from the admin Google
Fonts picker instead.
Orbitron is the geometric techno display face. Michroma (also OFL) is an equally good
heading alternative and is called out in the fallback stack; assign it from the Google
Fonts tab if you prefer its wider proportions.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
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).

43
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,43 @@
# Y2K theme persona block definitions (codeless .bnp, WO-TF-026).
# Keys are fully qualified (y2k:<key>) because the codeless loader registers
# definitions verbatim — there is no PluginBlockRegistry prefixing in the
# codeless path (unlike the wasm/templ path). The qualified keys match the
# data-block attributes and the master_pages.json / seed references.
#
# Builtin-duplicating blocks (navbar, footer, hero, divider, marquee->
# announcement-bar, merch->pricing/card) are NOT theme blocks — they are
# template overrides in templates/overrides/y2k/. The old nft_gallery block is
# DELETED per WO-TF-026. Only true band/musician persona furniture lives here.
blocks:
- key: y2k:waveform_player
title: Waveform Player
description: Track card with cover art, a static chrome equalizer strip, and a native audio player.
category: content
schema: waveform_player.schema.json
template: waveform_player.ninjatpl
- key: y2k:tracklist
title: Tracklist
description: Numbered tracklist styled like a CD jewel-case insert with durations.
category: content
schema: tracklist.schema.json
template: tracklist.ninjatpl
- key: y2k:tour_dates
title: Tour Dates
description: Live dates with venue, city, sold-out state, and a ticket link.
category: content
schema: tour_dates.schema.json
template: tour_dates.ninjatpl
sample_data: tour_dates.sample.json
- key: y2k:discography
title: Discography
description: Grid of releases with cover art, year, and format.
category: content
schema: discography.schema.json
template: discography.ninjatpl
sample_data: discography.sample.json
- key: y2k:webring_badge
title: Webring Badge
description: Canonical 88x31 webring button row with prev / ring / next links.
category: navigation
schema: webring_badge.schema.json
template: webring_badge.ninjatpl

View File

@ -0,0 +1,29 @@
{# y2k:discography — grid of releases with cover art, year, format, and a link. #}
<section data-block="y2k:discography" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="y2k-display glitch text-2xl font-semibold tracking-tight md:text-3xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if releases %}
<div class="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{% for release in releases %}
<article class="y2k-bevel y2k-card y2k-glow group overflow-hidden bg-card text-card-foreground">
{% if release.cover %}
<div class="aspect-square overflow-hidden bg-muted">
{% if release.link %}<a href="{{ release.link }}" class="block h-full w-full">{% img release.cover alt=release.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% else %}{% img release.cover alt=release.title class="h-full w-full object-cover" %}{% endif %}
</div>
{% else %}
<div class="flex h-44 items-center justify-center y2k-chrome-bg text-muted-foreground">
{% if release.link %}<a href="{{ release.link }}" class="flex h-full w-full items-center justify-center" aria-label="{{ release.title }}"><svg width="48" height="48" class="h-12 w-12" aria-hidden="true"><use href="/icons/lucide.svg#disc"></use></svg></a>{% else %}<svg width="48" height="48" class="h-12 w-12" aria-hidden="true"><use href="/icons/lucide.svg#disc"></use></svg>{% endif %}
</div>
{% endif %}
<div class="p-3">
{% if release.title %}<h3 class="y2k-heading truncate text-sm text-foreground">{% if release.link %}<a href="{{ release.link }}" class="hover:underline">{{ release.title }}</a>{% else %}{{ release.title }}{% endif %}</h3>{% endif %}
<p class="y2k-mono mt-1 flex items-center gap-2 text-xs uppercase tracking-wide text-muted-foreground">{% if release.year %}<span>{{ release.year }}</span>{% endif %}{% if release.format %}<span class="text-primary">{{ release.format }}</span>{% endif %}</p>
</div>
</article>
{% endfor %}
</div>
{% else %}
<p class="y2k-mono mt-8 text-muted-foreground">No releases yet.</p>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,9 @@
{
"heading": "Discography",
"releases": [
{ "title": "Chrome Hearts", "year": "2001", "format": "LP", "cover": "", "link": "#" },
{ "title": "Dial-Up Dreams", "year": "2000", "format": "EP", "cover": "", "link": "#" },
{ "title": "Frosted", "year": "1999", "format": "Single", "cover": "", "link": "#" },
{ "title": "Skyline Demos", "year": "1998", "format": "Cassette", "cover": "", "link": "#" }
]
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Discography",
"description": "A grid of releases with cover art, year, and a link.",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"x-editor": "text",
"default": "Discography"
},
"releases": {
"type": "array",
"title": "Releases",
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"title": { "type": "string", "title": "Title", "x-editor": "text" },
"year": { "type": "string", "title": "Year", "x-editor": "text" },
"format": { "type": "string", "title": "Format", "x-editor": "text" },
"cover": { "type": "string", "title": "Cover art", "x-editor": "media" },
"link": { "type": "string", "title": "Link", "x-editor": "text" }
}
},
"default": []
}
}
}

View File

@ -0,0 +1,23 @@
{# y2k:tour_dates — live dates with venue, city, and a ticket link. Chrome-bevel rows. #}
<section data-block="y2k:tour_dates" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="y2k-display glitch text-2xl font-semibold tracking-tight md:text-3xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-3 max-w-2xl text-muted-foreground">{{ intro }}</p>{% endif %}
{% if dates %}
<ul class="mt-8 space-y-3">
{% for show in dates %}
<li class="y2k-bevel y2k-card flex flex-wrap items-center gap-x-4 gap-y-2 bg-card p-4 text-card-foreground">
{% if show.date %}<span class="y2k-mono w-28 shrink-0 text-sm uppercase tracking-wide text-primary">{{ show.date }}</span>{% endif %}
<span class="min-w-0 flex-1">
{% if show.venue %}<span class="y2k-heading block truncate text-base text-foreground">{{ show.venue }}</span>{% endif %}
{% if show.city %}<span class="block text-sm text-muted-foreground">{{ show.city }}</span>{% endif %}
</span>
{% if show.soldOut %}<span class="y2k-mono shrink-0 rounded-md border border-border bg-muted px-3 py-1.5 text-xs uppercase tracking-wide text-muted-foreground">Sold out</span>{% elif show.ticketUrl %}<a href="{{ show.ticketUrl }}" class="y2k-button rgb-split shrink-0 text-sm">Tickets</a>{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="y2k-mono mt-8 text-muted-foreground">No dates announced yet.</p>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,9 @@
{
"heading": "Tour dates",
"intro": "Doors an hour before. All ages unless noted.",
"dates": [
{ "date": "Fri 12 Sep", "venue": "The Chrome Room", "city": "Melbourne", "ticketUrl": "#", "soldOut": false },
{ "date": "Sat 13 Sep", "venue": "Rollerdrome", "city": "Sydney", "ticketUrl": "#", "soldOut": true },
{ "date": "Fri 19 Sep", "venue": "Basement 88", "city": "Brisbane", "ticketUrl": "#", "soldOut": false }
]
}

View File

@ -0,0 +1,36 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Tour Dates",
"description": "A list of live dates with venue, city, and a ticket link.",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"x-editor": "text",
"default": "Tour dates"
},
"intro": {
"type": "string",
"title": "Intro",
"x-editor": "text",
"default": ""
},
"dates": {
"type": "array",
"title": "Dates",
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"date": { "type": "string", "title": "Date", "x-editor": "text" },
"venue": { "type": "string", "title": "Venue", "x-editor": "text" },
"city": { "type": "string", "title": "City", "x-editor": "text" },
"ticketUrl": { "type": "string", "title": "Ticket URL", "x-editor": "text" },
"soldOut": { "type": "boolean", "title": "Sold out", "x-editor": "boolean", "default": false }
}
},
"default": []
}
}
}

View File

@ -0,0 +1,8 @@
<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 not tracks %}<p class="px-4 py-6 text-center text-muted-foreground">No tracks yet.</p>{% else %}<ol class="divide-y divide-border">{% for track in 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">{{ forloop.Counter }}</span>
<span class="flex-1 min-w-0 truncate text-foreground">{% if track.url %}<a href="{{ track.url }}" class="hover:underline">{{ track.title|default:"untitled track" }}</a>{% else %}{{ track.title|default:"untitled track" }}{% endif %}</span>
<span class="text-muted-foreground text-sm tabular-nums whitespace-nowrap">{{ track.duration|default:"--:--" }}</span>
</li>{% endfor %}</ol>{% endif %}
</section>

View File

@ -0,0 +1,17 @@
{% if audioUrl or trackTitle %}<section class="py-8 bg-background" data-block-key="y2k:waveform_player">
<div class="mx-auto max-w-4xl px-4">
<figure class="y2k-bevel y2k-chrome-bg rounded-xl overflow-hidden flex flex-col gap-4 p-4 sm:flex-row sm:items-stretch">
<div class="w-20 h-20 shrink-0 y2k-image-frame overflow-hidden bg-muted">
{% if coverImage %}{% img coverImage alt=trackTitle class="w-full h-full object-cover" %}{% else %}<span class="flex h-full w-full items-center justify-center text-muted-foreground"><svg width="32" height="32" class="h-8 w-8" aria-hidden="true"><use href="/icons/lucide.svg#music"></use></svg></span>{% endif %}
</div>
<div class="flex-1 min-w-0 flex flex-col justify-between gap-2">
<figcaption class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 min-w-0">
<span class="y2k-heading text-base truncate text-foreground">{{ trackTitle|default:"untitled track" }}</span>
{% if artist %}<span class="y2k-mono text-xs uppercase tracking-wider text-muted-foreground shrink-0">{{ artist }}</span>{% endif %}
</figcaption>
<div class="w-full h-12 y2k-waveform" aria-hidden="true"></div>
{% if audioUrl %}<audio controls preload="none" class="w-full" src="{{ audioUrl }}">Your browser does not support the audio element.</audio>{% else %}<div class="y2k-mono flex items-center gap-2.5 text-xs uppercase tracking-widest text-foreground"><span aria-hidden="true" class="y2k-status-dot inline-block h-2 w-2 shrink-0 rounded-full bg-primary"></span><span class="font-semibold text-primary">Side A</span><span aria-hidden="true" class="text-foreground/40">//</span><span class="normal-case tracking-wide text-foreground/70">live to tape</span></div>{% endif %}
</div>
</figure>
</div>
</section>{% endif %}

View File

@ -0,0 +1,5 @@
<nav class="flex items-center gap-2 my-4" data-block-key="y2k:webring_badge" aria-label="webring">
<a href="{{ prevHref|default:"#" }}" class="y2k-webring-badge">prev</a>
<span class="y2k-webring-badge">{{ ringName|default:"the y2k.fm ring" }}</span>
<a href="{{ nextHref|default:"#" }}" class="y2k-webring-badge">next</a>
</nav>

View File

@ -1,28 +0,0 @@
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
}

View File

@ -1,9 +0,0 @@
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>
}

View File

@ -1,99 +0,0 @@
// 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

View File

@ -1,37 +0,0 @@
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
}

View File

@ -1,38 +0,0 @@
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
}

View File

@ -1,169 +0,0 @@
// 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
View File

@ -1,228 +0,0 @@
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;
}
}
`

View File

@ -1,83 +0,0 @@
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"
}

View File

@ -1,88 +0,0 @@
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>
}

View File

@ -1,251 +0,0 @@
// 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

View File

@ -1,57 +0,0 @@
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,
}
}

View File

@ -1 +1,28 @@
[]
[
{
"name": "Orbitron",
"family": "Orbitron",
"variants": [
{ "weight": "500", "style": "normal", "file": "fonts/orbitron-latin.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/orbitron-latin.woff2" },
{ "weight": "900", "style": "normal", "file": "fonts/orbitron-latin.woff2" }
]
},
{
"name": "Share Tech Mono",
"family": "Share Tech Mono",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/share-tech-mono-latin.woff2" }
]
},
{
"name": "Inter",
"family": "Inter",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/inter-latin.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/inter-latin.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/inter-latin.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/inter-latin.woff2" }
]
}
]

View File

@ -1,46 +0,0 @@
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
}

View File

@ -1,24 +0,0 @@
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>
}

View File

@ -1,102 +0,0 @@
// 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

View File

@ -1,26 +0,0 @@
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()
}

View File

@ -1,25 +0,0 @@
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 "*"
}
}

View File

@ -1,107 +0,0 @@
// 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
View File

@ -1,20 +0,0 @@
module git.dev.alexdunmow.com/block/themes/y2k
go 1.26.4
require (
git.dev.alexdunmow.com/block/core v0.14.1
github.com/a-h/templ v0.3.1020
)
require (
connectrpc.com/connect v1.20.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

44
go.sum
View File

@ -1,44 +0,0 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,39 +0,0 @@
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
}

View File

@ -1,41 +0,0 @@
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>
}
}

View File

@ -1,311 +0,0 @@
// 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

View File

@ -1,48 +0,0 @@
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
}
// 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
}

View File

@ -1,32 +0,0 @@
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
}

View File

@ -1,11 +0,0 @@
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>
}

View File

@ -1,108 +0,0 @@
// 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

519
manifest.yaml Normal file
View File

@ -0,0 +1,519 @@
# Y2K theme — codeless .bnp manifest (WO-TF-026, Wave A).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
# Persona: musician / band. Millennium-web nostalgia — liquid chrome, frosted
# iMac plastic (light) and rave blacklight (dark), marquees, glitter, a webring,
# and a dependency-free canvas metaball hero showpiece (hero/landing only).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs: ["lucide", "simple-icons"]
system_templates:
- key: y2k
title: Y2K
description: "Millennium-web band theme — liquid-chrome bevels, frosted plastic, marquees, glitter, a webring, and a canvas metaball hero."
page_templates:
- system: y2k
key: default
title: Default
description: "Chrome header, sparkle backdrop, plastic content column, chrome footer"
slots: [header, main, footer]
- system: y2k
key: full-width
title: Full Width
description: "Edge-to-edge showcase layout with a mesh backdrop, no max-width"
slots: [header, main, footer]
- system: y2k
key: landing
title: Landing
description: "Canvas metaball hero, chrome feature grid, glitter CTA, chrome footer"
slots: [header, main, footer]
- system: y2k
key: article
title: Article / Liner Notes
description: "Prose column with a chrome sidebar aside — liner-notes layout"
slots: [header, main, aside, footer]
- system: y2k
key: blog-index
title: Blog Index
description: "Zine feed of chrome post cards"
slots: [header, main, footer]
- system: y2k
key: contact
title: Contact
description: "Chrome contact console — form panel plus a plastic contact card"
slots: [header, main, footer]
- system: y2k
key: auth
title: Auth
description: "Centered chrome auth panel for login / register / reset"
slots: [header, main, footer]
# All 49 mandatory-tier builtin overrides (OVERRIDE-CONTRACT §1), dashed keys.
template_overrides:
# Chrome (4)
- { template: y2k, block: navbar }
- { template: y2k, block: footer }
- { template: y2k, block: utility-bar }
- { template: y2k, block: announcement-bar }
# Marketing (12)
- { template: y2k, block: hero }
- { template: y2k, block: feature-grid }
- { template: y2k, block: rich-section }
- { template: y2k, block: stats }
- { template: y2k, block: testimonial }
- { template: y2k, block: logo-strip }
- { template: y2k, block: pricing }
- { template: y2k, block: team }
- { template: y2k, block: faq }
- { template: y2k, block: timeline }
- { template: y2k, block: cta }
- { template: y2k, block: countdown }
# Primitives (11)
- { template: y2k, block: heading }
- { template: y2k, block: text }
- { template: y2k, block: rich-text }
- { template: y2k, block: image }
- { template: y2k, block: quote }
- { template: y2k, block: divider }
- { template: y2k, block: button }
- { template: y2k, block: card }
- { template: y2k, block: gallery }
- { template: y2k, block: video-embed }
- { template: y2k, block: audio-embed }
# Commerce / local (6)
- { template: y2k, block: menu-list }
- { template: y2k, block: event-list }
- { template: y2k, block: contact-form }
- { template: y2k, block: contact-card }
- { template: y2k, block: hours-location }
- { template: y2k, block: social-links }
# Blog family (12)
- { template: y2k, block: blog-hero }
- { template: y2k, block: featured-posts }
- { template: y2k, block: related-posts }
- { template: y2k, block: popular-posts }
- { template: y2k, block: category-list }
- { template: y2k, block: post-hero }
- { template: y2k, block: post-metadata }
- { template: y2k, block: author-bio }
- { template: y2k, block: author-bio-hero }
- { template: y2k, block: newsletter-signup }
- { template: y2k, block: breadcrumbs }
- { template: y2k, block: sidebar-nav }
# Auth surface (3)
- { template: y2k, block: auth-form }
- { template: y2k, block: password-reset-form }
- { template: y2k, block: auth-status }
# System (1)
- { template: y2k, block: page-suggestions }
email_wrappers:
- y2k
css:
input_css_append: |
/* ============================================================
Y2K — millennium-web band theme (WO-TF-026).
Liquid chrome, frosted-plastic bevels, gradient mesh, marquee,
glitter/sparkle, webring badge, and a canvas metaball hero.
Light = frosted iMac plastic. Dark = rave blacklight.
Every colour resolves through the host shadcn HSL tokens via
hsl(var(--token)); the extra chrome/mesh/bevel custom properties
below are HSL triples consumed the same way. No literal hex/rgb.
============================================================ */
/* --- Bundled display faces (CMS does not emit @font-face for
LoadedPlugin.BundledFonts — dead code; theme-side workaround per
gotham@1.2.0). Served from the extracted .bnp at /templates/y2k/fonts/*.
Presets set typography.fontHeading/body/mono to template:y2k:<Family>,
which resolves --font-heading/body/mono to these families. --------- */
@font-face { font-family: 'Orbitron'; src: url('/templates/y2k/fonts/orbitron-latin.woff2') format('woff2'); font-weight: 400 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Share Tech Mono'; src: url('/templates/y2k/fonts/share-tech-mono-latin.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/y2k/fonts/inter-latin.woff2') format('woff2'); font-weight: 400 700; font-style: normal; font-display: swap; }
/* --- Chrome / mesh / bevel channel tokens ----------------- */
:root {
--chrome-1: 220 40% 96%;
--chrome-2: 205 45% 82%;
--chrome-3: 300 45% 78%;
--chrome-4: 240 35% 55%;
--mesh-a: 320 95% 62%;
--mesh-b: 190 90% 60%;
--mesh-c: 55 95% 68%;
--bevel-light: 0 0% 100%;
--bevel-dark: 250 45% 22%;
}
.dark {
--chrome-1: 280 35% 24%;
--chrome-2: 250 40% 18%;
--chrome-3: 315 55% 30%;
--chrome-4: 265 45% 10%;
--mesh-a: 320 100% 66%;
--mesh-b: 150 95% 58%;
--mesh-c: 275 95% 66%;
--bevel-light: 300 60% 78%;
--bevel-dark: 270 80% 4%;
}
/* --- Keyframes -------------------------------------------- */
@keyframes y2k-marquee-x {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(-50%, 0, 0); }
}
@keyframes y2k-sparkle {
0%, 100% { opacity: 0; transform: scale(0.4) rotate(0deg); }
50% { opacity: 1; transform: scale(1) rotate(180deg); }
}
@keyframes y2k-shimmer-sweep {
0% { background-position: -150% 0; }
100% { background-position: 250% 0; }
}
@keyframes y2k-blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
@keyframes y2k-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.82); }
}
/* --- Typography ------------------------------------------- */
/* Families flow through the admin-bound CSS variables; the fallback
leads with the bundled OFL family so the intended look survives
before the admin assigns a slot. @font-face for the bundled
families is auto-emitted from fonts.json (source=template rows). */
.y2k-body {
font-family: var(--font-body, "Inter", system-ui, -apple-system, "Segoe UI", sans-serif);
}
.y2k-mono,
.y2k-mono code,
pre.y2k-mono,
code.y2k-mono {
font-family: var(--font-mono, "Share Tech Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, monospace);
}
.y2k-display {
font-family: var(--font-heading, "Orbitron", "Michroma", system-ui, sans-serif);
letter-spacing: 0.02em;
}
/* Beveled chrome headline — bright top rim, dark under-shadow, coloured halo. */
.y2k-heading,
.glitch {
font-family: var(--font-heading, "Orbitron", "Michroma", system-ui, sans-serif);
letter-spacing: 0.03em;
/* Subtle chrome bevel only — the old wide primary halo read as a
stray rendering artifact behind every heading, so it is gone. */
text-shadow:
0 1px 0 hsl(var(--bevel-light) / 0.55),
0 1px 1px hsl(var(--bevel-dark) / 0.22);
}
/* Chrome shimmer sweep across big headlines (motion, reduced-motion-safe).
Toned to a faint glint so it never smears the letters in a still frame. */
.glitch {
position: relative;
background-image: linear-gradient(
100deg,
transparent 35%,
hsl(var(--bevel-light) / 0.3) 48%,
hsl(var(--accent) / 0.22) 50%,
transparent 62%);
background-size: 220% 100%;
background-repeat: no-repeat;
-webkit-background-clip: text;
background-clip: text;
animation: y2k-shimmer-sweep 5.5s ease-in-out infinite;
}
.y2k-prose,
.y2k-prose p,
.y2k-prose li {
font-family: var(--font-body, "Inter", system-ui, sans-serif);
line-height: 1.75;
}
.y2k-prose code {
color: hsl(var(--primary));
background-color: hsl(var(--muted));
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-family: var(--font-mono, "Share Tech Mono", ui-monospace, monospace);
font-size: 0.875em;
}
/* Eyebrow / kicker: mono, uppercase, wide-tracked chrome readout. */
.y2k-eyebrow {
font-family: var(--font-mono, "Share Tech Mono", ui-monospace, monospace);
text-transform: uppercase;
letter-spacing: 0.22em;
font-size: 0.75rem;
color: hsl(var(--primary));
}
/* --- Chrome surfaces / backdrops -------------------------- */
.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 18% 20%, hsl(var(--mesh-a) / 0.8) 0%, transparent 55%),
radial-gradient(at 82% 28%, hsl(var(--mesh-b) / 0.75) 0%, transparent 55%),
radial-gradient(at 50% 82%, hsl(var(--mesh-c) / 0.65) 0%, transparent 55%),
linear-gradient(135deg, hsl(var(--background)) 0%, hsl(var(--card)) 100%);
}
/* Sticky header chrome bar. */
.y2k-chrome {
background-image: linear-gradient(180deg,
hsl(var(--chrome-1) / 0.92) 0%,
hsl(var(--chrome-2) / 0.88) 100%);
backdrop-filter: saturate(1.3) blur(6px);
box-shadow: inset 0 1px 0 hsl(var(--bevel-light) / 0.7);
}
/* --- Bevel / frame / glow --------------------------------- */
.y2k-bevel,
.hud-frame {
position: relative;
border: 2px solid hsl(var(--border));
box-shadow:
inset 0 1px 0 hsl(var(--bevel-light) / 0.8),
inset 0 -2px 0 hsl(var(--bevel-dark) / 0.3),
0 3px 10px hsl(var(--bevel-dark) / 0.28);
}
.y2k-image-frame {
border-radius: 0.75rem;
border: 2px solid hsl(var(--border));
box-shadow:
inset 0 1px 0 hsl(var(--bevel-light) / 0.5),
0 4px 16px hsl(var(--bevel-dark) / 0.35);
}
.y2k-glow,
.neon-edge-magenta {
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.4), 0 0 22px hsl(var(--primary) / 0.28);
transition: box-shadow 180ms ease, transform 180ms ease;
}
.y2k-glow:hover, .y2k-glow:focus-within,
.neon-edge-magenta:hover, .neon-edge-magenta:focus-within {
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.85), 0 0 32px hsl(var(--primary) / 0.48);
transform: translateY(-2px);
}
.neon-edge-cyan {
box-shadow: 0 0 0 1px hsl(var(--accent) / 0.4), 0 0 22px hsl(var(--accent) / 0.28);
transition: box-shadow 180ms ease, transform 180ms ease;
}
.neon-edge-cyan:hover, .neon-edge-cyan:focus-within {
box-shadow: 0 0 0 1px hsl(var(--accent) / 0.85), 0 0 32px hsl(var(--accent) / 0.48);
transform: translateY(-2px);
}
.neon-edge-lime {
box-shadow: 0 0 0 1px hsl(var(--ring) / 0.4), 0 0 22px hsl(var(--ring) / 0.28);
transition: box-shadow 180ms ease, transform 180ms ease;
}
.neon-edge-lime:hover, .neon-edge-lime:focus-within {
box-shadow: 0 0 0 1px hsl(var(--ring) / 0.85), 0 0 32px hsl(var(--ring) / 0.48);
transform: translateY(-2px);
}
/* clip-cut renders as soft rounded plastic corners in Y2K. */
.clip-cut { border-radius: 0.75rem; }
/* --- Buttons ---------------------------------------------- */
.y2k-button,
.y2k-btn,
.rgb-split {
transition: transform 90ms ease, box-shadow 90ms ease, filter 90ms ease;
}
.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.75rem;
background-image: linear-gradient(180deg,
hsl(var(--primary) / 0.96) 0%,
hsl(var(--primary)) 50%,
hsl(var(--primary) / 0.82) 100%);
color: hsl(var(--primary-foreground));
box-shadow:
inset 0 1px 0 hsl(var(--bevel-light) / 0.7),
inset 0 -2px 0 hsl(var(--bevel-dark) / 0.3),
0 2px 8px hsl(var(--bevel-dark) / 0.25);
}
.y2k-btn { border-radius: 0.75rem; }
.y2k-button:hover, .y2k-btn:hover, .rgb-split:hover, .rgb-split:focus-visible {
transform: translateY(-1px);
filter: brightness(1.05);
}
.y2k-button:active, .y2k-btn:active, .rgb-split:active {
transform: translateY(1px);
box-shadow:
inset 0 2px 5px hsl(var(--bevel-dark) / 0.45),
inset 0 -1px 0 hsl(var(--bevel-light) / 0.3);
}
.y2k-button:focus-visible, .y2k-btn:focus-visible,
.rgb-split:focus-visible,
button:focus-visible, a:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Cards ------------------------------------------------ */
.y2k-card {
border-radius: 0.75rem;
box-shadow:
inset 0 1px 0 hsl(var(--bevel-light) / 0.55),
0 4px 14px hsl(var(--bevel-dark) / 0.22);
}
/* --- 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: y2k-marquee-x;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: 28s;
}
.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 / glitter atmosphere ------------------------- */
.y2k-sparkle {
pointer-events: none;
animation: y2k-sparkle 1.6s ease-in-out infinite;
}
/* scanlines (ported name) is repurposed as a faint glitter dot field. */
.scanlines {
opacity: 0.4;
background-image:
radial-gradient(hsl(var(--primary) / 0.5) 1px, transparent 1.5px),
radial-gradient(hsl(var(--accent) / 0.4) 1px, transparent 1.5px);
background-size: 42px 42px, 68px 68px;
background-position: 0 0, 24px 30px;
}
/* --- Status / caret -------------------------------------- */
.y2k-status-dot {
animation: y2k-pulse 2.2s ease-in-out infinite;
}
.caret-blink {
display: inline-block;
animation: y2k-blink 1.05s steps(2, end) infinite;
}
/* --- Webring 88x31 badge ---------------------------------- */
.y2k-webring-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 88px;
height: 31px;
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(--primary-foreground));
text-decoration: none;
box-shadow: inset 0 1px 0 hsl(var(--bevel-light) / 0.6);
}
/* --- Static waveform strip (no canvas — motion tier) ------ */
.y2k-waveform {
background-image: repeating-linear-gradient(
90deg,
hsl(var(--primary) / 0.65) 0 2px,
hsl(var(--accent) / 0.4) 2px 4px,
transparent 4px 8px);
-webkit-mask-image: linear-gradient(90deg, transparent, hsl(0 0% 0%) 8%, hsl(0 0% 0%) 92%, transparent);
mask-image: linear-gradient(90deg, transparent, hsl(0 0% 0%) 8%, hsl(0 0% 0%) 92%, transparent);
border-radius: 0.25rem;
}
/* --- Empty state / article aside -------------------------- */
.y2k-empty {
font-family: var(--font-mono, "Share Tech Mono", ui-monospace, monospace);
letter-spacing: 0.14em;
}
.y2k-aside { border-radius: 0.75rem; }
/* --- Canvas metaball showpiece (hero / landing only) ------ */
/* The <canvas> lazy-inits from the hero's inline script; this styles the
container plus the static no-JS chrome-mesh fallback. */
.y2k-orb-wrap {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.y2k-orb-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
opacity: 0.65;
filter: blur(2px) contrast(1.15);
}
.y2k-orb-fallback {
position: absolute;
inset: 0;
background-image:
radial-gradient(60% 55% at 30% 35%, hsl(var(--mesh-a) / 0.55), transparent 60%),
radial-gradient(55% 50% at 72% 45%, hsl(var(--mesh-b) / 0.5), transparent 60%),
radial-gradient(50% 45% at 50% 80%, hsl(var(--mesh-c) / 0.45), transparent 60%),
linear-gradient(to bottom, transparent, hsl(var(--background) / 0.7));
}
/* Hero legibility scrim: a radial wash of the page background behind the
headline. In LIGHT mode --background is near-white so the centre stays
light and dark hero text reads over the bright mesh; in DARK mode
--background is near-black so the centre darkens and light text reads.
One rule fixes contrast in both modes without dulling the showpiece. */
.y2k-hero-scrim {
z-index: 5;
background-image: radial-gradient(
ellipse 78% 68% at 50% 46%,
hsl(var(--background) / 0.86) 0%,
hsl(var(--background) / 0.5) 46%,
transparent 80%);
}
/* --- Reduced motion: kill all motion ---------------------- */
@media (prefers-reduced-motion: reduce) {
.y2k-marquee-track,
.y2k-sparkle,
.glitch,
.caret-blink,
.y2k-status-dot,
.y2k-orb-canvas {
animation: none !important;
animation-duration: 0.01ms !important;
}
.glitch {
background-image: none;
-webkit-background-clip: border-box;
background-clip: border-box;
}
.y2k-orb-canvas { display: none; }
.y2k-button:hover, .y2k-btn:hover, .rgb-split:hover { transform: none; }
}
/* --- High-contrast: drop the glitter + orbs --------------- */
@media (prefers-contrast: more) {
.scanlines,
.y2k-orb-canvas { display: none; }
}

View File

@ -1,40 +0,0 @@
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
}

View File

@ -1,29 +0,0 @@
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>
}

View File

@ -1,111 +0,0 @@
// 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

22
master_pages.json Normal file
View File

@ -0,0 +1,22 @@
[
{
"key": "y2k:site-master",
"title": "Y2K Site Master",
"page_templates": ["default", "full-width", "landing", "article", "blog-index", "contact"],
"blocks": [
{ "block_key": "navbar", "title": "Chrome Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "CHROME STATIC" }, "companyName": "Chrome Static", "stickyBehavior": "sticky", "cta": { "text": "Listen", "url": "/#listen" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Chrome Footer", "content": { "layout": "columns", "companyName": "Chrome Static", "tagline": "A four-piece from the frosted end of the millennium.", "copyright": "Chrome Static" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "y2k:auth-master",
"title": "Y2K Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "navbar", "title": "Chrome Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "CHROME STATIC" }, "companyName": "Chrome Static" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Auth Panel", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Chrome Footer", "content": { "layout": "minimal", "companyName": "Chrome Static", "tagline": "A four-piece from the frosted end of the millennium.", "copyright": "Chrome Static" }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -1,41 +0,0 @@
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
}

View File

@ -1,52 +0,0 @@
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
}

View File

@ -1,178 +0,0 @@
// 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

View File

@ -1,41 +0,0 @@
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
}

View File

@ -1,64 +0,0 @@
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
}

View File

@ -1,137 +0,0 @@
// 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

View File

@ -1,49 +0,0 @@
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
}

View File

@ -1,33 +0,0 @@
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>
}

View File

@ -1,156 +0,0 @@
// 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

View File

@ -2,11 +2,8 @@
name = "y2k"
display_name = "Y2K"
scope = "@themes"
version = "0.1.1"
description = "Liquid-chrome Y2K revival theme for indie musicians, zines, and merch shops. Gradient mesh backgrounds, plastic-bevel buttons, marquee text, waveform blocks."
version = "0.4.0"
description = "Millennium-web band theme — liquid-chrome bevels, frosted plastic, gradient mesh, marquees, glitter, a webring, and a canvas metaball hero. Built for musicians and bands."
kind = "theme"
categories = ["templates"]
tags = ["retro", "chrome", "vaporwave", "pastel", "music", "creator", "nostalgia", "web3"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"
tags = ["retro", "chrome", "vaporwave", "music", "band", "creator", "nostalgia", "dark"]

View File

@ -1,109 +1,286 @@
[
{
"id": "chrome-dream",
"name": "Liquid Chrome Dream",
"description": "Magenta-to-teal mesh on tinted glass cards. Works in both light and dark modes.",
"id": "chrome-candy",
"name": "Chrome Candy",
"description": "Bondi-blue frosted plastic in light, UV magenta rave in dark — the Y2K default",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:y2k:Orbitron",
"fontBody": "template:y2k:Inter",
"fontMono": "template:y2k:Share Tech Mono",
"fontSizeBase": "16",
"lineHeightBase": "1.65"
},
"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%",
"background": "205 55% 96%",
"foreground": "230 40% 14%",
"card": "205 45% 99%",
"cardForeground": "230 40% 14%",
"popover": "205 45% 99%",
"popoverForeground": "230 40% 14%",
"primary": "320 90% 55%",
"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%",
"secondary": "200 45% 90%",
"secondaryForeground": "230 40% 14%",
"muted": "205 35% 92%",
"mutedForeground": "225 20% 40%",
"accent": "190 85% 45%",
"accentForeground": "230 40% 12%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 100%",
"border": "330 50% 85%",
"input": "330 40% 90%",
"ring": "330 95% 60%"
"border": "205 35% 84%",
"input": "205 35% 84%",
"ring": "270 80% 58%"
},
"darkColors": {
"background": "260 45% 6%",
"foreground": "300 25% 95%",
"card": "265 40% 10%",
"cardForeground": "300 25% 95%",
"popover": "265 40% 10%",
"popoverForeground": "300 25% 95%",
"primary": "320 100% 62%",
"primaryForeground": "260 45% 6%",
"secondary": "265 35% 16%",
"secondaryForeground": "300 25% 95%",
"muted": "265 30% 14%",
"mutedForeground": "285 20% 68%",
"accent": "150 95% 58%",
"accentForeground": "260 45% 6%",
"destructive": "0 95% 64%",
"destructiveForeground": "260 45% 6%",
"border": "270 40% 22%",
"input": "270 40% 20%",
"ring": "275 100% 68%"
}
}
},
{
"id": "bubblegum",
"name": "Bubblegum",
"description": "Cotton-candy pink plastic in light, hot-pink blacklight in dark",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:y2k:Orbitron",
"fontBody": "template:y2k:Inter",
"fontMono": "template:y2k:Share Tech Mono",
"fontSizeBase": "16",
"lineHeightBase": "1.65"
},
"lightColors": {
"background": "330 60% 97%",
"foreground": "320 35% 16%",
"card": "330 50% 99%",
"cardForeground": "320 35% 16%",
"popover": "330 50% 99%",
"popoverForeground": "320 35% 16%",
"primary": "330 90% 58%",
"primaryForeground": "0 0% 100%",
"secondary": "325 45% 92%",
"secondaryForeground": "320 35% 16%",
"muted": "330 35% 93%",
"mutedForeground": "320 20% 42%",
"accent": "270 80% 60%",
"accentForeground": "0 0% 100%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 100%",
"border": "330 35% 86%",
"input": "330 35% 86%",
"ring": "330 90% 58%"
},
"darkColors": {
"background": "315 45% 6%",
"foreground": "330 30% 95%",
"card": "315 42% 10%",
"cardForeground": "330 30% 95%",
"popover": "315 42% 10%",
"popoverForeground": "330 30% 95%",
"primary": "330 100% 66%",
"primaryForeground": "315 45% 6%",
"secondary": "315 35% 16%",
"secondaryForeground": "330 30% 95%",
"muted": "315 30% 14%",
"mutedForeground": "325 20% 68%",
"accent": "275 100% 72%",
"accentForeground": "315 45% 6%",
"destructive": "0 95% 64%",
"destructiveForeground": "315 45% 6%",
"border": "320 40% 22%",
"input": "320 40% 20%",
"ring": "330 100% 66%"
}
}
},
{
"id": "lime-fizz",
"name": "Lime Fizz",
"description": "Frosted lime soda in light, radioactive green rave in dark",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:y2k:Orbitron",
"fontBody": "template:y2k:Inter",
"fontMono": "template:y2k:Share Tech Mono",
"fontSizeBase": "16",
"lineHeightBase": "1.65"
},
"lightColors": {
"background": "150 45% 96%",
"foreground": "170 35% 12%",
"card": "150 40% 99%",
"cardForeground": "170 35% 12%",
"popover": "150 40% 99%",
"popoverForeground": "170 35% 12%",
"primary": "150 75% 38%",
"primaryForeground": "0 0% 100%",
"secondary": "150 35% 90%",
"secondaryForeground": "170 35% 12%",
"muted": "150 28% 92%",
"mutedForeground": "170 18% 40%",
"accent": "320 85% 55%",
"accentForeground": "0 0% 100%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 100%",
"border": "150 28% 82%",
"input": "150 28% 82%",
"ring": "150 75% 38%"
},
"darkColors": {
"background": "165 40% 5%",
"foreground": "140 30% 92%",
"card": "168 38% 8%",
"cardForeground": "140 30% 92%",
"popover": "168 38% 8%",
"popoverForeground": "140 30% 92%",
"primary": "145 100% 55%",
"primaryForeground": "165 40% 5%",
"secondary": "168 30% 14%",
"secondaryForeground": "140 30% 92%",
"muted": "168 26% 12%",
"mutedForeground": "150 18% 66%",
"accent": "320 100% 66%",
"accentForeground": "165 40% 5%",
"destructive": "0 95% 64%",
"destructiveForeground": "165 40% 5%",
"border": "155 35% 20%",
"input": "155 35% 18%",
"ring": "145 100% 55%"
}
}
},
{
"id": "graphite",
"name": "Graphite",
"description": "Brushed-silver chrome in light, restrained slate blacklight in dark",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:y2k:Orbitron",
"fontBody": "template:y2k:Inter",
"fontMono": "template:y2k:Share Tech Mono",
"fontSizeBase": "16",
"lineHeightBase": "1.65"
},
"lightColors": {
"background": "220 18% 95%",
"foreground": "225 25% 14%",
"card": "220 20% 99%",
"cardForeground": "225 25% 14%",
"popover": "220 20% 99%",
"popoverForeground": "225 25% 14%",
"primary": "230 30% 30%",
"primaryForeground": "0 0% 100%",
"secondary": "220 16% 89%",
"secondaryForeground": "225 25% 14%",
"muted": "220 14% 91%",
"mutedForeground": "225 14% 42%",
"accent": "265 70% 58%",
"accentForeground": "0 0% 100%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 100%",
"border": "220 16% 82%",
"input": "220 16% 82%",
"ring": "265 70% 58%"
},
"darkColors": {
"background": "230 25% 6%",
"foreground": "220 18% 92%",
"card": "230 24% 9%",
"cardForeground": "220 18% 92%",
"popover": "230 24% 9%",
"popoverForeground": "220 18% 92%",
"primary": "225 30% 78%",
"primaryForeground": "230 25% 6%",
"secondary": "230 22% 15%",
"secondaryForeground": "220 18% 92%",
"muted": "230 20% 13%",
"mutedForeground": "220 14% 66%",
"accent": "270 95% 72%",
"accentForeground": "230 25% 6%",
"destructive": "0 95% 64%",
"destructiveForeground": "230 25% 6%",
"border": "230 25% 20%",
"input": "230 25% 18%",
"ring": "270 95% 72%"
}
}
},
{
"id": "tangerine",
"name": "Tangerine",
"description": "Translucent tangerine iMac in light, sunset-orange rave in dark",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:y2k:Orbitron",
"fontBody": "template:y2k:Inter",
"fontMono": "template:y2k:Share Tech Mono",
"fontSizeBase": "16",
"lineHeightBase": "1.65"
},
"lightColors": {
"background": "35 70% 96%",
"foreground": "20 40% 15%",
"card": "35 60% 99%",
"cardForeground": "20 40% 15%",
"popover": "35 60% 99%",
"popoverForeground": "20 40% 15%",
"primary": "20 90% 52%",
"primaryForeground": "0 0% 100%",
"secondary": "35 50% 90%",
"secondaryForeground": "20 40% 15%",
"muted": "35 40% 92%",
"mutedForeground": "25 22% 40%",
"accent": "180 75% 40%",
"accentForeground": "0 0% 100%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 100%",
"border": "35 40% 84%",
"input": "35 40% 84%",
"ring": "20 90% 52%"
},
"darkColors": {
"background": "18 45% 6%",
"foreground": "30 35% 93%",
"card": "18 42% 9%",
"cardForeground": "30 35% 93%",
"popover": "18 42% 9%",
"popoverForeground": "30 35% 93%",
"primary": "22 100% 60%",
"primaryForeground": "18 45% 6%",
"secondary": "18 35% 15%",
"secondaryForeground": "30 35% 93%",
"muted": "18 30% 13%",
"mutedForeground": "25 22% 66%",
"accent": "175 95% 55%",
"accentForeground": "18 45% 6%",
"destructive": "0 95% 64%",
"destructiveForeground": "18 45% 6%",
"border": "20 40% 20%",
"input": "20 40% 18%",
"ring": "22 100% 60%"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

@ -1,173 +0,0 @@
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,
},
},
},
}
}

View File

@ -1,26 +0,0 @@
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

@ -1,29 +0,0 @@
{
"$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

@ -1,30 +0,0 @@
{
"$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

@ -1,15 +0,0 @@
{
"$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

@ -1,32 +0,0 @@
{
"$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

@ -1,41 +0,0 @@
{
"$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

@ -1,44 +0,0 @@
{
"$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

@ -1,41 +0,0 @@
{
"$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": []
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

BIN
seed/media/cassette.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
seed/media/neon-rave.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

552
seed/seed.json Normal file
View File

@ -0,0 +1,552 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "Chrome Static",
"tagline": "A four-piece from the frosted end of the millennium."
}
},
"media": [
{
"id": "b7c0de00-0000-4000-8000-000000000001",
"file": "media/chrome-liquid-metal.jpg",
"alt": "Liquid chrome poured into a mirror-bright abstract surface",
"folder": "Chrome Static"
},
{
"id": "b7c0de00-0000-4000-8000-000000000002",
"file": "media/holographic-foil.jpg",
"alt": "Rainbow holographic foil catching the light",
"folder": "Chrome Static"
},
{
"id": "b7c0de00-0000-4000-8000-000000000003",
"file": "media/abstract-neon.jpg",
"alt": "Abstract neon light trails in magenta and cyan",
"folder": "Chrome Static"
},
{
"id": "b7c0de00-0000-4000-8000-000000000004",
"file": "media/cassette.jpg",
"alt": "A worn audio cassette tape on a plain background",
"folder": "Chrome Static"
},
{
"id": "b7c0de00-0000-4000-8000-000000000005",
"file": "media/neon-rave.jpg",
"alt": "Blurred neon rave lights in the dark",
"folder": "Chrome Static"
},
{
"id": "b7c0de00-0000-4000-8000-000000000006",
"file": "media/chrome-metallic.jpg",
"alt": "Brushed metallic chrome texture, wide crop",
"folder": "Chrome Static"
}
],
"data_tables": [
{
"key": "contact_submissions",
"name": "Contact Enquiries",
"description": "Seeded contact-form submissions for the Chrome Static demo. A submission becomes a row here and emails the site admins.",
"columns": [
{
"key": "name",
"label": "Name",
"type": "text",
"required": true
},
{
"key": "email",
"label": "Email",
"type": "email",
"required": true
},
{
"key": "subject",
"label": "Subject",
"type": "text",
"required": false
},
{
"key": "message",
"label": "Message",
"type": "text",
"required": true
}
]
}
],
"pages": [
{
"slug": "/",
"title": "Chrome Static",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Hero",
"slot": "main",
"sort_order": 1,
"content": {
"template": "centered",
"minHeight": "screen",
"eyebrow": "New EP out now",
"headline": "Chrome Static",
"subheadline": "Four tracks. Recorded live to tape.",
"description": "We are a four-piece from Melbourne. We make loud, bright pop for the end of the millennium.",
"ctaPrimary": {
"text": "Listen",
"url": "#listen"
},
"ctaSecondary": {
"text": "See the shows",
"url": "#shows"
}
}
},
{
"block_key": "y2k:waveform_player",
"title": "Now playing",
"slot": "main",
"sort_order": 2,
"content": {
"trackTitle": "Frosted",
"artist": "Chrome Static",
"audioUrl": "",
"coverImage": "media:b7c0de00-0000-4000-8000-000000000005"
}
},
{
"block_key": "y2k:discography",
"title": "Records",
"slot": "main",
"sort_order": 3,
"content": {
"heading": "Records",
"releases": [
{
"title": "Chrome Hearts",
"year": "2001",
"format": "LP",
"cover": "media:b7c0de00-0000-4000-8000-000000000001",
"link": "#"
},
{
"title": "Dial-Up Dreams",
"year": "2000",
"format": "EP",
"cover": "media:b7c0de00-0000-4000-8000-000000000002",
"link": "#"
},
{
"title": "Frosted",
"year": "1999",
"format": "Single",
"cover": "media:b7c0de00-0000-4000-8000-000000000003",
"link": "#"
},
{
"title": "Skyline Demos",
"year": "1998",
"format": "Cassette",
"cover": "media:b7c0de00-0000-4000-8000-000000000004",
"link": "#"
}
]
}
},
{
"block_key": "y2k:tour_dates",
"title": "Shows",
"slot": "main",
"sort_order": 4,
"content": {
"heading": "Shows",
"intro": "Doors an hour before. All ages unless noted.",
"dates": [
{
"date": "Fri 12 Sep",
"venue": "The Chrome Room",
"city": "Melbourne",
"ticketUrl": "#",
"soldOut": false
},
{
"date": "Sat 13 Sep",
"venue": "Rollerdrome",
"city": "Sydney",
"ticketUrl": "#",
"soldOut": true
},
{
"date": "Fri 19 Sep",
"venue": "Basement 88",
"city": "Brisbane",
"ticketUrl": "#",
"soldOut": false
}
]
}
},
{
"block_key": "cta",
"title": "CTA",
"slot": "main",
"sort_order": 5,
"content": {
"background": "band",
"heading": "Join the mailing list",
"text": "One email when a record ships or a show goes up. Nothing else.",
"primaryLabel": "Sign up",
"primaryUrl": "/contact"
}
}
]
},
{
"slug": "/about",
"title": "The band",
"template_key": "default",
"blocks": [
{
"block_key": "image",
"title": "Band image",
"slot": "main",
"sort_order": 1,
"content": {
"src": "media:b7c0de00-0000-4000-8000-000000000006",
"alt": "Brushed metallic chrome texture",
"caption": "Chrome Static — live to tape, one room.",
"size": "full",
"aspectRatio": "wide"
}
},
{
"block_key": "heading",
"title": "Heading",
"slot": "main",
"sort_order": 2,
"content": {
"text": "The band",
"level": 1
}
},
{
"block_key": "rich-text",
"title": "Story",
"slot": "main",
"sort_order": 3,
"content": {
"html": "<p>Chrome Static started in a share house with a borrowed drum machine and a broken amp. We kept the sound and fixed the amp.</p><p>We record live to tape in one room. What you hear on the record is what we play on the night. Four people, no click track, one take when we can get it.</p>"
}
},
{
"block_key": "quote",
"title": "Quote",
"slot": "main",
"sort_order": 4,
"content": {
"style": "pull",
"quote": "Loud, bright, and done in one take.",
"attribution": "Chrome Static"
}
},
{
"block_key": "divider",
"title": "Divider",
"slot": "main",
"sort_order": 5,
"content": {
"variant": "line",
"weight": "medium"
}
},
{
"block_key": "y2k:tracklist",
"title": "Frosted tracklist",
"slot": "main",
"sort_order": 6,
"content": {
"tracks": [
{
"title": "Frosted",
"duration": "3:12",
"url": ""
},
{
"title": "Dial Tone",
"duration": "2:48",
"url": ""
},
{
"title": "Silver Static",
"duration": "4:01",
"url": ""
},
{
"title": "Skyline",
"duration": "3:35",
"url": ""
}
]
}
}
]
},
{
"slug": "/blog",
"title": "Journal",
"template_key": "blog-index",
"blocks": [
{
"block_key": "card",
"title": "Post: Tape",
"slot": "main",
"sort_order": 1,
"content": {
"layout": "vertical",
"title": "Why we still track to tape",
"text": "<p>Tape makes us commit. Here is what that costs and what we get back.</p>",
"link": "/blog/tracking-to-tape",
"linkLabel": "Read on"
}
},
{
"block_key": "card",
"title": "Post: Tour",
"slot": "main",
"sort_order": 2,
"content": {
"layout": "vertical",
"title": "Notes from the September run",
"text": "<p>Three cities in a van. What went right and what broke.</p>",
"link": "/blog/september-run",
"linkLabel": "Read on"
}
},
{
"block_key": "card",
"title": "Post: Gear",
"slot": "main",
"sort_order": 3,
"content": {
"layout": "vertical",
"title": "The one pedal we cannot replace",
"text": "<p>A cheap chorus pedal from 1999. It is the whole sound.</p>",
"link": "/blog/the-chorus-pedal",
"linkLabel": "Read on"
}
}
]
},
{
"slug": "/blog/tracking-to-tape",
"title": "Why we still track to tape",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "Why we still track to tape",
"level": 1
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 2,
"content": {
"html": "<p>Everyone asks why we still bother with tape. The honest answer is that it makes us better &mdash; not the sound first, the playing. Tape does not let you undo, so you stop treating a take like a draft and start treating it like a performance.</p><p>We tracked Frosted in a converted laundry in Brunswick over four nights in the winter of 1999. One room, one machine, four of us, and whoever wandered in. The reel we used was already half-worn from a demo we would rather forget, which is why side A has a faint ghost of an old chorus under the second verse. We kept it.</p><p>The rule was simple. Three takes, then we move on. If a song was not working by the third pass it was the arrangement, not the take, and no amount of punching in was going to save it. That rule cost us two songs and saved us a fortnight.</p><p>Digital gives you infinite tries and somehow that makes everyone worse. When you can fix it later, you play like you will fix it later. You lean back. Tape makes you lean in. The only click is your own pulse, and the only quantising is whether the four of you agree on where the beat lands.</p><p>There is a cost and we are not going to pretend there isn't. Tape is expensive, editing is a razor blade and a chinagraph pencil, and if the machine eats a reel you have lost the night. We lost one night. The heads needed cleaning and nobody noticed until the playback came back muddy, and by then it was three in the morning and the coffee was gone.</p><p>What you get back is a record that sounds like a room instead of a grid. You can hear the walls. You can hear the amp two feet from the kit bleeding into the overheads, and that bleed is the glue that makes four people sound like one band instead of four separate files.</p><p>We mix to tape too, which horrifies our engineer friends. The two-track we bounce down to adds a compression you cannot dial in on a plugin &mdash; a gentle softening on the transients that takes the ice off the top end. Chrome, without the cuts.</p><p>People say tape is nostalgia. Maybe. But nobody accuses a painter of nostalgia for using oil instead of a screen. It is a medium with a grain, and the grain is the point. We are not trying to sound like 1979. We are trying to sound like the four of us in a laundry in 1999, because that is what we are.</p><p>If you are thinking about doing it, do it once before you decide. Book a day, bring three songs you know cold, and give yourself three takes each. You will learn more about your band in eight hours than in a year of overdubs.</p><p>The next record will be tape as well. We already booked the room &mdash; same laundry, new reels &mdash; and we will keep whatever ghosts turn up.</p>"
}
}
]
},
{
"slug": "/blog/september-run",
"title": "Notes from the September run",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "Notes from the September run",
"level": 1
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 2,
"content": {
"html": "<p>Three cities, one van, four amps. The van held. One amp did not.</p><p>Sydney sold out and Brisbane nearly did. We ate too much servo food and slept in the van outside a Bunnings. We would do it again next week.</p>"
}
}
]
},
{
"slug": "/blog/the-chorus-pedal",
"title": "The one pedal we cannot replace",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "The one pedal we cannot replace",
"level": 1
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 2,
"content": {
"html": "<p>It is a plastic chorus pedal from 1999. It hisses and the knob is loose. It is also the whole guitar sound.</p><p>We have tried to replace it twice. Both times the record sounded wrong, so the old one went back on the board.</p>"
}
}
]
},
{
"slug": "/contact",
"title": "Contact",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Contact form",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Get in touch",
"description": "Booking, press, or just to say hello. We read every message.",
"submitLabel": "Send",
"successMessage": "Thanks. We will get back to you soon.",
"fields": [
{
"name": "name",
"label": "Your name",
"type": "text",
"required": true,
"width": "half"
},
{
"name": "email",
"label": "Email",
"type": "email",
"required": true,
"width": "half"
},
{
"name": "subject",
"label": "Subject",
"type": "text",
"required": false,
"width": "full"
},
{
"name": "message",
"label": "Message",
"type": "textarea",
"required": true,
"width": "full"
}
],
"formConfig": {
"enabled": true,
"captchaEnabled": false,
"targetTable": "contact_submissions"
}
}
},
{
"block_key": "contact-card",
"title": "Other ways to reach us",
"slot": "main",
"sort_order": 2,
"content": {
"heading": "Other ways in",
"columns": 3,
"channels": [
{
"icon": "lucide:mail",
"label": "Email",
"value": "hello@chromestatic.band",
"link": "mailto:hello@chromestatic.band"
},
{
"icon": "simple-icons:bandcamp",
"label": "Bandcamp",
"value": "chromestatic.bandcamp.com",
"link": "#"
},
{
"icon": "simple-icons:instagram",
"label": "Instagram",
"value": "@chromestatic",
"link": "#"
}
]
}
}
]
},
{
"slug": "/login",
"title": "Sign in",
"template_key": "auth",
"blocks": [
{
"block_key": "auth-form",
"title": "Sign in",
"slot": "main",
"sort_order": 1,
"content": {
"default_tab": "login",
"show_social": false
}
}
]
}
],
"menu_items": [
{
"menu": "main",
"label": "Home",
"page_slug": "/",
"sort_order": 1
},
{
"menu": "main",
"label": "The band",
"page_slug": "/about",
"sort_order": 2
},
{
"menu": "main",
"label": "Journal",
"page_slug": "/blog",
"sort_order": 3
},
{
"menu": "main",
"label": "Contact",
"page_slug": "/contact",
"sort_order": 4
}
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Contact enquiry to admins",
"description": "On a contact-form submission, email the site admins.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,244 +0,0 @@
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))
}

View File

@ -1,510 +0,0 @@
// 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

View File

@ -0,0 +1,53 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ site_name }}</title>
<style type="text/css">
body, table, td, p, a, li { -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="background-color: {{ colors.background|default:"#0c0820" }}; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
{% if preview_text %}<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 36px 12px; background-color: {{ colors.background|default:"#0c0820" }};">
<table role="presentation" class="y2k-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#150f2a" }}; border: 2px solid {{ colors.border|default:"#4d2b6f" }}; border-radius: 12px; overflow: hidden;">
<!-- gradient header bar -->
<tr>
<td style="padding: 18px 28px; background-image: linear-gradient(90deg, {{ colors.primary|default:"#ff4dd2" }} 0%, {{ colors.secondary|default:"#4ddff0" }} 100%); color: {{ colors.foreground|default:"#f4e8fa" }}; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;">
{% if site_name %}{{ site_name }}{% else %}y2k.fm{% endif %}
</td>
</tr>
<!-- body content -->
<tr>
<td class="y2k-email-content" style="padding: 32px 40px; color: {{ colors.foreground|default:"#f4e8fa" }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
</tr>
<!-- marquee pill footer -->
<tr>
<td align="center" style="padding: 18px 28px; background-color: {{ colors.muted|default:"#1f1740" }};">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="padding: 8px 18px; border-radius: 999px; background-image: linear-gradient(90deg, {{ colors.primary|default:"#ff4dd2" }} 0%, {{ colors.secondary|default:"#4ddff0" }} 100%); color: {{ colors.foreground|default:"#f4e8fa" }}; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;">* now playing * new drop friday * subscribe to the zine *</td>
</tr>
</table>
{% if unsubscribe_url %}<p style="margin: 12px 0 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#b9a8d3" }};"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#b9a8d3" }}; text-decoration: underline;">unsubscribe</a></p>{% endif %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,21 @@
{# announcement_bar (WO-TF-006): dismissable banner; dismissal persists in localStorage keyed by a content hash (variant + slugified message) so it reappears when the message changes; no-JS fallback is a static non-dismissable strip. Y2K override: mono broadcast readout — data-* + script preserved. #}
{% set anncls = "bg-primary/10 text-foreground" %}
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
<p class="y2k-mono uppercase tracking-wide font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed y2k override: neon panel, mono meta. Facade script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
<figure class="bn-audio my-6 overflow-hidden rounded-md border border-primary/40 bg-card text-card-foreground neon-edge-magenta{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-md border border-primary/30 bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="y2k-display truncate font-medium text-foreground">{{ title }}</p>{% endif %}{% if artist %}<p class="y2k-mono truncate text-sm text-muted-foreground">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="y2k-mono clip-cut inline-flex items-center gap-2 rounded-md bg-primary px-4 py-1.5 text-sm font-medium uppercase tracking-wide text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="y2k-mono inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"><svg width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,77 @@
{# auth-form y2k override — reproduces the builtin's security-critical wiring: hardcoded HTMX endpoints /api/auth/login + /api/auth/register (target #auth-message), /forgot-password link, OAuth start URLs gated on context.site.*, and the exact Cap widget markup when captchaEnabled. Only the chrome is themed. #}
{% if context.isPublicLoggedIn %}
<div class="auth-form-logged-in mx-auto max-w-md text-center py-6">
<p class="y2k-mono text-sm uppercase tracking-wide text-muted-foreground">Session active as <strong class="text-primary">{{ context.publicUsername }}</strong></p>
<a href="/account" class="y2k-mono clip-cut mt-3 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium uppercase tracking-wide text-primary-foreground hover:bg-primary/90">My Account</a>
</div>
{% else %}
{% set loginActive = "border-primary text-primary" %}
{% set registerActive = "border-transparent text-muted-foreground" %}
{% set loginHidden = "" %}
{% set registerHidden = "hidden" %}
{% if default_tab == "register" %}{% set loginActive = "border-transparent text-muted-foreground" %}{% set registerActive = "border-primary text-primary" %}{% set loginHidden = "hidden" %}{% set registerHidden = "" %}{% endif %}
{% set tabbase = "flex-1 py-2 text-center y2k-mono text-sm font-medium uppercase tracking-wider border-b-2 transition-colors" %}
{% macro captcha() %}<script>
window.CAP_CUSTOM_WASM_URL = "/assets/captcha/cap_wasm_bg.wasm";
window.CAP_PAKO_URL = "/assets/captcha/pako_inflate.min.js";
</script>
<cap-widget data-cap-api-endpoint="/api/captcha/" data-cap-worker-count="2"></cap-widget>
<script src="/assets/captcha/cap.min.js" defer></script>
<script>
(function(){
var s = document.currentScript;
var form = s && s.closest ? s.closest('form') : null;
if (!form) return;
form.addEventListener('htmx:afterRequest', function(){
var w = form.querySelector('cap-widget');
if (w && typeof w.reset === 'function') w.reset();
});
})();
</script>{% endmacro %}
{% macro social() %}{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}<div class="auth-social my-4">
<div class="relative my-4"><hr class="border-border"/><span class="y2k-mono absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-3 text-xs uppercase tracking-widest text-muted-foreground">or</span></div>
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="y2k-mono mb-2 flex w-full items-center justify-center gap-3 rounded-md border border-border px-4 py-2.5 uppercase tracking-wide transition-colors hover:bg-accent/10"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#google"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Google</span></a>{% endif %}
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="y2k-mono flex w-full items-center justify-center gap-3 rounded-md border border-border px-4 py-2.5 uppercase tracking-wide transition-colors hover:bg-accent/10"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#facebook"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Facebook</span></a>{% endif %}
</div>{% endif %}{% endmacro %}
<div class="auth-form hud-frame mx-auto max-w-md rounded-md border border-primary/40 bg-card p-6 text-card-foreground neon-edge-magenta">
<div id="auth-message"></div>
<div class="mb-5 flex border-b border-border">
<button type="button" data-auth-tab="login" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ loginActive }}">Login</button>
<button type="button" data-auth-tab="register" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ registerActive }}">Register</button>
</div>
<div id="login-panel" class="{{ loginHidden }}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="y2k-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-4"><label class="y2k-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Password</label><input type="password" name="password" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
{% if captchaEnabled %}{{ captcha() }}{% endif %}
<button type="submit" class="y2k-mono clip-cut rgb-split w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Login</button>
<p class="mt-3 text-center text-sm"><a href="/forgot-password" class="y2k-mono uppercase tracking-wide text-primary hover:underline">Forgot password?</a></p>
</form>
{{ social() }}
</div>
<div id="register-panel" class="{{ registerHidden }}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="y2k-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-3"><label class="y2k-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-4"><label class="y2k-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
{% if captchaEnabled %}{{ captcha() }}{% endif %}
<button type="submit" class="y2k-mono clip-cut rgb-split w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Create Account</button>
</form>
{{ social() }}
</div>
</div>
<script>
(function(){
var f=document.currentScript.closest('.auth-form')||document.querySelector('.auth-form');
if(!f)return;
var tabs=f.querySelectorAll('[data-auth-tab]');
tabs.forEach(function(btn){btn.addEventListener('click',function(){
var t=btn.getAttribute('data-auth-tab');
var lp=f.querySelector('#login-panel'),rp=f.querySelector('#register-panel');
if(lp)lp.classList.toggle('hidden',t!=='login');
if(rp)rp.classList.toggle('hidden',t!=='register');
tabs.forEach(function(b){var on=b===btn;b.className=b.getAttribute('data-tabbase')+(on?' border-primary text-primary':' border-transparent text-muted-foreground');});
});});
})();
</script>
{% endif %}

View File

@ -0,0 +1,36 @@
{# auth-status y2k override — reproduces the builtin's hardcoded wiring: /login + /login?tab=register when logged out; /account, /account/reviews, and hx-post /api/auth/logout dropdown when logged in; hx-post /api/auth/resend-verification banner when unverified. State from context.*. #}
{% if not context.isPublicLoggedIn %}
<div class="auth-status flex items-center gap-2">
<a href="/login" class="y2k-mono rounded-md px-3 py-1.5 text-sm font-medium uppercase tracking-wide opacity-80 transition-colors hover:bg-primary/10 hover:opacity-100">Login</a>
<a href="/login?tab=register" class="y2k-mono clip-cut rounded-md bg-accent px-3 py-1.5 text-sm font-medium uppercase tracking-wide text-accent-foreground hover:bg-accent/90">Sign Up</a>
</div>
{% else %}
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 border-y border-destructive/50 bg-destructive/15 px-4 py-2 text-center text-sm text-destructive">
<span class="y2k-mono uppercase tracking-wide">Verify your email address.</span> <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline font-medium">Resend verification email</button>
</div>{% endif %}
<div class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-primary/10 hover:opacity-100">
<span class="clip-cut flex h-7 w-7 items-center justify-center rounded-md bg-accent text-accent-foreground"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#user"></use></svg></span>
<span class="y2k-mono uppercase tracking-wide">{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="hud-frame hidden absolute right-0 top-full mt-1 w-48 rounded-md border border-primary/40 bg-card py-1 text-card-foreground neon-edge-magenta z-50">
<a href="/account" class="y2k-mono block px-4 py-2 text-sm uppercase tracking-wide text-card-foreground hover:bg-primary/10">My Profile</a>
<a href="/account/reviews" class="y2k-mono block px-4 py-2 text-sm uppercase tracking-wide text-card-foreground hover:bg-primary/10">My Reviews</a>
<hr class="my-1 border-border"/>
<button hx-post="/api/auth/logout" hx-swap="none" class="y2k-mono block w-full px-4 py-2 text-left text-sm uppercase tracking-wide text-destructive hover:bg-primary/10">Logout</button>
</div>
<script>
(function(){
document.addEventListener('click', function(e) {
var menu = document.getElementById('auth-dropdown-menu');
var btn = document.querySelector('.auth-status > button');
if (!menu || !btn) return;
if (!btn.contains(e.target) && !menu.contains(e.target)) {
menu.classList.add('hidden');
}
});
})();
</script>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (y2k override) — author-page header from the `authors` provider. #}
{% if author %}
<section class="relative overflow-hidden border-b border-primary/30 bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary neon-edge-magenta">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<h1 class="y2k-display glitch text-3xl font-bold tracking-tight md:text-4xl" data-text="{{ author.display_name }}">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="y2k-mono mt-1 text-sm uppercase tracking-wide text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="y2k-body mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
<div class="mt-5 flex flex-wrap items-center justify-center gap-4 y2k-mono text-sm uppercase tracking-wide text-accent">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,13 @@
{# author-bio (y2k override) — byline card sourced from the `post` provider's author_* fields. #}
{% if post.author_name %}
<aside class="hud-frame rounded-md border border-dashed border-primary/40 bg-card p-6 text-card-foreground neon-edge-magenta{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="y2k-eyebrow y2k-mono mb-4 text-xs font-semibold uppercase tracking-widest text-accent">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full border border-primary/40 bg-muted">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="y2k-display text-base font-semibold">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="y2k-body mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (y2k override) — blog-index header + featured posts from the `posts` provider. #}
<section class="relative overflow-hidden bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-6xl px-4">
<div class="max-w-2xl">
{% if title %}<h1 class="y2k-display glitch text-4xl font-bold tracking-tight md:text-5xl" data-text="{{ title }}">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="y2k-body mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
{% for post in posts %}
<article class="hud-frame group flex flex-col rounded-md border border-dashed border-primary/40 bg-card p-4 text-card-foreground neon-edge-magenta{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-3 block overflow-hidden rounded-md bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="y2k-display {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="y2k-body mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="mt-3 flex flex-wrap items-center gap-x-3 y2k-mono text-xs uppercase tracking-wide text-accent">{% if post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

Some files were not shown because too many files have changed in this diff Show More