commit 96b87b3e8132eb8f4bd203851ecfe7f7a53ca9f6 Author: Alex Dunmow Date: Sat Jun 6 14:11:43 2026 +0800 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f780e6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.so +*.test +tmp/ +.idea/ +.vscode/ diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..d23c6bc --- /dev/null +++ b/BUILD_REPORT.md @@ -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:`. +- 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15019c0 --- /dev/null +++ b/Makefile @@ -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" diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md new file mode 100644 index 0000000..007221c --- /dev/null +++ b/RECOMMENDED_FONTS.md @@ -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. diff --git a/assets/README.txt b/assets/README.txt new file mode 100644 index 0000000..d005690 --- /dev/null +++ b/assets/README.txt @@ -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. diff --git a/button_override.go b/button_override.go new file mode 100644 index 0000000..f1f4f0b --- /dev/null +++ b/button_override.go @@ -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() +} diff --git a/button_override.templ b/button_override.templ new file mode 100644 index 0000000..72c9024 --- /dev/null +++ b/button_override.templ @@ -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) { + + { text } + + +} + +// 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" + } +} diff --git a/button_override_templ.go b/button_override_templ.go new file mode 100644 index 0000000..79695fe --- /dev/null +++ b/button_override_templ.go @@ -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, "") + 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, " ") + 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, "") + 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 diff --git a/css.go b/css.go new file mode 100644 index 0000000..25daed3 --- /dev/null +++ b/css.go @@ -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); +} +` diff --git a/diagram_caption.go b/diagram_caption.go new file mode 100644 index 0000000..12b5fe3 --- /dev/null +++ b/diagram_caption.go @@ -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) + "%;" +} diff --git a/diagram_caption.templ b/diagram_caption.templ new file mode 100644 index 0000000..32fb7f4 --- /dev/null +++ b/diagram_caption.templ @@ -0,0 +1,49 @@ +package main + +// diagramCaptionComponent renders the figure with overlay pins and a Fig. heading. +templ diagramCaptionComponent(data DiagramCaptionData) { +
+
+ if data.Image == "" { +
+ + No diagram yet + +
+ } else { + { + for _, pin := range data.CalloutPins { + + { pin.Label } + + } + } +
+
+

+ { "Fig. " } + if data.FigureNumber != "" { + { data.FigureNumber } + } else { + { "00" } + } + { "." } + if data.Title != "" { + { data.Title } + } +

+ if data.Body != "" { +
+ @templ.Raw(data.Body) +
+ } +
+
+} diff --git a/diagram_caption_templ.go b/diagram_caption_templ.go new file mode 100644 index 0000000..65d9daa --- /dev/null +++ b/diagram_caption_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Image == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
No diagram yet
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"") ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, pin := range data.CalloutPins { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Body != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/email_wrapper.go b/email_wrapper.go new file mode 100644 index 0000000..9481082 --- /dev/null +++ b/email_wrapper.go @@ -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() +} diff --git a/email_wrapper.templ b/email_wrapper.templ new file mode 100644 index 0000000..ec3b132 --- /dev/null +++ b/email_wrapper.templ @@ -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) { + + + + + + + { emailCtx.SiteSettings.SiteName } + + + + if emailCtx.PreviewText != "" { +
{ emailCtx.PreviewText }
+ } + + + + +
+ + + + + + + + + + + +
+ + +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..07a75e1 --- /dev/null +++ b/email_wrapper_templ.go @@ -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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.PreviewText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_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, "") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.UnsubscribeURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + 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, "Unsubscribe
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..59d2496 --- /dev/null +++ b/embed.go @@ -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, + } +} diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/fonts.json @@ -0,0 +1 @@ +[] diff --git a/footer.go b/footer.go new file mode 100644 index 0000000..0ebb25b --- /dev/null +++ b/footer.go @@ -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 +} diff --git a/footer.templ b/footer.templ new file mode 100644 index 0000000..7b5ae61 --- /dev/null +++ b/footer.templ @@ -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) { +
+
+
+
+
+
+ { data.Callsign } +
+

+ Telemetry, mission briefs, and engineering notes from this surface. +

+
+ + if len(data.Links) > 0 { + + } + + if data.ShowSignup { +
+ +
+ + +
+
+ } +
+
+ Project Aurora + v0.1.0 +
+
+
+
+} diff --git a/footer_templ.go b/footer_templ.go new file mode 100644 index 0000000..058ae69 --- /dev/null +++ b/footer_templ.go @@ -0,0 +1,108 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// 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, "
") + 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, "

Telemetry, mission briefs, and engineering notes from this surface.

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

{ text }

+ case 2: +

{ text }

+ case 3: +

{ text }

+ case 4: +

{ text }

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

{ text }

+ } +
+} diff --git a/heading_override_templ.go b/heading_override_templ.go new file mode 100644 index 0000000..28754c9 --- /dev/null +++ b/heading_override_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if kicker != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(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, "
") + 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, "

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

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

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

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

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

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

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

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

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..d3a77f9 --- /dev/null +++ b/helpers.go @@ -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" +} diff --git a/mission_stat.go b/mission_stat.go new file mode 100644 index 0000000..6bd6511 --- /dev/null +++ b/mission_stat.go @@ -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 "→" // → + } +} diff --git a/mission_stat.templ b/mission_stat.templ new file mode 100644 index 0000000..8f6b474 --- /dev/null +++ b/mission_stat.templ @@ -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) { +
+ if data.Metric != "" { +
+ { data.Metric } +
+ } +
+ + if data.Value == "" { + { "—" } + } else { + { data.Value } + } + + if data.Unit != "" { + + { data.Unit } + + } +
+ if data.Delta != "" { +
+ { trendGlyph(data.Trend) } { data.Delta } +
+ } +
+} diff --git a/mission_stat_templ.go b/mission_stat_templ.go new file mode 100644 index 0000000..8d6a219 --- /dev/null +++ b/mission_stat_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Metric != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Unit != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Delta != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + 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, "
") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..f30559b --- /dev/null +++ b/plugin.mod @@ -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" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..4cbcf09 --- /dev/null +++ b/presets.json @@ -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%" + } + } + } +] diff --git a/register.go b/register.go new file mode 100644 index 0000000..9930212 --- /dev/null +++ b/register.go @@ -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} +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..79e05e7 --- /dev/null +++ b/registration.go @@ -0,0 +1,25 @@ +package main + +import ( + "io/fs" + "net/http" + + "git.dev.alexdunmow.com/block/core/blocks" + "git.dev.alexdunmow.com/block/core/plugin" + "git.dev.alexdunmow.com/block/core/templates" +) + +// Registration is the compile-time plugin registration for the 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() }, +} diff --git a/schemas/diagram_caption.schema.json b/schemas/diagram_caption.schema.json new file mode 100644 index 0000000..d8864d9 --- /dev/null +++ b/schemas/diagram_caption.schema.json @@ -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"] + } + } + } +} diff --git a/schemas/footer.schema.json b/schemas/footer.schema.json new file mode 100644 index 0000000..6855cc7 --- /dev/null +++ b/schemas/footer.schema.json @@ -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"] + } + } + } +} diff --git a/schemas/mission_stat.schema.json b/schemas/mission_stat.schema.json new file mode 100644 index 0000000..bc1a560 --- /dev/null +++ b/schemas/mission_stat.schema.json @@ -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" + } + } +} diff --git a/schemas/schematic_hero.schema.json b/schemas/schematic_hero.schema.json new file mode 100644 index 0000000..ba8e0d2 --- /dev/null +++ b/schemas/schematic_hero.schema.json @@ -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" + } + } + } + } +} diff --git a/schemas/status_bar.schema.json b/schemas/status_bar.schema.json new file mode 100644 index 0000000..2e7de3c --- /dev/null +++ b/schemas/status_bar.schema.json @@ -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" + } + } +} diff --git a/schemas/tech_spec.schema.json b/schemas/tech_spec.schema.json new file mode 100644 index 0000000..6dbbf82 --- /dev/null +++ b/schemas/tech_spec.schema.json @@ -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"] + } + } + } +} diff --git a/schematic_hero.go b/schematic_hero.go new file mode 100644 index 0000000..3aee57b --- /dev/null +++ b/schematic_hero.go @@ -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() +} diff --git a/schematic_hero.templ b/schematic_hero.templ new file mode 100644 index 0000000..8d79c82 --- /dev/null +++ b/schematic_hero.templ @@ -0,0 +1,55 @@ +package main + +// schematicHeroComponent renders the landing-page hero. +// UAT §13.6 requires the

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) { +
+ if data.Image != "" { +
+ +
+ } +
+
+ if data.Kicker != "" { +
+ { data.Kicker } +
+ } +

+ if data.Title == "" { + { "Untitled mission" } + } else { + { data.Title } + } +

+
+ if data.PrimaryCTA.Text != "" && data.PrimaryCTA.Href != "" { + + { data.PrimaryCTA.Text } + + } + if data.SecondaryCTA.Text != "" && data.SecondaryCTA.Href != "" { + + { data.SecondaryCTA.Text } + + } +
+
+
+
+} diff --git a/schematic_hero_templ.go b/schematic_hero_templ.go new file mode 100644 index 0000000..b7d3f82 --- /dev/null +++ b/schematic_hero_templ.go @@ -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

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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Image != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
\"\"
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Kicker != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.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, "

") + 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, "") + 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, " ") + 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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/status_bar.go b/status_bar.go new file mode 100644 index 0000000..7999bbd --- /dev/null +++ b/status_bar.go @@ -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() +} diff --git a/status_bar.templ b/status_bar.templ new file mode 100644 index 0000000..b634723 --- /dev/null +++ b/status_bar.templ @@ -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) { +
+
+
+ { data.Callsign } + | + PUBLIC +
+
+ if data.ShowClock { + + { "00:00:00 UTC" } + + } + if data.ShowBuild { + | + BLD a1b2c3 + } +
+
+ +
+} diff --git a/status_bar_templ.go b/status_bar_templ.go new file mode 100644 index 0000000..be9b523 --- /dev/null +++ b/status_bar_templ.go @@ -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, "
") + 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, " | PUBLIC
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ShowClock { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + 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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ShowBuild { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "| BLD a1b2c3") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/tech_spec.go b/tech_spec.go new file mode 100644 index 0000000..fce5383 --- /dev/null +++ b/tech_spec.go @@ -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() +} diff --git a/tech_spec.templ b/tech_spec.templ new file mode 100644 index 0000000..4af3ff5 --- /dev/null +++ b/tech_spec.templ @@ -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) { +
+
+ if data.Caption != "" { +

+ { data.Caption } +

+ } + + + if len(data.Rows) == 0 { + + + + + + } else { + for i, row := range data.Rows { + @techSpecRow(row, i == len(data.Rows)-1) + } + } + +
No spec rows yet.--
+
+
+} + +// techSpecRow renders a single row. Last-row check controls whether to drop +// the bottom hairline. +templ techSpecRow(row TechSpecRow, last bool) { + + + { row.Label } + + + { row.Value } + + + { row.Unit } + + +} + +func rowBorderClass(last bool) string { + if last { + return "" + } + return "hairline-b" +} diff --git a/tech_spec_templ.go b/tech_spec_templ.go new file mode 100644 index 0000000..fa42d59 --- /dev/null +++ b/tech_spec_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Rows) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + 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, "
No spec rows yet.--
") + 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, "") + 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, "") + 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, "") + 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, "") + 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 diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..b10c0a6 --- /dev/null +++ b/template.templ @@ -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 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) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+ No content blocks assigned to this page. +
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ===== Landing template (hero / specs / main / cta / footer) ===== + +templ ScifiCleanLanding(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["hero"]) +
+ if specs, ok := data.Slots["specs"]; ok && specs != "" { +
+
+
+ @templ.Raw(specs) +
+
+
+ } +
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } +
+ if cta, ok := data.Slots["cta"]; ok && cta != "" { +
+
+ @templ.Raw(cta) +
+
+ } +
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ===== Article template (header / rail / main / footer) ===== + +templ ScifiCleanArticle(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+
+ if rail, ok := data.Slots["rail"]; ok && rail != "" { + + } +
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +

No article body.

+ } +
+
+
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ===== Full-width template (header / main / footer) ===== + +templ ScifiCleanFullWidth(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+ No content blocks assigned to this page. +
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// ===== 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)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..bd1da52 --- /dev/null +++ b/template_templ.go @@ -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 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = 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, "
") + 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, "") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if main, ok := data.Slots["main"]; ok && main != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if cta, ok := data.Slots["cta"]; ok && cta != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// ===== 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "

No article body.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + 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, "
") + 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, "") + 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 diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..bff3dc7 --- /dev/null +++ b/text_override.go @@ -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() +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..2d0e799 --- /dev/null +++ b/text_override.templ @@ -0,0 +1,8 @@ +package main + +// scifiTextComponent renders Inter body copy with the tabular-nums numeral treatment. +templ scifiTextComponent(text, class string) { +
+ @templ.Raw(text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..5a95c1f --- /dev/null +++ b/text_override_templ.go @@ -0,0 +1,67 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate