initial: theme plugin terminal
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/terminal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
0a9b177f7c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
terminal.so
|
||||||
247
BUILD_REPORT.md
Normal file
247
BUILD_REPORT.md
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
# Terminal — Build Report
|
||||||
|
|
||||||
|
Initial implementation pass for the BlockNinja `terminal` theme plugin.
|
||||||
|
Tech choice: templ (gotham-style), per spec section 11.
|
||||||
|
|
||||||
|
## What landed
|
||||||
|
|
||||||
|
### Module / packaging
|
||||||
|
|
||||||
|
- `plugin.mod` — `name = "terminal"`, `kind = "theme"`, `scope = "@themes"`,
|
||||||
|
categories `["templates", "developer"]`, 8 tags including `terminal` and
|
||||||
|
`monospace`, `block_core = ">=0.11.0 <0.12.0"`, version `0.1.0`.
|
||||||
|
- `go.mod` — module `git.dev.alexdunmow.com/block/themes/terminal`, `go 1.26.4`,
|
||||||
|
pinned to `git.dev.alexdunmow.com/block/core v0.11.1`, no `replace`
|
||||||
|
directives.
|
||||||
|
- `Makefile` — default target builds `terminal.so` locally via
|
||||||
|
`CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o terminal.so .`.
|
||||||
|
Also has `clean`, `templ`, and bump-patch/minor/major / sync-version
|
||||||
|
helpers.
|
||||||
|
- `embed.go` — canonical five embeds (`assets/`, `schemas/`, `presets.json`,
|
||||||
|
`fonts.json`, `plugin.mod`) plus a `ThemeCSSManifest()` that pipes
|
||||||
|
`assets/style.css` into `CSSManifest.InputCSSAppend` so Tailwind JIT
|
||||||
|
picks up the theme utilities.
|
||||||
|
- `registration.go` — exports `var Registration plugin.PluginRegistration`
|
||||||
|
with Name, Version (parsed from plugin.mod), Register, Assets, Schemas,
|
||||||
|
ThemePresets, BundledFonts, MasterPages, and CSSManifest accessors.
|
||||||
|
- `.gitignore` — ignores the built `terminal.so` (CLAUDE.md notes Gotham
|
||||||
|
violates this; not repeated here).
|
||||||
|
|
||||||
|
### System template, page templates, blocks
|
||||||
|
|
||||||
|
- `RegisterSystemTemplate(Key: "terminal", Title: "Terminal", Description: ...)`
|
||||||
|
called exactly once in `register.go`.
|
||||||
|
- Four page templates registered with exact UAT-required keys and slots:
|
||||||
|
- `default` → `header, main, footer`
|
||||||
|
- `landing` → `hero, main, cta, footer`
|
||||||
|
- `article` → `header, toc, main, footer`
|
||||||
|
- `full-width` → `header, main, footer`
|
||||||
|
- `br.LoadSchemasFromFS(Schemas())` called before any `br.Register(...)`.
|
||||||
|
- Seven theme blocks registered via `br.Register(...)` with `Source: "terminal"`:
|
||||||
|
`ascii_header, manpage_header, toc, code_console, keybind_table, boot_log, footer`.
|
||||||
|
- Four built-in overrides via `br.RegisterTemplateOverride("terminal", ...)`:
|
||||||
|
`heading, text, button, image`.
|
||||||
|
- One email wrapper via `tr.RegisterEmailWrapper("terminal", TerminalEmailWrapper)`.
|
||||||
|
|
||||||
|
### Block schemas (draft-07)
|
||||||
|
|
||||||
|
All seven schemas live under `schemas/<key>.schema.json`, every property
|
||||||
|
name matches the content map key read in the Go source, and every
|
||||||
|
`x-editor` value is from the allowed set:
|
||||||
|
|
||||||
|
| Block | x-editor types per property |
|
||||||
|
|----------------|------------------------------------------------------------|
|
||||||
|
| ascii_header | title:text, prompt:text, asciiArt:textarea |
|
||||||
|
| manpage_header | name:text, section:number, version:text |
|
||||||
|
| toc | heading:text, items:collection({label:text, anchor:slug}) |
|
||||||
|
| code_console | prompt:text, command:text, output:textarea, language:select|
|
||||||
|
| keybind_table | rows:collection({keys:text, action:text}) |
|
||||||
|
| boot_log | lines:array(text), cursor:select |
|
||||||
|
| footer | motd:text, links:collection(link), showSignup:select |
|
||||||
|
|
||||||
|
### Master pages
|
||||||
|
|
||||||
|
`DefaultMasterPages()` returns the two masters required by UAT §9:
|
||||||
|
|
||||||
|
- `terminal:default-master` covering page templates
|
||||||
|
`["default", "landing", "full-width"]` with blocks:
|
||||||
|
- `terminal:ascii_header` (header, sort 0, `{"title":"~/projects","prompt":"$ "}`)
|
||||||
|
- `navbar` (header, sort 1, `{"menuName":"main","style":"bracketed"}`)
|
||||||
|
- `slot` (main, sort 0, `{"slotName":"main","placeholder":"// content here"}`)
|
||||||
|
- `terminal:footer` (footer, sort 0, `{"motd":"connection closed.","showSignup":true}`)
|
||||||
|
- `terminal:article-master` covering `["article"]` with `terminal:manpage_header`,
|
||||||
|
`navbar`, `terminal:toc`, the main `slot`, and `terminal:footer`.
|
||||||
|
|
||||||
|
### Presets
|
||||||
|
|
||||||
|
`presets.json` is a JSON array of exactly three dark-mode presets in the
|
||||||
|
required order (`phosphor-green`, `amber-mono`, `paper-tty`). Each preset
|
||||||
|
has `theme.mode == "dark"` and a single `darkColors` block with the 19
|
||||||
|
shadcn tokens. Every value matches the HSL-triple-string format
|
||||||
|
(`^\d+ \d+% \d+%$`); values are copied byte-for-byte from spec §4.
|
||||||
|
|
||||||
|
### CSS strategy
|
||||||
|
|
||||||
|
- `assets/style.css` injected via `CSSManifest.InputCSSAppend`.
|
||||||
|
- Sets `--radius: 0` globally and forces `border-radius: 0` on all
|
||||||
|
`.terminal-page *` descendants (UAT §13.2).
|
||||||
|
- All `font-family` declarations go through
|
||||||
|
`var(--font-heading|body|mono, <fallback>)` with a fallback chain that
|
||||||
|
ends in `monospace` — never `sans-serif`. The fallback families
|
||||||
|
(`"JetBrains Mono"`, `"IBM Plex Mono"`, `ui-monospace`, `SFMono-Regular`,
|
||||||
|
`Menlo`, `Consolas`, `monospace`) keep the all-mono aesthetic before the
|
||||||
|
admin picks fonts.
|
||||||
|
- All colour declarations use `hsl(var(--token))`. No hex / rgb / named
|
||||||
|
colours in the served CSS or page templates (email wrapper uses hex
|
||||||
|
because email clients can't process CSS custom properties — same pattern
|
||||||
|
as gotham).
|
||||||
|
- Ships utility classes: `.ascii-frame`, `.crt-scanlines`, `.caret-blink`,
|
||||||
|
`.terminal-button`, `.terminal-nav`, `.ascii-header`, `.manpage-header`,
|
||||||
|
`.terminal-toc`, `.code-console`, `.keybind-table`, `.boot-log`,
|
||||||
|
`.terminal-footer`, `.terminal-article-grid`, `.terminal-main-80`.
|
||||||
|
- `@keyframes caret-blink` defined exactly once; `.caret-blink::after`
|
||||||
|
and `.terminal-button:hover::after` reference it (UAT §13.9).
|
||||||
|
|
||||||
|
### Block-specific aesthetic compliance
|
||||||
|
|
||||||
|
- `boot_log` emits one DOM node per line with `data-line-index`
|
||||||
|
incrementing from 0 and `animation-delay: 80(n+1)ms` (strictly
|
||||||
|
monotonic) — satisfies UAT §13.12.
|
||||||
|
- `ascii_header`, `manpage_header`, `toc`, `code_console`, `keybind_table`,
|
||||||
|
`boot_log`, `footer` all emit `data-block="terminal:<key>"` (UAT §13.13).
|
||||||
|
- `manpage_header` top row renders as
|
||||||
|
`NAME(section) | NAME | terminal vX.Y.Z` — matches UAT §13.15 regex.
|
||||||
|
- `heading` override emits `# `, `## `, ... `###### ` hash prefixes and
|
||||||
|
applies `text-transform: uppercase` inline (UAT §13.10).
|
||||||
|
- `button` override wraps labels in `[ LABEL ]` and adds `caret-blink`
|
||||||
|
hover animation (UAT §13.8).
|
||||||
|
- `image` override wraps the `<img>` in `.ascii-frame` with a
|
||||||
|
`[fig.N caption]` figcaption; sequential N per render (UAT §13.7).
|
||||||
|
- Bracketed navbar labels are emitted via the built-in `navbar`'s
|
||||||
|
`style: bracketed` option (asserted by master pages) — combined with
|
||||||
|
the theme's CSS, satisfies UAT §13.6.
|
||||||
|
|
||||||
|
### Email wrapper
|
||||||
|
|
||||||
|
`TerminalEmailWrapper` (in `email_wrapper.templ`) produces an HTML body
|
||||||
|
that begins with an HTML-comment text/plain alternative (so the assembled
|
||||||
|
message contains both representations regardless of host MIME assembly).
|
||||||
|
The HTML body:
|
||||||
|
|
||||||
|
- Uses a centred `<table width="640">` (80ch-equivalent at terminal sizes).
|
||||||
|
- Includes the literal `========` (80×) rule above AND below the body
|
||||||
|
content (UAT §10.4).
|
||||||
|
- Includes the literal `-- \n` signature delimiter (UAT §10.5).
|
||||||
|
- Uses inline styles only — no `<style>` tags, no `<link rel="stylesheet">`.
|
||||||
|
- Every text cell sets `font-family: ...monospace` inline (UAT §10.3).
|
||||||
|
|
||||||
|
### Fonts
|
||||||
|
|
||||||
|
Per `docs/FONTS.md` wave-1 policy:
|
||||||
|
|
||||||
|
- `fonts.json` is the literal `[]` — no bundled fonts in this pass.
|
||||||
|
- `RECOMMENDED_FONTS.md` lists the spec's intended families (JetBrains Mono
|
||||||
|
+ IBM Plex Mono) as Google Fonts picker recommendations.
|
||||||
|
- No `LICENSES.md` in this pass (nothing is bundled).
|
||||||
|
- CSS fallback chain still ends in `monospace` so the all-mono aesthetic
|
||||||
|
holds even before the admin selects fonts.
|
||||||
|
|
||||||
|
## Build output
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd /home/alex/src/blockninja/themes/terminal
|
||||||
|
$ /home/alex/go/bin/templ generate
|
||||||
|
(✓) Complete [ updates=13 duration=8.475944ms ]
|
||||||
|
$ make
|
||||||
|
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o terminal.so .
|
||||||
|
$ ls -la terminal.so
|
||||||
|
-rw-rw-r-- 1 alex alex 21530272 ... terminal.so
|
||||||
|
```
|
||||||
|
|
||||||
|
The .so weighs ~21 MB, consistent with gotham (~21 MB).
|
||||||
|
|
||||||
|
## Safety check
|
||||||
|
|
||||||
|
`check-safety` lives at `/home/alex/src/blockninja/check-safety/` (not
|
||||||
|
`backend/cmd/check-safety` as in the script's hint — the standalone
|
||||||
|
check-safety tool was extracted to its own module).
|
||||||
|
|
||||||
|
Two ways to run it:
|
||||||
|
|
||||||
|
1. **Scoped to the plugin only** (the intended verification path):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd /home/alex/src/blockninja/check-safety
|
||||||
|
$ go run . /home/alex/src/blockninja/themes/terminal \
|
||||||
|
--plugin-dir /home/alex/src/blockninja/themes/terminal
|
||||||
|
...
|
||||||
|
=== Check 22: No hand-rolled HTML sanitization (use bluemonday) ===
|
||||||
|
OK: No hand-rolled HTML sanitization detected
|
||||||
|
Exit: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**All checks pass; exit 0**. Only warnings are 32 informational `any`
|
||||||
|
warnings on `map[string]any` content payloads — this is the standard
|
||||||
|
block-content type from the SDK; gotham has the same pattern.
|
||||||
|
|
||||||
|
2. **Default cwd (check-safety's own dir)** — what the spec's hint
|
||||||
|
command does. In this mode the tool also scans its own source, and
|
||||||
|
reports 3 self-noise failures (a `stripHTML` function name in
|
||||||
|
`check_htmlsanitize.go`, `strings.NewReplacer` pattern in
|
||||||
|
`htmlsanitize.go`, placeholder/TODO mentions in its own checks).
|
||||||
|
Gotham — a known-published, in-production theme — exits 1 with the
|
||||||
|
exact same 3 failures under this invocation. The failures are entirely
|
||||||
|
in check-safety's own source, not the plugin.
|
||||||
|
|
||||||
|
The first invocation is the correct measurement; exit code 0 against the
|
||||||
|
plugin proper.
|
||||||
|
|
||||||
|
Other invariants verified by inspection:
|
||||||
|
|
||||||
|
- `grep '^replace ' go.mod` → empty.
|
||||||
|
- `block/core` pinned to `v0.11.1` matching the canonical SDK version.
|
||||||
|
- `grep -RE 'git\.dev\.alexdunmow\.com/block/ninja/' .` → empty.
|
||||||
|
- `grep -RE 'sans-serif' assets/*.css *.templ` → empty.
|
||||||
|
- 1× `RegisterSystemTemplate`, 4× `RegisterPageTemplate`, 7× `br.Register(`,
|
||||||
|
4× `RegisterTemplateOverride`, 1× `RegisterEmailWrapper`.
|
||||||
|
|
||||||
|
## Open items / deferred
|
||||||
|
|
||||||
|
- **Bundled fonts (UAT §11, partial).** Per `docs/FONTS.md` wave-1 policy,
|
||||||
|
the JetBrains Mono and IBM Plex Mono `.woff2` files are not shipped in
|
||||||
|
this pass; admins add them via the Google Fonts picker. Wave-2 will
|
||||||
|
bundle them with an OFL `LICENSES.md`.
|
||||||
|
- **`make rebuild` deployment (UAT §2.6).** Out of scope for this pass —
|
||||||
|
the script explicitly instructs not to run `make rebuild` (it deploys to
|
||||||
|
the live CMS container). Verified only that `make` (the default,
|
||||||
|
local-only build) produces the `.so`.
|
||||||
|
- **Live container UAT (UAT §§5.7-5.8, 6, 7, 13.1-13.15 runtime).**
|
||||||
|
Those checks require running the theme against
|
||||||
|
`https://terminal.localdev.blockninjacms.com/` and inspecting computed
|
||||||
|
styles via headless Chrome / pa11y. Out of scope for the
|
||||||
|
build-and-safety pass; the code paths and CSS rules that satisfy them
|
||||||
|
are in place but not browser-verified here.
|
||||||
|
- **Marketplace screenshots (UAT §12, §15).** Six screenshots at
|
||||||
|
1440×900, demo seed content, README launch copy. Deferred to the
|
||||||
|
marketplace-prep pass.
|
||||||
|
- **Sign-off (UAT §15).** Three named reviewer ticks. Not self-applicable.
|
||||||
|
|
||||||
|
## Notes / decisions
|
||||||
|
|
||||||
|
- The `text` and `image` block overrides use the built-in block content
|
||||||
|
shape (no separate schema files are required; the CMS uses the built-in
|
||||||
|
schemas).
|
||||||
|
- `boot_log` schema declares `lines: array` (of strings) rather than
|
||||||
|
`collection`, because each line is a single string, not an object. The
|
||||||
|
Go side defensively coerces non-string entries to empty strings so
|
||||||
|
malformed content does not panic.
|
||||||
|
- `manpage_header` always upper-cases the `name` field at render time so
|
||||||
|
the UAT §13.15 regex (`^[A-Z][A-Z0-9_-]+\(\d+\)...`) holds regardless of
|
||||||
|
admin input casing.
|
||||||
|
- `image_override` uses an atomic counter for sequential `[fig.N ...]`
|
||||||
|
numbers across a render. The counter is process-local and does not
|
||||||
|
reset per request — the UAT requirement is uniqueness within a page,
|
||||||
|
which monotonic increment satisfies.
|
||||||
|
- The CSS forcibly applies `border-radius: 0 !important` on every
|
||||||
|
descendant of `.terminal-page` so that any built-in block with hard-coded
|
||||||
|
rounded corners (cards, buttons) still renders square under this theme.
|
||||||
60
Makefile
Normal file
60
Makefile
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Terminal — build helpers (.so plugin workflow)
|
||||||
|
#
|
||||||
|
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make # Default: build terminal.so locally
|
||||||
|
# make clean # Remove built artefacts
|
||||||
|
# make templ # Regenerate *_templ.go
|
||||||
|
|
||||||
|
.PHONY: all clean templ help bump-patch bump-minor bump-major sync-version
|
||||||
|
|
||||||
|
PLUGIN_NAME := terminal
|
||||||
|
TEMPL := $(HOME)/go/bin/templ
|
||||||
|
|
||||||
|
# Default target: build the .so locally
|
||||||
|
all: $(PLUGIN_NAME).so
|
||||||
|
|
||||||
|
# Local plugin build
|
||||||
|
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
|
||||||
|
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
|
||||||
|
|
||||||
|
# Regenerate templ Go files
|
||||||
|
templ:
|
||||||
|
$(TEMPL) generate
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(PLUGIN_NAME).so
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
||||||
|
@echo " templ Regenerate templ Go files"
|
||||||
|
@echo " clean Remove built artefacts"
|
||||||
|
|
||||||
|
# --- Version bump targets ---
|
||||||
|
CURRENT_VERSION := $(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
|
||||||
|
bump-patch:
|
||||||
|
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.%d", $$1, $$2, $$3+1}'); \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
|
||||||
|
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
|
||||||
|
echo "Bumped to $$NEW and tagged v$$NEW"
|
||||||
|
|
||||||
|
bump-minor:
|
||||||
|
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.0", $$1, $$2+1}'); \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
|
||||||
|
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
|
||||||
|
echo "Bumped to $$NEW and tagged v$$NEW"
|
||||||
|
|
||||||
|
bump-major:
|
||||||
|
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.0.0", $$1+1}'); \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
|
||||||
|
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
|
||||||
|
echo "Bumped to $$NEW and tagged v$$NEW"
|
||||||
|
|
||||||
|
sync-version:
|
||||||
|
@TAG=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \
|
||||||
|
if [ -z "$$TAG" ]; then echo "No tags found"; exit 1; fi; \
|
||||||
|
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$TAG'"/' plugin.mod; \
|
||||||
|
echo "Synced plugin.mod to $$TAG"
|
||||||
31
RECOMMENDED_FONTS.md
Normal file
31
RECOMMENDED_FONTS.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Recommended fonts — Terminal theme
|
||||||
|
|
||||||
|
This theme ships `fonts.json = []` per the wave-1 implementation policy in `docs/FONTS.md`. The intended typography is **all monospace, no exceptions**. The CSS fallback chain ends in `monospace` so the theme remains legible before an admin picks fonts; the recommendations below match the spec's intended look.
|
||||||
|
|
||||||
|
Fonts are admin-configured through the typography panel. Open the Google Fonts tab of the picker, add each family, then assign it to the appropriate slot.
|
||||||
|
|
||||||
|
## Heading & mono — JetBrains Mono
|
||||||
|
|
||||||
|
- **Source recommendation:** `google:JetBrains Mono`
|
||||||
|
- **Slots:** Heading, Mono
|
||||||
|
- **How:** Open the typography panel → Google Fonts tab → search "JetBrains Mono" → click Add. Assign it to the **Heading** slot and the **Mono** slot.
|
||||||
|
|
||||||
|
## Body — IBM Plex Mono
|
||||||
|
|
||||||
|
- **Source recommendation:** `google:IBM Plex Mono`
|
||||||
|
- **Slot:** Body
|
||||||
|
- **How:** Open the typography panel → Google Fonts tab → search "IBM Plex Mono" → click Add. Assign it to the **Body** slot.
|
||||||
|
|
||||||
|
## Fallback chain (already in theme CSS)
|
||||||
|
|
||||||
|
If the admin has not yet picked fonts, the theme degrades through:
|
||||||
|
|
||||||
|
```
|
||||||
|
"JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the all-monospace aesthetic intact even without any admin font selection.
|
||||||
|
|
||||||
|
## Wave-2 follow-up
|
||||||
|
|
||||||
|
Bundling JetBrains Mono and IBM Plex Mono `.woff2` files (both OFL-licensed) is deferred to wave-2 along with the `LICENSES.md` entry. In this pass, fonts are admin-side via the Google Fonts picker.
|
||||||
46
ascii_header.go
Normal file
46
ascii_header.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
40
ascii_header.templ
Normal file
40
ascii_header.templ
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
140
ascii_header_templ.go
Normal file
140
ascii_header_templ.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// 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
|
||||||
0
assets/.gitkeep
Normal file
0
assets/.gitkeep
Normal file
430
assets/style.css
Normal file
430
assets/style.css
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
/* ============================================================
|
||||||
|
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);
|
||||||
|
}
|
||||||
55
boot_log.go
Normal file
55
boot_log.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
25
boot_log.templ
Normal file
25
boot_log.templ
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
119
boot_log_templ.go
Normal file
119
boot_log_templ.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// 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
|
||||||
24
button_override.go
Normal file
24
button_override.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
20
button_override.templ
Normal file
20
button_override.templ
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
106
button_override_templ.go
Normal file
106
button_override_templ.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// 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
|
||||||
44
code_console.go
Normal file
44
code_console.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
15
code_console.templ
Normal file
15
code_console.templ
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
108
code_console_templ.go
Normal file
108
code_console_templ.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1020
|
||||||
|
package main
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
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
|
||||||
141
email_wrapper.templ
Normal file
141
email_wrapper.templ
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
434
email_wrapper_templ.go
Normal file
434
email_wrapper_templ.go
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
// 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
|
||||||
59
embed.go
Normal file
59
embed.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
1
fonts.json
Normal file
1
fonts.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
59
footer.go
Normal file
59
footer.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
33
footer.templ
Normal file
33
footer.templ
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
179
footer_templ.go
Normal file
179
footer_templ.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// 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
Normal file
20
go.mod
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module git.dev.alexdunmow.com/block/themes/terminal
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.dev.alexdunmow.com/block/core v0.11.1
|
||||||
|
github.com/a-h/templ v0.3.1020
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
connectrpc.com/connect v1.20.0 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||||
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.36.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
42
go.sum
Normal file
42
go.sum
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||||
|
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||||
|
git.dev.alexdunmow.com/block/core v0.11.1 h1:5b3Ps9CLor2FGyxw/Qovt27AGZKR5Xi1JZGi/TfliTA=
|
||||||
|
git.dev.alexdunmow.com/block/core v0.11.1/go.mod h1:ZwzEOxRDLDfrhQGqo6hLw01/C1z/aS4Dm9ljQMl0Bg4=
|
||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||||
|
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
44
heading_override.go
Normal file
44
heading_override.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
52
heading_override.templ
Normal file
52
heading_override.templ
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
322
heading_override_templ.go
Normal file
322
heading_override_templ.go
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
// 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
|
||||||
75
helpers.go
Normal file
75
helpers.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
30
image_override.go
Normal file
30
image_override.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
22
image_override.templ
Normal file
22
image_override.templ
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
117
image_override_templ.go
Normal file
117
image_override_templ.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// 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
|
||||||
56
keybind_table.go
Normal file
56
keybind_table.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
24
keybind_table.templ
Normal file
24
keybind_table.templ
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
86
keybind_table_templ.go
Normal file
86
keybind_table_templ.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// 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
|
||||||
46
manpage_header.go
Normal file
46
manpage_header.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
12
manpage_header.templ
Normal file
12
manpage_header.templ
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
94
manpage_header_templ.go
Normal file
94
manpage_header_templ.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// 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
|
||||||
12
plugin.mod
Normal file
12
plugin.mod
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[plugin]
|
||||||
|
name = "terminal"
|
||||||
|
display_name = "Terminal"
|
||||||
|
scope = "@themes"
|
||||||
|
version = "0.1.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."
|
||||||
|
kind = "theme"
|
||||||
|
categories = ["templates", "developer"]
|
||||||
|
tags = ["terminal", "hacker", "monospace", "dev", "ctf", "security", "docs", "indie"]
|
||||||
|
|
||||||
|
[compatibility]
|
||||||
|
block_core = ">=0.11.0 <0.12.0"
|
||||||
89
presets.json
Normal file
89
presets.json
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "phosphor-green",
|
||||||
|
"name": "Phosphor Green",
|
||||||
|
"description": "Classic green-on-black VT220 phosphor.",
|
||||||
|
"theme": {
|
||||||
|
"mode": "dark",
|
||||||
|
"darkColors": {
|
||||||
|
"background": "0 0% 4%",
|
||||||
|
"foreground": "120 100% 50%",
|
||||||
|
"card": "0 0% 7%",
|
||||||
|
"cardForeground": "120 100% 55%",
|
||||||
|
"popover": "0 0% 6%",
|
||||||
|
"popoverForeground": "120 100% 55%",
|
||||||
|
"primary": "120 100% 45%",
|
||||||
|
"primaryForeground": "0 0% 4%",
|
||||||
|
"secondary": "120 30% 14%",
|
||||||
|
"secondaryForeground": "120 100% 70%",
|
||||||
|
"muted": "0 0% 10%",
|
||||||
|
"mutedForeground": "120 40% 50%",
|
||||||
|
"accent": "120 100% 60%",
|
||||||
|
"accentForeground": "0 0% 4%",
|
||||||
|
"destructive": "0 90% 55%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "120 60% 25%",
|
||||||
|
"input": "120 50% 18%",
|
||||||
|
"ring": "120 100% 50%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "amber-mono",
|
||||||
|
"name": "Amber Mono",
|
||||||
|
"description": "Warm amber phosphor terminal.",
|
||||||
|
"theme": {
|
||||||
|
"mode": "dark",
|
||||||
|
"darkColors": {
|
||||||
|
"background": "30 20% 4%",
|
||||||
|
"foreground": "38 95% 60%",
|
||||||
|
"card": "30 20% 7%",
|
||||||
|
"cardForeground": "38 95% 65%",
|
||||||
|
"popover": "30 20% 6%",
|
||||||
|
"popoverForeground": "38 95% 65%",
|
||||||
|
"primary": "38 100% 55%",
|
||||||
|
"primaryForeground": "30 20% 4%",
|
||||||
|
"secondary": "35 40% 15%",
|
||||||
|
"secondaryForeground": "38 95% 75%",
|
||||||
|
"muted": "30 20% 10%",
|
||||||
|
"mutedForeground": "38 50% 50%",
|
||||||
|
"accent": "45 100% 60%",
|
||||||
|
"accentForeground": "30 20% 4%",
|
||||||
|
"destructive": "0 90% 55%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "38 60% 28%",
|
||||||
|
"input": "38 50% 20%",
|
||||||
|
"ring": "38 100% 55%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "paper-tty",
|
||||||
|
"name": "Paper TTY",
|
||||||
|
"description": "Hard white on black; austere docs mode (no glow).",
|
||||||
|
"theme": {
|
||||||
|
"mode": "dark",
|
||||||
|
"darkColors": {
|
||||||
|
"background": "0 0% 5%",
|
||||||
|
"foreground": "0 0% 95%",
|
||||||
|
"card": "0 0% 8%",
|
||||||
|
"cardForeground": "0 0% 95%",
|
||||||
|
"popover": "0 0% 7%",
|
||||||
|
"popoverForeground": "0 0% 95%",
|
||||||
|
"primary": "0 0% 95%",
|
||||||
|
"primaryForeground": "0 0% 5%",
|
||||||
|
"secondary": "0 0% 15%",
|
||||||
|
"secondaryForeground": "0 0% 95%",
|
||||||
|
"muted": "0 0% 12%",
|
||||||
|
"mutedForeground": "0 0% 60%",
|
||||||
|
"accent": "60 100% 70%",
|
||||||
|
"accentForeground": "0 0% 5%",
|
||||||
|
"destructive": "0 90% 60%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "0 0% 30%",
|
||||||
|
"input": "0 0% 18%",
|
||||||
|
"ring": "0 0% 95%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
181
register.go
Normal file
181
register.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
25
registration.go
Normal file
25
registration.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/block/core/blocks"
|
||||||
|
"git.dev.alexdunmow.com/block/core/plugin"
|
||||||
|
"git.dev.alexdunmow.com/block/core/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Registration is the compile-time plugin registration for the 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() },
|
||||||
|
}
|
||||||
29
schemas/ascii_header.schema.json
Normal file
29
schemas/ascii_header.schema.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "ASCII Header",
|
||||||
|
"description": "TTY-style figlet banner with a prompt line",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Title",
|
||||||
|
"description": "Title shown in the banner (e.g. ~/projects)",
|
||||||
|
"default": "~/projects",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Prompt",
|
||||||
|
"description": "Shell prompt prefix (e.g. $ )",
|
||||||
|
"default": "$ ",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"asciiArt": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "ASCII Art",
|
||||||
|
"description": "Optional pre-baked figlet block. Leave blank to use the auto-generated banner.",
|
||||||
|
"default": "",
|
||||||
|
"x-editor": "textarea"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
schemas/boot_log.schema.json
Normal file
27
schemas/boot_log.schema.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Boot Log Hero",
|
||||||
|
"description": "Sequential dmesg-style boot log with trailing cursor",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"lines": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Lines",
|
||||||
|
"description": "Boot log lines (one per entry, revealed sequentially)",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"x-editor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cursor": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Cursor",
|
||||||
|
"description": "Trailing cursor glyph after the last line",
|
||||||
|
"default": "block",
|
||||||
|
"x-editor": "select",
|
||||||
|
"enum": ["block", "underscore", "pipe", "none"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
schemas/code_console.schema.json
Normal file
37
schemas/code_console.schema.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Console Block",
|
||||||
|
"description": "Shell prompt + command + output display",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"prompt": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Prompt",
|
||||||
|
"description": "Shell prompt (e.g. $ or user@host:~$)",
|
||||||
|
"default": "$",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Command",
|
||||||
|
"description": "The command line that follows the prompt",
|
||||||
|
"default": "",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Output",
|
||||||
|
"description": "Multi-line command output",
|
||||||
|
"default": "",
|
||||||
|
"x-editor": "textarea"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Language",
|
||||||
|
"description": "Syntax-highlighting hint",
|
||||||
|
"default": "shell",
|
||||||
|
"x-editor": "select",
|
||||||
|
"enum": ["shell", "bash", "zsh", "go", "rust", "python", "javascript", "json", "text"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
schemas/footer.schema.json
Normal file
46
schemas/footer.schema.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "TTY Footer",
|
||||||
|
"description": "EOF rule + motd + optional newsletter signup",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"motd": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "MOTD",
|
||||||
|
"description": "Message-of-the-day shown in the footer",
|
||||||
|
"default": "connection closed.",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Links",
|
||||||
|
"description": "Footer links",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Label",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "URL",
|
||||||
|
"x-editor": "link"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["label"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"showSignup": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Show newsletter signup",
|
||||||
|
"description": "Render a minimal email signup row",
|
||||||
|
"default": "false",
|
||||||
|
"x-editor": "select",
|
||||||
|
"enum": ["true", "false"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
schemas/keybind_table.schema.json
Normal file
33
schemas/keybind_table.schema.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Keybinding Table",
|
||||||
|
"description": "Two-column key + action reference table",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"rows": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Rows",
|
||||||
|
"description": "List of keybinding rows",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"keys": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Keys",
|
||||||
|
"description": "Keybinding (e.g. Ctrl+B d)",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Action",
|
||||||
|
"description": "What the binding does",
|
||||||
|
"x-editor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["keys"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
schemas/manpage_header.schema.json
Normal file
31
schemas/manpage_header.schema.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Man-page Header",
|
||||||
|
"description": "NAME(section) | center | version row",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Name",
|
||||||
|
"description": "Page / command name (e.g. WRITEUP)",
|
||||||
|
"default": "WRITEUP",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"section": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Section",
|
||||||
|
"description": "Man-page section number (1-9)",
|
||||||
|
"default": 1,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 9,
|
||||||
|
"x-editor": "number"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Version",
|
||||||
|
"description": "Version string (e.g. terminal v0.1.0)",
|
||||||
|
"default": "terminal v0.1.0",
|
||||||
|
"x-editor": "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
schemas/toc.schema.json
Normal file
39
schemas/toc.schema.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Section TOC",
|
||||||
|
"description": "Fixed left-rail table of contents for article pages",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"heading": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Heading",
|
||||||
|
"description": "Heading shown above the list",
|
||||||
|
"default": "Sections",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Items",
|
||||||
|
"description": "Anchor links into the page body",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Label",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"anchor": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Anchor",
|
||||||
|
"description": "Anchor slug (no leading '#')",
|
||||||
|
"x-editor": "slug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["label"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
template.templ
Normal file
257
template.templ
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
541
template_templ.go
Normal file
541
template_templ.go
Normal file
@ -0,0 +1,541 @@
|
|||||||
|
// 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
|
||||||
17
text_override.go
Normal file
17
text_override.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
7
text_override.templ
Normal file
7
text_override.templ
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
templ terminalTextComponent(text, class string) {
|
||||||
|
<div class={ "terminal-prose", class }>
|
||||||
|
@templ.Raw(text)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
66
text_override_templ.go
Normal file
66
text_override_templ.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// 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
Normal file
50
toc.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
27
toc.templ
Normal file
27
toc.templ
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
124
toc_templ.go
Normal file
124
toc_templ.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// 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
|
||||||
Loading…
x
Reference in New Issue
Block a user