Compare commits

..

No commits in common. "main" and "v0.1.1" have entirely different histories.
main ... v0.1.1

136 changed files with 4674 additions and 2206 deletions

1
.gitignore vendored
View File

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

View File

@ -1,52 +1,182 @@
# Sci-Fi Clean — Build Report (Wave A, WO-TF-024)
Codeless `.bnp` theme. Persona: AI / robotics product. Blueprint schematics,
HUD guide lines, mono readouts, spec + benchmark furniture. Light = white-room
lab; dark = mission control. Motion tier MID (JS scroll-reveal + ticking
counters + CSS line-draw, no canvas, prefers-reduced-motion honored, static
no-JS fallback).
# Sci-Fi Clean — Build Report (v0.1.0, wave-1 pass)
## What landed
- `manifest.yaml`: 7 page templates (default, full-width, landing, article,
blog-index, contact, auth), 49 mandatory `template_overrides`, bundled fonts,
presets, master pages, `required_icon_packs` (lucide, simple-icons), and the
`sc-` component CSS system (blueprint/HUD identity layer over a shared base).
- `templates/overrides/scifi-clean/`: all 49 mandatory builtin overrides,
re-skinned from the frozen Wave-0 builtin defaults with every content field,
provider field, custom tag, and JS block preserved. Legacy button/card field
reads reconciled (§4): button reads `label`/`link`/`style`; card reads
`text`/`media`.
- `templates/scifi-clean/`: 7 page-template shells with the MID-tier motion
script and a line-draw schematic on the hero.
- `blocks/`: 4 persona blocks — `tech_spec`, `benchmark_table`,
`telemetry_grid`, `changelog_console`. The 5 builtin-duplicating theme blocks
(footer, hero, stats, status bar, image caption) were deleted and are now
builtin overrides.
- `presets.json`: 4 dual-mode presets (Flightline, Cleanroom, Signal Cyan,
Graphite Lab), all 19 tokens, light AND dark tuned.
- `fonts.json` + `assets/fonts/`: Space Grotesk (heading), Inter (body),
JetBrains Mono (mono), all SIL OFL 1.1 — see `assets/fonts/LICENSES.md`.
- `templates/email/scifi-clean.ninjatpl`: email-safe table wrapper retuned to
the scifi palette.
- `master_pages.json` + `seed/seed.json` + `seed/workflows.json`: a seeded
"Halcyon Robotics" demo site (home, about, blog with 3 posts, contact with a
data-table + row_inserted email-admins workflow, login).
### 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()`.
## Verification
### Registration
- One `RegisterSystemTemplate({Key: "scifi-clean", ...})`.
- Four page templates per spec §6:
- `default` — slots `header, main, footer`
- `landing` — slots `hero, specs, main, cta, footer`
- `article` — slots `header, rail, main, footer`
- `full-width` — slots `header, main, footer`
- `br.LoadSchemasFromFS(Schemas())` runs **before** any `br.Register(...)`. UAT §3.6.
- Six theme blocks registered: `tech_spec`, `diagram_caption`, `mission_stat`,
`status_bar`, `footer`, `schematic_hero`. Each has `Source = "scifi-clean"` and an
unqualified `Key`. Addressed at runtime as `scifi-clean:<key>`.
- Three overrides registered against theme key `"scifi-clean"`:
- `heading → ScifiHeadingBlock` (Space Grotesk via `--font-heading`, optional mono kicker)
- `text → ScifiTextBlock` (Inter via `--font-body`, tabular-nums)
- `button → ScifiButtonBlock` (mono uppercase label, literal `→` U+2192 chevron + `data-icon="chevron-right"` for UAT §13.14)
- `tr.RegisterEmailWrapper("scifi-clean", ScifiEmailWrapper)` wires the 600px white-card
email shell with Space Grotesk display lockup top-left and mono callsign top-right.
- `make build` -> exit 0 (codeless `.bnp`, 4 blocks, no wasm).
- `check-safety` -> exit 0 (32 checks: 19 ok, 13 skip).
### Master pages
`DefaultMasterPages()` returns two entries:
## Known platform limits (per OVERRIDE-CONTRACT §11/§12)
- `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.
- Auth trio (`auth-form`, `auth-status`, `password-reset-form`) and
`page-suggestions` are compiled builtins with no override dispatch — the
themed files are correct-to-model and inert until the platform fix lands.
- Provider-backed overrides (navbar/footer menus, featured/related/popular
posts, category-list, breadcrumbs, blog-hero, sidebar-nav, author-bio-hero)
render themed chrome but empty live lists under the codeless loader until the
loader registers overrides via the source path. Authored faithfully to the
builtin provider var names.
### 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.
Visual quality is UNVERIFIED until Wave B screenshots.
### 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.

View File

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

View File

@ -1,24 +0,0 @@
# Sci-Fi Clean
A minimalist futurist theme in off-white with technical blue and a signal-orange accent. Thin blueprint lines, mono numerals, and spec furniture give it the calm of a mission-control readout. It reads precise and technical, and it stays quiet where a product deserves to speak for itself.
## Good for
We built Sci-Fi Clean for hardware and research teams. Treat these as starting points, not limits. If the mood fits your product, use it:
- AI and robotics product sites
- Aerospace and hardware companies
- R&D labs and biotech
- Developer platforms and technical documentation
## What you get
- Four color presets, each with a tuned light mode and a tuned dark mode: Flightline, Cleanroom, Signal Cyan, and Graphite Lab.
- Space Grotesk headings, Inter body, and JetBrains Mono for readouts, bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a technical spec sheet, a benchmark comparison table, a live telemetry grid, and a changelog console.
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.

View File

@ -1,32 +1,33 @@
# Recommended fonts for Sci-Fi Clean
This theme **bundles** its type. `fonts.json` ships latin-subset woff2 files in
`assets/fonts/` (all SIL OFL 1.1 — see `assets/fonts/LICENSES.md`), so the
intended aesthetic renders out of the box with no admin setup.
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.
| Slot | Bundled family | Weights | Role |
|---------|---------------------|-----------|---------------------------------------------------|
| Heading | `Space Grotesk` | 500, 700 | Geometric display face for headlines and brand. |
| Body | `Inter` | 400, 600 | Workhorse UI sans for prose and controls. |
| Mono | `JetBrains Mono` | 400, 500 | Every numeral, identifier, readout, and spec row. |
Open **Admin → Settings → Typography**, switch to the **Google Fonts** tab, and assign:
The bundled families appear as `source=template` rows in **Admin → Settings →
Typography**. Fonts stay admin-controlled: an admin can override any slot from
the Google Fonts tab or an upload, and the theme follows because every
`font-family` goes through `var(--font-heading|body|mono, <fallback>)`.
| 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 (or unless) the admin reassigns a slot, templates fall through to these
CSS-variable fallbacks (declared on `:root` in the theme CSS manifest):
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`
- `--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`
## Good alternates
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.
If an admin prefers to swap the mono readout face, `Space Mono` (OFL) and
`IBM Plex Mono` (OFL) both hold tabular numerals well. For headings, `Sora`
(OFL) is a close geometric cousin of Space Grotesk. All are in the curated
Google Fonts list; no upload required.
## 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.

View File

@ -1,26 +0,0 @@
# Recommended icons for Sci-Fi Clean
Icons render only via the sprite system:
`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`. Block content stores refs
as `"pack:name"` strings (e.g. `lucide:mail`).
## Required packs
Declared in `plugin.mod` `required_icon_packs` and `manifest.yaml`
`required_icon_packs`. Until the loader auto-installs, install these manually
before activation:
| Pack | Slug | Why |
|----------------|----------------|------------------------------------------------------------|
| Lucide | `lucide` | Default UI icon set: nav toggle, arrows, mail, phone, home. |
| Simple Icons | `simple-icons` | Brand marks in social links and the author bio hero (X, LinkedIn, GitHub). |
Both are among the six packs that ship bundled with the CMS, so no download is
usually needed. If a pack is missing on a fresh instance, open **Admin ->
Icons**, install Lucide and Simple Icons, and enable them.
## Defaults are safe
Every block that renders an icon defaults to a `lucide:` glyph, so the theme
renders correctly even if Simple Icons is not yet installed (brand links simply
fall back to text/label until the pack is present).

View File

@ -1,15 +0,0 @@
# Bundled font licenses
All bundled woff2 files in this directory are latin-subset builds redistributed
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
subsetting to the latin range. Font-family stays admin-controlled; these appear
as `source=template` rows in the font picker and are consumed only via
`var(--font-heading|body|mono, <fallback>)`.
| Family | Files | License | Source |
|--------|-------|---------|--------|
| Space Grotesk | space-grotesk-500.woff2, space-grotesk-700.woff2 | SIL OFL 1.1 | Florian Karsten / Google Fonts |
| Inter | inter-400.woff2, inter-600.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
| JetBrains Mono | jetbrains-mono-400.woff2 (variable; theme uses 400 and 500) | SIL OFL 1.1 | JetBrains / Google Fonts |
Full OFL text: https://openfontlicense.org/.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
<section data-block="scifi-clean:benchmark_table" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div style="max-width: 44rem;">{% if heading %}<p class="sc-eyebrow">{{ eyebrow|default:"Benchmarks" }}</p><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 1.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}{% if rows %}{% for row in rows %}<div class="grid grid-cols-[1fr_auto] gap-x-4 gap-y-1.5 items-baseline mb-4" data-sc-reveal><span class="sc-bench-name">{{ row.label }}</span><span class="sc-bench-value">{{ row.value }}{% if row.unit %} <span class="sc-telemetry-unit">{{ row.unit }}</span>{% endif %}</span><span class="sc-bench-track"><span class="sc-bench-fill{% if row.highlight %} sc-bench-fill--accent{% endif %}" style="width: {{ row.bar|default:0 }}%;"></span></span></div>{% endfor %}{% else %}<p class="sc-text" style="color: hsl(var(--muted-foreground));">No benchmark rows yet.</p>{% endif %}{% if note %}<p class="sc-form-note" style="margin-top: 1rem;">{{ note }}</p>{% endif %}</div></div></section>

View File

@ -1,44 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Benchmark Table",
"description": "Labelled benchmark bars with mono values and a technical-blue fill track.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small mono label above the heading.",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"description": "Section heading.",
"x-editor": "text"
},
"rows": {
"type": "array",
"title": "Rows",
"description": "Each row is a labelled benchmark bar.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Label", "description": "Benchmark name.", "x-editor": "text" },
"value": { "type": "string", "title": "Value", "description": "Displayed value (e.g. 98.4).", "x-editor": "text" },
"unit": { "type": "string", "title": "Unit", "description": "Trailing unit (e.g. ms, %, tok/s).", "x-editor": "text" },
"bar": { "type": "number", "title": "Bar %", "description": "Fill width 0-100.", "default": 0, "minimum": 0, "maximum": 100, "x-editor": "number" },
"highlight": { "type": "boolean", "title": "Highlight", "description": "Fill this bar with the signal accent instead of technical blue.", "default": false, "x-editor": "select" }
},
"required": ["label", "value"]
}
},
"note": {
"type": "string",
"title": "Note",
"description": "Optional footnote below the bars.",
"x-editor": "text"
}
}
}

View File

@ -1,32 +0,0 @@
# Sci-Fi Clean theme — persona block definitions (codeless .bnp).
# Keys are fully qualified (scifi-clean:<key>): the codeless loader registers
# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path),
# and the qualified keys match the data-block attributes + master_pages / seed
# references. These four are TRUE persona furniture that no builtin covers;
# every duplicating theme block (footer, hero, stats, status bar, image
# caption) is now a builtin OVERRIDE under templates/overrides/scifi-clean/.
blocks:
- key: scifi-clean:tech_spec
title: Tech Spec Table
description: Specification sheet with right-aligned mono numerals, units, and hairline rows.
category: content
schema: tech_spec.schema.json
template: tech_spec.ninjatpl
- key: scifi-clean:benchmark_table
title: Benchmark Table
description: Labelled benchmark bars with mono values and a technical-blue fill track.
category: content
schema: benchmark_table.schema.json
template: benchmark_table.ninjatpl
- key: scifi-clean:telemetry_grid
title: Telemetry Grid
description: Grid of HUD readout tiles with mono figures, units, deltas, and optional ticking counters.
category: content
schema: telemetry_grid.schema.json
template: telemetry_grid.ninjatpl
- key: scifi-clean:changelog_console
title: Changelog Console
description: Terminal-style release log with version, date, and note per entry.
category: content
schema: changelog_console.schema.json
template: changelog_console.ninjatpl

View File

@ -1 +0,0 @@
<section data-block="scifi-clean:changelog_console" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div style="max-width: 44rem;">{% if heading %}<p class="sc-eyebrow">{{ eyebrow|default:"Release log" }}</p><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 1.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="sc-console" data-sc-reveal><div class="sc-console-bar"><span class="sc-console-dot"></span><span class="sc-console-dot"></span><span class="sc-console-dot"></span><span class="sc-console-title">{{ title|default:"changelog" }}</span></div><div class="sc-console-body">{% if entries %}{% for entry in entries %}<div class="sc-console-entry"><span class="sc-console-ver">{{ entry.version }}</span><span class="sc-console-date">{{ entry.date }}</span><span class="sc-console-note">{{ entry.note }}</span></div>{% endfor %}{% else %}<div class="sc-console-entry"><span class="sc-console-note sc-console-prompt">no entries logged</span></div>{% endif %}</div></div></div></div></section>

View File

@ -1,43 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Changelog Console",
"description": "Terminal-style release log with version, date, and note per entry.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small mono label above the heading.",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"description": "Section heading.",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Console title",
"description": "Label shown in the console title bar.",
"default": "changelog",
"x-editor": "text"
},
"entries": {
"type": "array",
"title": "Entries",
"description": "Each entry is a version / date / note row.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"version": { "type": "string", "title": "Version", "description": "Release tag (e.g. v2.4.0).", "x-editor": "text" },
"date": { "type": "string", "title": "Date", "description": "Release date.", "x-editor": "text" },
"note": { "type": "string", "title": "Note", "description": "One-line summary of the change.", "x-editor": "text" }
},
"required": ["version", "note"]
}
}
}
}

View File

@ -1 +0,0 @@
<section data-block="scifi-clean:tech_spec" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div class="sc-hud-corners" style="max-width: 34rem; padding: 1.25rem 1.5rem; overflow-x: auto;"><table class="sc-spec">{% if caption %}<caption>{{ caption }}</caption>{% endif %}<tbody>{% if rows %}{% for row in rows %}<tr><td class="sc-spec-label">{{ row.label }}</td><td class="sc-spec-value">{{ row.value }}</td><td class="sc-spec-unit">{{ row.unit }}</td></tr>{% endfor %}{% else %}<tr><td class="sc-spec-label">No spec rows yet</td><td class="sc-spec-value">--</td><td class="sc-spec-unit"></td></tr>{% endif %}</tbody></table></div></div></section>

View File

@ -1 +0,0 @@
<section data-block="scifi-clean:telemetry_grid" class="sc-section-tight {{ class }}"><div class="sc-content-well">{% if heading %}<p class="sc-eyebrow">{{ eyebrow|default:"Telemetry" }}</p><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 1.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="sc-telemetry-grid {% if columns == 4 %}sc-telemetry-4{% elif columns == 2 %}sc-telemetry-2{% else %}sc-telemetry-3{% endif %}">{% if tiles %}{% for tile in tiles %}<div class="sc-telemetry-cell" data-sc-reveal><p class="sc-telemetry-label">{{ tile.label }}</p><p style="margin: 0;"><span class="sc-telemetry-figure" data-sc-count="{{ tile.value }}">{{ tile.value }}</span>{% if tile.unit %} <span class="sc-telemetry-unit">{{ tile.unit }}</span>{% endif %}</p>{% if tile.delta %}<p class="sc-telemetry-delta">{{ tile.delta }}</p>{% endif %}</div>{% endfor %}{% else %}<div class="sc-telemetry-cell"><p class="sc-telemetry-label">No readouts</p><p class="sc-telemetry-figure">--</p></div>{% endif %}</div></div></section>

View File

@ -1,45 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Telemetry Grid",
"description": "Grid of HUD readout tiles with mono figures, units, deltas, and optional ticking counters.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small mono label above the heading.",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"description": "Section heading.",
"x-editor": "text"
},
"columns": {
"type": "number",
"title": "Columns",
"description": "Tiles per row on wide screens.",
"default": 3,
"enum": [2, 3, 4],
"x-editor": "select"
},
"tiles": {
"type": "array",
"title": "Tiles",
"description": "Each tile is a readout: label, value, optional unit and delta.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Label", "description": "Readout name.", "x-editor": "text" },
"value": { "type": "string", "title": "Value", "description": "Displayed value; leading number is animated by the ticking counter.", "x-editor": "text" },
"unit": { "type": "string", "title": "Unit", "description": "Trailing unit (e.g. ms, GB, %).", "x-editor": "text" },
"delta": { "type": "string", "title": "Delta", "description": "Optional change indicator (e.g. -12% latency).", "x-editor": "text" }
},
"required": ["label", "value"]
}
}
}
}

22
button_override.go Normal file
View File

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

27
button_override.templ Normal file
View File

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

112
button_override_templ.go Normal file
View File

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

119
css.go Normal file
View File

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

63
diagram_caption.go Normal file
View File

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

49
diagram_caption.templ Normal file
View File

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

210
diagram_caption_templ.go Normal file
View File

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

16
email_wrapper.go Normal file
View File

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

137
email_wrapper.templ Normal file
View File

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

340
email_wrapper_templ.go Normal file
View File

@ -0,0 +1,340 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// scifiEmailBgColor / scifiEmailCardColor / scifiEmailBorderColor pull from the
// caller-provided EmailColors so the wrapper inherits the active preset.
func scifiEmailBgColor(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return "#F6F7F8"
}
func scifiEmailCardColor(c templates.EmailContext) string {
if c.Colors.Card != "" {
return c.Colors.Card
}
return "#FFFFFF"
}
func scifiEmailBorderColor(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return "#D6D9DD"
}
func scifiEmailFgColor(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return "#16202E"
}
func scifiEmailMutedColor(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return "#5C6776"
}
func scifiEmailCallsign(c templates.EmailContext) string {
if c.SiteSettings.SiteName != "" {
return "SCF / " + c.SiteSettings.SiteName
}
return "SCF-CLN"
}
// scifiEmailTemplate is the Sci-Fi Clean branded email wrapper.
func scifiEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 61, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li {\n\t\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t\t}\n\t\t\t\timg { border: 0; height: auto; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\t/* Mono numeral preservation across clients. */\n\t\t\t\t.scifi-num {\n\t\t\t\t\tfont-family: ui-monospace, \"JetBrains Mono\", \"Menlo\", monospace !important;\n\t\t\t\t\tfont-variant-numeric: tabular-nums;\n\t\t\t\t}\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.scifi-email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.scifi-pad { padding-left: 20px !important; padding-right: 20px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: %s;", scifiEmailBgColor(emailCtx), scifiEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 80, Col: 225}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 82, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 12px; background-color: %s;", scifiEmailBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 86, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><table role=\"presentation\" class=\"scifi-email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", scifiEmailCardColor(emailCtx), scifiEmailBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 94, Col: 155}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><tr><td class=\"scifi-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 20px 28px; border-bottom: 1px solid %s;", scifiEmailBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 97, Col: 134}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"left\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: 'Space Grotesk', 'Inter', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: 0.02em; color: %s;", scifiEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 100, Col: 200}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 101, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td><td align=\"right\" class=\"scifi-num\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", scifiEmailMutedColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 103, Col: 236}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(scifiEmailCallsign(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 104, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td></tr></table></td></tr><tr><td class=\"scifi-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 28px; font-size: 15px; line-height: 1.55; color: %s;", scifiEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 111, Col: 143}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td></tr><tr><td class=\"scifi-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 16px 28px; border-top: 1px solid %s; background-color: %s;", scifiEmailBorderColor(emailCtx), scifiEmailCardColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 116, Col: 184}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td class=\"scifi-num\" align=\"left\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", scifiEmailMutedColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 119, Col: 235}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 120, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<td class=\"scifi-num\" align=\"right\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: ui-monospace, 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 123, Col: 194}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", scifiEmailMutedColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 157}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">Unsubscribe</a></td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</tr></table></td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

60
embed.go Normal file
View File

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

View File

@ -1,26 +1 @@
[
{
"name": "Space Grotesk",
"family": "Space Grotesk",
"variants": [
{ "weight": "500", "style": "normal", "file": "fonts/space-grotesk-500.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/space-grotesk-700.woff2" }
]
},
{
"name": "Inter",
"family": "Inter",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/inter-600.woff2" }
]
},
{
"name": "JetBrains Mono",
"family": "JetBrains Mono",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/jetbrains-mono-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/jetbrains-mono-400.woff2" }
]
}
]
[]

58
footer.go Normal file
View File

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

69
footer.templ Normal file
View File

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

108
footer_templ.go Normal file
View File

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

20
go.mod Normal file
View File

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

44
go.sum Normal file
View File

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

42
heading_override.go Normal file
View File

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

48
heading_override.templ Normal file
View File

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

338
heading_override_templ.go Normal file
View File

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

82
helpers.go Normal file
View File

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

View File

@ -1,491 +0,0 @@
# Sci-Fi Clean theme — codeless .bnp manifest (WO-TF-024, Wave A).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
#
# Identity: the precise chrome of an AI / robotics product company. Blueprint
# schematic line-work, HUD guide lines, mono readouts, spec + benchmark
# furniture. Light = white-room lab (bright ground, thin blueprint lines, one
# signal accent); dark = mission control (deep navy, technical-blue HUD lines,
# glowing readouts). Motion is MID tier: JS scroll-reveal + ticking counters +
# CSS line-draw, no canvas, prefers-reduced-motion honored, static no-JS
# fallback. Semantic tokens only.
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs:
- lucide
- simple-icons
system_templates:
- key: scifi-clean
title: Sci-Fi Clean
description: "Blueprint-precise theme for AI and robotics product sites: HUD guide lines, mono readouts, spec and benchmark furniture, a single signal accent, and dual white-room / mission-control presets."
page_templates:
- system: scifi-clean
key: default
title: Default
description: "Header rail + main column + footer instrument panel, capped at 1280px."
slots: [header, main, footer]
- system: scifi-clean
key: full-width
title: Full width
description: "Edge-to-edge schematic surface for diagrams and dashboards."
slots: [header, main, footer]
- system: scifi-clean
key: landing
title: Landing
description: "Hero band + main + closing CTA + footer."
slots: [hero, main, cta, footer]
- system: scifi-clean
key: article
title: Article
description: "Narrow measure for long-form technical writing, with a rail aside."
slots: [header, main, aside, footer]
- system: scifi-clean
key: blog-index
title: Blog index
description: "Title band, featured lead, and a hairline-ruled post grid."
slots: [header, featured, main, footer]
- system: scifi-clean
key: contact
title: Contact
description: "Two-column contact: form on the grid, coordinates in the aside."
slots: [header, main, aside, footer]
- system: scifi-clean
key: auth
title: Auth
description: "Centered console card on a quiet ground for login and reset."
slots: [main, footer]
template_overrides:
# Chrome (4)
- { template: scifi-clean, block: navbar }
- { template: scifi-clean, block: footer }
- { template: scifi-clean, block: utility-bar }
- { template: scifi-clean, block: announcement-bar }
# Marketing (12)
- { template: scifi-clean, block: hero }
- { template: scifi-clean, block: feature-grid }
- { template: scifi-clean, block: rich-section }
- { template: scifi-clean, block: stats }
- { template: scifi-clean, block: testimonial }
- { template: scifi-clean, block: logo-strip }
- { template: scifi-clean, block: pricing }
- { template: scifi-clean, block: team }
- { template: scifi-clean, block: faq }
- { template: scifi-clean, block: timeline }
- { template: scifi-clean, block: cta }
- { template: scifi-clean, block: countdown }
# Primitives (11)
- { template: scifi-clean, block: heading }
- { template: scifi-clean, block: text }
- { template: scifi-clean, block: rich-text }
- { template: scifi-clean, block: image }
- { template: scifi-clean, block: quote }
- { template: scifi-clean, block: divider }
- { template: scifi-clean, block: button }
- { template: scifi-clean, block: card }
- { template: scifi-clean, block: gallery }
- { template: scifi-clean, block: video-embed }
- { template: scifi-clean, block: audio-embed }
# Commerce / local (6)
- { template: scifi-clean, block: menu-list }
- { template: scifi-clean, block: event-list }
- { template: scifi-clean, block: contact-form }
- { template: scifi-clean, block: contact-card }
- { template: scifi-clean, block: hours-location }
- { template: scifi-clean, block: social-links }
# Blog family (12)
- { template: scifi-clean, block: blog-hero }
- { template: scifi-clean, block: featured-posts }
- { template: scifi-clean, block: related-posts }
- { template: scifi-clean, block: popular-posts }
- { template: scifi-clean, block: category-list }
- { template: scifi-clean, block: post-hero }
- { template: scifi-clean, block: post-metadata }
- { template: scifi-clean, block: author-bio }
- { template: scifi-clean, block: author-bio-hero }
- { template: scifi-clean, block: newsletter-signup }
- { template: scifi-clean, block: breadcrumbs }
- { template: scifi-clean, block: sidebar-nav }
# Auth surface (3)
- { template: scifi-clean, block: auth-form }
- { template: scifi-clean, block: password-reset-form }
- { template: scifi-clean, block: auth-status }
# System (1)
- { template: scifi-clean, block: page-suggestions }
email_wrappers:
- scifi-clean
css:
input_css_append: |
/* === Sci-Fi Clean: font-family fallback variables ===
* The admin assigns real fonts via typography settings; until then these
* fall through to the bundled stacks. Space Grotesk (heading), Inter
* (body), and JetBrains Mono (mono) ship bundled in assets/fonts.
*/
: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;
}
/* Bundled display faces. The host does not emit @font-face for bundled_fonts
* (LoadedPlugin.BundledFonts is unconsumed), so the theme declares its own
* faces against the served artifact paths (/templates/scifi-clean/fonts/...).
* Presets set typography.fontHeading/body/mono to template:scifi-clean:<Family>
* so --font-heading/--font-body/--font-mono resolve to these families. */
@font-face { font-family: 'Space Grotesk'; src: url('/templates/scifi-clean/fonts/space-grotesk-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Space Grotesk'; src: url('/templates/scifi-clean/fonts/space-grotesk-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/scifi-clean/fonts/inter-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/scifi-clean/fonts/inter-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('/templates/scifi-clean/fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('/templates/scifi-clean/fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
/* ============================================================
Corporate Modernist — appended component CSS.
Semantic tokens only: hsl(var(--token)). No literal colors.
Swiss grid + Bauhaus geometric marks + oversized numerals.
============================================================ */
/* --- Layout primitives --- */
.sc-content-well { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; padding-left: 1.5rem; padding-right: 1.5rem; box-sizing: border-box; }
.sc-measure { max-width: 70ch; }
.sc-article-measure { max-width: 72ch; }
.sc-section { padding-top: 3.5rem; padding-bottom: 3.5rem; }
.sc-section-tight { padding-top: 2.75rem; padding-bottom: 2.75rem; }
.sc-hairline { border: 1px solid hsl(var(--border)); }
.sc-hairline-top { border-top: 1px solid hsl(var(--border)); }
.sc-hairline-bottom { border-bottom: 1px solid hsl(var(--border)); }
.sc-hairline-y > * + * { border-top: 1px solid hsl(var(--border)); }
.sc-accent-rule { border-top: 2px solid hsl(var(--accent)); }
.sc-stack > * + * { margin-top: 1.5rem; }
.sc-stack-sm > * + * { margin-top: 0.75rem; }
/* --- Typography --- */
.sc-display { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.02em; color: hsl(var(--foreground)); margin: 0; }
.sc-body { font-family: var(--font-body, "Inter", system-ui, sans-serif); }
.sc-mono { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-variant-numeric: tabular-nums; }
.numeric-tabular { font-variant-numeric: tabular-nums; }
.sc-heading { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.02em; color: hsl(var(--foreground)); margin: 0; }
.sc-heading-h2-accent { border-bottom: 2px solid hsl(var(--accent)); padding-bottom: 0.25rem; display: inline-block; }
.sc-text { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 17px; line-height: 28px; color: hsl(var(--foreground)); font-variant-numeric: tabular-nums; }
.sc-text a { color: hsl(var(--accent)); text-decoration: underline; text-underline-offset: 2px; }
.sc-text strong { font-weight: 600; }
.sc-lede { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 1.25rem; line-height: 1.6; color: hsl(var(--muted-foreground)); }
.sc-eyebrow { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: hsl(var(--accent)); margin: 0 0 1rem 0; }
.sc-kicker { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
/* --- Bauhaus geometric marks (circle / triangle / square) --- */
.sc-mark { display: inline-block; width: 0.75rem; height: 0.75rem; flex: none; }
.sc-mark-circle { border-radius: 9999px; background-color: hsl(var(--accent)); }
.sc-mark-square { background-color: hsl(var(--accent)); }
.sc-mark-triangle { width: 0; height: 0; border-left: 0.4375rem solid transparent; border-right: 0.4375rem solid transparent; border-bottom: 0.75rem solid hsl(var(--accent)); background: none; }
.sc-mark-ring { border-radius: 9999px; background: none; border: 2px solid hsl(var(--accent)); }
/* --- Numerals / metrics (oversized signature) --- */
.sc-metric { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: clamp(2.5rem, 5vw, 3.5rem); line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); letter-spacing: -0.02em; }
.sc-stat-figure { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: clamp(2.25rem, 4vw, 3rem); line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); letter-spacing: -0.02em; }
.sc-stat-label { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.8125rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 0.5rem; }
.sc-numeral-index { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 2rem; line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); }
/* --- Buttons (squared, flat, no gradient/shadow) --- */
.sc-btn-primary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); border: 1px solid hsl(var(--accent)); border-radius: 4px; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 500; text-decoration: none; line-height: 1.4; box-shadow: none; background-image: none; transition: background-color 150ms ease; cursor: pointer; }
.sc-btn-primary:hover { background-color: hsl(var(--accent) / 0.9); }
.sc-btn-primary:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.sc-btn-outline { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: 4px; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 500; text-decoration: none; line-height: 1.4; box-shadow: none; background-image: none; transition: border-color 150ms ease, color 150ms ease; cursor: pointer; }
.sc-btn-outline:hover { border-color: hsl(var(--foreground)); }
.sc-btn-outline:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.sc-btn-ghost { display: inline-flex; align-items: center; gap: 0.375rem; color: hsl(var(--accent)); font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 500; text-decoration: none; line-height: 1.4; transition: gap 150ms ease; }
.sc-btn-ghost:hover { gap: 0.625rem; }
.sc-btn-sm { padding: 0.375rem 0.875rem; font-size: 0.875rem; }
.sc-btn-lg { padding: 0.875rem 1.75rem; font-size: 1.0625rem; }
.sc-btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.sc-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
/* --- Surfaces / cards --- */
.sc-card { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); border-radius: 4px; padding: 1.5rem; box-shadow: none; }
.sc-card-accent { border-top: 2px solid hsl(var(--accent)); }
.sc-panel { background-color: hsl(var(--muted)); border-radius: 4px; padding: 1.5rem; }
.sc-surface-muted { background-color: hsl(var(--muted)); }
.sc-surface-card { background-color: hsl(var(--card)); }
.sc-media-frame { display: block; width: 100%; overflow: hidden; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); }
.sc-media-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.sc-img-empty { display: block; width: 100%; aspect-ratio: 4 / 3; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); }
/* --- Chrome: navbar --- */
.sc-navbar { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding: 1.25rem 0; }
.sc-brand { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 700; font-size: 1.125rem; letter-spacing: -0.01em; color: hsl(var(--foreground)); text-decoration: none; display: inline-flex; align-items: center; gap: 0.625rem; }
.sc-nav-links { display: none; align-items: center; gap: 2rem; }
@media (min-width: 768px) { .sc-nav-links { display: flex; } }
.sc-nav-link { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding-bottom: 2px; border-bottom: 2px solid transparent; transition: border-color 150ms ease; }
.sc-nav-link:hover { border-bottom-color: hsl(var(--accent)); }
.sc-nav-toggle { display: inline-flex; align-items: center; justify-content: center; min-width: 42px; min-height: 42px; }
@media (min-width: 768px) { .sc-nav-toggle { display: none; } }
.sc-nav-drawer { display: none; flex-direction: column; gap: 0.25rem; padding: 0.5rem 0 1rem; border-top: 1px solid hsl(var(--border)); width: 100%; }
@media (min-width: 768px) { .sc-nav-drawer { display: none !important; } }
.sc-nav-drawer a { padding: 0.625rem 0; font-family: var(--font-body, "Inter", system-ui, sans-serif); color: hsl(var(--foreground)); text-decoration: none; border-bottom: 1px solid hsl(var(--border)); }
/* --- Chrome: utility bar + announcement bar --- */
.sc-utility-bar { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.8125rem; padding: 0.5rem 0; }
.sc-utility-bar--muted { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
.sc-utility-bar--primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
.sc-utility-bar--dark { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
.sc-utility-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.sc-utility-group { display: flex; align-items: center; gap: 1.25rem; }
.sc-utility-item { display: inline-flex; align-items: center; gap: 0.375rem; color: inherit; text-decoration: none; }
.sc-announce { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.875rem; padding: 0.625rem 0; }
.sc-announce--info { background-color: hsl(var(--muted)); color: hsl(var(--foreground)); }
.sc-announce--promo { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.sc-announce--alert { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
.sc-announce-row { display: flex; align-items: center; justify-content: center; gap: 0.75rem; }
.sc-announce a { color: inherit; text-decoration: underline; text-underline-offset: 2px; font-weight: 600; }
.sc-announce-close { margin-left: auto; background: none; border: 0; color: inherit; cursor: pointer; line-height: 1; padding: 0 0.25rem; }
/* --- Chrome: footer --- */
.sc-footer { background-color: hsl(var(--background)); color: hsl(var(--foreground)); border-top: 2px solid hsl(var(--accent)); padding: 3.5rem 0 2.5rem; }
.sc-footer-head { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--muted-foreground)); margin: 0 0 0.875rem; }
.sc-footer-link { display: block; font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding: 0.25rem 0; }
.sc-footer-link:hover { color: hsl(var(--accent)); }
.sc-footer-legal { color: hsl(var(--muted-foreground)); font-size: 0.8125rem; border-top: 1px solid hsl(var(--border)); padding-top: 1.5rem; margin-top: 2.5rem; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; }
/* --- Marketing: hero --- */
.sc-hero { padding: 5rem 0; }
.sc-hero-headline { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; font-size: clamp(2.5rem, 6vw, 4.5rem); color: hsl(var(--foreground)); margin: 0; }
.sc-hero-sub { font-size: clamp(1.125rem, 2vw, 1.5rem); line-height: 1.5; color: hsl(var(--muted-foreground)); margin: 1.25rem 0 0; }
/* --- Marketing: logo strip --- */
.sc-logo { filter: grayscale(1); opacity: 0.65; transition: filter 200ms ease, opacity 200ms ease; }
.sc-logo:hover { filter: grayscale(0); opacity: 1; }
.sc-logo-row { display: flex; flex-wrap: wrap; align-items: center; gap: 2.5rem; }
/* --- Marketing: feature grid --- */
.sc-feature { border-top: 2px solid hsl(var(--accent)); padding-top: 1.25rem; }
.sc-feature-title { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; font-size: 1.125rem; color: hsl(var(--foreground)); margin: 0.75rem 0 0.5rem; }
/* --- Marketing: pull quote / testimonial --- */
.sc-pull-quote { border-top: 2px solid hsl(var(--accent)); padding-top: 1.25rem; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 1.5rem; line-height: 1.4; font-weight: 500; color: hsl(var(--foreground)); }
.sc-quote-block { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 1.375rem; line-height: 1.45; font-weight: 500; color: hsl(var(--foreground)); }
.sc-quote-cite { font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-top: 0.75rem; }
.sc-avatar { width: 2.75rem; height: 2.75rem; border-radius: 9999px; object-fit: cover; background-color: hsl(var(--muted)); }
/* --- Marketing: pricing --- */
.sc-price { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 2.5rem; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); letter-spacing: -0.02em; }
.sc-price-period { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--muted-foreground)); }
.sc-price-tier { display: flex; flex-direction: column; }
.sc-price-tier--featured { border-top: 4px solid hsl(var(--accent)); }
.sc-badge { display: inline-block; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.6875rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.25rem 0.5rem; background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); border-radius: 3px; }
.sc-check { color: hsl(var(--accent)); flex: none; }
/* --- Marketing: faq --- */
.sc-faq-item { border-top: 1px solid hsl(var(--border)); }
.sc-faq-item summary { cursor: pointer; list-style: none; padding: 1.25rem 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; color: hsl(var(--foreground)); }
.sc-faq-item summary::-webkit-details-marker { display: none; }
.sc-faq-item[open] summary .sc-faq-chevron { transform: rotate(180deg); }
.sc-faq-chevron { transition: transform 150ms ease; color: hsl(var(--accent)); }
.sc-faq-answer { padding: 0 0 1.25rem; color: hsl(var(--muted-foreground)); font-size: 1rem; line-height: 1.65; }
/* --- Marketing: timeline --- */
.sc-timeline-step { display: grid; grid-template-columns: auto 1fr; gap: 1.25rem; padding-bottom: 2rem; }
.sc-timeline-rail { position: relative; display: flex; flex-direction: column; align-items: center; }
.sc-timeline-node { width: 2.5rem; height: 2.5rem; border: 2px solid hsl(var(--accent)); border-radius: 9999px; display: flex; align-items: center; justify-content: center; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-weight: 500; color: hsl(var(--accent)); flex: none; font-variant-numeric: tabular-nums; }
.sc-timeline-line { flex: 1 1 auto; width: 1px; background-color: hsl(var(--border)); margin-top: 0.5rem; }
/* --- Marketing: cta strip --- */
.sc-cta-strip { padding: 3.5rem 0; }
.sc-cta-strip--band { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
.sc-cta-strip--band .sc-btn-primary { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); border-color: hsl(var(--accent)); }
.sc-cta-strip--card { background-color: hsl(var(--muted)); }
.sc-cta-headline { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.02em; font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.15; margin: 0; }
/* --- Marketing: countdown --- */
.sc-count-unit { display: flex; flex-direction: column; align-items: center; }
.sc-count-figure { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: clamp(2rem, 4vw, 3rem); font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); line-height: 1; }
.sc-count-box { border: 1px solid hsl(var(--border)); padding: 1rem 1.25rem; border-radius: 4px; min-width: 4.5rem; }
/* --- Primitives: quote / divider / gallery / media --- */
.sc-divider-line { border: 0; border-top: 1px solid hsl(var(--border)); }
.sc-divider-thick { border-top-width: 2px; border-top-color: hsl(var(--accent)); }
.sc-divider-ornament { display: flex; align-items: center; justify-content: center; gap: 0.75rem; color: hsl(var(--muted-foreground)); }
.sc-gallery-grid { display: grid; gap: 0.75rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 768px) { .sc-gallery-grid.sc-gallery-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .sc-gallery-grid.sc-gallery-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.sc-video-frame { position: relative; display: block; width: 100%; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; }
.sc-video-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: hsl(var(--background)); background: none; border: 0; cursor: pointer; }
.sc-video-play-badge { width: 4rem; height: 4rem; border-radius: 9999px; background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); display: flex; align-items: center; justify-content: center; }
/* --- Commerce / local --- */
.sc-menu-item { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid hsl(var(--border)); }
.sc-menu-price { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); }
.sc-channel { display: flex; align-items: flex-start; gap: 0.75rem; }
.sc-channel-mark { width: 2.5rem; height: 2.5rem; border: 1px solid hsl(var(--border)); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: hsl(var(--accent)); flex: none; }
.sc-hours-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border)); }
.sc-social-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.sc-social-link { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.75rem; border: 1px solid hsl(var(--border)); border-radius: 4px; color: hsl(var(--foreground)); text-decoration: none; font-size: 0.875rem; transition: border-color 150ms ease; }
.sc-social-link:hover { border-color: hsl(var(--accent)); color: hsl(var(--accent)); }
/* --- Forms --- */
.sc-form { display: grid; gap: 1.25rem; }
.sc-form-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 640px) { .sc-form-grid { grid-template-columns: 1fr; } }
.sc-field { display: flex; flex-direction: column; gap: 0.375rem; }
.sc-field--full { grid-column: 1 / -1; }
.sc-label { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; color: hsl(var(--foreground)); text-transform: uppercase; letter-spacing: 0.04em; }
.sc-input { width: 100%; box-sizing: border-box; padding: 0.625rem 0.75rem; background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--input)); border-radius: 4px; font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 1rem; }
.sc-input:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; border-color: hsl(var(--ring)); }
.sc-form-note { font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.sc-alert-ok { border-left: 3px solid hsl(var(--accent)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); }
.sc-alert-err { border-left: 3px solid hsl(var(--destructive)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); }
/* --- Blog family --- */
.sc-post-card { display: flex; flex-direction: column; gap: 0.75rem; }
.sc-post-thumb { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); }
.sc-post-title { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; font-size: 1.25rem; line-height: 1.25; color: hsl(var(--foreground)); text-decoration: none; }
.sc-post-title:hover { color: hsl(var(--accent)); }
.sc-post-meta { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.04em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.sc-post-excerpt { color: hsl(var(--muted-foreground)); font-size: 0.9375rem; line-height: 1.6; }
.sc-tag { display: inline-block; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.04em; padding: 0.1875rem 0.5rem; border: 1px solid hsl(var(--border)); border-radius: 3px; color: hsl(var(--muted-foreground)); text-decoration: none; }
.sc-tag:hover { border-color: hsl(var(--accent)); color: hsl(var(--accent)); }
.sc-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.sc-crumbs a { color: hsl(var(--muted-foreground)); text-decoration: none; }
.sc-crumbs a:hover { color: hsl(var(--accent)); }
.sc-crumb-sep { color: hsl(var(--border)); }
.sc-sidebar-link { display: block; padding: 0.5rem 0.75rem; border-left: 2px solid transparent; color: hsl(var(--foreground)); text-decoration: none; font-size: 0.9375rem; }
.sc-sidebar-link:hover, .sc-sidebar-link[aria-current="page"] { border-left-color: hsl(var(--accent)); color: hsl(var(--accent)); }
.sc-rank { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 1.25rem; font-weight: 500; color: hsl(var(--accent)); font-variant-numeric: tabular-nums; width: 2rem; flex: none; }
/* --- Motion (CSS-first, reduced-motion honored) --- */
.sc-reveal { opacity: 0; transform: translateY(0.5rem); animation: sc-reveal 500ms ease forwards; }
@keyframes sc-reveal { to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
.sc-reveal { animation: none; opacity: 1; transform: none; }
.sc-btn-ghost, .sc-logo, .sc-nav-link, .sc-faq-chevron { transition: none; }
}
/* ============================================================
Sci-Fi Clean — identity layer (overrides the shared base above).
Persona: AI / robotics product. Blueprint schematics, HUD guide
lines, mono readouts, spec furniture. Light = white-room lab;
dark = mission control. Semantic tokens only. Tight 2px radii.
primary = technical blue (HUD lines), accent = signal (CTAs/deltas).
============================================================ */
/* --- Typography: mono readout eyebrows / kickers with // marker --- */
.sc-eyebrow { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: hsl(var(--primary)); margin: 0 0 1rem 0; }
.sc-eyebrow::before { content: "// "; color: hsl(var(--accent)); }
.sc-kicker { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.sc-display, .sc-heading, .sc-hero-headline, .sc-cta-headline, .sc-feature-title { font-family: var(--font-heading, "Space Grotesk", "Inter", system-ui, sans-serif); font-feature-settings: "ss01" on, "ss02" on; }
.sc-brand { font-family: var(--font-heading, "Space Grotesk", "Inter", system-ui, sans-serif); font-weight: 700; letter-spacing: -0.01em; }
.sc-brand::before { content: "◇ "; color: hsl(var(--accent)); font-family: var(--font-mono, ui-monospace, monospace); }
/* --- HUD guide lines: section rules use technical blue, thin --- */
.sc-accent-rule { border-top: 1px solid hsl(var(--primary)); }
.sc-hairline-top { border-top: 1px solid hsl(var(--border)); }
.sc-hairline-bottom { border-bottom: 1px solid hsl(var(--border)); }
.sc-heading-h2-accent { border-bottom: 2px solid hsl(var(--primary)); padding-bottom: 0.25rem; display: inline-block; }
.sc-divider-thick { border-top-width: 1px; border-top-color: hsl(var(--primary)); }
/* --- Buttons: sharp, mono label, precise --- */
.sc-btn-primary, .sc-btn-outline { border-radius: 2px; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-weight: 500; font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; }
.sc-btn-primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary)); }
.sc-btn-primary:hover { background-color: hsl(var(--primary) / 0.9); }
.sc-btn-outline:hover { border-color: hsl(var(--primary)); color: hsl(var(--primary)); }
.sc-btn-ghost { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-weight: 500; font-size: 0.8125rem; letter-spacing: 0.06em; text-transform: uppercase; color: hsl(var(--primary)); }
.sc-btn-lg { padding: 0.875rem 1.75rem; font-size: 0.875rem; }
.sc-btn-sm { padding: 0.375rem 0.875rem; font-size: 0.75rem; }
/* --- Surfaces: cornered blueprint cards --- */
.sc-card, .sc-panel, .sc-input, .sc-badge, .sc-tag, .sc-count-box, .sc-channel-mark, .sc-social-link { border-radius: 2px; }
.sc-card { position: relative; }
.sc-card-accent { border-top: 2px solid hsl(var(--primary)); }
.sc-card-accent::before, .sc-card-accent::after { content: ""; position: absolute; top: -1px; width: 10px; height: 10px; pointer-events: none; }
.sc-card-accent::before { left: -1px; border-left: 2px solid hsl(var(--accent)); border-top: 2px solid hsl(var(--accent)); }
.sc-card-accent::after { right: -1px; border-right: 2px solid hsl(var(--accent)); border-top: 2px solid hsl(var(--accent)); }
.sc-media-frame, .sc-post-thumb, .sc-video-frame { border-radius: 2px; }
/* --- Marks: HUD nodes/ticks in technical blue --- */
.sc-mark-square { background-color: hsl(var(--primary)); }
.sc-mark-circle { background-color: hsl(var(--primary)); }
.sc-mark-ring { border-color: hsl(var(--primary)); background: none; }
.sc-mark-triangle { border-bottom-color: hsl(var(--accent)); }
/* --- Readouts: metrics/stats/prices in mono, blue figures --- */
.sc-stat-figure, .sc-price, .sc-count-figure { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); }
.sc-stat-label, .sc-price-period, .sc-menu-price { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); }
.sc-rank { color: hsl(var(--accent)); }
.sc-timeline-node { border-color: hsl(var(--primary)); color: hsl(var(--primary)); border-radius: 2px; }
.sc-price-tier--featured { border-top: 2px solid hsl(var(--primary)); }
.sc-badge { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.sc-check { color: hsl(var(--primary)); }
/* --- Feature blocks + FAQ + chevrons: HUD blue --- */
.sc-feature { border-top: 1px solid hsl(var(--primary)); }
.sc-faq-chevron { color: hsl(var(--primary)); }
.sc-pull-quote, .sc-quote-block { border-color: hsl(var(--primary)); }
.sc-pull-quote { border-top: 1px solid hsl(var(--primary)); }
.sc-sidebar-link:hover, .sc-sidebar-link[aria-current="page"] { border-left-color: hsl(var(--primary)); color: hsl(var(--primary)); }
.sc-nav-link:hover { border-bottom-color: hsl(var(--primary)); }
.sc-tag:hover, .sc-social-link:hover, .sc-post-title:hover, .sc-footer-link:hover { color: hsl(var(--primary)); border-color: hsl(var(--primary)); }
/* --- Chrome: footer top rule = HUD blue --- */
.sc-footer { border-top: 2px solid hsl(var(--primary)); }
/* --- Blueprint grid ground (used by page-template mains) --- */
.sc-blueprint {
background-image:
linear-gradient(hsl(var(--border) / 0.5) 1px, transparent 1px),
linear-gradient(90deg, hsl(var(--border) / 0.5) 1px, transparent 1px);
background-size: 40px 40px;
}
.sc-hud-corners { position: relative; }
.sc-hud-corners::before, .sc-hud-corners::after { content: ""; position: absolute; width: 14px; height: 14px; pointer-events: none; }
.sc-hud-corners::before { top: 0; left: 0; border-left: 1px solid hsl(var(--primary)); border-top: 1px solid hsl(var(--primary)); }
.sc-hud-corners::after { bottom: 0; right: 0; border-right: 1px solid hsl(var(--primary)); border-bottom: 1px solid hsl(var(--primary)); }
/* --- Persona furniture: tech spec / benchmark / telemetry / console --- */
.sc-spec { width: 100%; border-collapse: collapse; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.875rem; }
.sc-spec caption { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--muted-foreground)); text-align: left; padding-bottom: 0.75rem; }
.sc-spec tr { border-bottom: 1px solid hsl(var(--border)); }
.sc-spec-label { text-align: left; width: 100%; padding: 0.625rem 0.5rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.75rem; }
.sc-spec-value { text-align: right; white-space: nowrap; padding: 0.625rem 0.375rem 0.625rem 1rem; color: hsl(var(--foreground)); font-variant-numeric: tabular-nums; font-weight: 500; }
.sc-spec-unit { text-align: left; white-space: nowrap; width: 1%; padding: 0.625rem 0.5rem 0.625rem 0; color: hsl(var(--muted-foreground)); font-size: 0.75rem; }
.sc-bench-name { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--foreground)); }
.sc-bench-value { font-family: var(--font-mono, ui-monospace, monospace); font-variant-numeric: tabular-nums; font-weight: 500; color: hsl(var(--foreground)); }
.sc-bench-track { grid-column: 1 / -1; height: 6px; background-color: hsl(var(--muted)); border-radius: 2px; overflow: hidden; }
.sc-bench-fill { height: 100%; background-color: hsl(var(--primary)); border-radius: 2px; }
.sc-bench-fill--accent { background-color: hsl(var(--accent)); }
.sc-telemetry-grid { display: grid; gap: 1px; background-color: hsl(var(--border)); border: 1px solid hsl(var(--border)); border-radius: 2px; grid-template-columns: repeat(1, minmax(0, 1fr)); overflow: hidden; }
@media (min-width: 640px) { .sc-telemetry-grid.sc-telemetry-2, .sc-telemetry-grid.sc-telemetry-3, .sc-telemetry-grid.sc-telemetry-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .sc-telemetry-grid.sc-telemetry-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .sc-telemetry-grid.sc-telemetry-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.sc-telemetry-cell { background-color: hsl(var(--card)); padding: 1.5rem 1.25rem; }
.sc-telemetry-label { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--muted-foreground)); margin: 0 0 0.5rem; }
.sc-telemetry-figure { font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(1.75rem, 3.5vw, 2.5rem); line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); }
.sc-telemetry-unit { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.sc-telemetry-delta { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); margin-top: 0.375rem; }
.sc-console { background-color: hsl(var(--card)); border: 1px solid hsl(var(--border)); border-radius: 2px; overflow: hidden; }
.sc-console-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1rem; border-bottom: 1px solid hsl(var(--border)); background-color: hsl(var(--muted)); }
.sc-console-dot { width: 8px; height: 8px; border-radius: 9999px; background-color: hsl(var(--border)); }
.sc-console-title { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.08em; color: hsl(var(--muted-foreground)); margin-left: 0.5rem; }
.sc-console-body { padding: 1.25rem 1rem; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; line-height: 1.7; }
.sc-console-entry { display: grid; grid-template-columns: auto auto 1fr; gap: 0.75rem; padding: 0.375rem 0; }
.sc-console-ver { color: hsl(var(--primary)); font-weight: 500; }
.sc-console-date { color: hsl(var(--muted-foreground)); }
.sc-console-note { color: hsl(var(--foreground)); }
.sc-console-prompt::before { content: "$ "; color: hsl(var(--accent)); }
/* --- Motion (MID tier): JS scroll-reveal + ticking counters.
No-JS fallback: without .sc-js on <html>, everything is fully
visible and counters show their static final text. --- */
.sc-js [data-sc-reveal] { opacity: 0; transform: translateY(12px); transition: opacity 600ms ease, transform 600ms ease; }
.sc-js [data-sc-reveal].sc-in { opacity: 1; transform: none; }
.sc-schematic { position: relative; }
.sc-js .sc-schematic .sc-schematic-line { transform: scaleX(0); transform-origin: left; transition: transform 900ms ease; }
.sc-js .sc-schematic.sc-in .sc-schematic-line { transform: scaleX(1); }
.sc-schematic-line { height: 1px; background-color: hsl(var(--primary)); }
@media (prefers-reduced-motion: reduce) {
.sc-js [data-sc-reveal] { opacity: 1 !important; transform: none !important; transition: none !important; }
.sc-js .sc-schematic .sc-schematic-line { transform: none !important; transition: none !important; }
.sc-reveal { animation: none; opacity: 1; transform: none; }
}

View File

@ -1,32 +0,0 @@
[
{
"key": "scifi-clean:default-master",
"title": "Sci-Fi Clean Default Master",
"page_templates": ["default", "article", "blog-index", "full-width"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Halcyon Robotics", "tagline": "Autonomy systems for the field.", "copyright": "Halcyon Robotics" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "scifi-clean:landing-master",
"title": "Sci-Fi Clean Landing Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "hero", "title": "Landing Hero", "content": { "template": "left-aligned", "eyebrow": "Field autonomy", "headline": "Robots that hold their own position", "subheadline": "Halcyon builds perception and control stacks for machines that work where the map runs out." }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "cta", "title": "Closing CTA", "content": { "background": "band", "heading": "Request a technical briefing", "text": "Thirty minutes with an engineer. Bring your hardest deployment.", "primaryLabel": "Book a briefing", "primaryUrl": "/contact" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Halcyon Robotics", "copyright": "Halcyon Robotics" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "scifi-clean:auth-master",
"title": "Sci-Fi Clean Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "slot", "title": "Auth Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
]
}
]

54
mission_stat.go Normal file
View File

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

35
mission_stat.templ Normal file
View File

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

162
mission_stat_templ.go Normal file
View File

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

View File

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

View File

@ -2,16 +2,9 @@
{
"id": "flightline",
"name": "Flightline",
"description": "The default Sci-Fi Clean look. Light is a white-room lab with technical-blue HUD lines and a signal-orange accent; dark is deep mission-control navy with lifted blue readouts.",
"description": "Off-white paper with technical blue primary and signal-orange accent — the default Sci-Fi Clean look.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"mode": "light",
"lightColors": {
"background": "210 14% 97%",
"foreground": "215 30% 12%",
@ -32,7 +25,15 @@
"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%",
@ -40,7 +41,7 @@
"cardForeground": "210 16% 92%",
"popover": "220 16% 10%",
"popoverForeground": "210 16% 92%",
"primary": "210 100% 62%",
"primary": "210 100% 60%",
"primaryForeground": "220 16% 7%",
"secondary": "220 14% 16%",
"secondaryForeground": "210 16% 92%",
@ -52,23 +53,16 @@
"destructiveForeground": "0 0% 100%",
"border": "220 14% 20%",
"input": "220 14% 18%",
"ring": "210 100% 62%"
"ring": "210 100% 60%"
}
}
},
{
"id": "cleanroom",
"name": "Cleanroom",
"description": "Highest-contrast surgical white with deep-navy ink in light, and near-black space navy in dark. For high-precision spec and dashboard work.",
"description": "Highest-contrast surgical white with deep navy ink — sister dark mode for night-lab work.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": {
"background": "0 0% 100%",
"foreground": "220 40% 10%",
@ -76,7 +70,7 @@
"cardForeground": "220 40% 10%",
"popover": "0 0% 100%",
"popoverForeground": "220 40% 10%",
"primary": "220 85% 38%",
"primary": "220 85% 35%",
"primaryForeground": "0 0% 100%",
"secondary": "210 18% 94%",
"secondaryForeground": "220 40% 10%",
@ -88,7 +82,7 @@
"destructiveForeground": "0 0% 100%",
"border": "215 16% 88%",
"input": "215 16% 90%",
"ring": "220 85% 38%"
"ring": "220 85% 35%"
},
"darkColors": {
"background": "220 30% 5%",
@ -97,7 +91,7 @@
"cardForeground": "210 20% 96%",
"popover": "220 26% 9%",
"popoverForeground": "210 20% 96%",
"primary": "210 100% 66%",
"primary": "210 100% 65%",
"primaryForeground": "220 30% 5%",
"secondary": "220 20% 16%",
"secondaryForeground": "210 20% 96%",
@ -109,121 +103,7 @@
"destructiveForeground": "0 0% 100%",
"border": "220 18% 20%",
"input": "220 18% 18%",
"ring": "210 100% 66%"
}
}
},
{
"id": "signal-cyan",
"name": "Signal Cyan",
"description": "Overt HUD styling. Cyan primary guide lines with an amber signal accent, on a cool white lab ground in light and a teal-tinted mission ground in dark.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": {
"background": "195 20% 97%",
"foreground": "200 40% 11%",
"card": "0 0% 100%",
"cardForeground": "200 40% 11%",
"popover": "0 0% 100%",
"popoverForeground": "200 40% 11%",
"primary": "192 82% 38%",
"primaryForeground": "0 0% 100%",
"secondary": "195 22% 91%",
"secondaryForeground": "200 40% 16%",
"muted": "195 20% 94%",
"mutedForeground": "200 14% 40%",
"accent": "35 95% 50%",
"accentForeground": "0 0% 100%",
"destructive": "0 76% 48%",
"destructiveForeground": "0 0% 100%",
"border": "195 18% 85%",
"input": "195 18% 87%",
"ring": "192 82% 38%"
},
"darkColors": {
"background": "198 30% 6%",
"foreground": "190 20% 92%",
"card": "198 28% 9%",
"cardForeground": "190 20% 92%",
"popover": "198 28% 9%",
"popoverForeground": "190 20% 92%",
"primary": "186 90% 55%",
"primaryForeground": "198 30% 6%",
"secondary": "198 22% 15%",
"secondaryForeground": "190 20% 92%",
"muted": "198 18% 13%",
"mutedForeground": "195 12% 60%",
"accent": "38 100% 58%",
"accentForeground": "200 40% 8%",
"destructive": "0 80% 58%",
"destructiveForeground": "0 0% 100%",
"border": "198 20% 19%",
"input": "198 20% 17%",
"ring": "186 90% 55%"
}
}
},
{
"id": "graphite-lab",
"name": "Graphite Lab",
"description": "Restrained neutral. Slate-blue primary and a muted signal accent on graphite surfaces. Quietest of the four for text-dense product docs.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": {
"background": "220 12% 96%",
"foreground": "220 20% 14%",
"card": "0 0% 100%",
"cardForeground": "220 20% 14%",
"popover": "0 0% 100%",
"popoverForeground": "220 20% 14%",
"primary": "218 42% 40%",
"primaryForeground": "0 0% 100%",
"secondary": "220 12% 90%",
"secondaryForeground": "220 20% 20%",
"muted": "220 12% 93%",
"mutedForeground": "220 10% 42%",
"accent": "24 80% 52%",
"accentForeground": "0 0% 100%",
"destructive": "0 74% 48%",
"destructiveForeground": "0 0% 100%",
"border": "220 12% 85%",
"input": "220 12% 87%",
"ring": "218 42% 40%"
},
"darkColors": {
"background": "220 14% 9%",
"foreground": "220 14% 90%",
"card": "220 14% 12%",
"cardForeground": "220 14% 90%",
"popover": "220 14% 12%",
"popoverForeground": "220 14% 90%",
"primary": "215 55% 62%",
"primaryForeground": "220 14% 9%",
"secondary": "220 12% 18%",
"secondaryForeground": "220 14% 90%",
"muted": "220 12% 15%",
"mutedForeground": "220 10% 60%",
"accent": "26 88% 58%",
"accentForeground": "220 20% 8%",
"destructive": "0 78% 56%",
"destructiveForeground": "0 0% 100%",
"border": "220 12% 22%",
"input": "220 12% 20%",
"ring": "215 55% 62%"
"ring": "210 100% 65%"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 136 KiB

197
register.go Normal file
View File

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

25
registration.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

55
schematic_hero.go Normal file
View File

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

55
schematic_hero.templ Normal file
View File

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

182
schematic_hero_templ.go Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

View File

@ -1,122 +0,0 @@
{
"demo": true,
"settings": {
"merge": {
"title": "Halcyon Robotics",
"site_name": "Halcyon Robotics",
"tagline": "Autonomy systems for the field."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Contact submissions",
"description": "Messages sent from the Halcyon Robotics contact form.",
"primary_key": "id",
"columns": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "organisation", "label": "Organisation", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Halcyon Robotics",
"template_key": "full-width",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 1, "content": { "template": "left-aligned", "eyebrow": { "text": "Field autonomy" }, "headline": { "text": "Robots that hold their own position" }, "subheadline": { "text": "Halcyon builds the perception and control stack for machines that work where the map runs out. One runtime, from sensor to actuator." }, "ctaPrimary": { "text": "Read the spec", "url": "/about" }, "ctaSecondary": { "text": "Book a briefing", "url": "/contact" } } },
{ "block_key": "scifi-clean:telemetry_grid", "title": "Telemetry", "slot": "main", "sort_order": 2, "content": { "eyebrow": "Runtime", "heading": "What the stack holds", "columns": 4, "tiles": [ { "label": "Control loop", "value": "1000", "unit": "Hz", "delta": "deterministic" }, { "label": "Localisation drift", "value": "4", "unit": "cm", "delta": "over 1 km" }, { "label": "Onboard latency", "value": "18", "unit": "ms", "delta": "sensor to command" }, { "label": "Power draw", "value": "42", "unit": "W", "delta": "full autonomy" } ] } },
{ "block_key": "scifi-clean:benchmark_table", "title": "Benchmarks", "slot": "main", "sort_order": 3, "content": { "eyebrow": "Perception", "heading": "Detection recall by surface", "rows": [ { "label": "Paved road", "value": "99.2", "unit": "%", "bar": 99, "highlight": false }, { "label": "Gravel and dirt", "value": "97.8", "unit": "%", "bar": 98, "highlight": false }, { "label": "Low light", "value": "94.1", "unit": "%", "bar": 94, "highlight": true }, { "label": "Heavy dust", "value": "91.6", "unit": "%", "bar": 92, "highlight": false } ], "note": "Measured on the internal Halcyon field set. Numbers are illustrative product data for this demo site." } },
{ "block_key": "stats", "title": "Deployment", "slot": "main", "sort_order": 4, "content": { "heading": "Built for the field", "columns": 3, "countUp": false, "items": [ { "value": "40", "suffix": " C", "label": "Rated operating ceiling" }, { "value": "8", "suffix": " h", "label": "Continuous mission time" }, { "value": "IP67", "label": "Sealed enclosure rating" } ] } },
{ "block_key": "scifi-clean:tech_spec", "title": "Reference platform", "slot": "main", "sort_order": 5, "content": { "caption": "Halcyon H1 reference unit", "rows": [ { "label": "Mass", "value": "28", "unit": "kg" }, { "label": "Top speed", "value": "3.2", "unit": "m/s" }, { "label": "Compute", "value": "128", "unit": "TOPS" }, { "label": "Sensors", "value": "6", "unit": "channels" } ] } },
{ "block_key": "cta", "title": "Closing CTA", "slot": "main", "sort_order": 6, "content": { "background": "band", "heading": "Request a technical briefing", "text": "Thirty minutes with an engineer. Bring your hardest deployment.", "primaryLabel": "Book a briefing", "primaryUrl": "/contact" } }
]
},
{
"slug": "/about",
"title": "About",
"template_key": "default",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "We build one autonomy stack", "level": 1 } },
{ "block_key": "text", "title": "Intro", "slot": "main", "sort_order": 2, "content": { "text": "<p>Halcyon Robotics builds a single runtime that carries a machine from raw sensor data to a motor command. We do not sell a demo. We sell the part that has to work at three in the morning in the rain.</p><p>The team came out of field robotics and controls. We test on real ground before we ship a line of code.</p>" } },
{ "block_key": "team", "title": "Team", "slot": "main", "sort_order": 3, "content": { "heading": "The founding engineers", "layout": "grid", "columns": 3, "members": [ { "name": "R. Novak", "role": "Perception", "bio": "Sensor fusion and detection for off-road ground vehicles." }, { "name": "S. Adeyemi", "role": "Controls", "bio": "Real-time control and state estimation on embedded targets." }, { "name": "T. Winters", "role": "Systems", "bio": "Runtime, safety cases, and field deployment." } ] } },
{ "block_key": "scifi-clean:changelog_console", "title": "Runtime changelog", "slot": "main", "sort_order": 4, "content": { "eyebrow": "Runtime", "heading": "Recent releases", "title": "halcyon-runtime", "entries": [ { "version": "v2.4.0", "date": "2026-06-28", "note": "Dust-robust detection model, retrained on the winter field set." }, { "version": "v2.3.1", "date": "2026-06-09", "note": "Localisation drift cut to 4 cm over a kilometre." }, { "version": "v2.3.0", "date": "2026-05-22", "note": "Control loop moved to a deterministic 1000 Hz scheduler." } ] } }
]
},
{
"slug": "/blog",
"title": "Field notes",
"template_key": "blog-index",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "card", "title": "Post 1", "slot": "main", "sort_order": 1, "content": { "title": "Why we run the control loop at 1000 Hz", "text": "<p>A slower loop looks fine on a bench and fails on gravel. Here is the reasoning.</p>", "link": "/blog/control-loop", "linkLabel": "Read" } },
{ "block_key": "card", "title": "Post 2", "slot": "main", "sort_order": 2, "content": { "title": "Measuring localisation drift honestly", "text": "<p>Drift over one clean kilometre is easy. Drift over a dusty one is the number that matters.</p>", "link": "/blog/localisation-drift", "linkLabel": "Read" } },
{ "block_key": "card", "title": "Post 3", "slot": "main", "sort_order": 3, "content": { "title": "Testing on real ground first", "text": "<p>Simulation earns a machine the right to a field test. It does not replace one.</p>", "link": "/blog/real-ground", "linkLabel": "Read" } }
]
},
{
"slug": "/blog/control-loop",
"parent_slug": "/blog",
"title": "Why we run the control loop at 1000 Hz",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Why we run the control loop at 1000 Hz", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>On a paved test track a 200 Hz loop tracks a path cleanly. On loose gravel the wheels slip between updates and the estimate lags the truth. The machine corrects late and starts to hunt.</p><p>At 1000 Hz the loop sees the slip inside a single wheel rotation. The correction is small and the path stays smooth. The cost is a tighter compute budget, which is why the runtime is deterministic rather than best effort.</p><p>The first version of the platform ran the controller at 250 Hz. It looked fine in the lab and on the paved apron behind the workshop. The trouble started the first time we drove it across a dry creek bed.</p><p>On that surface the tyres broke traction for tens of milliseconds at a time. Between control updates the vehicle rotated a few degrees more than the model expected, and by the time the next update landed the correction it applied was already stale. The machine oscillated around the path instead of settling on it.</p><p>We spent a week convinced the problem was the estimator. It was not. The state estimate was accurate to within the sensor noise floor. The gap was time: the world was changing faster than we were reading it back.</p><p>Raising the loop rate is not free. Every doubling halves the time budget for the estimator, the planner, and the control solve. At 1000 Hz the whole chain has to finish in under a millisecond, every millisecond, with no exceptions and no pauses to collect memory.</p><p>That constraint is why the runtime is written the way it is. Memory is allocated once at start-up. The control thread is pinned to an isolated core. The scheduler is deterministic rather than best effort, so a slow frame is a bug we can catch instead of a statistic we learn to tolerate.</p><p>The payoff shows up on exactly the surfaces that used to break it. On gravel and loose dirt the corrections are now small and continuous. The path traces stay inside a few centimetres of the reference even when the tyres are slipping the whole way through a turn.</p><p>There is a ceiling to this. Past a certain rate the actuators cannot respond any faster and the extra updates buy nothing but heat. For our drivetrain that ceiling sits a little above 1000 Hz, which is why we stopped there rather than chase a larger number for its own sake.</p><p>If you are specifying a system like this, do not ask a vendor for their loop rate in isolation. Ask what surface they measured on, what the worst-case frame time is, and what happens on the frame that runs long. Those three answers tell you far more than the headline figure.</p>" } }
]
},
{
"slug": "/blog/localisation-drift",
"parent_slug": "/blog",
"title": "Measuring localisation drift honestly",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Measuring localisation drift honestly", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Every vendor quotes a drift figure. Few say what surface it came from. Drift on a flat paved loop tells you almost nothing about a field run.</p><p>We report drift over a kilometre of mixed dirt and gravel with dust in the air. The number is worse and it is the one you can trust. Ask any supplier for the surface behind the figure.</p>" } }
]
},
{
"slug": "/blog/real-ground",
"parent_slug": "/blog",
"title": "Testing on real ground first",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Testing on real ground first", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Simulation is where a change proves it will not do something stupid. It is fast and it is cheap and it catches the obvious failures. It does not catch the ground.</p><p>Real dirt has ruts the model never saw and light the camera never met. A change earns a field test in simulation. It earns a release on the field.</p>" } }
]
},
{
"slug": "/contact",
"title": "Contact",
"template_key": "contact",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "contact-form", "title": "Contact form", "slot": "main", "sort_order": 1, "content": { "heading": "Request a briefing", "description": "Tell us the deployment. We reply within one business day.", "submitLabel": "Send message", "successMessage": "Received. We will be in touch within one business day.", "formConfig": { "targetTable": "contact_submissions", "enabled": true, "captchaEnabled": false }, "fields": [ { "name": "name", "label": "Name", "type": "text", "required": true, "width": "half" }, { "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" }, { "name": "organisation", "label": "Organisation", "type": "text", "required": false, "width": "full" }, { "name": "message", "label": "What are you deploying?", "type": "textarea", "required": true, "width": "full" } ] } },
{ "block_key": "contact-card", "title": "Contact details", "slot": "aside", "sort_order": 1, "content": { "heading": "Direct", "columns": 1, "channels": [ { "icon": "lucide:mail", "label": "Email", "value": "field@halcyon.example", "link": "mailto:field@halcyon.example" }, { "icon": "lucide:phone", "label": "Phone", "value": "+61 2 5550 0142", "link": "tel:+61255500142" } ] } },
{ "block_key": "hours-location", "title": "Lab hours", "slot": "aside", "sort_order": 2, "content": { "heading": "Lab", "address": "Unit 4, 22 Fabrication Way, Fremantle WA 6160", "hours": [ { "day": "Monday to Friday", "hours": "8:00 to 18:00" }, { "day": "Saturday and Sunday", "closed": true } ] } }
]
},
{
"slug": "/login",
"title": "Sign in",
"template_key": "auth",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "Field notes", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
]
}

View File

@ -1,19 +0,0 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Notify admins on contact submission",
"description": "Emails site admins whenever the Halcyon Robotics contact form receives a message.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

38
status_bar.go Normal file
View File

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

46
status_bar.templ Normal file
View File

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

84
status_bar_templ.go Normal file
View File

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

50
tech_spec.go Normal file
View File

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

53
tech_spec.templ Normal file
View File

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

177
tech_spec_templ.go Normal file
View File

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

281
template.templ Normal file
View File

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

643
template_templ.go Normal file
View File

@ -0,0 +1,643 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData holds the values lifted out of the render `doc` map for the
// four Sci-Fi Clean page templates.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
// parseScifiPageData lifts the well-known keys out of the inbound doc map.
// Unknown keys (e.g. block content) are ignored by the page renderers.
func parseScifiPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// htmlClassForMode returns the root <html> class for the current theme mode.
func htmlClassForMode(mode string) string {
if mode == "dark" {
return "dark"
}
return ""
}
// ===== Default template (header / main / footer) =====
func ScifiClean(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 = []any{htmlClassForMode(data.ThemeMode)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</header><main class=\"flex-grow max-w-5xl mx-auto w-full px-4 py-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"py-20 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Landing template (hero / specs / main / cta / footer) =====
func ScifiCleanLanding(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 = []any{htmlClassForMode(data.ThemeMode)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if specs, ok := data.Slots["specs"]; ok && specs != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<section class=\"w-full hairline-b py-12\"><div class=\"max-w-6xl mx-auto px-4\"><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(specs).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"max-w-6xl mx-auto px-4 py-16\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cta, ok := data.Slots["cta"]; ok && cta != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<section class=\"w-full hairline-t py-16\"><div class=\"max-w-6xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(cta).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Article template (header / rail / main / footer) =====
func ScifiCleanArticle(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 = []any{htmlClassForMode(data.ThemeMode)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<header class=\"w-full hairline-b\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</header><main class=\"flex-grow w-full\"><div class=\"max-w-5xl mx-auto px-4 py-12 grid grid-cols-12 gap-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if rail, ok := data.Slots["rail"]; ok && rail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<aside class=\"col-span-12 md:col-span-3 scifi-mono uppercase text-xs tracking-widest text-muted-foreground space-y-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(rail).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<article class=\"col-span-12 md:col-span-9 scifi-body max-w-2xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<p class=\"text-muted-foreground\">No article body.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</article></div></main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Full-width template (header / main / footer) =====
func ScifiCleanFullWidth(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 = []any{htmlClassForMode(data.ThemeMode)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<html lang=\"en\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<body class=\"scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col\" style=\"font-family: var(--font-body);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</header><main class=\"flex-grow w-full bg-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<div class=\"max-w-5xl mx-auto py-20 px-4 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ===== Render entry points (adapted by wrap() in register.go) =====
func RenderScifiClean(ctx context.Context, doc map[string]any) templ.Component {
return ScifiClean(parseScifiPageData(doc))
}
func RenderScifiCleanLanding(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanLanding(parseScifiPageData(doc))
}
func RenderScifiCleanArticle(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanArticle(parseScifiPageData(doc))
}
func RenderScifiCleanFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return ScifiCleanFullWidth(parseScifiPageData(doc))
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,63 +0,0 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ site_name }}</title>
<style type="text/css">
:root {
--sc-email-bg: hsl(210 16% 95%);
--sc-email-surface: hsl(0 0% 100%);
--sc-email-fg: hsl(215 30% 12%);
--sc-email-muted-fg: hsl(215 12% 42%);
--sc-email-border: hsl(215 14% 88%);
--sc-email-accent: hsl(215 90% 45%);
--sc-email-signal: hsl(18 95% 52%);
}
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
h1, h2, h3, h4, h5, h6 { font-family: 'Space Grotesk', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-weight: 600; letter-spacing: -0.02em; }
@media only screen and (max-width: 620px) {
.sc-email-container { width: 100% !important; max-width: 100% !important; }
.sc-email-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style="background-color: var(--sc-email-bg, hsl(210 16% 95%)); margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 32px 12px; background-color: var(--sc-email-bg, hsl(210 16% 95%));">
<table role="presentation" class="sc-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; width: 600px; background-color: var(--sc-email-surface, hsl(0 0% 100%)); border: 1px solid var(--sc-email-border, hsl(215 14% 88%)); border-collapse: separate;">
<!-- Header: title + single accent rule -->
<tr>
<td class="sc-email-padding" style="padding: 32px 48px 16px 48px; background-color: var(--sc-email-surface, hsl(0 0% 100%));">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 36px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 20px; font-weight: 600; color: var(--sc-email-fg, hsl(215 30% 12%)); letter-spacing: -0.02em; font-family: 'Space Grotesk', 'Inter', sans-serif;">{{ site_name }}</h1>{% endif %}</td>
</tr>
<tr>
<td style="padding: 0 48px;"><div style="height: 2px; background-color: var(--sc-email-accent, hsl(215 90% 45%)); line-height: 2px; font-size: 0;">&nbsp;</div><div style="height: 2px; width: 48px; background-color: var(--sc-email-signal, hsl(18 95% 52%)); line-height: 2px; font-size: 0; margin-top: 2px;">&nbsp;</div></td>
</tr>
<!-- Body -->
<tr>
<td class="sc-email-padding" style="padding: 32px 48px; color: var(--sc-email-fg, hsl(215 30% 12%)); font-size: 16px; line-height: 1.6; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">{{ body|safe }}</td>
</tr>
<!-- Footer: legal line + unsubscribe -->
<tr>
<td class="sc-email-padding" style="padding: 24px 48px 32px 48px; border-top: 1px solid var(--sc-email-border, hsl(215 14% 88%));">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="left">{% if site_name %}<p style="margin: 0 0 6px 0; font-size: 13px; color: var(--sc-email-fg, hsl(215 30% 12%)); font-family: 'Space Grotesk', 'Inter', sans-serif; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;">{{ site_name }}</p>{% endif %}<p style="margin: 0 0 12px 0; font-size: 12px; color: var(--sc-email-muted-fg, hsl(215 12% 42%)); font-family: 'Inter', sans-serif;">You are receiving this email because you subscribed to updates.</p>{% if site_url %}<p style="margin: 0 0 8px 0; font-size: 12px;"><a href="{{ site_url }}" style="color: var(--sc-email-accent, hsl(215 90% 45%)); text-decoration: none;">{{ site_url }}</a></p>{% endif %}{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: var(--sc-email-muted-fg, hsl(215 12% 42%)); text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,8 +0,0 @@
{# announcement-bar override — centered notice; carries data-bn-announcement / data-bn-annkey and a data-bn-ann-dismiss close button so the host dismissal JS binds. #}
{% if variant == "promo" %}{% set annvar = "sc-announce--promo" %}{% set annsty = "background:hsl(var(--primary));color:hsl(var(--primary-foreground));" %}{% elif variant == "alert" %}{% set annvar = "sc-announce--alert" %}{% set annsty = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground));" %}{% else %}{% set annvar = "sc-announce--info" %}{% set annsty = "background:hsl(var(--accent));color:hsl(var(--accent-foreground));" %}{% endif %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="sc-announce {{ annvar }} {{ class }}" style="{{ annsty }}">
<div class="sc-content-well sc-announce-row" style="position:relative;display:flex;align-items:center;justify-content:center;gap:0.75rem;padding-top:0.5rem;padding-bottom:0.5rem;font-size:0.875rem;text-align:center;">
<p style="margin:0;font-weight:500;">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="sc-announce-close" style="position:absolute;right:0.75rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;padding:0.25rem;background:none;border:0;color:inherit;cursor:pointer;">::lucide:x:sm::</button>{% endif %}
</div>
</div>

View File

@ -1,14 +0,0 @@
{# audio-embed override — hairline card row; carries data-bn-audio / data-audio-url / data-audio-media + data-bn-audio-slot so the host facade JS binds. #}
<figure class="sc-card {{ class }}" data-bn-audio data-audio-url="{{ url|default:'' }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}" style="margin:2rem 0;padding:0;overflow:hidden;">
<div style="display:flex;align-items:center;gap:1rem;padding:1rem;">
{% if artwork %}<div class="sc-media-frame" style="width:4rem;height:4rem;flex:0 0 auto;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;">{% img artwork alt=title|default:"" class="sc-audio-art" %}</div>{% endif %}
<div style="min-width:0;flex:1;">
{% if title %}<p class="sc-display" style="margin:0;font-size:0.9375rem;color:hsl(var(--foreground));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ title }}</p>{% endif %}
{% if artist %}<p style="margin:0;font-size:0.8125rem;color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}
<div data-bn-audio-slot style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem;">
<button type="button" data-bn-audio-play class="sc-btn-primary sc-btn-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}" style="display:inline-flex;align-items:center;gap:0.375rem;">::lucide:play:sm:: Play</button>
{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="sc-btn-ghost" style="display:inline-flex;align-items:center;gap:0.25rem;font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;">::lucide:download:sm:: Download</a>{% endif %}
</div>
</div>
</div>
</figure><style>.sc-audio-art{display:block;width:100%;height:100%;object-fit:cover;}</style>

View File

@ -1,22 +0,0 @@
<div class="sc-stack {{ class }}" data-block="scifi-clean:auth-form">
<input type="radio" name="sc-auth-tab" id="sc-auth-login" class="sc-auth-radio" style="display:none;"{% if default_tab != "register" %} checked{% endif %}>
<input type="radio" name="sc-auth-tab" id="sc-auth-register" class="sc-auth-radio" style="display:none;"{% if default_tab == "register" %} checked{% endif %}>
<div class="sc-auth-tabs" style="display:flex; gap:1.5rem; border-bottom:1px solid hsl(var(--border));">
<label for="sc-auth-login" class="sc-auth-tab sc-label" style="cursor:pointer; padding:0.75rem 0;">Sign in</label>
<label for="sc-auth-register" class="sc-auth-tab sc-label" style="cursor:pointer; padding:0.75rem 0;">Create account</label>
</div>
<div id="auth-message" role="status" aria-live="polite"></div>
<form class="sc-auth-panel sc-auth-panel-login sc-form" hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="sc-field sc-field--full"><label class="sc-label" for="sc-login-email">Email</label><input class="sc-input" type="email" name="email" id="sc-login-email" required></div>
<div class="sc-field sc-field--full"><label class="sc-label" for="sc-login-password">Password</label><input class="sc-input" type="password" name="password" id="sc-login-password" required></div>
<button type="submit" class="sc-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Sign in</button>
<p class="sc-form-note" style="text-align:center;"><a href="/forgot-password" style="color: hsl(var(--accent)); text-decoration: underline; text-underline-offset: 2px;">Forgot password?</a></p>
</form>
<form class="sc-auth-panel sc-auth-panel-register sc-form" hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="sc-field sc-field--full"><label class="sc-label" for="sc-reg-username">Username</label><input class="sc-input" type="text" name="username" id="sc-reg-username" required pattern="[a-zA-Z0-9_-]{3,30}"></div>
<div class="sc-field sc-field--full"><label class="sc-label" for="sc-reg-email">Email</label><input class="sc-input" type="email" name="email" id="sc-reg-email" required></div>
<div class="sc-field sc-field--full"><label class="sc-label" for="sc-reg-password">Password</label><input class="sc-input" type="password" name="password" id="sc-reg-password" required minlength="8"></div>
<button type="submit" class="sc-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Create account</button>
</form>
<style>.sc-auth-panel{display:none;} #sc-auth-login:checked ~ .sc-auth-panel-login{display:grid;} #sc-auth-register:checked ~ .sc-auth-panel-register{display:grid;}</style>
</div>

View File

@ -1 +0,0 @@
{% if context.isPublicLoggedIn %}<div class="sc-btn-row {{ class }}" data-block="scifi-clean:auth-status" style="align-items: center;"><span class="sc-mono" style="color: hsl(var(--muted-foreground));">{{ context.publicDisplayName|default:context.publicUsername }}</span><a class="sc-btn-outline sc-btn-sm" href="/account">Account</a><a class="sc-btn-ghost sc-btn-sm" href="/api/auth/logout">Sign out</a></div>{% else %}<div class="sc-btn-row {{ class }}" data-block="scifi-clean:auth-status" style="align-items: center;"><a class="sc-btn-outline sc-btn-sm" href="/login">Sign in</a><a class="sc-btn-primary sc-btn-sm" href="/login?tab=register">Create account</a></div>{% endif %}

View File

@ -1 +0,0 @@
{% if context.currentAuthor %}<section class="sc-section {{ class }}" data-block="scifi-clean:author-bio-hero"><div class="sc-content-well"><div class="sc-measure sc-stack" style="text-align: center; margin-left: auto; margin-right: auto;">{% if showAvatar and context.currentAuthor.avatarUrl %}<img class="sc-avatar" src="{{ context.currentAuthor.avatarUrl }}" alt="{{ context.currentAuthor.displayName }}" style="width: 6rem; height: 6rem; border-radius: 9999px; object-fit: cover; margin-left: auto; margin-right: auto;">{% else %}<span class="sc-mark sc-mark-ring" aria-hidden="true" style="margin-left: auto; margin-right: auto;"></span>{% endif %}<h1 class="sc-display" style="margin: 0;">{{ context.currentAuthor.displayName }}</h1>{% if context.currentAuthor.bio %}<p class="sc-lede" style="margin: 0;">{{ context.currentAuthor.bio }}</p>{% endif %}{% if showSocial %}<div class="sc-btn-row" style="justify-content: center;">{% if context.currentAuthor.websiteUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.websiteUrl }}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#globe"/></svg></a>{% endif %}{% if context.currentAuthor.twitterHandle %}<a class="sc-btn-ghost sc-btn-sm" href="https://twitter.com/{{ context.currentAuthor.twitterHandle|cut:"@" }}" target="_blank" rel="noopener noreferrer" title="X"><svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#x"/></svg></a>{% endif %}{% if context.currentAuthor.linkedinUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.linkedinUrl }}" target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"/></svg></a>{% endif %}</div>{% endif %}</div></div></section>{% endif %}

View File

@ -1 +0,0 @@
{% if context.currentPost.authorName %}<aside class="sc-card sc-card-accent {{ class }}" data-block="scifi-clean:author-bio">{% if heading %}<p class="sc-kicker" style="margin-top: 0;">{{ heading }}</p>{% endif %}<div style="display: flex; align-items: flex-start; gap: 1rem;">{% if showAvatar and context.currentPost.authorAvatarUrl %}<img class="sc-avatar" src="{{ context.currentPost.authorAvatarUrl }}" alt="{{ context.currentPost.authorName }}" style="width: 3.5rem; height: 3.5rem; border-radius: 9999px; object-fit: cover; flex-shrink: 0;">{% endif %}<div style="min-width: 0;"><h3 class="sc-heading" style="font-size: 1.125rem; margin: 0;">{% if context.currentPost.authorSlug %}<a href="/author/{{ context.currentPost.authorSlug }}">{{ context.currentPost.authorName }}</a>{% else %}{{ context.currentPost.authorName }}{% endif %}</h3>{% if context.currentAuthor.bio %}<p class="sc-text" style="margin: 0.5rem 0 0 0; font-size: 0.9375rem; color: hsl(var(--muted-foreground));">{{ context.currentAuthor.bio }}</p>{% endif %}</div></div></aside>{% endif %}

View File

@ -1 +0,0 @@
<section class="sc-section {{ class }}" data-block="scifi-clean:blog-hero"><div class="sc-content-well"><div class="sc-stack">{% if title %}<h1 class="sc-heading" style="font-size: clamp(2rem, 4vw, 3rem); margin: 0;"><span class="sc-heading-h2-accent">{{ title }}</span></h1>{% endif %}{% if subtitle %}<p class="sc-lede" style="max-width: 60ch; margin: 0.75rem 0 0 0;">{{ subtitle }}</p>{% endif %}</div>{% if posts %}<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-3" style="margin-top: 2.5rem;">{% for post in posts %}<article class="sc-post-card{% if forloop.First %} sc-card-accent{% endif %}">{% if post.featured_image_url %}<a href="{{ post.url }}" class="sc-post-thumb sc-media-frame" style="display: block; aspect-ratio: 16 / 9;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<h2 class="sc-post-title"{% if forloop.First %} style="font-size: 1.5rem;"{% endif %}><a href="{{ post.url }}">{{ post.title }}</a></h2>{% if showExcerpt and post.excerpt %}<p class="sc-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="sc-post-meta">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="sc-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div></article>{% endfor %}</div>{% endif %}</div></section>

View File

@ -1 +0,0 @@
<nav aria-label="Breadcrumb" class="sc-crumbs {{ class }}" data-block="scifi-clean:breadcrumbs"><ol style="display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; list-style: none; margin: 0; padding: 0;">{% if showHome %}<li style="display: flex; align-items: center;"><a class="sc-sidebar-link" href="/" style="padding: 0;">{{ homeLabel|default:"Home" }}</a></li>{% endif %}{% for item in breadcrumbs %}<li style="display: flex; align-items: center;">{% if showHome or not forloop.First %}<span class="sc-crumb-sep" aria-hidden="true">{{ separator|default:"/" }}</span>{% endif %}{% if item.is_current %}{% if showCurrent %}<span aria-current="page" style="color: hsl(var(--foreground)); font-weight: 500;">{{ item.label }}</span>{% endif %}{% else %}<a class="sc-sidebar-link" href="{{ item.url }}" style="padding: 0;">{{ item.label }}</a>{% endif %}</li>{% endfor %}</ol></nav>

View File

@ -1,6 +0,0 @@
{% set btnStyle = style|default:"primary" %}
{% set btnClass = "sc-btn-primary" %}
{% if btnStyle == "outline" or btnStyle == "secondary" %}{% set btnClass = "sc-btn-outline" %}{% elif btnStyle == "ghost" or btnStyle == "link" %}{% set btnClass = "sc-btn-ghost" %}{% endif %}
{% set sizeClass = "" %}
{% if size == "sm" %}{% set sizeClass = "sc-btn-sm" %}{% elif size == "lg" %}{% set sizeClass = "sc-btn-lg" %}{% endif %}
<div class="sc-btn-row {{ class }}" style="{% if align == "center" %}justify-content: center;{% elif align == "right" %}justify-content: flex-end;{% endif %}">{% if newTab %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}" target="_blank" rel="noopener noreferrer">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>{% else %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>{% endif %}</div>

View File

@ -1 +0,0 @@
<article class="sc-card sc-card-accent {{ class }}">{% if media %}<div class="sc-media-frame" style="aspect-ratio: 16 / 9; margin: -1.5rem -1.5rem 1.25rem -1.5rem; border: 0; border-bottom: 1px solid hsl(var(--border));">{% img media alt=title|default:"" class="sc-card-media" %}</div>{% endif %}{% if title %}<h3 class="sc-feature-title" style="margin-top: 0; font-size: 1.25rem;">{{ title }}</h3>{% endif %}{% if text %}<div class="sc-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<a class="sc-btn-ghost" href="{{ link }}" style="margin-top: 1rem;">{{ linkLabel }}::lucide:arrow-right:sm::</a>{% endif %}</article>

View File

@ -1 +0,0 @@
<section class="sc-section-tight {{ class }}" data-block="scifi-clean:category-list">{% if heading %}<h2 class="sc-kicker">{{ heading }}</h2>{% endif %}{% if style == "pills" %}<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">{% for cat in categories %}<a href="{{ cat.url }}" class="sc-tag">{{ cat.name }}{% if showCount %} <span class="sc-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a>{% endfor %}</div>{% else %}<ul class="sc-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for cat in categories %}<li><a href="{{ cat.url }}" class="sc-sidebar-link" style="display: flex; align-items: center; justify-content: space-between;"><span>{{ cat.name }}</span>{% if showCount %}<span class="sc-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a></li>{% endfor %}</ul>{% endif %}</section>

View File

@ -1,14 +0,0 @@
{# contact-card override — grouped contact channels; each channel a hairline mark + label + value. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="sc-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
<div style="display:grid;gap:1rem;{% if columns == 3 %}grid-template-columns:repeat(auto-fill, minmax(240px, 1fr));{% elif columns == 1 %}grid-template-columns:1fr;{% else %}grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));{% endif %}">
{% for channel in channels %}<div class="sc-channel sc-card" style="display:flex;align-items:flex-start;gap:1rem;">
{% if channel.icon %}<span class="sc-channel-mark" style="display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;flex:0 0 auto;border:1px solid hsl(var(--border));border-radius:4px;color:hsl(var(--accent));">::{{ channel.icon }}:sm::</span>{% endif %}
<div style="min-width:0;">
{% if channel.label %}<div class="sc-stat-label" style="margin:0;">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div style="margin-top:0.25rem;font-weight:500;word-break:break-word;color:hsl(var(--foreground));">{% if channel.link %}<a href="{{ channel.link }}" style="color:inherit;text-decoration:none;">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>{% endfor %}
</div>
</div></section>

View File

@ -1,34 +0,0 @@
{# contact-form override — server-driven; block-id resolution, {% form %} wiring, honeypot, and field loop copied verbatim from the builtin, reskinned with sc-form primitives. #}
{% with bid=_block_id|default:context.blockId %}
<section id="contact-form-{{ bid }}" class="sc-section {{ class }}"><div class="sc-content-well" style="max-width:640px;">
{% if form.submitted %}
<div class="sc-card sc-card-accent sc-alert-ok" style="text-align:center;padding:2rem;">
<span class="sc-mark sc-mark-circle" aria-hidden="true" style="display:inline-block;margin-bottom:1rem;"></span>
<p class="sc-display" style="margin:0;font-size:1.125rem;">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
</div>
{% else %}
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="sc-lede" style="margin:0 0 2rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
{% if form.errors._form %}<div class="sc-alert-err" style="margin-bottom:1.5rem;border:1px solid hsl(var(--destructive));border-radius:4px;padding:0.75rem 1rem;font-size:0.875rem;color:hsl(var(--destructive));background:hsl(var(--destructive) / 0.08);">{{ form.errors._form }}</div>{% endif %}
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="sc-form-grid" %}
{% for field in fields %}
<div class="sc-field {% if field.width == "half" %}{% else %}sc-field--full{% endif %}">
{% if field.label %}<label class="sc-label" for="cf-{{ bid }}-{{ field.name }}">{{ field.label }}{% if field.required %} <span style="color:hsl(var(--destructive));">*</span>{% endif %}</label>{% endif %}
{% if field.type == "textarea" %}
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="sc-input"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="sc-input">{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}</select>
{% else %}
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="sc-input">
{% endif %}
</div>
{% endfor %}
<div class="sc-field--full" aria-hidden="true" style="display:none;"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
<div class="sc-field--full"><button type="submit" class="sc-btn-primary">{{ submitLabel|default:"Send message" }}</button></div>
{% endform %}
{% endwith %}
{% endif %}
</div></section>
<style>#contact-form-{{ bid }} .sc-form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.25rem;margin-top:2rem;}@media (max-width:640px){ #contact-form-{{ bid }} .sc-form-grid{grid-template-columns:1fr;}}#contact-form-{{ bid }} .sc-field--full{grid-column:1 / -1;}#contact-form-{{ bid }} .sc-label{display:block;margin-bottom:0.375rem;font-size:0.8125rem;font-weight:500;color:hsl(var(--foreground));font-family:var(--font-heading, "Inter Tight", "Inter", system-ui, sans-serif);}#contact-form-{{ bid }} .sc-input{width:100%;padding:0.5rem 0.75rem;font-size:0.9375rem;font-family:var(--font-body, "Inter", system-ui, sans-serif);color:hsl(var(--foreground));background:hsl(var(--background));border:1px solid hsl(var(--input));border-radius:4px;box-sizing:border-box;}#contact-form-{{ bid }} .sc-input:focus{outline:2px solid hsl(var(--ring));outline-offset:1px;}</style>
{% endwith %}

View File

@ -1,44 +0,0 @@
{# countdown override — oversized mono unit boxes; host countdown JS binds via data-* attrs. #}
<section class="sc-section {{ class }}"><div class="sc-content-well" style="text-align: center;">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 auto; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
<div data-countdown style="margin-top: 2.5rem;">
<div data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining" style="display: flex; align-items: flex-start; justify-content: center; gap: 1rem; flex-wrap: wrap;">
<div class="sc-count-unit {% if variant == "boxes" %}sc-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="sc-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="days">--</span><span class="sc-stat-label" style="margin-top: 0.5rem;">{{ labelDays|default:"Days" }}</span></div>
<div class="sc-count-unit {% if variant == "boxes" %}sc-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="sc-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="hours">--</span><span class="sc-stat-label" style="margin-top: 0.5rem;">{{ labelHours|default:"Hours" }}</span></div>
<div class="sc-count-unit {% if variant == "boxes" %}sc-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="sc-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="minutes">--</span><span class="sc-stat-label" style="margin-top: 0.5rem;">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="sc-count-unit {% if variant == "boxes" %}sc-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="sc-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="seconds">--</span><span class="sc-stat-label" style="margin-top: 0.5rem;">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div data-countdown-expired style="display: none; margin-top: 2rem;"><p class="sc-heading" style="font-size: 1.25rem;">{{ expiredMessage|default:"This has started." }}</p>{% if expiredUrl and expiredLabel %}<a class="sc-btn-primary" href="{{ expiredUrl }}" style="margin-top: 1rem;">{{ expiredLabel }}</a>{% endif %}</div>
</div>
<noscript><p class="sc-text" style="margin-top: 1.5rem; color: hsl(var(--muted-foreground));">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
</div></section>
<script>
(function(){
var timers = document.querySelectorAll('[data-countdown-timer]');
timers.forEach(function(el){
var target = new Date(el.getAttribute('data-target')).getTime();
if (isNaN(target)) return;
var wrap = el.closest('[data-countdown]');
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
function pad(n){ return (n < 10 ? '0' : '') + n; }
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
var interval;
function tick(){
var diff = target - Date.now();
if (diff <= 0){
el.style.display = 'none';
if (expired) expired.style.display = 'block';
if (interval) clearInterval(interval);
return;
}
set('days', Math.floor(diff / 86400000));
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
}
tick();
interval = setInterval(tick, 1000);
});
})();
</script>

View File

@ -1,8 +0,0 @@
{# cta override — full-width strip; band / card / split variants. #}
<section class="sc-cta-strip {% if background == "band" %}sc-cta-strip--band{% elif background == "card" %}sc-cta-strip--card{% endif %} {{ class }}" style="padding-top: 4rem; padding-bottom: 4rem;"><div class="sc-content-well" style="display: flex; flex-wrap: wrap; align-items: center; gap: 2rem; {% if background == "split" %}justify-content: space-between;{% endif %}">
<div style="{% if background != "split" %}max-width: 60ch;{% endif %}">
{% if heading %}<h2 class="sc-cta-headline sc-heading" style="font-size: clamp(1.75rem, 4vw, 2.75rem); margin: 0; color: inherit;">{{ heading }}</h2>{% endif %}
{% if text %}<p class="sc-lede" style="margin: 0.75rem 0 0 0; max-width: 55ch; color: {% if background == "band" %}inherit{% else %}hsl(var(--muted-foreground)){% endif %};">{{ text }}</p>{% endif %}
</div>
{% if primaryLabel or secondaryLabel %}<div class="sc-btn-row" style="display: flex; flex-wrap: wrap; gap: 0.75rem; flex: none;">{% if primaryUrl and primaryLabel %}<a class="sc-btn-primary sc-btn-lg" href="{{ primaryUrl }}">{{ primaryLabel }}</a>{% endif %}{% if secondaryUrl and secondaryLabel %}<a class="sc-btn-outline sc-btn-lg" href="{{ secondaryUrl }}">{{ secondaryLabel }}</a>{% endif %}</div>{% endif %}
</div></section>

View File

@ -1,2 +0,0 @@
{# divider override — plain hairline / thick spacer rule / centered ornament between two hairlines. #}
{% if variant == "ornament" %}<div class="sc-divider-ornament {{ class }}" role="separator" style="display:flex;align-items:center;gap:1rem;margin:2.5rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}"><span class="sc-hairline-top" style="flex:1;height:0;"></span><span style="display:inline-flex;color:hsl(var(--muted-foreground));">::{{ icon|default:"lucide:asterisk" }}:md::</span><span class="sc-hairline-top" style="flex:1;height:0;"></span></div>{% elif variant == "spacer-rule" %}<hr class="sc-divider-line sc-divider-thick {{ class }}" role="separator" style="border:0;border-top:{% if weight == "thick" %}4px{% elif weight == "medium" %}2px{% else %}1px{% endif %} solid hsl(var(--border));margin:3rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}">{% else %}<hr class="sc-divider-line {{ class }}" role="separator" style="border:0;border-top:{% if weight == "thick" %}4px{% elif weight == "medium" %}2px{% else %}1px{% endif %} solid hsl(var(--border));margin:2rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}">{% endif %}

View File

@ -1,31 +0,0 @@
{# event-list override — dates shown VERBATIM in each event's own timezone (never converted); list rows or card grid. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div style="margin-top:2.5rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(260px, 1fr));gap:1.5rem;">
{% for event in events %}{% if not (hidePast and event.past) %}<article class="sc-card" style="display:flex;flex-direction:column;padding:0;overflow:hidden;{% if event.past %}opacity:0.6;{% endif %}">
{% if event.media %}<div class="sc-media-frame" style="aspect-ratio:16 / 9;overflow:hidden;border-bottom:1px solid hsl(var(--border));">{% img event.media alt=event.title|default:"" class="sc-event-media" %}</div>{% endif %}
<div style="display:flex;flex-direction:column;gap:0.5rem;padding:1.25rem;flex:1;">
<div class="sc-mono numeric-tabular" style="font-size:0.8125rem;color:hsl(var(--accent));"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} &ndash; {{ event.end }}{% endif %}</time>{% if event.timezone %} <span style="color:hsl(var(--muted-foreground));">{{ event.timezone }}</span>{% endif %}</div>
<h3 class="sc-feature-title" style="font-size:1.125rem;margin:0;">{{ event.title }}</h3>
{% if event.venue %}<p style="margin:0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p style="margin:0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.description }}</p>{% endif %}
{% if event.link %}<a class="sc-btn-ghost" href="{{ event.link }}" style="margin-top:auto;display:inline-flex;align-items:center;gap:0.375rem;">{{ event.linkLabel|default:"Details" }}::lucide:arrow-right:sm::</a>{% endif %}
</div>
</article>{% endif %}{% endfor %}
</div><style>.sc-event-media{display:block;width:100%;height:100%;object-fit:cover;}</style>
{% else %}
<ul class="sc-hairline-top" style="list-style:none;margin:2.5rem 0 0 0;padding:0;">
{% for event in events %}{% if not (hidePast and event.past) %}<li class="sc-hairline-bottom" style="display:flex;flex-wrap:wrap;align-items:baseline;gap:0.75rem 1.5rem;padding:1.25rem 0;{% if event.past %}opacity:0.6;{% endif %}">
<div class="sc-mono numeric-tabular" style="flex:0 0 12rem;font-size:0.875rem;"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time>{% if event.end %}<div style="font-size:0.75rem;color:hsl(var(--muted-foreground));">to {{ event.end }}</div>{% endif %}{% if event.timezone %}<div style="font-size:0.75rem;color:hsl(var(--muted-foreground));">{{ event.timezone }}</div>{% endif %}</div>
<div style="min-width:0;flex:1;">
<h3 class="sc-feature-title" style="font-size:1rem;margin:0;">{{ event.title }}</h3>
{% if event.venue %}<p style="margin:0.125rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p style="margin:0.25rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a class="sc-btn-outline sc-btn-sm" href="{{ event.link }}" style="flex:0 0 auto;">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>{% endif %}{% endfor %}
</ul>
{% endif %}
</div></section>

View File

@ -1,6 +0,0 @@
{# faq override — native hairline-divided disclosure list. #}
<section class="sc-section {{ class }}"><div class="sc-content-well sc-measure">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="sc-hairline-top" style="margin-top: 1.5rem;">{% for item in items %}<details class="sc-faq-item sc-hairline-bottom"{% if variant == "open-list" %} open{% endif %}><summary class="sc-heading" style="display: flex; justify-content: space-between; align-items: center; gap: 1rem; cursor: pointer; padding: 1rem 0; font-size: 1.125rem; list-style: none;">{{ item.question }}<span class="sc-faq-chevron" aria-hidden="true" style="color: hsl(var(--muted-foreground)); flex: none;">::lucide:chevron-down:sm::</span></summary><div class="sc-faq-answer sc-text" style="padding-bottom: 1rem; color: hsl(var(--muted-foreground));">{{ item.answer|safe }}</div></details>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -1,7 +0,0 @@
{# feature-grid override — schematic cells, each led by a blueprint square mark. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if eyebrow %}<p class="sc-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="grid grid-cols-1 gap-6 {% if columns == 2 %}sm:grid-cols-2{% elif columns == 4 %}sm:grid-cols-2 lg:grid-cols-4{% else %}sm:grid-cols-2 lg:grid-cols-3{% endif %}">{% for item in items %}<div class="sc-feature">{% if item.icon %}<span class="sc-mark sc-mark-square" aria-hidden="true">::{{ item.icon }}:sm::</span>{% else %}<span class="sc-mark sc-mark-square" aria-hidden="true"></span>{% endif %}{% if item.title %}<h3 class="sc-feature-title">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" style="color: inherit; text-decoration: none;">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>{% endif %}{% if item.text %}<p class="sc-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ item.text }}</p>{% endif %}{% if item.linkUrl and item.linkLabel %}<a class="sc-btn-ghost" href="{{ item.linkUrl }}" style="margin-top: 0.75rem;">{{ item.linkLabel }}::lucide:arrow-right:sm::</a>{% endif %}</div>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -1 +0,0 @@
<section class="sc-section {{ class }}" data-block="scifi-clean:featured-posts"><div class="sc-content-well">{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 2rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="grid grid-cols-1 gap-10 {% if columns == 2 %}sm:grid-cols-2{% else %}sm:grid-cols-2 lg:grid-cols-3{% endif %}">{% for post in posts %}<article class="sc-post-card">{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="sc-post-thumb sc-media-frame" style="display: block; aspect-ratio: 16 / 9;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<h3 class="sc-post-title"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if showExcerpt and post.excerpt %}<p class="sc-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="sc-post-meta">{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if showDate and post.published_at %}<span class="sc-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="sc-mono numeric-tabular">{{ post.reading_time }} min</span>{% endif %}</div></article>{% endfor %}</div></div></section>

View File

@ -1,17 +0,0 @@
{# footer override — schematic-grid footer chrome from CONTENT fields (brand, contact, newsletter, legal); menu columns / social / legal rows render from the footer providers (empty today, forward-correct). #}
<footer class="sc-footer {{ class }}"><div class="sc-content-well">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:2.5rem;align-items:start;">
<div class="sc-stack" style="grid-column:span 2;min-width:220px;display:flex;flex-direction:column;gap:1rem;">
<a href="/" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;color:hsl(var(--foreground));">{% if logo %}{% img logo alt=companyName|default:"Home" class="sc-footer-logo" %}{% else %}<span class="sc-mark sc-mark-square" aria-hidden="true"></span><span class="sc-display" style="font-size:1.125rem;">{{ companyName|default:context.site.title|default:"Studio" }}</span>{% endif %}</a>
{% if tagline %}<p class="sc-text" style="font-size:0.9375rem;max-width:34ch;margin:0;color:hsl(var(--muted-foreground));">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="sc-mono numeric-tabular" style="font-style:normal;font-size:0.8125rem;line-height:1.8;color:hsl(var(--muted-foreground));">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="sc-footer-link" style="color:inherit;text-decoration:none;">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="sc-footer-link" style="color:inherit;text-decoration:none;">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{% if showNewsletter %}<div class="sc-stack-sm" style="margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem;"><p class="sc-footer-head" style="margin:0;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:hsl(var(--foreground));">{{ newsletterTitle|default:"Stay in the loop" }}</p>{% if newsletterDescription %}<p class="sc-text" style="margin:0;font-size:0.8125rem;color:hsl(var(--muted-foreground));">{{ newsletterDescription }}</p>{% endif %}{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div>{% endif %}
</div>
{% for col in columns %}<div class="sc-stack-sm"><p class="sc-footer-head" style="margin:0 0 0.75rem;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:hsl(var(--foreground));">{{ col.header }}</p><ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.5rem;">{% for item in col.items %}<li><a class="sc-footer-link" href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} style="font-size:0.875rem;color:hsl(var(--muted-foreground));text-decoration:none;">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
</div>
<div class="sc-footer-legal" style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:1rem;">
<p style="margin:0;">{% if copyright %}&copy; {{ copyright }}{% else %}&copy; {{ companyName }}{% endif %}</p>
{% if legal_items %}<nav style="display:flex;flex-wrap:wrap;gap:1.25rem;">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="sc-footer-link" style="font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;">{{ l.label }}</a>{% endfor %}</nav>{% endif %}
{% if showSocial and social_items %}<div class="sc-social-row" style="display:flex;align-items:center;gap:0.75rem;">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="sc-social-link" style="display:inline-flex;color:hsl(var(--muted-foreground));">{% if s.icon %}::{{ s.icon }}:sm::{% endif %}</a>{% endfor %}</div>{% endif %}
</div>
</div><style>.sc-footer-logo{max-height:2rem;width:auto;display:block;}.sc-footer-link:hover{color:hsl(var(--foreground));}</style></footer>

View File

@ -1,6 +0,0 @@
{# gallery override — HUD grid of hairline-framed images; carries data-bn-gallery / data-lightbox / data-bn-gallery-item so the host lightbox JS binds. #}
{% if columns == 4 %}{% set gcols = "sc-gallery-4" %}{% set gn = 4 %}{% elif columns == 2 %}{% set gcols = "" %}{% set gn = 2 %}{% else %}{% set gcols = "sc-gallery-3" %}{% set gn = 3 %}{% endif %}
{% if gap == "sm" %}{% set ggap = "0.5rem" %}{% elif gap == "lg" %}{% set ggap = "1.5rem" %}{% else %}{% set ggap = "1rem" %}{% endif %}
<div class="sc-gallery-grid {{ gcols }} {{ class }}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}" style="margin:1.5rem 0;gap:{{ ggap }};">
{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:'' }}" class="sc-media-frame" style="display:block;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));">{% img image.src alt=image.alt|default:"" class="sc-gallery-img" %}</a>{% endfor %}
</div><style>.sc-gallery-grid .sc-gallery-img{display:block;width:100%;height:100%;aspect-ratio:1;object-fit:cover;}</style>

View File

@ -1 +0,0 @@
{% if level|integer == 1 %}<h1 class="sc-heading sc-heading-h1 {{ textClass }}" style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1;">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="sc-heading sc-heading-h3 {{ textClass }}" style="font-size: 1.5rem; line-height: 1.3;">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="sc-heading sc-heading-h4 {{ textClass }}" style="font-size: 1.25rem; line-height: 1.3;">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="sc-heading sc-heading-h5 {{ textClass }}" style="font-size: 1.125rem; line-height: 1.4;">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="sc-heading sc-heading-h6 {{ textClass }}" style="font-size: 1rem; line-height: 1.4;">{{ text }}</h6>{% else %}<h2 class="sc-heading sc-heading-h2 {{ textClass }}" style="font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2;"><span class="sc-heading-h2-accent">{{ text }}</span></h2>{% endif %}

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