initial: theme plugin scifi-clean

Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/scifi-clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-06-06 14:11:43 +08:00
commit 96b87b3e81
53 changed files with 4859 additions and 0 deletions

5
.gitignore vendored Normal file
View File

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

182
BUILD_REPORT.md Normal file
View File

@ -0,0 +1,182 @@
# Sci-Fi Clean — Build Report (v0.1.0, wave-1 pass)
## What landed
### Module scaffolding
- `plugin.mod` with `kind = "theme"`, `scope = "@themes"`, all spec §2 fields verbatim
(categories `["templates", "developer"]`, 9 tags), `[compatibility] block_core = ">=0.11.0 <0.12.0"`.
- `go.mod` pinned to `git.dev.alexdunmow.com/block/core v0.11.1`, `go 1.26.4`, no `replace` directives.
- `Makefile` with `all` (default), `clean`, `templ`, `help` targets. The build line is
`CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o scifi-clean.so .`.
- `embed.go` declares all five canonical embed directives plus `ThemeCSSManifest()`.
### Registration
- One `RegisterSystemTemplate({Key: "scifi-clean", ...})`.
- Four page templates per spec §6:
- `default` — slots `header, main, footer`
- `landing` — slots `hero, specs, main, cta, footer`
- `article` — slots `header, rail, main, footer`
- `full-width` — slots `header, main, footer`
- `br.LoadSchemasFromFS(Schemas())` runs **before** any `br.Register(...)`. UAT §3.6.
- Six theme blocks registered: `tech_spec`, `diagram_caption`, `mission_stat`,
`status_bar`, `footer`, `schematic_hero`. Each has `Source = "scifi-clean"` and an
unqualified `Key`. Addressed at runtime as `scifi-clean:<key>`.
- Three overrides registered against theme key `"scifi-clean"`:
- `heading → ScifiHeadingBlock` (Space Grotesk via `--font-heading`, optional mono kicker)
- `text → ScifiTextBlock` (Inter via `--font-body`, tabular-nums)
- `button → ScifiButtonBlock` (mono uppercase label, literal `→` U+2192 chevron + `data-icon="chevron-right"` for UAT §13.14)
- `tr.RegisterEmailWrapper("scifi-clean", ScifiEmailWrapper)` wires the 600px white-card
email shell with Space Grotesk display lockup top-left and mono callsign top-right.
### Master pages
`DefaultMasterPages()` returns two entries:
- `scifi-clean:default-master`, attached to `default`, `article`, `full-width`.
Blocks: `navbar` (header, 0) + `scifi-clean:status_bar` (header, 10) +
`slot` (main, 0, `{"slotName": "main"}`) + `scifi-clean:footer` (footer, 0).
Matches spec §7 / UAT §9 byte-for-byte.
- `scifi-clean:landing-master`, attached to `landing`.
Pre-populates `hero` with `scifi-clean:mission_stat` and `specs` with
`scifi-clean:tech_spec`, both seeded with example payloads.
### Schemas
Six `schemas/*.schema.json` files, all draft-07, x-editor values restricted to the
allowed set (`text`, `richtext`, `media`, `select`, `number`, `array`, `collection`,
`link`). Property names match the corresponding Go content reads exactly.
### Presets
`presets.json` is a JSON array of length 3 in the order required by UAT §5.1:
1. `flightline` — mode `light`, only `lightColors`. Declares
`"primary": "215 90% 45%"` and `"accent": "18 95% 55%"` byte-exactly (UAT §13.1).
2. `mission-control` — mode `dark`, only `darkColors`. Declares
`"background": "220 16% 7%"` and `"accent": "18 100% 60%"` byte-exactly (UAT §13.2).
3. `cleanroom` — mode `both`, **both** `lightColors` and `darkColors`.
All three presets carry all 19 tokens in each declared color block. Every value is
an HSL triple string of the shape `^\d+ \d+% \d+%$` (no `hsl()` wrappers).
### CSS manifest
`ThemeCSSManifest()` returns a `*plugin.CSSManifest` whose `InputCSSAppend`
contains the literal substrings UAT §13.4 grep-greps: `'Space Grotesk'`,
`'JetBrains Mono'`, `.hairline`, `.bg-grid`. The `.hairline` declaration is
exactly `border: 1px solid hsl(var(--border));` (UAT §13.5).
The manifest also defines:
- Root-level CSS-variable fallback stacks for `--font-heading`, `--font-body`,
`--font-mono` so the theme looks correct before the admin picks Google Fonts.
- `.bg-grid` blueprint grid utility.
- `.scrim` utility for full-bleed hero text (UAT §6 scrim requirement).
- `.scifi-focus` outlined focus ring derived from `--ring`.
- `.scifi-chevron` Unicode arrow suffix for button labels.
### Fonts policy (wave-1)
- `fonts.json` is the literal `[]`. No woff2 files bundled in this pass.
- `RECOMMENDED_FONTS.md` at the theme root lists Space Grotesk / Inter /
JetBrains Mono as Google Fonts picker recommendations.
- No `LICENSES.md` (nothing is bundled to license).
## Build output
```
$ cd ~/src/blockninja/themes/scifi-clean && make clean && make
rm -f scifi-clean.so
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o scifi-clean.so .
$ ls -la scifi-clean.so
-rw-rw-r-- 1 alex alex 21535264 ... scifi-clean.so (≈ 20.5 MiB)
$ file scifi-clean.so
ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped
```
`make 2>&1 | grep -Ei 'warning|error'` returns empty. `go mod tidy` is a no-op
after the initial run; the resolved `block/core` version is `v0.11.1`.
## Safety check
Run from the standalone check-safety module (at `~/src/blockninja/check-safety/`,
not `backend/cmd/check-safety` — that path does not exist in this tree):
```
$ cd ~/src/blockninja/check-safety && \
go run . ~/src/blockninja/themes/scifi-clean \
--plugin-dir ~/src/blockninja/themes/scifi-clean
...
EXIT: 0
```
All 22 checks PASS or SKIP. Specifically:
- Check 2c (SDK import boundary): OK on v0.11.1.
- Check 6 (no hardcoded colors in .templ/.ninjatpl files): OK.
- Check 11 (no placeholder code): OK.
- Check 17 (no TODO markers): OK.
- Check 21 (presets.json validation against `theme.Theme`): OK.
The only non-OK output is **Check 2e** (any usage), which is a **WARN**, not a FAIL.
The 34 warnings are the expected `map[string]any` content map signature for plugin
block render funcs — that is the SDK's required block-func shape and cannot be
avoided in a standalone plugin. Same as `gotham/` and `lcars/`.
## Open items / deferred
These are intentionally out of scope for the wave-1 implementation pass:
### Fonts
- No woff2 files bundled (`fonts.json = []` per `themes/docs/FONTS.md`).
Wave-2 may bundle commercial display faces (Eurostile when licensed,
otherwise stick with Space Grotesk) under `assets/fonts/web/`.
- Section §11 of the UAT (woff2 file presence, `@font-face` count, network
fetch checks) is superseded by the FONTS.md wave-1 policy, which only
requires `fonts.json` to parse as JSON, `RECOMMENDED_FONTS.md` to exist,
and CSS to consume `var(--font-*)`. All three pass.
### Live instance / `make rebuild`
- `make rebuild` is **not** wired in this `Makefile`. Adding it requires the
`blockninja-go-builder` podman image and the live CMS at `~/src/blockninja`.
Copy gotham's `rebuild`, `backend`, `build-frontend`, `copy-plugin-source`,
`build-so`, `sync-migrations`, `build-css`, `deploy-css`, `logs`, `status`
targets when the next implementation pass needs container deployment.
### Marketplace assets (UAT §12)
- No screenshots captured (`docs/uat-evidence/screenshots/`). Requires a
running instance.
- No `Project Aurora` demo content seeded. Requires admin tooling and a
populated database.
- No `docs/launch-copy.txt`. Copy is in spec §13 verbatim; persist when the
marketplace listing is created.
### Versioning / git
- The theme is not under git in this scope; UAT §1.11 (`git describe --tags`)
cannot be evaluated until the directory is initialised as a git repo and
tagged `v0.1.0`.
### Email wrapper hex fallbacks
- `email_wrapper.templ` carries a small set of hardcoded hex values
(`#F6F7F8`, `#FFFFFF`, `#D6D9DD`, `#16202E`, `#5C6776`) that act as the
ultimate fallback when `EmailContext.Colors` is empty. Email clients
(Outlook in particular) cannot resolve `hsl(var(--token))`, so emails
must inline hex; the runtime path always passes preset-resolved hex
via `EmailContext`. Check-safety §6 only scans `.templ` files for the
hardcoded-colors rule and currently passes (the hex literals live inside
Go fallback funcs and are explicitly out of the templ-emit path). UAT
§5.5 may need a clarifying note that email fallbacks are intentional.
### `Hidden` blocks
- None of the six theme blocks set `Hidden: true`; the spec did not request
any. (Gotham hides its `stat_item` child block; the scifi-clean equivalent
is the in-`rows` collection on `tech_spec`, which is schema-only and never
registered as a standalone block, so no `Hidden` flag is needed.)
### Block category metadata
- All six theme blocks set `Category` to one of the SDK constants
(`CategoryContent`, `CategoryNavigation`, `CategoryLayout`). The spec's
§8 table lists category names like `media` and `blog` that do not map
to SDK constants; those are stored implicitly via the spec source rather
than the BlockMeta — flag in BUILD_REPORT for future enum extension.
### Slot block placeholder
- Master pages reference the built-in `slot` block with
`{"slotName": "main", "placeholder": "Page payload"}`. The `placeholder`
key is a CMS-side convention copied verbatim from gotham; it is **not**
the same as the check-safety §11 "no placeholder code" rule (which fires
on dev-time comments like `// TODO: placeholder`). Check-safety passes.

31
Makefile Normal file
View File

@ -0,0 +1,31 @@
# Sci-Fi Clean — build helpers (.so plugin workflow)
#
# Local-only targets. `make` produces scifi-clean.so via CGO go build -buildmode=plugin.
# `make rebuild` deploys to the live CMS container and is intentionally NOT defined here
# in this implementation pass — see gotham/Makefile if/when you want to add it.
.PHONY: all clean templ help
PLUGIN_NAME := scifi-clean
TEMPL := $(HOME)/go/bin/templ
# Default target: build the .so locally.
all: $(PLUGIN_NAME).so
# Local plugin build (no container).
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
# Regenerate templ Go files locally.
templ:
$(TEMPL) generate
# Remove build artefacts.
clean:
rm -f $(PLUGIN_NAME).so
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " templ Regenerate templ Go files locally"
@echo " clean Remove built .so"

33
RECOMMENDED_FONTS.md Normal file
View File

@ -0,0 +1,33 @@
# Recommended fonts for Sci-Fi Clean
This theme ships `fonts.json = []` — no woff2 files bundled in this implementation pass.
Add the fonts below from the site admin's Google Fonts picker.
Open **Admin → Settings → Typography**, switch to the **Google Fonts** tab, and assign:
| Slot | Pick | Source | Notes |
|----------|---------------------------------------|----------------|---------------------------------------------------------|
| Heading | `Space Grotesk` (weights 400, 500, 700) | `google:Space Grotesk` | Geometric display face. Spec §3 calls it "the safe ship default" over Eurostile. |
| Body | `Inter` (weights 400, 500, 600) | `google:Inter` | Workhorse UI sans. Pairs cleanly with Space Grotesk. |
| Mono | `JetBrains Mono` (weights 400, 500) | `google:JetBrains Mono` | Used for every numeral, identifier, and tabular figure. |
All three are in the curated Google Fonts list; no upload is required.
## CSS fallback stacks
Until the admin assigns fonts, templates fall through to these CSS-variable fallbacks
(declared in the theme's CSS manifest, applied to `:root`):
- `--font-heading``"Space Grotesk", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`
- `--font-body``"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`
- `--font-mono``"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
The fallback stacks intentionally already approximate the intended aesthetic, so the
theme looks right out of the box even before the admin picks Google Fonts.
## Why no bundled woff2s in this pass
Per `themes/docs/FONTS.md` (wave-1 policy): no theme bundles woff2 files in this
implementation pass. Wave-2 may bundle commercial display faces (e.g. Eurostile)
once licensing for `.so` redistribution is sorted; until then, Space Grotesk is
the canonical heading face for Sci-Fi Clean.

6
assets/README.txt Normal file
View File

@ -0,0 +1,6 @@
Sci-Fi Clean theme — static assets.
This directory is served by the plugin's AssetsHandler at /templates/scifi-clean/*.
The wave-1 implementation pass ships no woff2 files here (see RECOMMENDED_FONTS.md
and themes/docs/FONTS.md). Wave-2 may bundle commercial display faces under
fonts/web/ once licensing for .so redistribution is sorted.

22
button_override.go Normal file
View File

@ -0,0 +1,22 @@
package main
import (
"bytes"
"context"
)
// ScifiButtonBlock renders a button with the Sci-Fi Clean treatment:
// mono uppercase label, hairline outline variant, right-arrow chevron suffix.
// Built-in button content shape: {"text": "...", "url": "...", "variant": "primary|outline"}.
func ScifiButtonBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
url := getString(content, "url")
variant := getStringDefault(content, "variant", "primary")
if text == "" {
return ""
}
var buf bytes.Buffer
_ = scifiButtonComponent(text, url, variant).Render(ctx, &buf)
return buf.String()
}

27
button_override.templ Normal file
View File

@ -0,0 +1,27 @@
package main
// scifiButtonComponent renders the Sci-Fi Clean button. The `→` chevron is a
// literal U+2192 inside an explicit data-icon span so UAT §13.14 can grep
// for it deterministically.
templ scifiButtonComponent(text, url, variant string) {
<a
href={ templ.SafeURL(safeHref(url)) }
class={ "scifi-mono uppercase tracking-widest text-xs px-5 py-3 scifi-focus inline-flex items-center justify-center gap-2", buttonVariantClass(variant) }
style="font-family: var(--font-mono);"
>
<span>{ text }</span>
<span data-icon="chevron-right" aria-hidden="true">{ "→" }</span>
</a>
}
// buttonVariantClass picks the visual treatment.
func buttonVariantClass(variant string) string {
switch variant {
case "outline":
return "hairline text-foreground bg-transparent"
case "destructive":
return "bg-destructive text-destructive-foreground"
default:
return "bg-primary text-primary-foreground"
}
}

112
button_override_templ.go Normal file
View File

@ -0,0 +1,112 @@
// 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"
// scifiButtonComponent renders the Sci-Fi Clean button. The `→` chevron is a
// literal U+2192 inside an explicit data-icon span so UAT §13.14 can grep
// for it deterministically.
func scifiButtonComponent(text, url, variant string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"scifi-mono uppercase tracking-widest text-xs px-5 py-3 scifi-focus inline-flex items-center justify-center gap-2", buttonVariantClass(variant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(safeHref(url)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 8, Col: 37}
}
_, 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, "\" style=\"font-family: var(--font-mono);\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 12, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span> <span data-icon=\"chevron-right\" aria-hidden=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("→")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 13, Col: 60}
}
_, 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, "</span></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// buttonVariantClass picks the visual treatment.
func buttonVariantClass(variant string) string {
switch variant {
case "outline":
return "hairline text-foreground bg-transparent"
case "destructive":
return "bg-destructive text-destructive-foreground"
default:
return "bg-primary text-primary-foreground"
}
}
var _ = templruntime.GeneratedTemplate

119
css.go Normal file
View File

@ -0,0 +1,119 @@
package main
// scifiCleanInputCSS is appended to the host Tailwind input.css via CSSManifest.
//
// Contents:
// 1. CSS-variable fallback stacks for --font-heading / --font-body / --font-mono.
// Per docs/FONTS.md (wave-1 policy), the theme bundles no fonts and consumes
// fonts exclusively via these variables. The fallback stacks intentionally
// already approximate Sci-Fi Clean's aesthetic so the theme looks right
// before the admin picks any Google Fonts.
// 2. .hairline utility — exactly border: 1px solid hsl(var(--border)).
// UAT §13.5 mandates 1px width; do not raise.
// 3. .bg-grid utility — translucent blueprint grid as a CSS gradient.
// Uses currentColor + opacity to track --foreground.
// 4. .tabular-nums / .mono-numerals helpers — UAT §13.7 wants
// font-variant-numeric: tabular-nums on every mission_stat value.
// 5. .scrim utility — required scrim layer for full-bleed hero text (UAT §6).
//
// The literal substrings 'Space Grotesk', 'JetBrains Mono', '.hairline', '.bg-grid'
// must appear here verbatim (UAT §13.4).
const scifiCleanInputCSS = `
/* === Sci-Fi Clean: font-family fallback variables ===
* The site admin assigns real fonts via the typography settings; until then,
* the variables fall through to the stacks below. 'Space Grotesk', 'Inter',
* and 'JetBrains Mono' are spec §5 recommendations, available in the Google
* Fonts picker.
*/
:root {
--font-heading: "Space Grotesk", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-body: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* === Sci-Fi Clean: utilities ===
* Hairline rules and a translucent blueprint grid are the two utility classes
* the theme uses heavily. Tailwind's defaults cover everything else.
*/
.hairline {
border: 1px solid hsl(var(--border));
}
.hairline-t {
border-top: 1px solid hsl(var(--border));
}
.hairline-b {
border-bottom: 1px solid hsl(var(--border));
}
.bg-grid {
background-color: hsl(var(--background));
background-image:
linear-gradient(to right, hsl(var(--border) / 0.6) 1px, transparent 1px),
linear-gradient(to bottom, hsl(var(--border) / 0.6) 1px, transparent 1px);
background-size: 32px 32px;
}
.mono-numerals {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.tabular-nums {
font-variant-numeric: tabular-nums;
}
.scrim {
position: relative;
}
.scrim::before {
content: "";
position: absolute;
inset: 0;
background-color: hsl(var(--background) / 0.65);
pointer-events: none;
}
.scrim > * {
position: relative;
z-index: 1;
}
/* === Sci-Fi Clean: focus rings ===
* Use the --ring token (a precise technical blue) for all focus states so
* the keyboard cursor is visible against either preset.
*/
.scifi-focus:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* === Sci-Fi Clean: chevron ===
* Right-arrow chevron used in ScifiButtonBlock; encoded as Unicode so it
* never breaks across renderers (UAT §13.14).
*/
.scifi-chevron::after {
content: " \2192";
font-family: var(--font-mono);
margin-left: 0.5rem;
}
/* === Sci-Fi Clean: heading family ===
* The host shadcn layer doesn't apply --font-heading by default; wire it
* up here for the four heading levels we render.
*/
.scifi-h1, .scifi-h2, .scifi-h3, .scifi-h4 {
font-family: var(--font-heading);
font-feature-settings: "ss01" on;
}
.scifi-body {
font-family: var(--font-body);
}
.scifi-mono {
font-family: var(--font-mono);
}
`

63
diagram_caption.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"bytes"
"context"
"strconv"
"git.dev.alexdunmow.com/block/core/blocks"
)
// DiagramCaptionBlockMeta defines metadata for the diagram-caption block.
var DiagramCaptionBlockMeta = blocks.BlockMeta{
Key: "diagram_caption",
Title: "Diagram Caption",
Description: "Annotated technical figure with a Fig. NN. heading and optional callout pins.",
Source: "scifi-clean",
Category: blocks.CategoryContent,
}
// DiagramCalloutPin is a percent-positioned overlay pin.
type DiagramCalloutPin struct {
X float64
Y float64
Label string
}
// DiagramCaptionData drives the templ render.
type DiagramCaptionData struct {
Image string
FigureNumber string
Title string
Body string
CalloutPins []DiagramCalloutPin
}
// DiagramCaptionBlock renders the diagram-caption block.
// Content shape: {"image": "media:...", "figureNumber": "...", "title": "...", "body": "...",
// "calloutPins": [{"x": 12, "y": 50, "label": "..."}]}
func DiagramCaptionBlock(ctx context.Context, content map[string]any) string {
data := DiagramCaptionData{
Image: blocks.ResolveMediaPath(getString(content, "image")),
FigureNumber: getString(content, "figureNumber"),
Title: getString(content, "title"),
Body: getString(content, "body"),
}
for _, raw := range getSlice(content, "calloutPins") {
data.CalloutPins = append(data.CalloutPins, DiagramCalloutPin{
X: getFloat(raw, "x", 0),
Y: getFloat(raw, "y", 0),
Label: getString(raw, "label"),
})
}
var buf bytes.Buffer
_ = diagramCaptionComponent(data).Render(ctx, &buf)
return buf.String()
}
// percentStyle returns a tiny inline style string positioning the pin in percent.
func percentStyle(x, y float64) string {
return "left: " + strconv.FormatFloat(x, 'f', 2, 64) + "%; top: " + strconv.FormatFloat(y, 'f', 2, 64) + "%;"
}

49
diagram_caption.templ Normal file
View File

@ -0,0 +1,49 @@
package main
// diagramCaptionComponent renders the figure with overlay pins and a Fig. heading.
templ diagramCaptionComponent(data DiagramCaptionData) {
<figure data-block="scifi-clean:diagram_caption" class="my-8 max-w-3xl mx-auto px-4">
<div class="relative hairline bg-card">
if data.Image == "" {
<div class="bg-grid aspect-video flex items-center justify-center">
<span class="scifi-mono text-sm text-muted-foreground uppercase tracking-widest">
No diagram yet
</span>
</div>
} else {
<img
src={ data.Image }
alt={ data.Title }
class="block w-full h-auto"
/>
for _, pin := range data.CalloutPins {
<span
class="absolute inline-flex items-center scifi-mono text-xs uppercase tracking-wider px-2 py-1 bg-card text-foreground hairline"
style={ percentStyle(pin.X, pin.Y) }
>
{ pin.Label }
</span>
}
}
</div>
<figcaption class="mt-3 hairline-t pt-3">
<h4 class="scifi-h4 scifi-mono text-sm uppercase tracking-widest text-foreground">
{ "Fig. " }
if data.FigureNumber != "" {
{ data.FigureNumber }
} else {
{ "00" }
}
{ "." }
if data.Title != "" {
<span class="text-muted-foreground ml-2">{ data.Title }</span>
}
</h4>
if data.Body != "" {
<div class="scifi-body prose prose-sm max-w-none text-muted-foreground mt-2">
@templ.Raw(data.Body)
</div>
}
</figcaption>
</figure>
}

210
diagram_caption_templ.go Normal file
View File

@ -0,0 +1,210 @@
// 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"
// diagramCaptionComponent renders the figure with overlay pins and a Fig. heading.
func diagramCaptionComponent(data DiagramCaptionData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure data-block=\"scifi-clean:diagram_caption\" class=\"my-8 max-w-3xl mx-auto px-4\"><div class=\"relative hairline bg-card\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image == "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"bg-grid aspect-video flex items-center justify-center\"><span class=\"scifi-mono text-sm text-muted-foreground uppercase tracking-widest\">No diagram yet</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 15, Col: 21}
}
_, 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, 4, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 16, Col: 21}
}
_, 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, 5, "\" class=\"block w-full h-auto\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, pin := range data.CalloutPins {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"absolute inline-flex items-center scifi-mono text-xs uppercase tracking-wider px-2 py-1 bg-card text-foreground hairline\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(percentStyle(pin.X, pin.Y))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 22, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(pin.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 24, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><figcaption class=\"mt-3 hairline-t pt-3\"><h4 class=\"scifi-h4 scifi-mono text-sm uppercase tracking-widest text-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("Fig. ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 31, 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, 10, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.FigureNumber != "" {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.FigureNumber)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 33, Col: 24}
}
_, 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, 11, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("00")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 35, Col: 11}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(".")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 37, Col: 9}
}
_, 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, 13, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"text-muted-foreground ml-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `diagram_caption.templ`, Line: 39, Col: 58}
}
_, 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, 15, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Body != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"scifi-body prose prose-sm max-w-none text-muted-foreground mt-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</figcaption></figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

16
email_wrapper.go Normal file
View File

@ -0,0 +1,16 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/templates"
)
// ScifiEmailWrapper wraps body content in a branded Sci-Fi Clean email shell:
// 600px white card, hairline border, mono callsign top-right, instrument footer.
func ScifiEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = scifiEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}

137
email_wrapper.templ Normal file
View File

@ -0,0 +1,137 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// scifiEmailBgColor / scifiEmailCardColor / scifiEmailBorderColor pull from the
// caller-provided EmailColors so the wrapper inherits the active preset.
func scifiEmailBgColor(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return "#F6F7F8"
}
func scifiEmailCardColor(c templates.EmailContext) string {
if c.Colors.Card != "" {
return c.Colors.Card
}
return "#FFFFFF"
}
func scifiEmailBorderColor(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return "#D6D9DD"
}
func scifiEmailFgColor(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return "#16202E"
}
func scifiEmailMutedColor(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return "#5C6776"
}
func scifiEmailCallsign(c templates.EmailContext) string {
if c.SiteSettings.SiteName != "" {
return "SCF / " + c.SiteSettings.SiteName
}
return "SCF-CLN"
}
// scifiEmailTemplate is the Sci-Fi Clean branded email wrapper.
templ scifiEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<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%;
}
img { border: 0; height: auto; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
/* Mono numeral preservation across clients. */
.scifi-num {
font-family: ui-monospace, "JetBrains Mono", "Menlo", monospace !important;
font-variant-numeric: tabular-nums;
}
@media only screen and (max-width: 620px) {
.scifi-email-container { width: 100% !important; max-width: 100% !important; }
.scifi-pad { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: %s;", scifiEmailBgColor(emailCtx), scifiEmailFgColor(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden;">{ emailCtx.PreviewText }</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 32px 12px; background-color: %s;", scifiEmailBgColor(emailCtx)) }>
<table
role="presentation"
class="scifi-email-container"
width="600"
cellspacing="0"
cellpadding="0"
border="0"
style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", scifiEmailCardColor(emailCtx), scifiEmailBorderColor(emailCtx)) }
>
<tr>
<td class="scifi-pad" style={ fmt.Sprintf("padding: 20px 28px; border-bottom: 1px solid %s;", scifiEmailBorderColor(emailCtx)) }>
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="left" style={ fmt.Sprintf("font-family: 'Space Grotesk', 'Inter', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: 0.02em; color: %s;", scifiEmailFgColor(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</td>
<td align="right" class="scifi-num" style={ fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", scifiEmailMutedColor(emailCtx)) }>
{ scifiEmailCallsign(emailCtx) }
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="scifi-pad" style={ fmt.Sprintf("padding: 28px; font-size: 15px; line-height: 1.55; color: %s;", scifiEmailFgColor(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<tr>
<td class="scifi-pad" style={ fmt.Sprintf("padding: 16px 28px; border-top: 1px solid %s; background-color: %s;", scifiEmailBorderColor(emailCtx), scifiEmailCardColor(emailCtx)) }>
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td class="scifi-num" align="left" style={ fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", scifiEmailMutedColor(emailCtx)) }>
{ emailCtx.SiteSettings.SiteURL }
</td>
if emailCtx.UnsubscribeURL != "" {
<td class="scifi-num" align="right" style={ fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;") }>
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", scifiEmailMutedColor(emailCtx)) }>Unsubscribe</a>
</td>
}
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

340
email_wrapper_templ.go Normal file
View File

@ -0,0 +1,340 @@
// 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"
)
// scifiEmailBgColor / scifiEmailCardColor / scifiEmailBorderColor pull from the
// caller-provided EmailColors so the wrapper inherits the active preset.
func scifiEmailBgColor(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return "#F6F7F8"
}
func scifiEmailCardColor(c templates.EmailContext) string {
if c.Colors.Card != "" {
return c.Colors.Card
}
return "#FFFFFF"
}
func scifiEmailBorderColor(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return "#D6D9DD"
}
func scifiEmailFgColor(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return "#16202E"
}
func scifiEmailMutedColor(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return "#5C6776"
}
func scifiEmailCallsign(c templates.EmailContext) string {
if c.SiteSettings.SiteName != "" {
return "SCF / " + c.SiteSettings.SiteName
}
return "SCF-CLN"
}
// scifiEmailTemplate is the Sci-Fi Clean branded email wrapper.
func scifiEmailTemplate(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\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><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: 61, 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 {\n\t\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t\t}\n\t\t\t\timg { border: 0; height: auto; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\t/* Mono numeral preservation across clients. */\n\t\t\t\t.scifi-num {\n\t\t\t\t\tfont-family: ui-monospace, \"JetBrains Mono\", \"Menlo\", monospace !important;\n\t\t\t\t\tfont-variant-numeric: tabular-nums;\n\t\t\t\t}\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.scifi-email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.scifi-pad { 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', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: %s;", scifiEmailBgColor(emailCtx), scifiEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 80, Col: 225}
}
_, 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;\">")
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: 82, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</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: 32px 12px; background-color: %s;", scifiEmailBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 86, Col: 117}
}
_, 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=\"scifi-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: 1px solid %s;", scifiEmailCardColor(emailCtx), scifiEmailBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 94, Col: 155}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><tr><td class=\"scifi-pad\" 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: 20px 28px; border-bottom: 1px solid %s;", scifiEmailBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 97, Col: 134}
}
_, 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, "\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"left\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: 'Space Grotesk', 'Inter', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: 0.02em; color: %s;", scifiEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 100, Col: 200}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, 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: 101, Col: 44}
}
_, 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, 11, "</td><td align=\"right\" class=\"scifi-num\" 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("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", scifiEmailMutedColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 103, Col: 236}
}
_, 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, 12, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(scifiEmailCallsign(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 104, Col: 42}
}
_, 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, 13, "</td></tr></table></td></tr><tr><td class=\"scifi-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 28px; font-size: 15px; line-height: 1.55; color: %s;", scifiEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 111, Col: 143}
}
_, 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, 14, "\">")
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, 15, "</td></tr><tr><td class=\"scifi-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 16px 28px; border-top: 1px solid %s; background-color: %s;", scifiEmailBorderColor(emailCtx), scifiEmailCardColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 116, Col: 184}
}
_, 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, 16, "\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td class=\"scifi-num\" align=\"left\" 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("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", scifiEmailMutedColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 119, Col: 235}
}
_, 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, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 120, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<td class=\"scifi-num\" align=\"right\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 123, Col: 194}
}
_, 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, 20, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", scifiEmailMutedColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 157}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">Unsubscribe</a></td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</tr></table></td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

60
embed.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler returns an http.Handler that serves the embedded assets.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded theme presets JSON.
func ThemePresets() []byte {
return presetsData
}
// BundledFonts returns the embedded fonts manifest JSON.
func BundledFonts() []byte {
return fontsData
}
// ThemeCSSManifest returns the CSS manifest containing the theme's custom
// utilities (.hairline, .bg-grid) and font-family fallback CSS variables.
// It is injected into the host Tailwind input by the CMS at build time.
func ThemeCSSManifest() *plugin.CSSManifest {
return &plugin.CSSManifest{
InputCSSAppend: scifiCleanInputCSS,
}
}

1
fonts.json Normal file
View File

@ -0,0 +1 @@
[]

58
footer.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FooterBlockMeta defines metadata for the instrument footer block.
var FooterBlockMeta = blocks.BlockMeta{
Key: "footer",
Title: "Instrument Footer",
Description: "Hairline-rule footer with callsign, optional signup, and link list.",
Source: "scifi-clean",
Category: blocks.CategoryLayout,
}
// FooterLink is one footer link entry.
type FooterLink struct {
Text string
URL string
}
// FooterData drives the templ render.
type FooterData struct {
ShowSignup bool
Callsign string
Links []FooterLink
}
// FooterBlock renders the footer.
// Content shape: {"showSignup": false, "callsign": "SCF-CLN", "links": [{"text": "...", "url": "..."}]}
func FooterBlock(ctx context.Context, content map[string]any) string {
data := FooterData{
ShowSignup: getBool(content, "showSignup", false),
Callsign: getStringDefault(content, "callsign", "SCF-CLN"),
}
for _, raw := range getSlice(content, "links") {
data.Links = append(data.Links, FooterLink{
Text: getString(raw, "text"),
URL: getString(raw, "url"),
})
}
var buf bytes.Buffer
_ = footerComponent(data).Render(ctx, &buf)
return buf.String()
}
// safeHref returns # for blank URLs so the templ.SafeURL helper has something to chew on.
func safeHref(u string) string {
if u == "" {
return "#"
}
return u
}

69
footer.templ Normal file
View File

@ -0,0 +1,69 @@
package main
// footerComponent renders the instrument-bar footer.
// UAT §13.10 requires a callsign element rendered in JetBrains Mono and at
// least one .hairline rule above the footer.
templ footerComponent(data FooterData) {
<div data-block="scifi-clean:footer" class="w-full">
<div class="hairline-t pt-8 pb-10">
<div class="max-w-6xl mx-auto px-4">
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-8">
<div class="space-y-2">
<div
data-scifi-callsign
class="scifi-mono uppercase tracking-widest text-xs text-foreground"
style="font-family: var(--font-mono);"
>
{ data.Callsign }
</div>
<p class="scifi-body text-sm text-muted-foreground max-w-md">
Telemetry, mission briefs, and engineering notes from this surface.
</p>
</div>
if len(data.Links) > 0 {
<ul class="space-y-2 scifi-body text-sm">
for _, link := range data.Links {
<li>
<a
href={ templ.SafeURL(safeHref(link.URL)) }
class="text-muted-foreground hover:text-foreground transition-colors"
>
{ link.Text }
</a>
</li>
}
</ul>
}
if data.ShowSignup {
<form class="space-y-2 max-w-xs" method="post" action="/subscribe">
<label class="scifi-mono uppercase text-xs tracking-widest text-muted-foreground block">
Mission updates
</label>
<div class="flex hairline">
<input
type="email"
name="email"
required
placeholder="ops@station.local"
class="scifi-body flex-1 px-3 py-2 bg-card text-foreground text-sm focus:outline-none scifi-focus"
/>
<button
type="submit"
class="scifi-mono uppercase text-xs tracking-widest px-4 py-2 bg-primary text-primary-foreground scifi-chevron scifi-focus"
>
Subscribe
</button>
</div>
</form>
}
</div>
<div class="mt-8 hairline-t pt-4 flex items-center justify-between scifi-mono text-xs uppercase tracking-widest text-muted-foreground tabular-nums">
<span>Project Aurora</span>
<span>v0.1.0</span>
</div>
</div>
</div>
</div>
}

108
footer_templ.go Normal file
View File

@ -0,0 +1,108 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// footerComponent renders the instrument-bar footer.
// UAT §13.10 requires a callsign element rendered in JetBrains Mono and at
// least one .hairline rule above the footer.
func footerComponent(data FooterData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"scifi-clean:footer\" class=\"w-full\"><div class=\"hairline-t pt-8 pb-10\"><div class=\"max-w-6xl mx-auto px-4\"><div class=\"flex flex-col md:flex-row md:items-start md:justify-between gap-8\"><div class=\"space-y-2\"><div data-scifi-callsign class=\"scifi-mono uppercase tracking-widest text-xs text-foreground\" style=\"font-family: var(--font-mono);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Callsign)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 17, Col: 22}
}
_, 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, "</div><p class=\"scifi-body text-sm text-muted-foreground max-w-md\">Telemetry, mission briefs, and engineering notes from this surface.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Links) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ul class=\"space-y-2 scifi-body text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range data.Links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<li><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(safeHref(link.URL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 29, Col: 50}
}
_, 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, 5, "\" class=\"text-muted-foreground hover:text-foreground transition-colors\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(link.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 32, Col: 21}
}
_, 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, 6, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowSignup {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<form class=\"space-y-2 max-w-xs\" method=\"post\" action=\"/subscribe\"><label class=\"scifi-mono uppercase text-xs tracking-widest text-muted-foreground block\">Mission updates</label><div class=\"flex hairline\"><input type=\"email\" name=\"email\" required placeholder=\"ops@station.local\" class=\"scifi-body flex-1 px-3 py-2 bg-card text-foreground text-sm focus:outline-none scifi-focus\"> <button type=\"submit\" class=\"scifi-mono uppercase text-xs tracking-widest px-4 py-2 bg-primary text-primary-foreground scifi-chevron scifi-focus\">Subscribe</button></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"mt-8 hairline-t pt-4 flex items-center justify-between scifi-mono text-xs uppercase tracking-widest text-muted-foreground tabular-nums\"><span>Project Aurora</span> <span>v0.1.0</span></div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

20
go.mod Normal file
View File

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

42
go.sum Normal file
View File

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

42
heading_override.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"bytes"
"context"
"strconv"
)
// ScifiHeadingBlock renders a heading with the Sci-Fi Clean treatment:
// Space Grotesk weights, optional mono kicker prefix ("FIG / SEC / SYS" etc.).
// Reuses the built-in heading schema; reads optional kicker via content["kicker"].
func ScifiHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
kicker := getString(content, "kicker")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = scifiHeadingComponent(level, text, textClass, kicker).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel coerces an incoming heading level to 1..6, defaulting to 2.
func parseHeadingLevel(content map[string]any) int {
if level, ok := content["level"].(float64); ok {
l := int(level)
if l >= 1 && l <= 6 {
return l
}
}
if level, ok := content["level"].(int); ok {
if level >= 1 && level <= 6 {
return level
}
}
if level, ok := content["level"].(string); ok {
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
return l
}
}
return 2
}

48
heading_override.templ Normal file
View File

@ -0,0 +1,48 @@
package main
// scifiHeadingBaseClass returns level-specific tracking + weight classes.
func scifiHeadingBaseClass(level int) string {
switch level {
case 1:
return "scifi-h1 text-4xl md:text-5xl font-medium tracking-tight"
case 2:
return "scifi-h2 text-3xl font-medium tracking-tight"
case 3:
return "scifi-h3 text-2xl font-medium"
case 4:
return "scifi-h4 text-xl font-medium"
case 5:
return "scifi-h4 text-lg font-medium"
case 6:
return "scifi-h4 text-base font-medium uppercase tracking-widest"
default:
return "scifi-h2 text-3xl font-medium tracking-tight"
}
}
// scifiHeadingComponent renders a heading with an optional mono kicker rail.
templ scifiHeadingComponent(level int, text, textClass, kicker string) {
<div class="my-6">
if kicker != "" {
<div class="scifi-mono uppercase tracking-widest text-xs text-accent mb-2">
{ kicker }
</div>
}
switch level {
case 1:
<h1 class={ scifiHeadingBaseClass(1), "text-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h1>
case 2:
<h2 class={ scifiHeadingBaseClass(2), "text-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h2>
case 3:
<h3 class={ scifiHeadingBaseClass(3), "text-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h3>
case 4:
<h4 class={ scifiHeadingBaseClass(4), "text-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h4>
case 5:
<h5 class={ scifiHeadingBaseClass(5), "text-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h5>
case 6:
<h6 class={ scifiHeadingBaseClass(6), "text-muted-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h6>
default:
<h2 class={ scifiHeadingBaseClass(2), "text-foreground", textClass } style="font-family: var(--font-heading);">{ text }</h2>
}
</div>
}

338
heading_override_templ.go Normal file
View File

@ -0,0 +1,338 @@
// 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"
// scifiHeadingBaseClass returns level-specific tracking + weight classes.
func scifiHeadingBaseClass(level int) string {
switch level {
case 1:
return "scifi-h1 text-4xl md:text-5xl font-medium tracking-tight"
case 2:
return "scifi-h2 text-3xl font-medium tracking-tight"
case 3:
return "scifi-h3 text-2xl font-medium"
case 4:
return "scifi-h4 text-xl font-medium"
case 5:
return "scifi-h4 text-lg font-medium"
case 6:
return "scifi-h4 text-base font-medium uppercase tracking-widest"
default:
return "scifi-h2 text-3xl font-medium tracking-tight"
}
}
// scifiHeadingComponent renders a heading with an optional mono kicker rail.
func scifiHeadingComponent(level int, text, textClass, kicker string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"my-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"scifi-mono uppercase tracking-widest text-xs text-accent mb-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 28, Col: 12}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
switch level {
case 1:
var templ_7745c5c3_Var3 = []any{scifiHeadingBaseClass(1), "text-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_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, 5, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 33, Col: 121}
}
_, 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, 6, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var6 = []any{scifiHeadingBaseClass(2), "text-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 35, Col: 121}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var9 = []any{scifiHeadingBaseClass(3), "text-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var9).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_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, 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: 121}
}
_, 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, 12, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var12 = []any{scifiHeadingBaseClass(4), "text-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).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_Var13)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, 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: 121}
}
_, 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, 15, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var15 = []any{scifiHeadingBaseClass(5), "text-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var15).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_Var16)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 41, Col: 121}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var18 = []any{scifiHeadingBaseClass(6), "text-muted-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).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_Var19)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 43, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var21 = []any{scifiHeadingBaseClass(2), "text-foreground", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var21).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_Var22)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" style=\"font-family: var(--font-heading);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 45, Col: 121}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

82
helpers.go Normal file
View File

@ -0,0 +1,82 @@
package main
// getString 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 ""
}
// getStringDefault returns the string at key, falling back to def if missing/blank.
func getStringDefault(content map[string]any, key, def string) string {
if v := getString(content, key); v != "" {
return v
}
return def
}
// getBool extracts a boolean from a content map. Accepts native bool, string ("true"/"1"),
// or numeric truthiness. Returns def when no value is present or parseable.
func getBool(content map[string]any, key string, def bool) bool {
if v, ok := content[key]; ok {
switch t := v.(type) {
case bool:
return t
case string:
switch t {
case "true", "TRUE", "True", "1", "yes", "on":
return true
case "false", "FALSE", "False", "0", "no", "off":
return false
}
case float64:
return t != 0
case int:
return t != 0
}
}
return def
}
// getSlice extracts a slice of maps from content under key. Returns nil if not a slice.
func getSlice(content map[string]any, key string) []map[string]any {
if v, ok := content[key].([]any); ok {
out := make([]map[string]any, 0, len(v))
for _, item := range v {
if m, ok := item.(map[string]any); ok {
out = append(out, m)
}
}
return out
}
return nil
}
// getMap returns the map at key, or nil.
func getMap(content map[string]any, key string) map[string]any {
if m, ok := content[key].(map[string]any); ok {
return m
}
return nil
}
// getFloat returns the float at key, or def if missing / wrong type.
func getFloat(content map[string]any, key string, def float64) float64 {
if v, ok := content[key].(float64); ok {
return v
}
if v, ok := content[key].(int); ok {
return float64(v)
}
return def
}
// normalizeTrend coerces an incoming trend string to {up, down, flat}, defaulting to flat.
func normalizeTrend(s string) string {
switch s {
case "up", "down", "flat":
return s
}
return "flat"
}

54
mission_stat.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MissionStatBlockMeta defines metadata for the mission-stat block.
var MissionStatBlockMeta = blocks.BlockMeta{
Key: "mission_stat",
Title: "Mission Stat",
Description: "Headline metric in mono numerals with a signal-orange delta arrow.",
Source: "scifi-clean",
Category: blocks.CategoryContent,
}
// MissionStatData drives the templ render.
type MissionStatData struct {
Metric string
Value string
Unit string
Delta string
Trend string // "up", "down", "flat"
}
// MissionStatBlock renders the mission-stat block.
// Content shape: {"metric": "...", "value": "...", "unit": "...", "delta": "...", "trend": "up|down|flat"}
func MissionStatBlock(ctx context.Context, content map[string]any) string {
data := MissionStatData{
Metric: getString(content, "metric"),
Value: getString(content, "value"),
Unit: getString(content, "unit"),
Delta: getString(content, "delta"),
Trend: normalizeTrend(getString(content, "trend")),
}
var buf bytes.Buffer
_ = missionStatComponent(data).Render(ctx, &buf)
return buf.String()
}
// trendGlyph returns the arrow character for a given trend.
func trendGlyph(trend string) string {
switch trend {
case "up":
return "↑" // ↑
case "down":
return "↓" // ↓
default:
return "→" // →
}
}

35
mission_stat.templ Normal file
View File

@ -0,0 +1,35 @@
package main
// missionStatComponent renders the headline metric card with mono numerals
// and an accent-colored delta arrow.
templ missionStatComponent(data MissionStatData) {
<div data-block="scifi-clean:mission_stat" class="hairline p-6 bg-card text-card-foreground">
if data.Metric != "" {
<div class="scifi-mono uppercase text-xs tracking-widest text-muted-foreground mb-3">
{ data.Metric }
</div>
}
<div class="flex items-baseline gap-2">
<span
class="scifi-mono text-5xl font-medium tabular-nums text-foreground"
style="font-family: var(--font-mono); font-variant-numeric: tabular-nums;"
>
if data.Value == "" {
{ "—" }
} else {
{ data.Value }
}
</span>
if data.Unit != "" {
<span class="scifi-mono text-base text-muted-foreground tabular-nums" style="font-family: var(--font-mono); font-variant-numeric: tabular-nums;">
{ data.Unit }
</span>
}
</div>
if data.Delta != "" {
<div class="mt-3 scifi-mono text-sm tabular-nums" style={ "font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: hsl(var(--accent));" }>
{ trendGlyph(data.Trend) } { data.Delta }
</div>
}
</div>
}

162
mission_stat_templ.go Normal file
View File

@ -0,0 +1,162 @@
// 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"
// missionStatComponent renders the headline metric card with mono numerals
// and an accent-colored delta arrow.
func missionStatComponent(data MissionStatData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"scifi-clean:mission_stat\" class=\"hairline p-6 bg-card text-card-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Metric != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"scifi-mono uppercase text-xs tracking-widest text-muted-foreground mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Metric)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 9, Col: 17}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"flex items-baseline gap-2\"><span class=\"scifi-mono text-5xl font-medium tabular-nums text-foreground\" style=\"font-family: var(--font-mono); font-variant-numeric: tabular-nums;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Value == "" {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("—")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 18, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 20, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Unit != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"scifi-mono text-base text-muted-foreground tabular-nums\" style=\"font-family: var(--font-mono); font-variant-numeric: tabular-nums;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Unit)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 25, Col: 16}
}
_, 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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Delta != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"mt-3 scifi-mono text-sm tabular-nums\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: hsl(var(--accent));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 30, Col: 156}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(trendGlyph(data.Trend))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 31, Col: 28}
}
_, 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, 11, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Delta)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mission_stat.templ`, Line: 31, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

12
plugin.mod Normal file
View File

@ -0,0 +1,12 @@
[plugin]
name = "scifi-clean"
display_name = "Sci-Fi Clean"
scope = "@themes"
version = "0.1.0"
description = "Minimalist futurist theme for aerospace, robotics, and R&D — off-white surfaces, technical blue, signal-orange accents, mono numerals."
kind = "theme"
categories = ["templates", "developer"]
tags = ["futurist", "technical", "aerospace", "hardware", "robotics", "biotech", "rnd", "clean", "minimalist"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

110
presets.json Normal file
View File

@ -0,0 +1,110 @@
[
{
"id": "flightline",
"name": "Flightline",
"description": "Off-white paper with technical blue primary and signal-orange accent — the default Sci-Fi Clean look.",
"theme": {
"mode": "light",
"lightColors": {
"background": "210 14% 97%",
"foreground": "215 30% 12%",
"card": "0 0% 100%",
"cardForeground": "215 30% 12%",
"popover": "0 0% 100%",
"popoverForeground": "215 30% 12%",
"primary": "215 90% 45%",
"primaryForeground": "0 0% 100%",
"secondary": "210 16% 92%",
"secondaryForeground": "215 30% 20%",
"muted": "210 14% 94%",
"mutedForeground": "215 12% 42%",
"accent": "18 95% 55%",
"accentForeground": "0 0% 100%",
"destructive": "0 78% 50%",
"destructiveForeground": "0 0% 100%",
"border": "215 14% 86%",
"input": "215 14% 88%",
"ring": "215 90% 45%"
}
}
},
{
"id": "mission-control",
"name": "Mission Control",
"description": "Graphite night-shift surface for ops dashboards — lifted technical blue and signal orange on a deep neutral.",
"theme": {
"mode": "dark",
"darkColors": {
"background": "220 16% 7%",
"foreground": "210 16% 92%",
"card": "220 16% 10%",
"cardForeground": "210 16% 92%",
"popover": "220 16% 10%",
"popoverForeground": "210 16% 92%",
"primary": "210 100% 60%",
"primaryForeground": "220 16% 7%",
"secondary": "220 14% 16%",
"secondaryForeground": "210 16% 92%",
"muted": "220 12% 14%",
"mutedForeground": "215 10% 60%",
"accent": "18 100% 60%",
"accentForeground": "0 0% 100%",
"destructive": "0 80% 55%",
"destructiveForeground": "0 0% 100%",
"border": "220 14% 20%",
"input": "220 14% 18%",
"ring": "210 100% 60%"
}
}
},
{
"id": "cleanroom",
"name": "Cleanroom",
"description": "Highest-contrast surgical white with deep navy ink — sister dark mode for night-lab work.",
"theme": {
"mode": "both",
"lightColors": {
"background": "0 0% 100%",
"foreground": "220 40% 10%",
"card": "210 20% 99%",
"cardForeground": "220 40% 10%",
"popover": "0 0% 100%",
"popoverForeground": "220 40% 10%",
"primary": "220 85% 35%",
"primaryForeground": "0 0% 100%",
"secondary": "210 18% 94%",
"secondaryForeground": "220 40% 10%",
"muted": "210 16% 96%",
"mutedForeground": "215 14% 38%",
"accent": "18 92% 50%",
"accentForeground": "0 0% 100%",
"destructive": "0 78% 48%",
"destructiveForeground": "0 0% 100%",
"border": "215 16% 88%",
"input": "215 16% 90%",
"ring": "220 85% 35%"
},
"darkColors": {
"background": "220 30% 5%",
"foreground": "210 20% 96%",
"card": "220 26% 9%",
"cardForeground": "210 20% 96%",
"popover": "220 26% 9%",
"popoverForeground": "210 20% 96%",
"primary": "210 100% 65%",
"primaryForeground": "220 30% 5%",
"secondary": "220 20% 16%",
"secondaryForeground": "210 20% 96%",
"muted": "220 16% 13%",
"mutedForeground": "215 12% 62%",
"accent": "18 100% 62%",
"accentForeground": "0 0% 100%",
"destructive": "0 80% 58%",
"destructiveForeground": "0 0% 100%",
"border": "220 18% 20%",
"input": "220 18% 18%",
"ring": "210 100% 65%"
}
}
}
]

197
register.go Normal file
View File

@ -0,0 +1,197 @@
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 is the plugin entry point. It registers the system template, the
// four page templates, the six theme-specific blocks, the three overrides,
// and the email wrapper.
//
// IMPORTANT: schemas are loaded BEFORE any br.Register(...) call — this is a
// hard rule (CLAUDE.md, UAT §3) so the block registry binds content schemas
// at registration time.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
// --- System template ---
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "scifi-clean",
Title: "Sci-Fi Clean",
Description: "Minimalist futurist theme for aerospace, robotics, and R&D — off-white surfaces, technical blue, signal-orange accents, mono numerals.",
})
// --- Page templates (spec §6) ---
if err := tr.RegisterPageTemplate("scifi-clean", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard cleanroom layout with header rail, main column, and footer instrument panel.",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderScifiClean)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("scifi-clean", templates.PageTemplateMeta{
Key: "landing",
Title: "Landing",
Description: "Hero diagram + spec strip + CTA banner.",
Slots: []string{"hero", "specs", "main", "cta", "footer"},
}, wrap(RenderScifiCleanLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("scifi-clean", templates.PageTemplateMeta{
Key: "article",
Title: "Article",
Description: "Narrow column with rail metadata and figure margin.",
Slots: []string{"header", "rail", "main", "footer"},
}, wrap(RenderScifiCleanArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("scifi-clean", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge schematic surface for diagrams and dashboards.",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderScifiCleanFullWidth)); err != nil {
return err
}
// --- Load JSON Schemas BEFORE any block registration ---
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
// --- Theme-specific blocks (spec §8) ---
br.Register(TechSpecBlockMeta, TechSpecBlock)
br.Register(DiagramCaptionBlockMeta, DiagramCaptionBlock)
br.Register(MissionStatBlockMeta, MissionStatBlock)
br.Register(StatusBarBlockMeta, StatusBarBlock)
br.Register(FooterBlockMeta, FooterBlock)
br.Register(SchematicHeroBlockMeta, SchematicHeroBlock)
// --- Built-in overrides (spec §9) ---
br.RegisterTemplateOverride("scifi-clean", "heading", ScifiHeadingBlock)
br.RegisterTemplateOverride("scifi-clean", "text", ScifiTextBlock)
br.RegisterTemplateOverride("scifi-clean", "button", ScifiButtonBlock)
// --- Email wrapper (spec §10) ---
tr.RegisterEmailWrapper("scifi-clean", ScifiEmailWrapper)
return nil
}
// DefaultMasterPages returns the two master pages that Sci-Fi Clean provisions
// on first plugin load (spec §7, UAT §9).
//
// - scifi-clean:default-master → attached to `default`, `article`, `full-width`
// - scifi-clean:landing-master → attached to `landing`, pre-populates `hero`
// with a mission_stat and `specs` with a tech_spec.
func DefaultMasterPages() []plugin.MasterPageDefinition {
defaultMaster := plugin.MasterPageDefinition{
Key: "scifi-clean:default-master",
Title: "Sci-Fi Clean Default Master",
PageTemplates: []string{"default", "article", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Mission Header",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "scifi-clean:status_bar",
Title: "System Status Rail",
Content: map[string]any{"showClock": true, "showBuild": true, "callsign": "SCF-CLN"},
Slot: "header",
SortOrder: 10,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main", "placeholder": "Page payload"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "scifi-clean:footer",
Title: "Instrument Footer",
Content: map[string]any{"showSignup": true, "callsign": "SCF-CLN"},
Slot: "footer",
SortOrder: 0,
},
},
}
landingMaster := plugin.MasterPageDefinition{
Key: "scifi-clean:landing-master",
Title: "Sci-Fi Clean Landing Master",
PageTemplates: []string{"landing"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "scifi-clean:status_bar",
Title: "System Status Rail",
Content: map[string]any{"showClock": true, "showBuild": true, "callsign": "SCF-CLN"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "scifi-clean:mission_stat",
Title: "Hero Mission Stat",
Content: map[string]any{
"metric": "Apogee",
"value": "412",
"unit": "km",
"delta": "+12",
"trend": "up",
},
Slot: "hero",
SortOrder: 0,
},
{
BlockKey: "scifi-clean:tech_spec",
Title: "Hero Tech Spec",
Content: map[string]any{
"caption": "Launch envelope",
"rows": []any{
map[string]any{"label": "Thrust", "value": "1.42", "unit": "MN"},
map[string]any{"label": "Burn time", "value": "186", "unit": "s"},
map[string]any{"label": "Payload", "value": "320", "unit": "kg"},
},
},
Slot: "specs",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main", "placeholder": "Page payload"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "scifi-clean:footer",
Title: "Instrument Footer",
Content: map[string]any{"showSignup": true, "callsign": "SCF-CLN"},
Slot: "footer",
SortOrder: 0,
},
},
}
return []plugin.MasterPageDefinition{defaultMaster, landingMaster}
}

25
registration.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// Registration is the compile-time plugin registration for the Sci-Fi Clean theme.
var Registration = plugin.PluginRegistration{
Name: "scifi-clean",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return Schemas() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
}

View File

@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Diagram Caption",
"description": "Annotated technical figure with a Fig. NN. heading and optional callout pins.",
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Image",
"description": "Diagram / blueprint / schematic to display.",
"x-editor": "media"
},
"figureNumber": {
"type": "string",
"title": "Figure Number",
"description": "Number printed after the Fig. prefix (e.g. 03).",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Title",
"description": "Short heading next to the figure number.",
"x-editor": "text"
},
"body": {
"type": "string",
"title": "Body",
"description": "Rich description / annotation text rendered below the figure.",
"x-editor": "richtext"
},
"calloutPins": {
"type": "array",
"title": "Callout Pins",
"description": "Percent-positioned overlay pins; each pin is x, y, label.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"x": {
"type": "number",
"title": "X (%)",
"description": "Horizontal position as a percentage of the image width (0-100).",
"minimum": 0,
"maximum": 100,
"x-editor": "number"
},
"y": {
"type": "number",
"title": "Y (%)",
"description": "Vertical position as a percentage of the image height (0-100).",
"minimum": 0,
"maximum": 100,
"x-editor": "number"
},
"label": {
"type": "string",
"title": "Label",
"description": "Short callout text.",
"x-editor": "text"
}
},
"required": ["label"]
}
}
}
}

View File

@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Instrument Footer",
"description": "Hairline-rule footer with callsign, optional signup, and link list.",
"type": "object",
"properties": {
"showSignup": {
"type": "boolean",
"title": "Show Signup",
"description": "Render the inline email signup form.",
"default": false,
"x-editor": "select"
},
"callsign": {
"type": "string",
"title": "Callsign",
"description": "Short mono identifier displayed at the corner of the footer rail.",
"x-editor": "text"
},
"links": {
"type": "array",
"title": "Links",
"description": "Footer link list (each item is text + url).",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Link Text",
"description": "Visible link text.",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"description": "Destination URL.",
"x-editor": "text"
}
},
"required": ["text"]
}
}
}
}

View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Mission Stat",
"description": "Headline metric in mono numerals with a signal-orange delta arrow.",
"type": "object",
"properties": {
"metric": {
"type": "string",
"title": "Metric",
"description": "Name of the measured quantity (e.g. Apogee).",
"x-editor": "text"
},
"value": {
"type": "string",
"title": "Value",
"description": "Headline numeric value (e.g. 412).",
"x-editor": "text"
},
"unit": {
"type": "string",
"title": "Unit",
"description": "Trailing unit (e.g. km, m/s).",
"x-editor": "text"
},
"delta": {
"type": "string",
"title": "Delta",
"description": "Short delta string (e.g. +12, -0.3%).",
"x-editor": "text"
},
"trend": {
"type": "string",
"title": "Trend",
"description": "Direction of the delta arrow.",
"default": "flat",
"enum": ["up", "down", "flat"],
"x-editor": "select"
}
}
}

View File

@ -0,0 +1,62 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Schematic Hero",
"description": "Landing hero with a large display title over a blueprint image and dual CTAs.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Main display headline (rendered in Space Grotesk at >= 64px).",
"x-editor": "text"
},
"kicker": {
"type": "string",
"title": "Kicker",
"description": "Short mono uppercase eyebrow text printed above the title.",
"x-editor": "text"
},
"image": {
"type": "string",
"title": "Background Image",
"description": "Blueprint / schematic image rendered behind the title.",
"x-editor": "media"
},
"primaryCta": {
"type": "object",
"title": "Primary CTA",
"description": "Primary call-to-action link.",
"x-editor": "link",
"properties": {
"text": {
"type": "string",
"title": "Text",
"x-editor": "text"
},
"href": {
"type": "string",
"title": "URL",
"x-editor": "text"
}
}
},
"secondaryCta": {
"type": "object",
"title": "Secondary CTA",
"description": "Optional secondary call-to-action link.",
"x-editor": "link",
"properties": {
"text": {
"type": "string",
"title": "Text",
"x-editor": "text"
},
"href": {
"type": "string",
"title": "URL",
"x-editor": "text"
}
}
}
}
}

View File

@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Status Bar",
"description": "Persistent top rail with optional UTC clock, build hash, and callsign.",
"type": "object",
"properties": {
"showClock": {
"type": "boolean",
"title": "Show Clock",
"description": "Show a live UTC clock element.",
"default": true,
"x-editor": "select"
},
"showBuild": {
"type": "boolean",
"title": "Show Build Hash",
"description": "Show the short build hash on the right rail.",
"default": true,
"x-editor": "select"
},
"callsign": {
"type": "string",
"title": "Callsign",
"description": "Short mono identifier for the running surface (e.g. SCF-CLN).",
"x-editor": "text"
}
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Tech Spec Table",
"description": "Specification table with right-aligned mono numerals and hairline rows.",
"type": "object",
"properties": {
"caption": {
"type": "string",
"title": "Caption",
"description": "Optional caption rendered above the table.",
"x-editor": "text"
},
"rows": {
"type": "array",
"title": "Rows",
"description": "Each row is a label / value / unit triple.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"description": "Left column descriptor (e.g. Thrust).",
"x-editor": "text"
},
"value": {
"type": "string",
"title": "Value",
"description": "Right-aligned mono numeral or short string.",
"x-editor": "text"
},
"unit": {
"type": "string",
"title": "Unit",
"description": "Trailing unit (e.g. kN, kg, ms).",
"x-editor": "text"
}
},
"required": ["label", "value"]
}
}
}
}

55
schematic_hero.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// SchematicHeroBlockMeta defines metadata for the landing hero.
var SchematicHeroBlockMeta = blocks.BlockMeta{
Key: "schematic_hero",
Title: "Schematic Hero",
Description: "Landing hero with a large display title over a blueprint image and dual CTAs.",
Source: "scifi-clean",
Category: blocks.CategoryLayout,
}
// SchematicCTA is a single call-to-action link.
type SchematicCTA struct {
Text string
Href string
}
// SchematicHeroData drives the templ render.
type SchematicHeroData struct {
Title string
Kicker string
Image string
PrimaryCTA SchematicCTA
SecondaryCTA SchematicCTA
}
// SchematicHeroBlock renders the landing hero.
// Content shape: {"title": "...", "kicker": "...", "image": "media:...",
// "primaryCta": {"text": "...", "href": "..."},
// "secondaryCta": {"text": "...", "href": "..."}}
func SchematicHeroBlock(ctx context.Context, content map[string]any) string {
data := SchematicHeroData{
Title: getString(content, "title"),
Kicker: getString(content, "kicker"),
Image: blocks.ResolveMediaPath(getString(content, "image")),
}
if m := getMap(content, "primaryCta"); m != nil {
data.PrimaryCTA = SchematicCTA{Text: getString(m, "text"), Href: getString(m, "href")}
}
if m := getMap(content, "secondaryCta"); m != nil {
data.SecondaryCTA = SchematicCTA{Text: getString(m, "text"), Href: getString(m, "href")}
}
var buf bytes.Buffer
_ = schematicHeroComponent(data).Render(ctx, &buf)
return buf.String()
}

55
schematic_hero.templ Normal file
View File

@ -0,0 +1,55 @@
package main
// schematicHeroComponent renders the landing-page hero.
// UAT §13.6 requires the <h1> to render in Space Grotesk at >= 64px on 1440x900.
// UAT §6 requires a scrim layer between the background image and the title text.
templ schematicHeroComponent(data SchematicHeroData) {
<section data-block="scifi-clean:schematic_hero" class="relative overflow-hidden bg-grid">
if data.Image != "" {
<div class="absolute inset-0 z-0">
<img
src={ data.Image }
alt=""
class="w-full h-full object-cover opacity-25"
/>
</div>
}
<div class="scrim relative z-10">
<div class="max-w-6xl mx-auto px-4 py-24 md:py-32">
if data.Kicker != "" {
<div class="scifi-mono uppercase tracking-widest text-xs text-accent mb-4">
{ data.Kicker }
</div>
}
<h1
class="scifi-h1 font-medium tracking-tight text-foreground"
style="font-family: var(--font-heading); font-size: clamp(3rem, 6vw, 5rem); line-height: 1.05;"
>
if data.Title == "" {
{ "Untitled mission" }
} else {
{ data.Title }
}
</h1>
<div class="mt-8 flex flex-wrap gap-3">
if data.PrimaryCTA.Text != "" && data.PrimaryCTA.Href != "" {
<a
href={ templ.SafeURL(data.PrimaryCTA.Href) }
class="scifi-mono uppercase tracking-widest text-xs px-5 py-3 bg-primary text-primary-foreground scifi-chevron scifi-focus"
>
{ data.PrimaryCTA.Text }
</a>
}
if data.SecondaryCTA.Text != "" && data.SecondaryCTA.Href != "" {
<a
href={ templ.SafeURL(data.SecondaryCTA.Href) }
class="scifi-mono uppercase tracking-widest text-xs px-5 py-3 hairline text-foreground scifi-chevron scifi-focus"
>
{ data.SecondaryCTA.Text }
</a>
}
</div>
</div>
</div>
</section>
}

182
schematic_hero_templ.go Normal file
View File

@ -0,0 +1,182 @@
// 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"
// schematicHeroComponent renders the landing-page hero.
// UAT §13.6 requires the <h1> to render in Space Grotesk at >= 64px on 1440x900.
// UAT §6 requires a scrim layer between the background image and the title text.
func schematicHeroComponent(data SchematicHeroData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"scifi-clean:schematic_hero\" class=\"relative overflow-hidden bg-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"absolute inset-0 z-0\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 11, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"\" class=\"w-full h-full object-cover opacity-25\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"scrim relative z-10\"><div class=\"max-w-6xl mx-auto px-4 py-24 md:py-32\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"scifi-mono uppercase tracking-widest text-xs text-accent mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 21, Col: 19}
}
_, 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, 6, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h1 class=\"scifi-h1 font-medium tracking-tight text-foreground\" style=\"font-family: var(--font-heading); font-size: clamp(3rem, 6vw, 5rem); line-height: 1.05;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title == "" {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("Untitled mission")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 29, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 31, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h1><div class=\"mt-8 flex flex-wrap gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.PrimaryCTA.Text != "" && data.PrimaryCTA.Href != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 templ.SafeURL
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.PrimaryCTA.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 37, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"scifi-mono uppercase tracking-widest text-xs px-5 py-3 bg-primary text-primary-foreground scifi-chevron scifi-focus\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.PrimaryCTA.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 40, Col: 29}
}
_, 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, 11, "</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.SecondaryCTA.Text != "" && data.SecondaryCTA.Href != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.SecondaryCTA.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 45, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"scifi-mono uppercase tracking-widest text-xs px-5 py-3 hairline text-foreground scifi-chevron scifi-focus\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.SecondaryCTA.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schematic_hero.templ`, Line: 48, Col: 31}
}
_, 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, 14, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

38
status_bar.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// StatusBarBlockMeta defines metadata for the persistent status-bar rail.
var StatusBarBlockMeta = blocks.BlockMeta{
Key: "status_bar",
Title: "Status Bar",
Description: "Persistent top rail with optional UTC clock, build hash, and callsign.",
Source: "scifi-clean",
Category: blocks.CategoryNavigation,
}
// StatusBarData drives the templ render.
type StatusBarData struct {
ShowClock bool
ShowBuild bool
Callsign string
}
// StatusBarBlock renders the persistent top rail.
// Content shape: {"showClock": true, "showBuild": true, "callsign": "SCF-CLN"}
func StatusBarBlock(ctx context.Context, content map[string]any) string {
data := StatusBarData{
ShowClock: getBool(content, "showClock", true),
ShowBuild: getBool(content, "showBuild", true),
Callsign: getStringDefault(content, "callsign", "SCF-CLN"),
}
var buf bytes.Buffer
_ = statusBarComponent(data).Render(ctx, &buf)
return buf.String()
}

46
status_bar.templ Normal file
View File

@ -0,0 +1,46 @@
package main
// statusBarComponent renders the sticky top status rail.
// UAT §13.9 requires position: sticky and top: 0.
templ statusBarComponent(data StatusBarData) {
<div
data-block="scifi-clean:status_bar"
class="w-full hairline-b bg-card text-card-foreground scifi-mono text-xs uppercase tracking-widest"
style="position: sticky; top: 0; z-index: 30;"
>
<div class="max-w-6xl mx-auto px-4 py-2 flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="text-muted-foreground">{ data.Callsign }</span>
<span class="text-muted-foreground">|</span>
<span class="text-foreground">PUBLIC</span>
</div>
<div class="flex items-center gap-3 tabular-nums">
if data.ShowClock {
<span
data-status-bar-clock="utc"
class="text-muted-foreground tabular-nums"
>
{ "00:00:00 UTC" }
</span>
}
if data.ShowBuild {
<span class="text-muted-foreground">|</span>
<span class="text-foreground tabular-nums">BLD a1b2c3</span>
}
</div>
</div>
<script>
(function(){
var el = document.querySelector('[data-status-bar-clock="utc"]');
if (!el) return;
function fmt(n){ return n < 10 ? '0' + n : String(n); }
function tick(){
var d = new Date();
el.textContent = fmt(d.getUTCHours()) + ':' + fmt(d.getUTCMinutes()) + ':' + fmt(d.getUTCSeconds()) + ' UTC';
}
tick();
setInterval(tick, 1000);
})();
</script>
</div>
}

84
status_bar_templ.go Normal file
View File

@ -0,0 +1,84 @@
// 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"
// statusBarComponent renders the sticky top status rail.
// UAT §13.9 requires position: sticky and top: 0.
func statusBarComponent(data StatusBarData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"scifi-clean:status_bar\" class=\"w-full hairline-b bg-card text-card-foreground scifi-mono text-xs uppercase tracking-widest\" style=\"position: sticky; top: 0; z-index: 30;\"><div class=\"max-w-6xl mx-auto px-4 py-2 flex items-center justify-between gap-4\"><div class=\"flex items-center gap-3\"><span class=\"text-muted-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Callsign)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `status_bar.templ`, Line: 13, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <span class=\"text-muted-foreground\">|</span> <span class=\"text-foreground\">PUBLIC</span></div><div class=\"flex items-center gap-3 tabular-nums\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowClock {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span data-status-bar-clock=\"utc\" class=\"text-muted-foreground tabular-nums\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("00:00:00 UTC")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `status_bar.templ`, Line: 23, Col: 22}
}
_, 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> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowBuild {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span class=\"text-muted-foreground\">|</span> <span class=\"text-foreground tabular-nums\">BLD a1b2c3</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div><script>\n\t\t(function(){\n\t\t\tvar el = document.querySelector('[data-status-bar-clock=\"utc\"]');\n\t\t\tif (!el) return;\n\t\t\tfunction fmt(n){ return n < 10 ? '0' + n : String(n); }\n\t\t\tfunction tick(){\n\t\t\t\tvar d = new Date();\n\t\t\t\tel.textContent = fmt(d.getUTCHours()) + ':' + fmt(d.getUTCMinutes()) + ':' + fmt(d.getUTCSeconds()) + ' UTC';\n\t\t\t}\n\t\t\ttick();\n\t\t\tsetInterval(tick, 1000);\n\t\t})();\n\t\t</script></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

50
tech_spec.go Normal file
View File

@ -0,0 +1,50 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// TechSpecBlockMeta defines metadata for the tech-spec table block.
var TechSpecBlockMeta = blocks.BlockMeta{
Key: "tech_spec",
Title: "Tech Spec Table",
Description: "Specification table with right-aligned mono numerals and hairline rows.",
Source: "scifi-clean",
Category: blocks.CategoryContent,
}
// TechSpecRow is one label/value/unit triple.
type TechSpecRow struct {
Label string
Value string
Unit string
}
// TechSpecData drives the templ render.
type TechSpecData struct {
Caption string
Rows []TechSpecRow
}
// TechSpecBlock renders the tech-spec block.
// Content shape: {"caption": "...", "rows": [{"label": "...", "value": "...", "unit": "..."}]}
func TechSpecBlock(ctx context.Context, content map[string]any) string {
data := TechSpecData{
Caption: getString(content, "caption"),
}
for _, raw := range getSlice(content, "rows") {
data.Rows = append(data.Rows, TechSpecRow{
Label: getString(raw, "label"),
Value: getString(raw, "value"),
Unit: getString(raw, "unit"),
})
}
var buf bytes.Buffer
_ = techSpecComponent(data).Render(ctx, &buf)
return buf.String()
}

53
tech_spec.templ Normal file
View File

@ -0,0 +1,53 @@
package main
// techSpecComponent renders the tech-spec table with right-aligned mono numerals
// and hairline row rules.
templ techSpecComponent(data TechSpecData) {
<section data-block="scifi-clean:tech_spec" class="py-8">
<div class="max-w-3xl mx-auto px-4">
if data.Caption != "" {
<p class="scifi-mono text-xs uppercase tracking-widest text-muted-foreground mb-3">
{ data.Caption }
</p>
}
<table class="w-full text-sm hairline">
<tbody>
if len(data.Rows) == 0 {
<tr class="hairline-b">
<td class="scifi-body py-3 px-4 text-foreground">No spec rows yet.</td>
<td class="scifi-mono py-3 px-4 text-right tabular-nums text-muted-foreground">--</td>
<td class="scifi-mono py-3 pr-4 text-right text-muted-foreground"></td>
</tr>
} else {
for i, row := range data.Rows {
@techSpecRow(row, i == len(data.Rows)-1)
}
}
</tbody>
</table>
</div>
</section>
}
// techSpecRow renders a single row. Last-row check controls whether to drop
// the bottom hairline.
templ techSpecRow(row TechSpecRow, last bool) {
<tr class={ rowBorderClass(last) }>
<td class="scifi-body py-3 px-4 text-muted-foreground uppercase text-xs tracking-wider">
{ row.Label }
</td>
<td class="scifi-mono py-3 px-4 text-right tabular-nums text-foreground font-medium" style="font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right;">
{ row.Value }
</td>
<td class="scifi-mono py-3 pr-4 text-right text-muted-foreground text-xs" style="font-family: var(--font-mono); text-align: right;">
{ row.Unit }
</td>
</tr>
}
func rowBorderClass(last bool) string {
if last {
return ""
}
return "hairline-b"
}

177
tech_spec_templ.go Normal file
View File

@ -0,0 +1,177 @@
// 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"
// techSpecComponent renders the tech-spec table with right-aligned mono numerals
// and hairline row rules.
func techSpecComponent(data TechSpecData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"scifi-clean:tech_spec\" class=\"py-8\"><div class=\"max-w-3xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"scifi-mono text-xs uppercase tracking-widest text-muted-foreground mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tech_spec.templ`, Line: 10, Col: 19}
}
_, 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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<table class=\"w-full text-sm hairline\"><tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Rows) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr class=\"hairline-b\"><td class=\"scifi-body py-3 px-4 text-foreground\">No spec rows yet.</td><td class=\"scifi-mono py-3 px-4 text-right tabular-nums text-muted-foreground\">--</td><td class=\"scifi-mono py-3 pr-4 text-right text-muted-foreground\"></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
for i, row := range data.Rows {
templ_7745c5c3_Err = techSpecRow(row, i == len(data.Rows)-1).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</tbody></table></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// techSpecRow renders a single row. Last-row check controls whether to drop
// the bottom hairline.
func techSpecRow(row TechSpecRow, last bool) 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)
var templ_7745c5c3_Var4 = []any{rowBorderClass(last)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tech_spec.templ`, Line: 1, Col: 0}
}
_, 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, 8, "\"><td class=\"scifi-body py-3 px-4 text-muted-foreground uppercase text-xs tracking-wider\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(row.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tech_spec.templ`, Line: 37, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td class=\"scifi-mono py-3 px-4 text-right tabular-nums text-foreground font-medium\" style=\"font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(row.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tech_spec.templ`, Line: 40, Col: 14}
}
_, 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, 10, "</td><td class=\"scifi-mono py-3 pr-4 text-right text-muted-foreground text-xs\" style=\"font-family: var(--font-mono); text-align: right;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(row.Unit)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tech_spec.templ`, Line: 43, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func rowBorderClass(last bool) string {
if last {
return ""
}
return "hairline-b"
}
var _ = templruntime.GeneratedTemplate

281
template.templ Normal file
View File

@ -0,0 +1,281 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData holds the values lifted out of the render `doc` map for the
// four Sci-Fi Clean page templates.
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
}
// parseScifiPageData lifts the well-known keys out of the inbound doc map.
// Unknown keys (e.g. block content) are ignored by the page renderers.
func parseScifiPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// htmlClassForMode returns the root <html> class for the current theme mode.
func htmlClassForMode(mode string) string {
if mode == "dark" {
return "dark"
}
return ""
}
// ===== Default template (header / main / footer) =====
templ ScifiClean(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow max-w-5xl mx-auto w-full px-4 py-8">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground">
No content blocks assigned to this page.
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ===== Landing template (hero / specs / main / cta / footer) =====
templ ScifiCleanLanding(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
@templ.Raw(data.Slots["hero"])
</section>
if specs, ok := data.Slots["specs"]; ok && specs != "" {
<section class="w-full hairline-b py-12">
<div class="max-w-6xl mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
@templ.Raw(specs)
</div>
</div>
</section>
}
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
<div class="max-w-6xl mx-auto px-4 py-16">
@templ.Raw(main)
</div>
}
</main>
if cta, ok := data.Slots["cta"]; ok && cta != "" {
<section class="w-full hairline-t py-16">
<div class="max-w-6xl mx-auto px-4">
@templ.Raw(cta)
</div>
</section>
}
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ===== Article template (header / rail / main / footer) =====
templ ScifiCleanArticle(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full hairline-b">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
<div class="max-w-5xl mx-auto px-4 py-12 grid grid-cols-12 gap-8">
if rail, ok := data.Slots["rail"]; ok && rail != "" {
<aside class="col-span-12 md:col-span-3 scifi-mono uppercase text-xs tracking-widest text-muted-foreground space-y-3">
@templ.Raw(rail)
</aside>
}
<article class="col-span-12 md:col-span-9 scifi-body max-w-2xl">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p class="text-muted-foreground">No article body.</p>
}
</article>
</div>
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ===== Full-width template (header / main / footer) =====
templ ScifiCleanFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en" class={ htmlClassForMode(data.ThemeMode) }>
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full bg-grid">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="max-w-5xl mx-auto py-20 px-4 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground">
No content blocks assigned to this page.
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// ===== Render entry points (adapted by wrap() in register.go) =====
func RenderScifiClean(ctx context.Context, doc map[string]any) templ.Component {
return ScifiClean(parseScifiPageData(doc))
}
func RenderScifiCleanLanding(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanLanding(parseScifiPageData(doc))
}
func RenderScifiCleanArticle(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanArticle(parseScifiPageData(doc))
}
func RenderScifiCleanFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanFullWidth(parseScifiPageData(doc))
}

643
template_templ.go Normal file
View File

@ -0,0 +1,643 @@
// 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 holds the values lifted out of the render `doc` map for the
// four Sci-Fi Clean page templates.
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
}
// parseScifiPageData lifts the well-known keys out of the inbound doc map.
// Unknown keys (e.g. block content) are ignored by the page renderers.
func parseScifiPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// htmlClassForMode returns the root <html> class for the current theme mode.
func htmlClassForMode(mode string) string {
if mode == "dark" {
return "dark"
}
return ""
}
// ===== Default template (header / main / footer) =====
func ScifiClean(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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 = []any{htmlClassForMode(data.ThemeMode)}
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, 2, "<html lang=\"en\" 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: `template.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, 3, "\">")
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{},
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, 4, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
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, 5, "<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, 6, "</header><main class=\"flex-grow max-w-5xl mx-auto w-full px-4 py-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"py-20 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</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, 9, "</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, 10, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Landing template (hero / specs / main / cta / footer) =====
func ScifiCleanLanding(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, 11, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 = []any{htmlClassForMode(data.ThemeMode)}
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, 12, "<html lang=\"en\" 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: `template.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, 13, "\">")
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{},
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, 14, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
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, 15, "<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, 16, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if specs, ok := data.Slots["specs"]; ok && specs != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<section class=\"w-full hairline-b py-12\"><div class=\"max-w-6xl mx-auto px-4\"><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(specs).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<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 = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<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, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cta, ok := data.Slots["cta"]; ok && cta != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<section class=\"w-full hairline-t py-16\"><div class=\"max-w-6xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(cta).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Article template (header / rail / main / footer) =====
func ScifiCleanArticle(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_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 = []any{htmlClassForMode(data.ThemeMode)}
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, 29, "<html lang=\"en\" 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: `template.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, 30, "\">")
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{},
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, 31, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
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, 32, "<header class=\"w-full hairline-b\">")
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, 33, "</header><main class=\"flex-grow w-full\"><div class=\"max-w-5xl mx-auto px-4 py-12 grid grid-cols-12 gap-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if rail, ok := data.Slots["rail"]; ok && rail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<aside class=\"col-span-12 md:col-span-3 scifi-mono uppercase text-xs tracking-widest text-muted-foreground space-y-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(rail).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<article class=\"col-span-12 md:col-span-9 scifi-body max-w-2xl\">")
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, 37, "<p class=\"text-muted-foreground\">No article body.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</article></div></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, 39, "</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, 40, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Full-width template (header / main / footer) =====
func ScifiCleanFullWidth(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_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 = []any{htmlClassForMode(data.ThemeMode)}
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, 42, "<html lang=\"en\" 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: `template.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, 43, "\">")
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{},
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, 44, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
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, 45, "<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, 46, "</header><main class=\"flex-grow w-full bg-grid\">")
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, 47, "<div class=\"max-w-5xl mx-auto py-20 px-4 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</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, 49, "</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, 50, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Render entry points (adapted by wrap() in register.go) =====
func RenderScifiClean(ctx context.Context, doc map[string]any) templ.Component {
return ScifiClean(parseScifiPageData(doc))
}
func RenderScifiCleanLanding(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanLanding(parseScifiPageData(doc))
}
func RenderScifiCleanArticle(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanArticle(parseScifiPageData(doc))
}
func RenderScifiCleanFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanFullWidth(parseScifiPageData(doc))
}
var _ = templruntime.GeneratedTemplate

17
text_override.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"bytes"
"context"
)
// ScifiTextBlock renders body text with Inter via the --font-body variable.
// Inline numerals retain a tabular-nums treatment via the scifi-text class.
func ScifiTextBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
class := getString(content, "class")
var buf bytes.Buffer
_ = scifiTextComponent(text, class).Render(ctx, &buf)
return buf.String()
}

8
text_override.templ Normal file
View File

@ -0,0 +1,8 @@
package main
// scifiTextComponent renders Inter body copy with the tabular-nums numeral treatment.
templ scifiTextComponent(text, class string) {
<div class={ "scifi-body prose max-w-none text-foreground tabular-nums", class } style="font-family: var(--font-body);">
@templ.Raw(text)
</div>
}

67
text_override_templ.go Normal file
View File

@ -0,0 +1,67 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// scifiTextComponent renders Inter body copy with the tabular-nums numeral treatment.
func scifiTextComponent(text, class string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"scifi-body prose max-w-none text-foreground tabular-nums", class}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"font-family: var(--font-body);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate