feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-04 15:31:20 +08:00
parent d494eedb00
commit f6e2c1dccc
72 changed files with 662 additions and 4031 deletions

View File

@ -1,26 +1,27 @@
# terminal — build & publish helpers (wasm .bnp workflow) # terminal — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
# #
# The theme compiles to a wasip1 reactor-mode wasm module packed into a .bnp # This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# artifact (`ninja plugin build`). The legacy .so path is retired (WO-WZ-015): # (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# the CMS is wasm-native and can no longer load .so plugins. Distribution is # an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# publish-to-registry -> InstallFromRegistry (hot-load, no restart). # 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: # Usage:
# make build # compile wasm + pack terminal-<version>.bnp # make build # pack terminal-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (layout/abi/name/size) # make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` compiles to wasm # make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry) # make publish # ninja plugin publish --bnp (to the active registry)
# make templ # regenerate templ Go files
# make clean # remove build artefacts # make clean # remove build artefacts
.PHONY: build verify archive-check publish templ clean help .PHONY: build verify archive-check publish clean help
PLUGIN_NAME := terminal PLUGIN_NAME := terminal
NINJA := ninja NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Compile to wasm and pack the .bnp (GOOS=wasip1 GOARCH=wasm, DESCRIBE probe, # Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
# tar.zst of plugin.wasm + plugin.mod + manifest.pb + schemas/ + assets/). # + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build: build:
$(NINJA) plugin build --dir . $(NINJA) plugin build --dir .
@ -28,12 +29,12 @@ build:
verify: verify:
$(NINJA) plugin verify $(BNP) $(NINJA) plugin verify $(BNP)
# Prove a clean `git archive HEAD` (what publish ships) compiles to wasm. # Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check: archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \ @T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \ trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \ git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o "$$T/check.wasm" . && \ cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK" echo "archive-check OK"
# Publish the prebuilt .bnp to the active registry. # Publish the prebuilt .bnp to the active registry.
@ -41,19 +42,14 @@ archive-check:
publish: build publish: build
$(NINJA) plugin publish --bnp $(BNP) $(NINJA) plugin publish --bnp $(BNP)
# Regenerate templ Go files locally.
templ:
templ generate
# Remove build artefacts. # Remove build artefacts.
clean: clean:
rm -f $(PLUGIN_NAME)-*.bnp $(PLUGIN_NAME).so plugin.wasm rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help: help:
@echo "Targets:" @echo "Targets:"
@echo " build Compile wasm + pack the .bnp" @echo " build Pack the codeless .bnp (no wasm)"
@echo " verify Validate the built .bnp (loader-identical checks)" @echo " verify Validate the built .bnp (loader-identical checks)"
@echo " archive-check Prove a clean git archive HEAD compiles to wasm" @echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry" @echo " publish Publish the .bnp to the active registry"
@echo " templ Regenerate templ Go files"
@echo " clean Remove build artefacts" @echo " clean Remove build artefacts"

View File

@ -1,46 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// AsciiHeaderBlockMeta defines metadata for the TTY ascii header block.
var AsciiHeaderBlockMeta = blocks.BlockMeta{
Key: "ascii_header",
Title: "ASCII Header",
Description: "Figlet-style TTY banner with a shell prompt line",
Source: "terminal",
Category: blocks.CategoryLayout,
}
// AsciiHeaderData is the parsed content for the ascii_header block.
type AsciiHeaderData struct {
Title string
Prompt string
AsciiArt string
}
// AsciiHeaderBlock renders the TTY banner.
// Content shape: {title, prompt, asciiArt}.
func AsciiHeaderBlock(ctx context.Context, content map[string]any) string {
data := AsciiHeaderData{
Title: getString(content, "title"),
Prompt: getString(content, "prompt"),
AsciiArt: getString(content, "asciiArt"),
}
// Sensible empty-state defaults so unconfigured blocks still render
// something terminal-flavoured rather than an empty surface.
if data.Title == "" && data.AsciiArt == "" {
data.Title = "~"
}
if data.Prompt == "" {
data.Prompt = "$ "
}
var buf bytes.Buffer
_ = asciiHeaderComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,40 +0,0 @@
package main
// figletBanner returns a small block-letter banner for the given title.
// This is intentionally simple — admins who want bespoke figlet output can
// paste it into the asciiArt field which overrides this.
func figletBanner(title string) string {
if title == "" {
return ""
}
return "/*===========================================*/\n" +
"/* " + padRight(title, 41) + " */\n" +
"/*===========================================*/"
}
// padRight pads s with spaces on the right so the total length is at least n.
func padRight(s string, n int) string {
for len(s) < n {
s = s + " "
}
if len(s) > n {
s = s[:n]
}
return s
}
templ asciiHeaderComponent(data AsciiHeaderData) {
<div class="ascii-header" data-block="terminal:ascii_header">
if data.AsciiArt != "" {
<pre>{ data.AsciiArt }</pre>
} else {
<pre>{ figletBanner(data.Title) }</pre>
}
<div class="prompt-line">
<span class="terminal-mono">{ data.Prompt }</span>
if data.Title != "" {
<span class="terminal-mono">{ data.Title }</span>
}
</div>
</div>
}

View File

@ -1,140 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// figletBanner returns a small block-letter banner for the given title.
// This is intentionally simple — admins who want bespoke figlet output can
// paste it into the asciiArt field which overrides this.
func figletBanner(title string) string {
if title == "" {
return ""
}
return "/*===========================================*/\n" +
"/* " + padRight(title, 41) + " */\n" +
"/*===========================================*/"
}
// padRight pads s with spaces on the right so the total length is at least n.
func padRight(s string, n int) string {
for len(s) < n {
s = s + " "
}
if len(s) > n {
s = s[:n]
}
return s
}
func asciiHeaderComponent(data AsciiHeaderData) 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=\"ascii-header\" data-block=\"terminal:ascii_header\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.AsciiArt != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<pre>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.AsciiArt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ascii_header.templ`, Line: 29, Col: 23}
}
_, 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, "</pre>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<pre>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(figletBanner(data.Title))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ascii_header.templ`, Line: 31, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</pre>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"prompt-line\"><span class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Prompt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ascii_header.templ`, Line: 34, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: `ascii_header.templ`, Line: 36, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,3 @@
{% set eff_title = title %}{% if not title and not asciiArt %}{% set eff_title = "~" %}{% endif %}<div class="ascii-header" data-block="terminal:ascii_header">{% if asciiArt %}<pre>{{ asciiArt }}</pre>{% else %}<pre>/*===========================================*/
/* {{ eff_title|slice:":41"|ljust:41 }} */
/*===========================================*/</pre>{% endif %}<div class="prompt-line"><span class="terminal-mono">{{ prompt|default:"$ " }}</span>{% if eff_title %}<span class="terminal-mono">{{ eff_title }}</span>{% endif %}</div></div>

48
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,48 @@
# Terminal theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (terminal:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the hardcoded data-block attributes and the master_pages.json references.
blocks:
- key: terminal:ascii_header
title: ASCII Header
description: Figlet-style TTY banner with a shell prompt line
category: layout
schema: ascii_header.schema.json
template: ascii_header.ninjatpl
- key: terminal:manpage_header
title: Man-page Header
description: NAME(section) ... center ... version row, like a man-page top line
category: layout
schema: manpage_header.schema.json
template: manpage_header.ninjatpl
- key: terminal:toc
title: Section TOC
description: Fixed left-rail table of contents for article pages
category: navigation
schema: toc.schema.json
template: toc.ninjatpl
- key: terminal:code_console
title: Console Block
description: Shell prompt + command + output, with language hint
category: content
schema: code_console.schema.json
template: code_console.ninjatpl
- key: terminal:keybind_table
title: Keybinding Table
description: Two-column key + action reference table
category: content
schema: keybind_table.schema.json
template: keybind_table.ninjatpl
- key: terminal:boot_log
title: Boot Log Hero
description: Sequential dmesg-style boot lines revealed one after another
category: content
schema: boot_log.schema.json
template: boot_log.ninjatpl
- key: terminal:footer
title: TTY Footer
description: EOF rule, MOTD, optional newsletter signup
category: layout
schema: footer.schema.json
template: footer.ninjatpl

1
blocks/boot_log.ninjatpl Normal file
View File

@ -0,0 +1 @@
{% if lines %}<div class="boot-log" data-block="terminal:boot_log">{% for line in lines %}<span class="boot-line" data-line-index="{{ forloop.Counter0 }}" style="animation-delay: {{ forloop.Counter * 80 }}ms;">[ {{ forloop.Counter0 }} ] {{ line }}</span>{% endfor %}{% set cur = cursor|default:"block" %}{% if cur != "none" %}<span class="boot-cursor">{% if cur == "underscore" %}_{% elif cur == "pipe" %}|{% else %}█{% endif %}</span>{% endif %}</div>{% endif %}

View File

@ -0,0 +1 @@
{% if command or output %}<div class="code-console" data-block="terminal:code_console" data-language="{{ language|default:"shell" }}">{% if command %}<div class="cmd-line"><span class="cmd-prompt">{{ prompt|default:"$" }}</span><span class="cmd-text">{{ command }}</span></div>{% endif %}{% if output %}<pre>{{ output }}</pre>{% endif %}</div>{% endif %}

1
blocks/footer.ninjatpl Normal file
View File

@ -0,0 +1 @@
{% set m = motd|default:"connection closed." %}<div class="terminal-footer" data-block="terminal:footer"><div class="terminal-footer-rule">===========================================</div>{% if m %}<div class="terminal-footer-motd">-- {{ m }}</div>{% endif %}{% if links %}<div class="terminal-footer-links">{% for l in links %}<a href="{% if l.url %}{{ l.url }}{% else %}#{% endif %}">[ {{ l.label }} ]</a>{% endfor %}</div>{% endif %}{% if showSignup == true or showSignup == "true" or showSignup == "yes" or showSignup == "1" %}<form class="terminal-footer-signup" method="post" action="#"><label class="terminal-mono">$ subscribe <input type="email" name="email" placeholder="you@example.com" required></label></form>{% endif %}<div class="terminal-footer-rule">===========================================</div><div class="terminal-mono">EOF</div></div>

View File

@ -0,0 +1 @@
{% if rows %}<table class="keybind-table" data-block="terminal:keybind_table"><thead><tr><th>Keys</th><th>Action</th></tr></thead><tbody>{% for row in rows %}<tr><td>{% for k in row.keys|split:" " %}{% if k %}<kbd>{{ k }}</kbd>{% endif %}{% endfor %}</td><td>{{ row.action }}</td></tr>{% endfor %}</tbody></table>{% endif %}

View File

@ -0,0 +1 @@
{% set nm = name|default:"PAGE"|upper %}{% set sec = section|default:1|integer %}{% if sec < 1 %}{% set sec = 1 %}{% endif %}{% set ver = version|default:"terminal v0.1.0" %}<div class="manpage-header" data-block="terminal:manpage_header"><div class="left">{{ nm }}({{ sec }})</div><div class="center">{{ nm }}</div><div class="right">{{ ver }}</div></div><div class="terminal-mono" style="display:none">{{ nm }}({{ sec }}) — {{ ver }}</div>

1
blocks/toc.ninjatpl Normal file
View File

@ -0,0 +1 @@
<nav class="terminal-toc" data-block="terminal:toc"><h4>{{ heading|default:"Sections" }}</h4>{% if items %}<ul>{% for item in items %}<li><a href="{% if item.anchor %}#{{ item.anchor }}{% else %}#{% endif %}">§ {{ item.label }}</a></li>{% endfor %}</ul>{% else %}<p class="terminal-mono">// no sections yet</p>{% endif %}</nav>

View File

@ -1,55 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// BootLogBlockMeta defines metadata for the boot log hero block.
var BootLogBlockMeta = blocks.BlockMeta{
Key: "boot_log",
Title: "Boot Log Hero",
Description: "Sequential dmesg-style boot lines revealed one after another",
Source: "terminal",
Category: blocks.CategoryContent,
}
// BootLogData is the parsed content for the boot_log block.
type BootLogData struct {
Lines []string
Cursor string
}
// BootLogBlock renders a sequential boot-log style hero.
// Content shape: {lines: []string, cursor: "block"|"underscore"|"pipe"|"none"}.
func BootLogBlock(ctx context.Context, content map[string]any) string {
data := BootLogData{
Lines: getStringSlice(content, "lines"),
Cursor: getStringDefault(content, "cursor", "block"),
}
if len(data.Lines) == 0 {
return ""
}
var buf bytes.Buffer
_ = bootLogComponent(data).Render(ctx, &buf)
return buf.String()
}
// cursorGlyph maps the cursor selector to its rendered character.
func cursorGlyph(name string) string {
switch name {
case "underscore":
return "_"
case "pipe":
return "|"
case "none":
return ""
case "block":
fallthrough
default:
return "█" // full block
}
}

View File

@ -1,25 +0,0 @@
package main
import (
"fmt"
"strconv"
)
func bootLineStyle(idx int) string {
// Stagger reveal: 80ms per line so each delay is strictly greater
// than the previous, per UAT 13.12.
return fmt.Sprintf("animation-delay: %dms;", 80*(idx+1))
}
templ bootLogComponent(data BootLogData) {
<div class="boot-log" data-block="terminal:boot_log">
for idx, line := range data.Lines {
<span class="boot-line" data-line-index={ strconv.Itoa(idx) } style={ bootLineStyle(idx) }>
{ "[ "+strconv.Itoa(idx)+" ] " + line }
</span>
}
if cursorGlyph(data.Cursor) != "" {
<span class="boot-cursor">{ cursorGlyph(data.Cursor) }</span>
}
</div>
}

View File

@ -1,119 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"strconv"
)
func bootLineStyle(idx int) string {
// Stagger reveal: 80ms per line so each delay is strictly greater
// than the previous, per UAT 13.12.
return fmt.Sprintf("animation-delay: %dms;", 80*(idx+1))
}
func bootLogComponent(data BootLogData) 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=\"boot-log\" data-block=\"terminal:boot_log\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for idx, line := range data.Lines {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"boot-line\" data-line-index=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.Itoa(idx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `boot_log.templ`, Line: 17, Col: 62}
}
_, 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, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(bootLineStyle(idx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `boot_log.templ`, Line: 17, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("[ " + strconv.Itoa(idx) + " ] " + line)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `boot_log.templ`, Line: 18, Col: 41}
}
_, 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 cursorGlyph(data.Cursor) != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"boot-cursor\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(cursorGlyph(data.Cursor))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `boot_log.templ`, Line: 22, Col: 55}
}
_, 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
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,24 +0,0 @@
package main
import (
"bytes"
"context"
)
// TerminalButtonBlock renders a button label wrapped in [ LABEL ] brackets
// with a blinking caret on hover (via the .terminal-button class).
//
// Content shape (same as built-in button): {text, url, variant, target}.
func TerminalButtonBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
url := getString(content, "url")
target := getString(content, "target")
if text == "" {
return ""
}
var buf bytes.Buffer
_ = terminalButtonComponent(text, url, target).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,20 +0,0 @@
package main
func buttonHref(url string) string {
if url == "" {
return "#"
}
return url
}
templ terminalButtonComponent(text, url, target string) {
if target == "_blank" {
<a class="terminal-button caret-blink" href={ templ.SafeURL(buttonHref(url)) } target="_blank" rel="noopener noreferrer">
{ "[ " + text + " ]" }
</a>
} else {
<a class="terminal-button caret-blink" href={ templ.SafeURL(buttonHref(url)) }>
{ "[ " + text + " ]" }
</a>
}
}

View File

@ -1,106 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func buttonHref(url string) string {
if url == "" {
return "#"
}
return url
}
func terminalButtonComponent(text, url, target 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)
if target == "_blank" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a class=\"terminal-button caret-blink\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(buttonHref(url)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 12, Col: 78}
}
_, 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, "\" target=\"_blank\" rel=\"noopener noreferrer\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("[ " + text + " ]")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 13, Col: 23}
}
_, 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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<a class=\"terminal-button caret-blink\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(buttonHref(url)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 16, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 17, Col: 23}
}
_, 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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,44 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// CodeConsoleBlockMeta defines metadata for the console code block.
var CodeConsoleBlockMeta = blocks.BlockMeta{
Key: "code_console",
Title: "Console Block",
Description: "Shell prompt + command + output, with language hint",
Source: "terminal",
Category: blocks.CategoryContent,
}
// CodeConsoleData is the parsed content for the code_console block.
type CodeConsoleData struct {
Prompt string
Command string
Output string
Language string
}
// CodeConsoleBlock renders an interactive-looking shell block.
// Content shape: {prompt, command, output, language}.
func CodeConsoleBlock(ctx context.Context, content map[string]any) string {
data := CodeConsoleData{
Prompt: getStringDefault(content, "prompt", "$"),
Command: getString(content, "command"),
Output: getString(content, "output"),
Language: getStringDefault(content, "language", "shell"),
}
// Empty state: render nothing if both command and output are blank.
if data.Command == "" && data.Output == "" {
return ""
}
var buf bytes.Buffer
_ = codeConsoleComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,15 +0,0 @@
package main
templ codeConsoleComponent(data CodeConsoleData) {
<div class="code-console" data-block="terminal:code_console" data-language={ data.Language }>
if data.Command != "" {
<div class="cmd-line">
<span class="cmd-prompt">{ data.Prompt }</span>
<span class="cmd-text">{ data.Command }</span>
</div>
}
if data.Output != "" {
<pre>{ data.Output }</pre>
}
</div>
}

View File

@ -1,108 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func codeConsoleComponent(data CodeConsoleData) 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=\"code-console\" data-block=\"terminal:code_console\" data-language=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Language)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `code_console.templ`, Line: 4, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Command != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"cmd-line\"><span class=\"cmd-prompt\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Prompt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `code_console.templ`, Line: 7, Col: 42}
}
_, 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> <span class=\"cmd-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Command)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `code_console.templ`, Line: 8, Col: 41}
}
_, 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></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Output != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<pre>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Output)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `code_console.templ`, Line: 12, Col: 21}
}
_, 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, "</pre>")
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
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,141 +0,0 @@
package main
import (
"bytes"
"context"
"fmt"
"strings"
"git.dev.alexdunmow.com/block/core/templates"
)
const terminalEmailRule = "================================================================================"
const terminalSigDelim = "-- \n"
// TerminalEmailWrapper produces a plain-text-first, monospace, 80-col centred
// email body. The first part is the plain text fallback; the HTML version
// follows it as a comment-delimited block. Inline styles only — no <style>
// tags, no external stylesheets.
//
// Many CMS pipelines accept a single string body and MIME-multipart it
// themselves; we emit the plain text up front via an HTML comment block so
// the assembled message contains both representations in a deterministic
// order regardless of the host's MIME assembly.
func TerminalEmailWrapper(body string, emailCtx templates.EmailContext) string {
plain := buildTerminalPlainText(body, emailCtx)
var buf bytes.Buffer
terminalEmailHTML(emailCtx, body, plain).Render(context.Background(), &buf)
return buf.String()
}
// buildTerminalPlainText assembles the text/plain payload: ASCII rule above
// and below, the body, then a `-- \n` signature delimiter, then site info
// and unsubscribe URL.
//
// The body is passed through as-is. Senders that want pristine plain text
// should compose the text version themselves; we do not attempt to strip
// HTML markup here.
func buildTerminalPlainText(body string, emailCtx templates.EmailContext) string {
var b strings.Builder
b.WriteString(terminalEmailRule + "\n")
b.WriteString(strings.TrimSpace(body) + "\n")
b.WriteString(terminalEmailRule + "\n")
b.WriteString(terminalSigDelim)
if emailCtx.SiteSettings.SiteName != "" {
b.WriteString(emailCtx.SiteSettings.SiteName + "\n")
}
if emailCtx.SiteSettings.SiteURL != "" {
b.WriteString(emailCtx.SiteSettings.SiteURL + "\n")
}
if emailCtx.UnsubscribeURL != "" {
b.WriteString("unsubscribe: " + emailCtx.UnsubscribeURL + "\n")
}
return b.String()
}
func terminalEmailBgColor(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return "#0a0a0a"
}
func terminalEmailFgColor(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return "#00ff00"
}
func terminalEmailBorderColor(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return "#1a4a1a"
}
func terminalEmailMutedFgColor(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return "#5a8c5a"
}
// terminalEmailHTML emits an HTML document with inline styles only. The
// plain text representation is included in an HTML comment so callers and
// hand-inspection both see both parts.
templ terminalEmailHTML(emailCtx templates.EmailContext, body, plain string) {
<!DOCTYPE html>
@templ.Raw(fmt.Sprintf("<!-- text/plain alternative\n%s\n-->\n", plain))
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;", terminalEmailBgColor(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" style={ fmt.Sprintf("background-color: %s;", terminalEmailBgColor(emailCtx)) }>
<tr>
<td align="center" style={ fmt.Sprintf("padding: 32px 12px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s;", terminalEmailFgColor(emailCtx)) }>
<table role="presentation" width="640" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 640px; width: 100%%; border: 1px solid %s; background-color: %s;", terminalEmailBorderColor(emailCtx), terminalEmailBgColor(emailCtx)) }>
<tr>
<td style={ fmt.Sprintf("padding: 16px 24px 0; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; letter-spacing: 0.05em;", terminalEmailFgColor(emailCtx)) }>
{ terminalEmailRule }
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 16px 24px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; font-size: 14px; line-height: 1.6;", terminalEmailFgColor(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 0 24px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; letter-spacing: 0.05em;", terminalEmailFgColor(emailCtx)) }>
{ terminalEmailRule }
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 16px 24px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; font-size: 12px;", terminalEmailMutedFgColor(emailCtx)) }>
<pre style={ fmt.Sprintf("margin: 0; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; white-space: pre-wrap;", terminalEmailMutedFgColor(emailCtx)) }>{ terminalSigDelim }</pre>
if emailCtx.SiteSettings.SiteName != "" {
<div>{ emailCtx.SiteSettings.SiteName }</div>
}
if emailCtx.SiteSettings.SiteURL != "" {
<div><a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", terminalEmailFgColor(emailCtx)) }>{ emailCtx.SiteSettings.SiteURL }</a></div>
}
if emailCtx.UnsubscribeURL != "" {
<div><a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", terminalEmailMutedFgColor(emailCtx)) }>unsubscribe</a></div>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

View File

@ -1,434 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"bytes"
"context"
"fmt"
"strings"
"git.dev.alexdunmow.com/block/core/templates"
)
const terminalEmailRule = "================================================================================"
const terminalSigDelim = "-- \n"
// TerminalEmailWrapper produces a plain-text-first, monospace, 80-col centred
// email body. The first part is the plain text fallback; the HTML version
// follows it as a comment-delimited block. Inline styles only — no <style>
// tags, no external stylesheets.
//
// Many CMS pipelines accept a single string body and MIME-multipart it
// themselves; we emit the plain text up front via an HTML comment block so
// the assembled message contains both representations in a deterministic
// order regardless of the host's MIME assembly.
func TerminalEmailWrapper(body string, emailCtx templates.EmailContext) string {
plain := buildTerminalPlainText(body, emailCtx)
var buf bytes.Buffer
terminalEmailHTML(emailCtx, body, plain).Render(context.Background(), &buf)
return buf.String()
}
// buildTerminalPlainText assembles the text/plain payload: ASCII rule above
// and below, the body, then a `-- \n` signature delimiter, then site info
// and unsubscribe URL.
//
// The body is passed through as-is. Senders that want pristine plain text
// should compose the text version themselves; we do not attempt to strip
// HTML markup here.
func buildTerminalPlainText(body string, emailCtx templates.EmailContext) string {
var b strings.Builder
b.WriteString(terminalEmailRule + "\n")
b.WriteString(strings.TrimSpace(body) + "\n")
b.WriteString(terminalEmailRule + "\n")
b.WriteString(terminalSigDelim)
if emailCtx.SiteSettings.SiteName != "" {
b.WriteString(emailCtx.SiteSettings.SiteName + "\n")
}
if emailCtx.SiteSettings.SiteURL != "" {
b.WriteString(emailCtx.SiteSettings.SiteURL + "\n")
}
if emailCtx.UnsubscribeURL != "" {
b.WriteString("unsubscribe: " + emailCtx.UnsubscribeURL + "\n")
}
return b.String()
}
func terminalEmailBgColor(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return "#0a0a0a"
}
func terminalEmailFgColor(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return "#00ff00"
}
func terminalEmailBorderColor(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return "#1a4a1a"
}
func terminalEmailMutedFgColor(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return "#5a8c5a"
}
// terminalEmailHTML emits an HTML document with inline styles only. The
// plain text representation is included in an HTML comment so callers and
// hand-inspection both see both parts.
func terminalEmailHTML(emailCtx templates.EmailContext, body, plain 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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(fmt.Sprintf("<!-- text/plain alternative\n%s\n-->\n", plain)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><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: 96, 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, 3, "</title></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: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;", terminalEmailBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 98, Col: 213}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<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: 100, Col: 84}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" 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("background-color: %s;", terminalEmailBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 102, Col: 162}
}
_, 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, "\"><tr><td align=\"center\" 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("padding: 32px 12px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s;", terminalEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 104, Col: 215}
}
_, 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, "\"><table role=\"presentation\" width=\"640\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" 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("max-width: 640px; width: 100%%; border: 1px solid %s; background-color: %s;", terminalEmailBorderColor(emailCtx), terminalEmailBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 105, Col: 254}
}
_, 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, "\"><tr><td 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("padding: 16px 24px 0; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; letter-spacing: 0.05em;", terminalEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 107, Col: 229}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(terminalEmailRule)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 108, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td></tr><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 16px 24px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; font-size: 14px; line-height: 1.6;", terminalEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 112, Col: 238}
}
_, 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, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</td></tr><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 0 24px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; letter-spacing: 0.05em;", terminalEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 117, Col: 224}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(terminalEmailRule)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 118, Col: 28}
}
_, 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, 16, "</td></tr><tr><td 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 24px; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; font-size: 12px;", terminalEmailMutedFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 122, Col: 225}
}
_, 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, 17, "\"><pre 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("margin: 0; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: %s; white-space: pre-wrap;", terminalEmailMutedFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 123, Col: 224}
}
_, 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, 18, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(terminalSigDelim)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 123, Col: 245}
}
_, 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, 19, "</pre>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, 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: 125, Col: 47}
}
_, 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, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div><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.SiteSettings.SiteURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 128, Col: 69}
}
_, 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, 23, "\" 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;", terminalEmailFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 128, Col: 165}
}
_, 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, 24, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, 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: 128, Col: 199}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 templ.SafeURL
templ_7745c5c3_Var20, 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: 131, Col: 63}
}
_, 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, 27, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", terminalEmailMutedFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 131, Col: 164}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">unsubscribe</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,59 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem (rooted at assets/).
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem (rooted at schemas/).
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler serves the embedded assets at /templates/terminal/...
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded presets.json bytes.
func ThemePresets() []byte { return presetsData }
// BundledFonts returns the embedded fonts.json bytes.
func BundledFonts() []byte { return fontsData }
// ThemeCSSManifest injects the theme's custom utility CSS into the host
// Tailwind input so JIT picks up .ascii-frame, .caret-blink, etc.
func ThemeCSSManifest() *plugin.CSSManifest {
css, err := assetsFS.ReadFile("assets/style.css")
if err != nil {
return &plugin.CSSManifest{}
}
return &plugin.CSSManifest{
InputCSSAppend: string(css),
}
}

View File

@ -1,59 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FooterBlockMeta defines metadata for the TTY footer block.
var FooterBlockMeta = blocks.BlockMeta{
Key: "footer",
Title: "TTY Footer",
Description: "EOF rule, MOTD, optional newsletter signup",
Source: "terminal",
Category: blocks.CategoryLayout,
}
// FooterLink is a single footer link.
type FooterLink struct {
Label string
URL string
}
// FooterData is the parsed content for the footer block.
type FooterData struct {
Motd string
Links []FooterLink
ShowSignup bool
}
// FooterBlock renders the TTY footer.
// Content shape: {motd, links[{label, url}], showSignup}.
func FooterBlock(ctx context.Context, content map[string]any) string {
links := getSlice(content, "links")
footerLinks := make([]FooterLink, 0, len(links))
for _, l := range links {
footerLinks = append(footerLinks, FooterLink{
Label: getString(l, "label"),
URL: getString(l, "url"),
})
}
// showSignup tolerates bool, string "true", or select "true"/"false".
signup := getBool(content, "showSignup")
if !signup {
if s := getString(content, "showSignup"); s == "true" {
signup = true
}
}
data := FooterData{
Motd: getStringDefault(content, "motd", "connection closed."),
Links: footerLinks,
ShowSignup: signup,
}
var buf bytes.Buffer
_ = footerComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,33 +0,0 @@
package main
func footerLinkHref(url string) string {
if url == "" {
return "#"
}
return url
}
templ footerComponent(data FooterData) {
<div class="terminal-footer" data-block="terminal:footer">
<div class="terminal-footer-rule">{ "===========================================" }</div>
if data.Motd != "" {
<div class="terminal-footer-motd">{ "-- " }{ data.Motd }</div>
}
if len(data.Links) > 0 {
<div class="terminal-footer-links">
for _, l := range data.Links {
<a href={ templ.SafeURL(footerLinkHref(l.URL)) }>{ "[ " + l.Label + " ]" }</a>
}
</div>
}
if data.ShowSignup {
<form class="terminal-footer-signup" method="post" action="#">
<label class="terminal-mono">{ "$ subscribe " }
<input type="email" name="email" placeholder="you@example.com" required/>
</label>
</form>
}
<div class="terminal-footer-rule">{ "===========================================" }</div>
<div class="terminal-mono">{ "EOF" }</div>
</div>
}

View File

@ -1,179 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func footerLinkHref(url string) string {
if url == "" {
return "#"
}
return url
}
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 class=\"terminal-footer\" data-block=\"terminal:footer\"><div class=\"terminal-footer-rule\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("===========================================")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 12, Col: 83}
}
_, 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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Motd != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"terminal-footer-motd\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: `footer.templ`, Line: 14, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Motd)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 14, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Links) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"terminal-footer-links\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, l := range data.Links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(footerLinkHref(l.URL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 19, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("[ " + l.Label + " ]")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 19, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowSignup {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<form class=\"terminal-footer-signup\" method=\"post\" action=\"#\"><label class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("$ subscribe ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 25, Col: 49}
}
_, 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, " <input type=\"email\" name=\"email\" placeholder=\"you@example.com\" required></label></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"terminal-footer-rule\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("===========================================")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 30, Col: 83}
}
_, 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, "</div><div class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("EOF")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 31, Col: 36}
}
_, 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, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

20
go.mod
View File

@ -1,20 +0,0 @@
module git.dev.alexdunmow.com/block/themes/terminal
go 1.26.4
require (
git.dev.alexdunmow.com/block/core v0.15.2
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
)

46
go.sum
View File

@ -1,46 +0,0 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.15.2 h1:qTjJxa1pClxwehkdTxXiJDpHQLr5DoXBUCa40QRO0L8=
git.dev.alexdunmow.com/block/core v0.15.2/go.mod h1:A+mgA9oeWoPhXruv4Mjpywwl2A9B5I3cKzlJjalBdlE=
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=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
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/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,44 +0,0 @@
package main
import (
"bytes"
"context"
"strconv"
"strings"
)
// TerminalHeadingBlock renders a heading with markdown-style hash prefix and
// uppercase tracking, per the terminal aesthetic.
//
// Content shape (same as built-in heading): {text, level, textClass}.
func TerminalHeadingBlock(ctx context.Context, content map[string]any) string {
text := strings.ToUpper(getString(content, "text"))
textClass := getString(content, "textClass")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = terminalHeadingComponent(level, text, textClass).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses the level from content, defaulting to 2. Tolerates
// JSON float64, native int, and string-encoded levels.
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
}

View File

@ -1,52 +0,0 @@
package main
import "strings"
// hashPrefix returns the markdown-style hash prefix for a heading level.
func hashPrefix(level int) string {
if level < 1 {
level = 1
}
if level > 6 {
level = 6
}
return strings.Repeat("#", level) + " "
}
func terminalHeadingClass(level int) string {
switch level {
case 1:
return "terminal-heading terminal-heading-1 text-3xl font-bold"
case 2:
return "terminal-heading terminal-heading-2 text-2xl font-bold"
case 3:
return "terminal-heading terminal-heading-3 text-xl font-semibold"
case 4:
return "terminal-heading terminal-heading-4 text-lg font-semibold"
case 5:
return "terminal-heading terminal-heading-5 text-base font-medium"
case 6:
return "terminal-heading terminal-heading-6 text-base font-medium"
default:
return "terminal-heading terminal-heading-2 text-2xl font-bold"
}
}
templ terminalHeadingComponent(level int, text, textClass string) {
switch level {
case 1:
<h1 class={ terminalHeadingClass(1), textClass } style="text-transform: uppercase;">{ hashPrefix(1) + text }</h1>
case 2:
<h2 class={ terminalHeadingClass(2), textClass } style="text-transform: uppercase;">{ hashPrefix(2) + text }</h2>
case 3:
<h3 class={ terminalHeadingClass(3), textClass } style="text-transform: uppercase;">{ hashPrefix(3) + text }</h3>
case 4:
<h4 class={ terminalHeadingClass(4), textClass } style="text-transform: uppercase;">{ hashPrefix(4) + text }</h4>
case 5:
<h5 class={ terminalHeadingClass(5), textClass } style="text-transform: uppercase;">{ hashPrefix(5) + text }</h5>
case 6:
<h6 class={ terminalHeadingClass(6), textClass } style="text-transform: uppercase;">{ hashPrefix(6) + text }</h6>
default:
<h2 class={ terminalHeadingClass(2), textClass } style="text-transform: uppercase;">{ hashPrefix(2) + text }</h2>
}
}

View File

@ -1,322 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "strings"
// hashPrefix returns the markdown-style hash prefix for a heading level.
func hashPrefix(level int) string {
if level < 1 {
level = 1
}
if level > 6 {
level = 6
}
return strings.Repeat("#", level) + " "
}
func terminalHeadingClass(level int) string {
switch level {
case 1:
return "terminal-heading terminal-heading-1 text-3xl font-bold"
case 2:
return "terminal-heading terminal-heading-2 text-2xl font-bold"
case 3:
return "terminal-heading terminal-heading-3 text-xl font-semibold"
case 4:
return "terminal-heading terminal-heading-4 text-lg font-semibold"
case 5:
return "terminal-heading terminal-heading-5 text-base font-medium"
case 6:
return "terminal-heading terminal-heading-6 text-base font-medium"
default:
return "terminal-heading terminal-heading-2 text-2xl font-bold"
}
}
func terminalHeadingComponent(level int, text, textClass string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch level {
case 1:
var templ_7745c5c3_Var2 = []any{terminalHeadingClass(1), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(1) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 38, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var5 = []any{terminalHeadingClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(2) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 40, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var8 = []any{terminalHeadingClass(3), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(3) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 42, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var11 = []any{terminalHeadingClass(4), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(4) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 44, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var14 = []any{terminalHeadingClass(5), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(5) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 46, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var17 = []any{terminalHeadingClass(6), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(6) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 48, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var20 = []any{terminalHeadingClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"text-transform: uppercase;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(hashPrefix(2) + text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 50, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,75 +0,0 @@
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 extracts a string with a fallback.
func getStringDefault(content map[string]any, key, def string) string {
if v, ok := content[key].(string); ok && v != "" {
return v
}
return def
}
// getInt extracts an int (handles JSON float64).
func getInt(content map[string]any, key string, defaultVal int) int {
if v, ok := content[key].(float64); ok {
return int(v)
}
if v, ok := content[key].(int); ok {
return v
}
return defaultVal
}
// getBool extracts a bool (handles string "true"/"false" too).
func getBool(content map[string]any, key string) bool {
if v, ok := content[key].(bool); ok {
return v
}
if v, ok := content[key].(string); ok {
return v == "true" || v == "yes" || v == "1"
}
return false
}
// getSlice extracts a slice of maps from content.
func getSlice(content map[string]any, key string) []map[string]any {
if v, ok := content[key].([]any); ok {
result := make([]map[string]any, 0, len(v))
for _, item := range v {
if m, ok := item.(map[string]any); ok {
result = append(result, m)
}
}
return result
}
return nil
}
// getStringSlice extracts a flat slice of strings (e.g. boot_log lines).
// Tolerates non-string entries by converting fmt.Sprint-style; missing keys
// or wrong types yield an empty slice (no panic).
func getStringSlice(content map[string]any, key string) []string {
v, ok := content[key].([]any)
if !ok {
return nil
}
out := make([]string, 0, len(v))
for _, item := range v {
switch s := item.(type) {
case string:
out = append(out, s)
case nil:
out = append(out, "")
default:
out = append(out, "")
}
}
return out
}

View File

@ -1,30 +0,0 @@
package main
import (
"bytes"
"context"
"sync/atomic"
)
// imgCounter assigns sequential [fig.N] numbers in render order so each
// rendered terminal image gets a unique caption number.
var imgCounter uint64
// TerminalImageBlock wraps the built-in image in a +----+ ASCII frame with
// a [fig.N caption] line beneath it.
//
// Content shape (same as built-in image): {src, alt, caption, width, height}.
func TerminalImageBlock(ctx context.Context, content map[string]any) string {
src := getString(content, "src")
alt := getString(content, "alt")
caption := getString(content, "caption")
if alt == "" {
alt = caption
}
n := atomic.AddUint64(&imgCounter, 1)
var buf bytes.Buffer
_ = terminalImageComponent(src, alt, caption, n).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,22 +0,0 @@
package main
import "strconv"
func figCaptionText(n uint64, caption string) string {
base := "[fig." + strconv.FormatUint(n, 10)
if caption == "" {
return base + "]"
}
return base + " " + caption + "]"
}
templ terminalImageComponent(src, alt, caption string, n uint64) {
<figure class="ascii-frame">
if src != "" {
<img src={ src } alt={ alt }/>
} else {
<div class="terminal-mono" style="padding: 1rem;">{ "[ no image ]" }</div>
}
<figcaption>{ figCaptionText(n, caption) }</figcaption>
</figure>
}

View File

@ -1,117 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "strconv"
func figCaptionText(n uint64, caption string) string {
base := "[fig." + strconv.FormatUint(n, 10)
if caption == "" {
return base + "]"
}
return base + " " + caption + "]"
}
func terminalImageComponent(src, alt, caption string, n uint64) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure class=\"ascii-frame\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if src != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 16, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 16, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"terminal-mono\" style=\"padding: 1rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("[ no image ]")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 18, Col: 69}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(figCaptionText(n, caption))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 20, Col: 42}
}
_, 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, "</figcaption></figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,56 +0,0 @@
package main
import (
"bytes"
"context"
"strings"
"git.dev.alexdunmow.com/block/core/blocks"
)
// KeybindTableBlockMeta defines metadata for the keybind table block.
var KeybindTableBlockMeta = blocks.BlockMeta{
Key: "keybind_table",
Title: "Keybinding Table",
Description: "Two-column key + action reference table",
Source: "terminal",
Category: blocks.CategoryContent,
}
// KeybindRow is a single keybind table row.
type KeybindRow struct {
Keys []string
Action string
}
// KeybindTableData is the parsed content for the keybind_table block.
type KeybindTableData struct {
Rows []KeybindRow
}
// KeybindTableBlock renders a key/action reference table.
// Content shape: {rows[{keys, action}]}.
func KeybindTableBlock(ctx context.Context, content map[string]any) string {
rows := getSlice(content, "rows")
tableRows := make([]KeybindRow, 0, len(rows))
for _, r := range rows {
keys := getString(r, "keys")
// Split combos on whitespace so we can render individual <kbd> elements.
parts := strings.Fields(keys)
if len(parts) == 0 && keys != "" {
parts = []string{keys}
}
tableRows = append(tableRows, KeybindRow{
Keys: parts,
Action: getString(r, "action"),
})
}
if len(tableRows) == 0 {
return ""
}
data := KeybindTableData{Rows: tableRows}
var buf bytes.Buffer
_ = keybindTableComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,24 +0,0 @@
package main
templ keybindTableComponent(data KeybindTableData) {
<table class="keybind-table" data-block="terminal:keybind_table">
<thead>
<tr>
<th>Keys</th>
<th>Action</th>
</tr>
</thead>
<tbody>
for _, row := range data.Rows {
<tr>
<td>
for _, k := range row.Keys {
<kbd>{ k }</kbd>
}
</td>
<td>{ row.Action }</td>
</tr>
}
</tbody>
</table>
}

View File

@ -1,86 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func keybindTableComponent(data KeybindTableData) 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, "<table class=\"keybind-table\" data-block=\"terminal:keybind_table\"><thead><tr><th>Keys</th><th>Action</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, row := range data.Rows {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, k := range row.Keys {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<kbd>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(k)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `keybind_table.templ`, Line: 16, Col: 15}
}
_, 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, 4, "</kbd>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(row.Action)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `keybind_table.templ`, Line: 19, Col: 21}
}
_, 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, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

17
main.go
View File

@ -1,17 +0,0 @@
//go:build wasip1
// Package main compiles the terminal theme to a wasip1 reactor-mode wasm guest
// (WO-WZ-015). The .so build path is retired; `ninja plugin build` produces the
// .bnp artifact the registry serves.
//
// Reactor mode: the host runs `_initialize` once per pooled instance, which
// runs this init func (hence wasmguest.Serve) before any bn_invoke. Serve is
// non-blocking; all theme work then arrives over the ABI hook calls. main is
// never called.
package main
import "git.dev.alexdunmow.com/block/core/plugin/wasmguest"
func init() { wasmguest.Serve(Registration) }
func main() {} // never called — reactor mode

474
manifest.yaml Normal file
View File

@ -0,0 +1,474 @@
# Terminal theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
system_templates:
- key: terminal
title: Terminal
description: "Phosphor green on black terminal aesthetic with ASCII art and monospace type."
page_templates:
- system: terminal
key: default
title: Default
description: "Standard 80-col centered TTY layout"
slots: [header, main, footer]
- system: terminal
key: landing
title: Landing
description: "ASCII hero + feature grid for project README sites"
slots: [hero, main, cta, footer]
- system: terminal
key: article
title: Article (man-page)
description: "Man-page two-column with section nav, ideal for writeups"
slots: [header, toc, main, footer]
- system: terminal
key: full-width
title: Full Width
description: "Edge-to-edge for tool dashboards & status pages"
slots: [header, main, footer]
template_overrides:
- { template: terminal, block: heading }
- { template: terminal, block: text }
- { template: terminal, block: button }
- { template: terminal, block: image }
email_wrappers:
- terminal
css:
input_css_append: |
/* ============================================================
Terminal Theme — Phosphor green TTY aesthetic
============================================================ */
/* --- Globals: square corners, mono stack --- */
:root {
--radius: 0;
}
.terminal-page {
font-family: var(--font-body, "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
letter-spacing: 0.01em;
}
.terminal-page * {
border-radius: 0 !important;
}
/* --- Typography helpers --- */
.terminal-heading {
font-family: var(--font-heading, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
text-transform: uppercase;
letter-spacing: 0.08em;
color: hsl(var(--foreground));
}
.terminal-mono {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
}
.terminal-prose {
font-family: var(--font-body, "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
color: hsl(var(--foreground));
line-height: 1.6;
max-width: 80ch;
}
.terminal-prose a {
color: hsl(var(--accent));
text-decoration: underline;
text-underline-offset: 0.2em;
}
/* --- ASCII frame (image override + figure wrappers) --- */
.ascii-frame {
display: inline-block;
padding: 0.5rem;
border: 1px solid hsl(var(--border));
background-color: hsl(var(--card));
position: relative;
}
.ascii-frame::before,
.ascii-frame::after {
content: "+";
position: absolute;
color: hsl(var(--border));
font-family: var(--font-mono, ui-monospace, monospace);
font-weight: 700;
}
.ascii-frame::before {
top: -0.5em;
left: -0.5em;
}
.ascii-frame::after {
bottom: -0.5em;
right: -0.5em;
}
.ascii-frame figcaption {
display: block;
margin-top: 0.5rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.875rem;
text-align: center;
}
/* --- CRT scanlines overlay --- */
.crt-scanlines {
position: relative;
}
.crt-scanlines::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1000;
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 2px,
hsl(var(--background) / 0.15) 2px,
hsl(var(--background) / 0.15) 3px
);
}
/* --- Caret blink keyframe + utility --- */
@keyframes caret-blink {
0%, 50% { opacity: 1; }
50.01%, 100% { opacity: 0; }
}
.caret-blink::after {
content: "_";
display: inline-block;
margin-left: 0.15em;
animation: caret-blink 1s step-end infinite;
animation-name: caret-blink;
color: hsl(var(--foreground));
}
/* --- Bracketed button (override) --- */
.terminal-button {
font-family: var(--font-mono, ui-monospace, monospace);
background: transparent;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
padding: 0.4rem 0.8rem;
text-decoration: none;
display: inline-block;
text-transform: uppercase;
letter-spacing: 0.08em;
position: relative;
}
.terminal-button:hover {
background: hsl(var(--foreground));
color: hsl(var(--background));
}
.terminal-button:hover::after {
content: "_";
position: absolute;
margin-left: 0.2em;
animation: caret-blink 1s step-end infinite;
animation-name: caret-blink;
}
/* --- Bracketed nav --- */
.terminal-nav {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-family: var(--font-mono, ui-monospace, monospace);
}
.terminal-nav a {
color: hsl(var(--foreground));
text-decoration: none;
white-space: nowrap;
}
.terminal-nav a:hover {
color: hsl(var(--accent));
}
/* --- ASCII header block --- */
.ascii-header {
font-family: var(--font-mono, ui-monospace, monospace);
color: hsl(var(--accent));
padding: 1rem 0;
border-bottom: 1px solid hsl(var(--border));
white-space: pre;
overflow-x: auto;
}
.ascii-header pre {
margin: 0;
font-family: inherit;
line-height: 1.1;
}
.ascii-header .prompt-line {
margin-top: 0.5rem;
color: hsl(var(--foreground));
}
/* --- Man-page header --- */
.manpage-header {
font-family: var(--font-mono, ui-monospace, monospace);
border-bottom: 1px solid hsl(var(--border));
padding: 0.75rem 0;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
gap: 1rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--foreground));
}
.manpage-header .left,
.manpage-header .center,
.manpage-header .right {
flex: 1;
}
.manpage-header .center { text-align: center; }
.manpage-header .right { text-align: right; }
/* --- TOC (left rail on article) --- */
.terminal-toc {
font-family: var(--font-mono, ui-monospace, monospace);
color: hsl(var(--muted-foreground));
padding: 1rem;
border: 1px solid hsl(var(--border));
background: hsl(var(--card));
}
.terminal-toc h4 {
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--foreground));
margin: 0 0 0.5rem;
font-size: 0.875rem;
}
.terminal-toc ul {
list-style: none;
margin: 0;
padding: 0;
}
.terminal-toc li {
margin: 0.2rem 0;
}
.terminal-toc a {
color: hsl(var(--muted-foreground));
text-decoration: none;
}
.terminal-toc a:hover {
color: hsl(var(--foreground));
}
@media (max-width: 768px) {
.terminal-toc {
display: none;
}
}
/* --- Code console block --- */
.code-console {
font-family: var(--font-mono, ui-monospace, monospace);
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
padding: 0.75rem 1rem;
margin: 1rem 0;
overflow-x: auto;
color: hsl(var(--card-foreground));
}
.code-console .cmd-line {
display: flex;
align-items: center;
gap: 0.5em;
color: hsl(var(--accent));
margin: 0;
}
.code-console .cmd-prompt {
color: hsl(var(--accent));
}
.code-console .cmd-text {
color: hsl(var(--foreground));
}
.code-console pre {
margin: 0.5rem 0 0;
white-space: pre-wrap;
color: hsl(var(--muted-foreground));
font-family: inherit;
}
/* --- Keybinding table --- */
.keybind-table {
font-family: var(--font-mono, ui-monospace, monospace);
border-collapse: collapse;
margin: 1rem 0;
width: 100%;
max-width: 80ch;
color: hsl(var(--foreground));
}
.keybind-table th,
.keybind-table td {
border-bottom: 1px dashed hsl(var(--border));
padding: 0.4rem 0.6rem;
text-align: left;
vertical-align: top;
}
.keybind-table th {
text-transform: uppercase;
letter-spacing: 0.08em;
color: hsl(var(--accent));
font-weight: 600;
}
.keybind-table kbd {
font-family: inherit;
background: hsl(var(--muted));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
padding: 0.05rem 0.4rem;
margin-right: 0.25rem;
font-size: 0.875em;
}
/* --- Boot log --- */
.boot-log {
font-family: var(--font-mono, ui-monospace, monospace);
background: hsl(var(--background));
color: hsl(var(--foreground));
padding: 1rem;
border: 1px solid hsl(var(--border));
margin: 1rem 0;
white-space: pre-wrap;
overflow-x: auto;
}
.boot-log .boot-line {
display: block;
opacity: 0;
animation: boot-line-in 0.2s ease-out forwards;
}
@keyframes boot-line-in {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
.boot-log .boot-cursor {
display: inline-block;
color: hsl(var(--accent));
animation: caret-blink 1s step-end infinite;
animation-name: caret-blink;
}
/* --- Footer --- */
.terminal-footer {
font-family: var(--font-mono, ui-monospace, monospace);
color: hsl(var(--muted-foreground));
border-top: 1px solid hsl(var(--border));
padding: 1rem 0;
margin-top: 2rem;
text-align: center;
}
.terminal-footer .terminal-footer-rule {
color: hsl(var(--border));
margin: 0.25rem 0;
letter-spacing: 0.2em;
}
.terminal-footer .terminal-footer-motd {
color: hsl(var(--foreground));
margin: 0.5rem 0;
}
.terminal-footer .terminal-footer-links {
margin: 0.5rem 0;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
.terminal-footer a {
color: hsl(var(--muted-foreground));
text-decoration: none;
}
.terminal-footer a:hover {
color: hsl(var(--foreground));
}
.terminal-footer .terminal-footer-signup {
margin: 0.75rem auto;
max-width: 40ch;
}
.terminal-footer .terminal-footer-signup input[type="email"] {
font-family: inherit;
background: hsl(var(--input));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
padding: 0.4rem 0.6rem;
}
/* --- Article layout grid --- */
.terminal-article-grid {
display: grid;
grid-template-columns: 18ch 1fr;
gap: 2rem;
align-items: start;
}
@media (max-width: 768px) {
.terminal-article-grid {
grid-template-columns: 1fr;
}
}
.terminal-main-80 {
max-width: 80ch;
margin: 0 auto;
font-family: var(--font-body, ui-monospace, monospace);
}

View File

@ -1,46 +0,0 @@
package main
import (
"bytes"
"context"
"fmt"
"strings"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ManpageHeaderBlockMeta defines metadata for the man-page header block.
var ManpageHeaderBlockMeta = blocks.BlockMeta{
Key: "manpage_header",
Title: "Man-page Header",
Description: "NAME(section) ... center ... version row, like a man-page top line",
Source: "terminal",
Category: blocks.CategoryLayout,
}
// ManpageHeaderData is the parsed content for the manpage_header block.
type ManpageHeaderData struct {
Name string
Section int
Version string
// Banner is the rendered top-line text: "NAME(1) ... version".
Banner string
}
// ManpageHeaderBlock renders the man-page top row.
// Content shape: {name, section, version}.
func ManpageHeaderBlock(ctx context.Context, content map[string]any) string {
data := ManpageHeaderData{
Name: strings.ToUpper(getStringDefault(content, "name", "PAGE")),
Section: getInt(content, "section", 1),
Version: getStringDefault(content, "version", "terminal v0.1.0"),
}
if data.Section < 1 {
data.Section = 1
}
data.Banner = fmt.Sprintf("%s(%d) %s %s", data.Name, data.Section, "—", data.Version)
var buf bytes.Buffer
_ = manpageHeaderComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,12 +0,0 @@
package main
import "fmt"
templ manpageHeaderComponent(data ManpageHeaderData) {
<div class="manpage-header" data-block="terminal:manpage_header">
<div class="left">{ fmt.Sprintf("%s(%d)", data.Name, data.Section) }</div>
<div class="center">{ data.Name }</div>
<div class="right">{ data.Version }</div>
</div>
<div class="terminal-mono" style="display:none">{ data.Banner }</div>
}

View File

@ -1,94 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
func manpageHeaderComponent(data ManpageHeaderData) 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=\"manpage-header\" data-block=\"terminal:manpage_header\"><div class=\"left\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s(%d)", data.Name, data.Section))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `manpage_header.templ`, Line: 7, Col: 68}
}
_, 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><div class=\"center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `manpage_header.templ`, Line: 8, Col: 33}
}
_, 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, "</div><div class=\"right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `manpage_header.templ`, Line: 9, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div><div class=\"terminal-mono\" style=\"display:none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Banner)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `manpage_header.templ`, Line: 11, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

25
master_pages.json Normal file
View File

@ -0,0 +1,25 @@
[
{
"key": "terminal:default-master",
"title": "Terminal Default Master",
"page_templates": ["default", "landing", "full-width"],
"blocks": [
{ "block_key": "terminal:ascii_header", "title": "TTY Header", "content": { "title": "~/projects", "prompt": "$ " }, "slot": "header", "sort_order": 0 },
{ "block_key": "navbar", "title": "Main Nav", "content": { "menuName": "main", "style": "bracketed" }, "slot": "header", "sort_order": 1 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "// content here" }, "slot": "main", "sort_order": 0 },
{ "block_key": "terminal:footer", "title": "TTY Footer", "content": { "motd": "connection closed.", "showSignup": true }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "terminal:article-master",
"title": "Terminal Article Master",
"page_templates": ["article"],
"blocks": [
{ "block_key": "terminal:manpage_header", "title": "Man-page Header", "content": { "name": "WRITEUP", "section": 1, "version": "terminal v0.1.0" }, "slot": "header", "sort_order": 0 },
{ "block_key": "navbar", "title": "Main Nav", "content": { "menuName": "main", "style": "bracketed" }, "slot": "header", "sort_order": 1 },
{ "block_key": "terminal:toc", "title": "Section TOC", "content": { "heading": "Sections", "items": [] }, "slot": "toc", "sort_order": 0 },
{ "block_key": "slot", "title": "Article Body", "content": { "slotName": "main", "placeholder": "// article body" }, "slot": "main", "sort_order": 0 },
{ "block_key": "terminal:footer", "title": "TTY Footer", "content": { "motd": "connection closed.", "showSignup": false }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -2,11 +2,8 @@
name = "terminal" name = "terminal"
display_name = "Terminal" display_name = "Terminal"
scope = "@themes" scope = "@themes"
version = "0.1.3" version = "0.2.0"
description = "Phosphor green on black terminal aesthetic with ASCII art, monospace type, and man-page article layouts. For indie dev sites, CTF writeups, and CLI tool docs." description = "Phosphor green on black terminal aesthetic with ASCII art, monospace type, and man-page article layouts. For indie dev sites, CTF writeups, and CLI tool docs."
kind = "theme" kind = "theme"
categories = ["templates"] categories = ["templates"]
tags = ["terminal", "hacker", "monospace", "dev", "ctf", "security", "docs", "indie"] tags = ["terminal", "hacker", "monospace", "dev", "ctf", "security", "docs", "indie"]
[compatibility]
block_core = ">=0.15.0 <0.16.0"

View File

@ -1,181 +0,0 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function to templates.TemplateFunc.
// templ.Component already implements templates.HTMLComponent via Render.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register is the plugin entry point that wires up the Terminal system
// template, its four page templates, all theme blocks, the four built-in
// overrides, and the email wrapper.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
// 1) System template
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "terminal",
Title: "Terminal",
Description: "Phosphor green on black terminal aesthetic with ASCII art and monospace type.",
})
// 2) Page templates (exact keys + slots per spec §6 / UAT §3)
if err := tr.RegisterPageTemplate("terminal", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard 80-col centered TTY layout",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderTerminal)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("terminal", templates.PageTemplateMeta{
Key: "landing",
Title: "Landing",
Description: "ASCII hero + feature grid for project README sites",
Slots: []string{"hero", "main", "cta", "footer"},
}, wrap(RenderTerminalLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("terminal", templates.PageTemplateMeta{
Key: "article",
Title: "Article (man-page)",
Description: "Man-page two-column with section nav, ideal for writeups",
Slots: []string{"header", "toc", "main", "footer"},
}, wrap(RenderTerminalArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("terminal", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge for tool dashboards & status pages",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderTerminalFullWidth)); err != nil {
return err
}
// 3) Schemas BEFORE block registration (per CLAUDE.md).
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
// 4) Theme blocks. Keys are unqualified at registration; address as
// terminal:<key> from master pages and page block lists.
br.Register(AsciiHeaderBlockMeta, AsciiHeaderBlock)
br.Register(ManpageHeaderBlockMeta, ManpageHeaderBlock)
br.Register(TocBlockMeta, TocBlock)
br.Register(CodeConsoleBlockMeta, CodeConsoleBlock)
br.Register(KeybindTableBlockMeta, KeybindTableBlock)
br.Register(BootLogBlockMeta, BootLogBlock)
br.Register(FooterBlockMeta, FooterBlock)
// 5) Built-in block overrides — only applied while the Terminal theme
// is active.
br.RegisterTemplateOverride("terminal", "heading", TerminalHeadingBlock)
br.RegisterTemplateOverride("terminal", "text", TerminalTextBlock)
br.RegisterTemplateOverride("terminal", "button", TerminalButtonBlock)
br.RegisterTemplateOverride("terminal", "image", TerminalImageBlock)
// 6) Email wrapper (plain text first, then HTML — handled by wrapper).
tr.RegisterEmailWrapper("terminal", TerminalEmailWrapper)
return nil
}
// DefaultMasterPages returns the two master pages Terminal seeds on first
// load (per UAT §9): one default master covering default/landing/full-width,
// and one article master that swaps in the man-page header and adds the
// TOC block.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "terminal:default-master",
Title: "Terminal Default Master",
PageTemplates: []string{"default", "landing", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "terminal:ascii_header",
Title: "TTY Header",
Content: map[string]any{"title": "~/projects", "prompt": "$ "},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "navbar",
Title: "Main Nav",
Content: map[string]any{"menuName": "main", "style": "bracketed"},
Slot: "header",
SortOrder: 1,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main", "placeholder": "// content here"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "terminal:footer",
Title: "TTY Footer",
Content: map[string]any{"motd": "connection closed.", "showSignup": true},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "terminal:article-master",
Title: "Terminal Article Master",
PageTemplates: []string{"article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "terminal:manpage_header",
Title: "Man-page Header",
Content: map[string]any{"name": "WRITEUP", "section": 1, "version": "terminal v0.1.0"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "navbar",
Title: "Main Nav",
Content: map[string]any{"menuName": "main", "style": "bracketed"},
Slot: "header",
SortOrder: 1,
},
{
BlockKey: "terminal:toc",
Title: "Section TOC",
Content: map[string]any{"heading": "Sections", "items": []any{}},
Slot: "toc",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Article Body",
Content: map[string]any{"slotName": "main", "placeholder": "// article body"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "terminal:footer",
Title: "TTY Footer",
Content: map[string]any{"motd": "connection closed.", "showSignup": false},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

View File

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

View File

@ -1,257 +0,0 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData is the shared payload for every Terminal page renderer.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseTerminalPageData(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 := "dark"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: bn.ParseSiteSettings(doc),
PageMeta: bn.ParsePageMeta(doc),
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: bn.ParseEngagementConfig(doc),
}
}
// --- Default template: header / main / footer ---
templ Terminal(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
<div class="terminal-main-80 px-4">
@templ.Raw(data.Slots["header"])
</div>
</header>
<main class="terminal-main-80 w-full px-4 py-8 flex-grow">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p class="terminal-mono">{ "// no content blocks assigned to this page" }</p>
}
</main>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// --- Landing template: hero / main / cta / footer ---
templ TerminalLanding(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
<div class="terminal-main-80 px-4 py-8">
@templ.Raw(data.Slots["hero"])
</div>
</section>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
<div class="terminal-main-80 px-4 py-8">
@templ.Raw(main)
</div>
}
</main>
<section class="w-full">
<div class="terminal-main-80 px-4 py-6">
@templ.Raw(data.Slots["cta"])
</div>
</section>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// --- Article template: header / toc / main / footer (man-page two-column) ---
templ TerminalArticle(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
<div class="terminal-main-80 px-4">
@templ.Raw(data.Slots["header"])
</div>
</header>
<div class="terminal-main-80 w-full px-4 py-6 flex-grow">
<div class="terminal-article-grid">
<aside class="w-full">
@templ.Raw(data.Slots["toc"])
</aside>
<main class="w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p class="terminal-mono">{ "// no content blocks assigned to this page" }</p>
}
</main>
</div>
</div>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// --- Full-width template: header / main / footer, edge-to-edge ---
templ TerminalFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full px-4">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full px-4 py-6">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p class="terminal-mono">{ "// no content blocks assigned to this page" }</p>
}
</main>
<footer class="w-full mt-auto px-4">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// --- Render functions wired up in register.go via wrap(). ---
func RenderTerminal(ctx context.Context, doc map[string]any) templ.Component {
return Terminal(parseTerminalPageData(doc))
}
func RenderTerminalLanding(ctx context.Context, doc map[string]any) templ.Component {
return TerminalLanding(parseTerminalPageData(doc))
}
func RenderTerminalArticle(ctx context.Context, doc map[string]any) templ.Component {
return TerminalArticle(parseTerminalPageData(doc))
}
func RenderTerminalFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return TerminalFullWidth(parseTerminalPageData(doc))
}

View File

@ -1,541 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData is the shared payload for every Terminal page renderer.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseTerminalPageData(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 := "dark"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: bn.ParseSiteSettings(doc),
PageMeta: bn.ParsePageMeta(doc),
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: bn.ParseEngagementConfig(doc),
}
}
// --- Default template: header / main / footer ---
func Terminal(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\"><div class=\"terminal-main-80 px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></header><main class=\"terminal-main-80 w-full px-4 py-8 flex-grow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("// no content blocks assigned to this page")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 100, Col: 76}
}
_, 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, 6, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</main><footer class=\"w-full mt-auto\"><div class=\"terminal-main-80 px-4\">")
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, 8, "</div></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, 9, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// --- Landing template: hero / main / cta / footer ---
func TerminalLanding(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<body class=\"terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<section class=\"w-full\"><div class=\"terminal-main-80 px-4 py-8\">")
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, 13, "</div></section><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, 14, "<div class=\"terminal-main-80 px-4 py-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</main><section class=\"w-full\"><div class=\"terminal-main-80 px-4 py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></section><footer class=\"w-full mt-auto\"><div class=\"terminal-main-80 px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// --- Article template: header / toc / main / footer (man-page two-column) ---
func TerminalArticle(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, 20, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<body class=\"terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<header class=\"w-full\"><div class=\"terminal-main-80 px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div></header><div class=\"terminal-main-80 w-full px-4 py-6 flex-grow\"><div class=\"terminal-article-grid\"><aside class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["toc"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside><main class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<p class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("// no content blocks assigned to this page")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 190, Col: 78}
}
_, 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, 26, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</main></div></div><footer class=\"w-full mt-auto\"><div class=\"terminal-main-80 px-4\">")
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, 28, "</div></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, 29, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// --- Full-width template: header / main / footer, edge-to-edge ---
func TerminalFullWidth(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_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/terminal/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<body class=\"terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<header class=\"w-full px-4\">")
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 px-4 py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<p class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("// no content blocks assigned to this page")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `template.templ`, Line: 230, Col: 76}
}
_, 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, 35, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</main><footer class=\"w-full mt-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// --- Render functions wired up in register.go via wrap(). ---
func RenderTerminal(ctx context.Context, doc map[string]any) templ.Component {
return Terminal(parseTerminalPageData(doc))
}
func RenderTerminalLanding(ctx context.Context, doc map[string]any) templ.Component {
return TerminalLanding(parseTerminalPageData(doc))
}
func RenderTerminalArticle(ctx context.Context, doc map[string]any) templ.Component {
return TerminalArticle(parseTerminalPageData(doc))
}
func RenderTerminalFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return TerminalFullWidth(parseTerminalPageData(doc))
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,11 @@
{% set c_bg = colors.background|default:"#0a0a0a" %}{% set c_fg = colors.foreground|default:"#00ff00" %}{% set c_bd = colors.border|default:"#1a4a1a" %}{% set c_mf = colors.mutedForeground|default:"#5a8c5a" %}<!doctype html><!-- text/plain alternative
================================================================================
{{ body|safe }}
================================================================================
--
{% if site_name %}{{ site_name }}
{% endif %}{% if site_url %}{{ site_url }}
{% endif %}{% if unsubscribe_url %}unsubscribe: {{ unsubscribe_url }}
{% endif %}
--><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{{ site_name }}</title></head><body style="background-color: {{ c_bg }}; margin: 0; padding: 0; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;">{% if preview_text %}<div style="display:none; max-height:0; overflow:hidden;">{{ preview_text }}</div>{% endif %}<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ c_bg }};"><tr><td align="center" style="padding: 32px 12px; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }};"><table role="presentation" width="640" cellspacing="0" cellpadding="0" border="0" style="max-width: 640px; width: 100%; border: 1px solid {{ c_bd }}; background-color: {{ c_bg }};"><tr><td style="padding: 16px 24px 0; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }}; letter-spacing: 0.05em;">================================================================================</td></tr><tr><td style="padding: 16px 24px; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }}; font-size: 14px; line-height: 1.6;">{{ body|safe }}</td></tr><tr><td style="padding: 0 24px; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }}; letter-spacing: 0.05em;">================================================================================</td></tr><tr><td style="padding: 16px 24px; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_mf }}; font-size: 12px;"><pre style="margin: 0; font-family: &amp;#39;JetBrains Mono&amp;#39;, &amp;#39;IBM Plex Mono&amp;#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_mf }}; white-space: pre-wrap;">--
</pre>{% if site_name %}<div>{{ site_name }}</div>{% endif %}{% if site_url %}<div><a href="{{ site_url }}" style="color: {{ c_fg }}; text-decoration: underline;">{{ site_url }}</a></div>{% endif %}{% if unsubscribe_url %}<div><a href="{{ unsubscribe_url }}" style="color: {{ c_mf }}; text-decoration: underline;">unsubscribe</a></div>{% endif %}</td></tr></table></td></tr></table></body></html>

View File

@ -0,0 +1 @@
{% if text %}{% if target == "_blank" %}<a class="terminal-button caret-blink" href="{% if url %}{{ url }}{% else %}#{% endif %}" target="_blank" rel="noopener noreferrer">[ {{ text }} ]</a>{% else %}<a class="terminal-button caret-blink" href="{% if url %}{{ url }}{% else %}#{% endif %}">[ {{ text }} ]</a>{% endif %}{% endif %}

View File

@ -0,0 +1 @@
{% set lvl = level|default:2|integer %}{% if lvl < 1 or lvl > 6 %}{% set lvl = 2 %}{% endif %}{% set t = text|upper %}{% if lvl == 1 %}<h1 class="terminal-heading terminal-heading-1 text-3xl font-bold {{ textClass }}" style="text-transform: uppercase;"># {{ t }}</h1>{% elif lvl == 3 %}<h3 class="terminal-heading terminal-heading-3 text-xl font-semibold {{ textClass }}" style="text-transform: uppercase;">### {{ t }}</h3>{% elif lvl == 4 %}<h4 class="terminal-heading terminal-heading-4 text-lg font-semibold {{ textClass }}" style="text-transform: uppercase;">#### {{ t }}</h4>{% elif lvl == 5 %}<h5 class="terminal-heading terminal-heading-5 text-base font-medium {{ textClass }}" style="text-transform: uppercase;">##### {{ t }}</h5>{% elif lvl == 6 %}<h6 class="terminal-heading terminal-heading-6 text-base font-medium {{ textClass }}" style="text-transform: uppercase;">###### {{ t }}</h6>{% else %}<h2 class="terminal-heading terminal-heading-2 text-2xl font-bold {{ textClass }}" style="text-transform: uppercase;">## {{ t }}</h2>{% endif %}

View File

@ -0,0 +1 @@
<figure class="ascii-frame">{% if src %}<img src="{{ src }}" alt="{{ alt|default:caption }}">{% else %}<div class="terminal-mono" style="padding: 1rem;">[ no image ]</div>{% endif %}<figcaption>[fig.{% if caption %} {{ caption }}{% endif %}]</figcaption></figure>

View File

@ -0,0 +1 @@
<div class="terminal-prose {{ class }}">{{ text|safe }}</div>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" class="dark">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
<div class="terminal-main-80 px-4">{{ slots.header|safe }}</div>
</header>
<div class="terminal-main-80 w-full px-4 py-6 flex-grow">
<div class="terminal-article-grid">
<aside class="w-full">{{ slots.toc|safe }}</aside>
<main class="w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono">// no content blocks assigned to this page</p>{% endif %}
</main>
</div>
</div>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="dark">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
<div class="terminal-main-80 px-4">{{ slots.header|safe }}</div>
</header>
<main class="terminal-main-80 w-full px-4 py-8 flex-grow">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono">// no content blocks assigned to this page</p>{% endif %}
</main>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en" class="dark">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full px-4">{{ slots.header|safe }}</header>
<main class="flex-grow w-full px-4 py-6">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono">// no content blocks assigned to this page</p>{% endif %}
</main>
<footer class="w-full mt-auto px-4">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" class="dark">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<section class="w-full">
<div class="terminal-main-80 px-4 py-8">{{ slots.hero|safe }}</div>
</section>
<main class="flex-grow w-full">
{% if slots.main %}<div class="terminal-main-80 px-4 py-8">{{ slots.main|safe }}</div>{% endif %}
</main>
<section class="w-full">
<div class="terminal-main-80 px-4 py-6">{{ slots.cta|safe }}</div>
</section>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,17 +0,0 @@
package main
import (
"bytes"
"context"
)
// TerminalTextBlock renders text with 80ch wrap and a subtle phosphor glow.
// Uses the same schema as the built-in text block: {text, class}.
func TerminalTextBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
class := getString(content, "class")
var buf bytes.Buffer
_ = terminalTextComponent(text, class).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,7 +0,0 @@
package main
templ terminalTextComponent(text, class string) {
<div class={ "terminal-prose", class }>
@templ.Raw(text)
</div>
}

View File

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

50
toc.go
View File

@ -1,50 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// TocBlockMeta defines metadata for the section TOC block.
var TocBlockMeta = blocks.BlockMeta{
Key: "toc",
Title: "Section TOC",
Description: "Fixed left-rail table of contents for article pages",
Source: "terminal",
Category: blocks.CategoryNavigation,
}
// TocItem is a single TOC entry.
type TocItem struct {
Label string
Anchor string
}
// TocData is the parsed content for the toc block.
type TocData struct {
Heading string
Items []TocItem
}
// TocBlock renders the section TOC.
// Content shape: {heading, items[{label, anchor}]}.
func TocBlock(ctx context.Context, content map[string]any) string {
items := getSlice(content, "items")
tocItems := make([]TocItem, 0, len(items))
for _, it := range items {
tocItems = append(tocItems, TocItem{
Label: getString(it, "label"),
Anchor: getString(it, "anchor"),
})
}
data := TocData{
Heading: getStringDefault(content, "heading", "Sections"),
Items: tocItems,
}
var buf bytes.Buffer
_ = tocComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,27 +0,0 @@
package main
func tocAnchorHref(anchor string) string {
if anchor == "" {
return "#"
}
return "#" + anchor
}
templ tocComponent(data TocData) {
<nav class="terminal-toc" data-block="terminal:toc">
<h4>{ data.Heading }</h4>
if len(data.Items) > 0 {
<ul>
for _, item := range data.Items {
<li>
<a href={ templ.SafeURL(tocAnchorHref(item.Anchor)) }>
{ "§ " + item.Label }
</a>
</li>
}
</ul>
} else {
<p class="terminal-mono">{ "// no sections yet" }</p>
}
</nav>
}

View File

@ -1,124 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func tocAnchorHref(anchor string) string {
if anchor == "" {
return "#"
}
return "#" + anchor
}
func tocComponent(data TocData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav class=\"terminal-toc\" data-block=\"terminal:toc\"><h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Heading)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `toc.templ`, Line: 12, Col: 20}
}
_, 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, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Items) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data.Items {
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(tocAnchorHref(item.Anchor)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `toc.templ`, Line: 17, Col: 57}
}
_, 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, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("§ " + item.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `toc.templ`, Line: 18, Col: 27}
}
_, 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
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"terminal-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("// no sections yet")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `toc.templ`, Line: 24, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate