Compare commits
No commits in common. "main" and "v0.1.2" have entirely different histories.
2
.gitignore
vendored
@ -1,3 +1 @@
|
|||||||
terminal.so
|
terminal.so
|
||||||
*.so
|
|
||||||
*.bnp
|
|
||||||
|
|||||||
@ -217,7 +217,7 @@ Other invariants verified by inspection:
|
|||||||
local-only build) produces the `.so`.
|
local-only build) produces the `.so`.
|
||||||
- **Live container UAT (UAT §§5.7-5.8, 6, 7, 13.1-13.15 runtime).**
|
- **Live container UAT (UAT §§5.7-5.8, 6, 7, 13.1-13.15 runtime).**
|
||||||
Those checks require running the theme against
|
Those checks require running the theme against
|
||||||
`https://terminal.blockninja.dev/` and inspecting computed
|
`https://terminal.localdev.blockninjacms.com/` and inspecting computed
|
||||||
styles via headless Chrome / pa11y. Out of scope for the
|
styles via headless Chrome / pa11y. Out of scope for the
|
||||||
build-and-safety pass; the code paths and CSS rules that satisfy them
|
build-and-safety pass; the code paths and CSS rules that satisfy them
|
||||||
are in place but not browser-verified here.
|
are in place but not browser-verified here.
|
||||||
|
|||||||
87
Makefile
@ -1,55 +1,60 @@
|
|||||||
# terminal — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
# Terminal — build helpers (.so plugin workflow)
|
||||||
#
|
#
|
||||||
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
|
||||||
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
|
||||||
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
|
||||||
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
|
|
||||||
# classifies the repo as codeless (no Go at the root) and packs the artifact
|
|
||||||
# with manifest.pb (codeless: true) and NO plugin.wasm.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make build # pack terminal-<version>.bnp (codeless — no wasm)
|
# make # Default: build terminal.so locally
|
||||||
# make verify # validate the built .bnp (loader-identical checks)
|
# make clean # Remove built artefacts
|
||||||
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
# make templ # Regenerate *_templ.go
|
||||||
# make publish # ninja plugin publish --bnp (to the active registry)
|
|
||||||
# make clean # remove build artefacts
|
|
||||||
|
|
||||||
.PHONY: build verify archive-check publish clean help
|
.PHONY: all clean templ help bump-patch bump-minor bump-major sync-version
|
||||||
|
|
||||||
PLUGIN_NAME := terminal
|
PLUGIN_NAME := terminal
|
||||||
NINJA := ninja
|
TEMPL := $(HOME)/go/bin/templ
|
||||||
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
|
||||||
|
|
||||||
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
# Default target: build the .so locally
|
||||||
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
all: $(PLUGIN_NAME).so
|
||||||
build:
|
|
||||||
$(NINJA) plugin build --dir .
|
|
||||||
|
|
||||||
# Validate the built .bnp exactly as the registry/loader will.
|
# Local plugin build
|
||||||
verify:
|
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
|
||||||
$(NINJA) plugin verify $(BNP)
|
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
|
||||||
|
|
||||||
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
# Regenerate templ Go files
|
||||||
archive-check:
|
templ:
|
||||||
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
$(TEMPL) generate
|
||||||
trap 'rm -rf "$$T"' EXIT && \
|
|
||||||
git archive HEAD | tar -x -C "$$T" && \
|
|
||||||
cd "$$T" && $(NINJA) plugin build --dir . && \
|
|
||||||
echo "archive-check OK"
|
|
||||||
|
|
||||||
# Publish the prebuilt .bnp to the active registry.
|
|
||||||
# Pass --host https://my.blockninja.dev for the dev orchestrator.
|
|
||||||
publish: build
|
|
||||||
$(NINJA) plugin publish --bnp $(BNP)
|
|
||||||
|
|
||||||
# Remove build artefacts.
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
rm -f $(PLUGIN_NAME).so
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " build Pack the codeless .bnp (no wasm)"
|
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
||||||
@echo " verify Validate the built .bnp (loader-identical checks)"
|
@echo " templ Regenerate templ Go files"
|
||||||
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
@echo " clean Remove built artefacts"
|
||||||
@echo " publish Publish the .bnp to the active registry"
|
|
||||||
@echo " clean Remove build 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"
|
||||||
|
|||||||
25
README.md
@ -1,25 +0,0 @@
|
|||||||
# Terminal
|
|
||||||
|
|
||||||
A command-line theme in phosphor green on near-black, with TTY window chrome and man-page article layouts. Monospace type runs throughout, and a paper-teletype light mode gives you an off-white, printer-ribbon alternative. It reads like a console a developer already trusts.
|
|
||||||
|
|
||||||
## Good for
|
|
||||||
|
|
||||||
We built Terminal for developer tools and open-source projects. Treat these as starting points, not limits. If the mood fits your project, use it:
|
|
||||||
|
|
||||||
- Dev tools and CLI utilities
|
|
||||||
- Open-source docs and changelogs
|
|
||||||
- Hacker and CTF communities
|
|
||||||
- Technical blogs and engineering handbooks
|
|
||||||
|
|
||||||
## What you get
|
|
||||||
|
|
||||||
- Five color presets, each with a tuned light mode and a tuned dark mode: Phosphor Green, Amber CRT, Paper TTY, Ice Terminal, and Magenta Hacker.
|
|
||||||
- JetBrains Mono and IBM Plex Mono, bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
|
||||||
- Four persona blocks: a man-page section header, a code console, a keybind reference table, and a boot-log sequence.
|
|
||||||
- A typed-terminal hero that draws itself on a canvas at the top of the page. It respects reduced-motion and falls back to a static command block with JavaScript off.
|
|
||||||
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
|
|
||||||
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.
|
|
||||||
@ -1,43 +1,31 @@
|
|||||||
# Recommended fonts — Terminal
|
# Recommended fonts — Terminal theme
|
||||||
|
|
||||||
Every family flows through the admin typography picker via CSS variables. Templates never
|
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.
|
||||||
hardcode a `font-family`; they read `var(--font-heading, ...)`, `var(--font-body, ...)`,
|
|
||||||
and `var(--font-mono, ...)`, each with a bundled fallback so the intended look survives
|
|
||||||
before the admin assigns a slot.
|
|
||||||
|
|
||||||
## Bundled (ship as-is)
|
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.
|
||||||
|
|
||||||
The theme bundles two OFL monospace families (latin subset, ~69 KB total) as
|
## Heading & mono — JetBrains Mono
|
||||||
`source=template` rows, already selectable in the picker. They back the fallbacks:
|
|
||||||
|
|
||||||
| Slot | Fallback family | File |
|
- **Source recommendation:** `google:JetBrains Mono`
|
||||||
|---|---|---|
|
- **Slots:** Heading, Mono
|
||||||
| Heading | JetBrains Mono | `assets/fonts/jetbrains-mono-latin.woff2` |
|
- **How:** Open the typography panel → Google Fonts tab → search "JetBrains Mono" → click Add. Assign it to the **Heading** slot and the **Mono** slot.
|
||||||
| Mono | JetBrains Mono | `assets/fonts/jetbrains-mono-latin.woff2` |
|
|
||||||
| Body | IBM Plex Mono | `assets/fonts/ibm-plex-mono-400.woff2`, `ibm-plex-mono-500.woff2` |
|
|
||||||
|
|
||||||
A terminal theme is monospace top to bottom, so all three slots default to a mono family.
|
## Body — IBM Plex Mono
|
||||||
License record: `assets/fonts/LICENSES.md`.
|
|
||||||
|
|
||||||
## Recommended admin assignments (Google Fonts tab)
|
- **Source recommendation:** `google:IBM Plex Mono`
|
||||||
|
- **Slot:** Body
|
||||||
Assign these from Admin -> Appearance -> Typography if you want the full family (extended
|
- **How:** Open the typography panel → Google Fonts tab → search "IBM Plex Mono" → click Add. Assign it to the **Body** slot.
|
||||||
glyph coverage) or the display accent the bundled subset omits:
|
|
||||||
|
|
||||||
| Slot | Assign | Why |
|
|
||||||
|---|---|---|
|
|
||||||
| Heading | JetBrains Mono or IBM Plex Mono | Sharp, even monospace headings that read as terminal chrome. |
|
|
||||||
| Body | IBM Plex Mono | Slightly wider, highly readable for long docs and man pages. |
|
|
||||||
| Mono | JetBrains Mono | Code blocks, prompts, keybind tables. |
|
|
||||||
| Display accent (optional) | VT323 | Chunky bitmap-CRT face for big phosphor hero display type. Not bundled; assign from the Google Fonts tab if you want the retro-CRT accent. |
|
|
||||||
|
|
||||||
All four are SIL Open Font License 1.1. Nothing here hardcodes a family, so any admin
|
|
||||||
assignment overrides the bundled fallback cleanly.
|
|
||||||
|
|
||||||
## Fallback chain (already in theme CSS)
|
## Fallback chain (already in theme CSS)
|
||||||
|
|
||||||
Before any admin selection the theme degrades through:
|
If the admin has not yet picked fonts, the theme degrades through:
|
||||||
|
|
||||||
```
|
```
|
||||||
"JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
"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.
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
# Recommended icons — Terminal
|
|
||||||
|
|
||||||
Icons render through the CMS sprite pipeline: `<svg><use href="/icons/<pack>.svg#<name>"/></svg>`
|
|
||||||
(or the `::pack:name:size::` shorthand). The theme's overrides reference two bundled packs.
|
|
||||||
|
|
||||||
## Required packs
|
|
||||||
|
|
||||||
Declared in `plugin.mod` as `required_icon_packs = ["lucide", "simple-icons"]`
|
|
||||||
(forward-declared — the loader does not yet auto-install; install manually until it does).
|
|
||||||
|
|
||||||
| Pack | Used for | Install |
|
|
||||||
|---|---|---|
|
|
||||||
| **lucide** | UI glyphs the builtin/override templates use: `arrow-right`, `check`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `terminal`, `cpu`, `mail`, `map-pin`, `download`, `play`, `radio`, `message-square` | Ships bundled with the CMS — no action needed. |
|
|
||||||
| **simple-icons** | Author/social brand glyphs: `github`, `x`, `linkedin`, `discord` | Ships bundled with the CMS — no action needed. |
|
|
||||||
|
|
||||||
Both packs ship bundled with the CMS, so a fresh instance renders every override icon with
|
|
||||||
no manual step. If you author blocks that reference other packs (e.g. `tabler`, `phosphor`),
|
|
||||||
install them from `/admin/icons` (Browse -> install -> enable) before activation.
|
|
||||||
|
|
||||||
## Note
|
|
||||||
|
|
||||||
Icon values in block content are stored as `"pack:name"` strings and split with
|
|
||||||
`|split:":"|first` / `|last`. Never inline raw SVG path data for content icons — that
|
|
||||||
bypasses the sprite system and the admin pack manager.
|
|
||||||
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
@ -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
@ -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
|
||||||
@ -1,21 +0,0 @@
|
|||||||
# Bundled font licenses — Terminal theme
|
|
||||||
|
|
||||||
Both families are bundled as latin-subset `.woff2` and are licensed under the SIL Open
|
|
||||||
Font License 1.1 (OFL). Redistribution inside this theme artifact is permitted.
|
|
||||||
|
|
||||||
| File | Family | Weights | License | Source |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `jetbrains-mono-latin.woff2` | JetBrains Mono (variable) | 400 / 500 / 700 | OFL 1.1 | Google Fonts (fonts.gstatic.com), latin subset |
|
|
||||||
| `ibm-plex-mono-400.woff2` | IBM Plex Mono | 400 | OFL 1.1 | Google Fonts (fonts.gstatic.com), latin subset |
|
|
||||||
| `ibm-plex-mono-500.woff2` | IBM Plex Mono | 500 | OFL 1.1 | Google Fonts (fonts.gstatic.com), latin subset |
|
|
||||||
|
|
||||||
OFL 1.1 full text: https://openfontlicense.org
|
|
||||||
|
|
||||||
- JetBrains Mono (c) 2020 The JetBrains Mono Project Authors.
|
|
||||||
- IBM Plex Mono (c) 2017 IBM Corp., the IBM Plex Project Authors.
|
|
||||||
|
|
||||||
These are latin subsets (Latin + Latin-1 punctuation), chosen to keep the bundle small
|
|
||||||
(~69 KB total). They cover English content. Sites needing extended Latin, Cyrillic, Greek,
|
|
||||||
or Vietnamese glyphs should assign the full family from the admin Google Fonts picker.
|
|
||||||
The bundled families back the theme's `--font-heading`, `--font-body`, and `--font-mono`
|
|
||||||
fallbacks; the admin typography picker overrides them per slot.
|
|
||||||
@ -23,10 +23,7 @@
|
|||||||
font-family: var(--font-heading, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
font-family: var(--font-heading, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
/* Inherit colour from context so headings placed on an inverted band
|
color: hsl(var(--foreground));
|
||||||
(e.g. a bg-primary CTA) read as primary-foreground, not a hardcoded
|
|
||||||
foreground that goes dark-on-bright and fails contrast. */
|
|
||||||
color: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-mono {
|
.terminal-mono {
|
||||||
@ -410,3 +407,24 @@
|
|||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
padding: 0.4rem 0.6rem;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
# Terminal theme block definitions (codeless .bnp, WO-TF-025).
|
|
||||||
# Only true persona furniture stays a theme block; blocks that duplicated a
|
|
||||||
# builtin (ascii masthead, footer) became template overrides, and the section
|
|
||||||
# rail (toc) became article page-template furniture. Keys are fully qualified
|
|
||||||
# (terminal:<key>) because the codeless loader registers definitions verbatim.
|
|
||||||
blocks:
|
|
||||||
- key: terminal:manpage_header
|
|
||||||
title: Man-page Header
|
|
||||||
description: NAME(section) ... center ... version row, like a man-page top line
|
|
||||||
category: layout
|
|
||||||
schema: manpage_header.schema.json
|
|
||||||
template: manpage_header.ninjatpl
|
|
||||||
- key: terminal:code_console
|
|
||||||
title: Console Block
|
|
||||||
description: Shell prompt + command + output, with language hint
|
|
||||||
category: content
|
|
||||||
schema: code_console.schema.json
|
|
||||||
template: code_console.ninjatpl
|
|
||||||
- key: terminal:keybind_table
|
|
||||||
title: Keybinding Table
|
|
||||||
description: Two-column key + action reference table
|
|
||||||
category: content
|
|
||||||
schema: keybind_table.schema.json
|
|
||||||
template: keybind_table.ninjatpl
|
|
||||||
- key: terminal:boot_log
|
|
||||||
title: Boot Log
|
|
||||||
description: Sequential dmesg-style boot lines revealed one after another
|
|
||||||
category: content
|
|
||||||
schema: boot_log.schema.json
|
|
||||||
template: boot_log.ninjatpl
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{% if lines %}<div class="w-full max-w-4xl mx-auto my-6 px-4"><div class="boot-log" data-block="terminal:boot_log">{% for line in lines %}<span class="boot-line" data-line-index="{{ forloop.Counter0 }}" style="animation-delay: {{ forloop.Counter * 80 }}ms;">[ {{ forloop.Counter0 }} ] {{ line }}</span>{% endfor %}{% set cur = cursor|default:"block" %}{% if cur != "none" %}<span class="boot-cursor">{% if cur == "underscore" %}_{% elif cur == "pipe" %}|{% else %}█{% endif %}</span>{% endif %}</div></div>{% endif %}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{% if command or output %}<div class="w-full max-w-4xl mx-auto my-6 px-4"><div class="code-console" data-block="terminal:code_console" data-language="{{ language|default:"shell" }}">{% if command %}<div class="cmd-line"><span class="cmd-prompt">{{ prompt|default:"$" }}</span><span class="cmd-text">{{ command }}</span></div>{% endif %}{% if output %}<pre>{{ output }}</pre>{% endif %}</div></div>{% endif %}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{% if rows %}<table class="keybind-table" data-block="terminal:keybind_table"><thead><tr><th>Keys</th><th>Action</th></tr></thead><tbody>{% for row in rows %}<tr><td>{% for k in row.keys|split:" " %}{% if k %}<kbd>{{ k }}</kbd>{% endif %}{% endfor %}</td><td>{{ row.action }}</td></tr>{% endfor %}</tbody></table>{% endif %}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{% set nm = name|default:"PAGE"|upper %}{% set sec = section|default:1|integer %}{% if sec < 1 %}{% set sec = 1 %}{% endif %}{% set ver = version|default:"terminal v0.1.0" %}<div class="manpage-header" data-block="terminal:manpage_header"><div class="left">{{ nm }}({{ sec }})</div><div class="center">{{ nm }}</div><div class="right">{{ ver }}</div></div><div class="terminal-mono" style="display:none">{{ nm }}({{ sec }}) — {{ ver }}</div>
|
|
||||||
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
20
fonts.json
@ -1,19 +1 @@
|
|||||||
[
|
[]
|
||||||
{
|
|
||||||
"name": "JetBrains Mono",
|
|
||||||
"family": "JetBrains Mono",
|
|
||||||
"variants": [
|
|
||||||
{ "weight": "400", "style": "normal", "file": "fonts/jetbrains-mono-latin.woff2" },
|
|
||||||
{ "weight": "500", "style": "normal", "file": "fonts/jetbrains-mono-latin.woff2" },
|
|
||||||
{ "weight": "700", "style": "normal", "file": "fonts/jetbrains-mono-latin.woff2" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "IBM Plex Mono",
|
|
||||||
"family": "IBM Plex Mono",
|
|
||||||
"variants": [
|
|
||||||
{ "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
|
|
||||||
{ "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.woff2" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
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
@ -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
@ -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
@ -0,0 +1,20 @@
|
|||||||
|
module git.dev.alexdunmow.com/block/themes/terminal
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.dev.alexdunmow.com/block/core v0.14.1
|
||||||
|
github.com/a-h/templ v0.3.1020
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
connectrpc.com/connect v1.20.0 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||||
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.36.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
44
go.sum
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||||
|
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||||
|
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
|
||||||
|
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
||||||
|
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
|
||||||
|
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||||
|
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
|
||||||
483
manifest.yaml
@ -1,483 +0,0 @@
|
|||||||
# Terminal theme — codeless .bnp manifest (WO-TF-025, Wave A).
|
|
||||||
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
|
||||||
theme_presets: presets.json
|
|
||||||
bundled_fonts: fonts.json
|
|
||||||
master_pages: master_pages.json
|
|
||||||
required_icon_packs: ["lucide", "simple-icons"]
|
|
||||||
|
|
||||||
system_templates:
|
|
||||||
- key: terminal
|
|
||||||
title: Terminal
|
|
||||||
description: "Phosphor-on-black TTY theme for dev tools and OSS docs — window chrome, man-page article layouts, keybind tables, boot logs, monospace type, and a typed-terminal canvas hero. Paper-teletype light, phosphor dark."
|
|
||||||
|
|
||||||
page_templates:
|
|
||||||
- system: terminal
|
|
||||||
key: default
|
|
||||||
title: Default
|
|
||||||
description: "Centered 80-col TTY page with window chrome and a sticky header"
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: terminal
|
|
||||||
key: full-width
|
|
||||||
title: Full Width
|
|
||||||
description: "Edge-to-edge layout for tool dashboards and status pages"
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: terminal
|
|
||||||
key: landing
|
|
||||||
title: Landing
|
|
||||||
description: "Typed-terminal canvas hero, feature grid, boot log, and CTA for README-style project sites"
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: terminal
|
|
||||||
key: article
|
|
||||||
title: Article (man-page)
|
|
||||||
description: "Man-page two-column layout with a section rail, ideal for docs and writeups"
|
|
||||||
slots: [header, main, aside, footer]
|
|
||||||
- system: terminal
|
|
||||||
key: blog-index
|
|
||||||
title: Blog Index
|
|
||||||
description: "Changelog-style post feed with a sidebar"
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: terminal
|
|
||||||
key: contact
|
|
||||||
title: Contact
|
|
||||||
description: "Terminal contact console — form panel plus a contact card"
|
|
||||||
slots: [header, main, footer]
|
|
||||||
- system: terminal
|
|
||||||
key: auth
|
|
||||||
title: Auth
|
|
||||||
description: "Centered login/register/reset console in a TTY window"
|
|
||||||
slots: [header, main, footer]
|
|
||||||
|
|
||||||
template_overrides:
|
|
||||||
# Chrome (4)
|
|
||||||
- { template: terminal, block: navbar }
|
|
||||||
- { template: terminal, block: footer }
|
|
||||||
- { template: terminal, block: utility-bar }
|
|
||||||
- { template: terminal, block: announcement-bar }
|
|
||||||
# Marketing (12)
|
|
||||||
- { template: terminal, block: hero }
|
|
||||||
- { template: terminal, block: feature-grid }
|
|
||||||
- { template: terminal, block: rich-section }
|
|
||||||
- { template: terminal, block: stats }
|
|
||||||
- { template: terminal, block: testimonial }
|
|
||||||
- { template: terminal, block: logo-strip }
|
|
||||||
- { template: terminal, block: pricing }
|
|
||||||
- { template: terminal, block: team }
|
|
||||||
- { template: terminal, block: faq }
|
|
||||||
- { template: terminal, block: timeline }
|
|
||||||
- { template: terminal, block: cta }
|
|
||||||
- { template: terminal, block: countdown }
|
|
||||||
# Primitives (11)
|
|
||||||
- { template: terminal, block: heading }
|
|
||||||
- { template: terminal, block: text }
|
|
||||||
- { template: terminal, block: rich-text }
|
|
||||||
- { template: terminal, block: image }
|
|
||||||
- { template: terminal, block: quote }
|
|
||||||
- { template: terminal, block: divider }
|
|
||||||
- { template: terminal, block: button }
|
|
||||||
- { template: terminal, block: card }
|
|
||||||
- { template: terminal, block: gallery }
|
|
||||||
- { template: terminal, block: video-embed }
|
|
||||||
- { template: terminal, block: audio-embed }
|
|
||||||
# Commerce / local (6)
|
|
||||||
- { template: terminal, block: menu-list }
|
|
||||||
- { template: terminal, block: event-list }
|
|
||||||
- { template: terminal, block: contact-form }
|
|
||||||
- { template: terminal, block: contact-card }
|
|
||||||
- { template: terminal, block: hours-location }
|
|
||||||
- { template: terminal, block: social-links }
|
|
||||||
# Blog family (12)
|
|
||||||
- { template: terminal, block: blog-hero }
|
|
||||||
- { template: terminal, block: featured-posts }
|
|
||||||
- { template: terminal, block: related-posts }
|
|
||||||
- { template: terminal, block: popular-posts }
|
|
||||||
- { template: terminal, block: category-list }
|
|
||||||
- { template: terminal, block: post-hero }
|
|
||||||
- { template: terminal, block: post-metadata }
|
|
||||||
- { template: terminal, block: author-bio }
|
|
||||||
- { template: terminal, block: author-bio-hero }
|
|
||||||
- { template: terminal, block: newsletter-signup }
|
|
||||||
- { template: terminal, block: breadcrumbs }
|
|
||||||
- { template: terminal, block: sidebar-nav }
|
|
||||||
# Auth surface (3)
|
|
||||||
- { template: terminal, block: auth-form }
|
|
||||||
- { template: terminal, block: password-reset-form }
|
|
||||||
- { template: terminal, block: auth-status }
|
|
||||||
# System (1)
|
|
||||||
- { template: terminal, block: page-suggestions }
|
|
||||||
|
|
||||||
email_wrappers:
|
|
||||||
- terminal
|
|
||||||
|
|
||||||
css:
|
|
||||||
input_css_append: |
|
|
||||||
/* ============================================================
|
|
||||||
Terminal — Phosphor-on-black TTY theme (WO-TF-025).
|
|
||||||
All colours consume the host shadcn HSL tokens via
|
|
||||||
hsl(var(--token)). No hardcoded hex / rgb / named colours.
|
|
||||||
Dual-mode: light = paper teletype, dark = phosphor CRT.
|
|
||||||
============================================================ */
|
|
||||||
|
|
||||||
/* Bundled mono faces. The host does not yet emit @font-face for
|
|
||||||
* bundled_fonts (LoadedPlugin.BundledFonts is unconsumed), so the theme
|
|
||||||
* declares its own faces against the served artifact paths
|
|
||||||
* (/templates/terminal/fonts/...). Presets set typography.fontHeading/
|
|
||||||
* body/mono to template:terminal:<Family> so --font-heading/body/mono
|
|
||||||
* resolve to these families. JetBrains Mono is one variable-weight file
|
|
||||||
* (400–700); IBM Plex Mono ships 400 + 500. */
|
|
||||||
@font-face { font-family: 'JetBrains Mono'; src: url('/templates/terminal/fonts/jetbrains-mono-latin.woff2') format('woff2'); font-weight: 400 700; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/terminal/fonts/ibm-plex-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
|
||||||
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/terminal/fonts/ibm-plex-mono-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
|
|
||||||
|
|
||||||
/* --- 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.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-mono {
|
|
||||||
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-eyebrow {
|
|
||||||
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-prose,
|
|
||||||
.terminal-prose p,
|
|
||||||
.terminal-prose li {
|
|
||||||
font-family: var(--font-body, "IBM Plex Mono", ui-monospace, monospace);
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-prose a {
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-prose code,
|
|
||||||
code.terminal-mono {
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
padding: 0.1rem 0.35rem;
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
|
||||||
font-size: 0.875em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Caret blink (element opacity; used as <span class="caret-blink">_</span>) --- */
|
|
||||||
|
|
||||||
@keyframes caret-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
|
|
||||||
|
|
||||||
.caret-blink {
|
|
||||||
display: inline-block;
|
|
||||||
animation: caret-blink 1s step-end infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- TTY window chrome (signature) ------------------------ */
|
|
||||||
|
|
||||||
.tty-window {
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
background-color: hsl(var(--card));
|
|
||||||
color: hsl(var(--card-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.tty-titlebar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tty-dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.6rem;
|
|
||||||
height: 0.6rem;
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
background-color: hsl(var(--secondary));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- TTY frame: bordered panel with hover glow ------------ */
|
|
||||||
|
|
||||||
.tty-frame {
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
background-color: hsl(var(--card));
|
|
||||||
transition: box-shadow 180ms ease, border-color 180ms ease;
|
|
||||||
}
|
|
||||||
.tty-frame:hover,
|
|
||||||
.tty-frame:focus-within {
|
|
||||||
border-color: hsl(var(--primary));
|
|
||||||
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.5), 0 0 18px hsl(var(--primary) / 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- ASCII corner brackets (accent + on the corners) ------ */
|
|
||||||
|
|
||||||
.tty-corners { position: relative; }
|
|
||||||
.tty-corners::before,
|
|
||||||
.tty-corners::after {
|
|
||||||
content: "+";
|
|
||||||
position: absolute;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
font-family: var(--font-mono, ui-monospace, monospace);
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.tty-corners::before { top: -0.55em; left: -0.35em; }
|
|
||||||
.tty-corners::after { bottom: -0.55em; right: -0.35em; }
|
|
||||||
|
|
||||||
/* --- ASCII figure frame (image override) ------------------ */
|
|
||||||
|
|
||||||
.ascii-frame {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
background-color: hsl(var(--card));
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- TTY scanlines overlay element (hero/landing) --------- */
|
|
||||||
|
|
||||||
.tty-scanlines {
|
|
||||||
opacity: 0.06;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
hsl(var(--foreground)) 0px,
|
|
||||||
hsl(var(--foreground)) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 3px
|
|
||||||
);
|
|
||||||
background-size: 100% 3px;
|
|
||||||
animation: tty-scan-drift 6s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes tty-scan-drift { 0% { background-position: 0 0; } 100% { background-position: 0 3px; } }
|
|
||||||
|
|
||||||
/* --- Page-level CRT scanline wash (subtle, whole page) ---- */
|
|
||||||
|
|
||||||
.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(--foreground) / 0.04) 2px,
|
|
||||||
hsl(var(--foreground) / 0.04) 3px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Status dot pulse ------------------------------------- */
|
|
||||||
|
|
||||||
.terminal-status-dot { animation: terminal-pulse 2.2s ease-in-out infinite; }
|
|
||||||
@keyframes terminal-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
||||||
|
|
||||||
/* --- Typed-terminal canvas showpiece (hero override only) - */
|
|
||||||
|
|
||||||
.tty-rain-wrap {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.tty-rain-canvas {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
.tty-rain-fallback {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
radial-gradient(120% 80% at 50% 0%, hsl(var(--primary) / 0.16), transparent 60%),
|
|
||||||
linear-gradient(to bottom, transparent, hsl(var(--background) / 0.7));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Man-page header block -------------------------------- */
|
|
||||||
|
|
||||||
.manpage-header {
|
|
||||||
font-family: var(--font-mono, ui-monospace, monospace);
|
|
||||||
border-top: 1px solid hsl(var(--border));
|
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
|
||||||
padding: 0.6rem 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; }
|
|
||||||
|
|
||||||
/* --- Section rail (article aside furniture) --------------- */
|
|
||||||
|
|
||||||
.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));
|
|
||||||
position: sticky;
|
|
||||||
top: 1rem;
|
|
||||||
}
|
|
||||||
.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: baseline; gap: 0.5em; 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Reduced motion: kill all motion ---------------------- */
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.caret-blink,
|
|
||||||
.tty-scanlines,
|
|
||||||
.terminal-status-dot,
|
|
||||||
.tty-rain-canvas,
|
|
||||||
.boot-log .boot-line,
|
|
||||||
.boot-log .boot-cursor {
|
|
||||||
animation: none !important;
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
.boot-log .boot-line { opacity: 1; transform: none; }
|
|
||||||
.tty-scanlines { opacity: 0; }
|
|
||||||
.tty-rain-canvas { display: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- High contrast: drop the CRT overlays ----------------- */
|
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
.tty-scanlines,
|
|
||||||
.crt-scanlines::before,
|
|
||||||
.tty-rain-canvas { display: none; }
|
|
||||||
}
|
|
||||||
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
@ -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
@ -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
|
||||||
@ -1,22 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"key": "terminal:site-master",
|
|
||||||
"title": "Terminal Site Master",
|
|
||||||
"page_templates": ["default", "full-width", "landing", "article", "blog-index", "contact"],
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "TTY Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "grid" }, "companyName": "grid", "stickyBehavior": "sticky", "borderBottom": true, "cta": { "text": "Install", "url": "/contact" } }, "slot": "header", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "// page content" }, "slot": "main", "sort_order": 0 },
|
|
||||||
{ "block_key": "footer", "title": "TTY Footer", "content": { "layout": "columns", "companyName": "grid", "tagline": "A fast parallel task runner and build cache for the terminal.", "copyright": "grid contributors" }, "slot": "footer", "sort_order": 0 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "terminal:auth-master",
|
|
||||||
"title": "Terminal Auth Master",
|
|
||||||
"page_templates": ["auth"],
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "navbar", "title": "TTY Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "grid" }, "companyName": "grid" }, "slot": "header", "sort_order": 0 },
|
|
||||||
{ "block_key": "slot", "title": "Auth Console", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
|
||||||
{ "block_key": "footer", "title": "TTY Footer", "content": { "layout": "minimal", "companyName": "grid", "tagline": "A fast parallel task runner and build cache for the terminal.", "copyright": "grid contributors" }, "slot": "footer", "sort_order": 0 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -2,8 +2,11 @@
|
|||||||
name = "terminal"
|
name = "terminal"
|
||||||
display_name = "Terminal"
|
display_name = "Terminal"
|
||||||
scope = "@themes"
|
scope = "@themes"
|
||||||
version = "0.4.0"
|
version = "0.1.2"
|
||||||
description = "Phosphor-on-black TTY theme for dev tools and OSS docs. Window chrome, man-page article layouts, keybind tables, boot logs, monospace type, and a typed-terminal canvas hero. Paper-teletype light mode, phosphor dark mode."
|
description = "Phosphor green on black terminal aesthetic with ASCII art, monospace type, and man-page article layouts. For indie dev sites, CTF writeups, and CLI tool docs."
|
||||||
kind = "theme"
|
kind = "theme"
|
||||||
categories = ["templates"]
|
categories = ["templates"]
|
||||||
tags = ["terminal", "hacker", "monospace", "dev", "ctf", "docs", "devtools", "oss"]
|
tags = ["terminal", "hacker", "monospace", "dev", "ctf", "security", "docs", "indie"]
|
||||||
|
|
||||||
|
[compatibility]
|
||||||
|
block_core = ">=0.11.0 <0.12.0"
|
||||||
|
|||||||
204
presets.json
@ -2,35 +2,9 @@
|
|||||||
{
|
{
|
||||||
"id": "phosphor-green",
|
"id": "phosphor-green",
|
||||||
"name": "Phosphor Green",
|
"name": "Phosphor Green",
|
||||||
"description": "Green printer-ribbon on paper for light, VT220 green phosphor for dark. The flagship terminal look.",
|
"description": "Classic green-on-black VT220 phosphor.",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "dark",
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:terminal:JetBrains Mono",
|
|
||||||
"fontBody": "template:terminal:IBM Plex Mono",
|
|
||||||
"fontMono": "template:terminal:JetBrains Mono"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "60 20% 97%",
|
|
||||||
"foreground": "120 30% 12%",
|
|
||||||
"card": "60 20% 99%",
|
|
||||||
"cardForeground": "120 30% 12%",
|
|
||||||
"popover": "60 20% 99%",
|
|
||||||
"popoverForeground": "120 30% 12%",
|
|
||||||
"primary": "130 60% 28%",
|
|
||||||
"primaryForeground": "60 20% 98%",
|
|
||||||
"secondary": "90 20% 90%",
|
|
||||||
"secondaryForeground": "120 30% 20%",
|
|
||||||
"muted": "60 15% 92%",
|
|
||||||
"mutedForeground": "120 12% 38%",
|
|
||||||
"accent": "140 65% 30%",
|
|
||||||
"accentForeground": "60 20% 98%",
|
|
||||||
"destructive": "0 70% 42%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "90 15% 78%",
|
|
||||||
"input": "90 15% 84%",
|
|
||||||
"ring": "130 60% 28%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "0 0% 4%",
|
"background": "0 0% 4%",
|
||||||
"foreground": "120 100% 50%",
|
"foreground": "120 100% 50%",
|
||||||
@ -55,37 +29,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "amber-crt",
|
"id": "amber-mono",
|
||||||
"name": "Amber CRT",
|
"name": "Amber Mono",
|
||||||
"description": "Warm cream paper with amber ribbon for light, amber phosphor on warm black for dark.",
|
"description": "Warm amber phosphor terminal.",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "dark",
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:terminal:JetBrains Mono",
|
|
||||||
"fontBody": "template:terminal:IBM Plex Mono",
|
|
||||||
"fontMono": "template:terminal:JetBrains Mono"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "40 30% 96%",
|
|
||||||
"foreground": "30 25% 15%",
|
|
||||||
"card": "40 30% 99%",
|
|
||||||
"cardForeground": "30 25% 15%",
|
|
||||||
"popover": "40 30% 99%",
|
|
||||||
"popoverForeground": "30 25% 15%",
|
|
||||||
"primary": "30 80% 40%",
|
|
||||||
"primaryForeground": "40 30% 98%",
|
|
||||||
"secondary": "38 25% 90%",
|
|
||||||
"secondaryForeground": "30 25% 22%",
|
|
||||||
"muted": "40 20% 91%",
|
|
||||||
"mutedForeground": "30 15% 40%",
|
|
||||||
"accent": "25 85% 43%",
|
|
||||||
"accentForeground": "40 30% 98%",
|
|
||||||
"destructive": "0 70% 45%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "38 20% 78%",
|
|
||||||
"input": "38 20% 84%",
|
|
||||||
"ring": "30 80% 40%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "30 20% 4%",
|
"background": "30 20% 4%",
|
||||||
"foreground": "38 95% 60%",
|
"foreground": "38 95% 60%",
|
||||||
@ -112,35 +60,9 @@
|
|||||||
{
|
{
|
||||||
"id": "paper-tty",
|
"id": "paper-tty",
|
||||||
"name": "Paper TTY",
|
"name": "Paper TTY",
|
||||||
"description": "Crisp black-on-white docs for light with a blue link accent, hard white on black for dark. Austere, no glow.",
|
"description": "Hard white on black; austere docs mode (no glow).",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "dark",
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:terminal:JetBrains Mono",
|
|
||||||
"fontBody": "template:terminal:IBM Plex Mono",
|
|
||||||
"fontMono": "template:terminal:JetBrains Mono"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "0 0% 100%",
|
|
||||||
"foreground": "0 0% 10%",
|
|
||||||
"card": "0 0% 100%",
|
|
||||||
"cardForeground": "0 0% 10%",
|
|
||||||
"popover": "0 0% 100%",
|
|
||||||
"popoverForeground": "0 0% 10%",
|
|
||||||
"primary": "0 0% 12%",
|
|
||||||
"primaryForeground": "0 0% 98%",
|
|
||||||
"secondary": "0 0% 94%",
|
|
||||||
"secondaryForeground": "0 0% 12%",
|
|
||||||
"muted": "0 0% 95%",
|
|
||||||
"mutedForeground": "0 0% 40%",
|
|
||||||
"accent": "210 90% 42%",
|
|
||||||
"accentForeground": "0 0% 100%",
|
|
||||||
"destructive": "0 72% 46%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "0 0% 85%",
|
|
||||||
"input": "0 0% 88%",
|
|
||||||
"ring": "210 90% 42%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "0 0% 5%",
|
"background": "0 0% 5%",
|
||||||
"foreground": "0 0% 95%",
|
"foreground": "0 0% 95%",
|
||||||
@ -163,115 +85,5 @@
|
|||||||
"ring": "0 0% 95%"
|
"ring": "0 0% 95%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ice-terminal",
|
|
||||||
"name": "Ice Terminal",
|
|
||||||
"description": "Cool slate-on-white for light with a teal accent, cyan phosphor on blue-black for dark.",
|
|
||||||
"theme": {
|
|
||||||
"mode": "both",
|
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:terminal:JetBrains Mono",
|
|
||||||
"fontBody": "template:terminal:IBM Plex Mono",
|
|
||||||
"fontMono": "template:terminal:JetBrains Mono"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "200 30% 97%",
|
|
||||||
"foreground": "200 40% 14%",
|
|
||||||
"card": "200 30% 99%",
|
|
||||||
"cardForeground": "200 40% 14%",
|
|
||||||
"popover": "200 30% 99%",
|
|
||||||
"popoverForeground": "200 40% 14%",
|
|
||||||
"primary": "190 80% 30%",
|
|
||||||
"primaryForeground": "200 30% 98%",
|
|
||||||
"secondary": "195 25% 90%",
|
|
||||||
"secondaryForeground": "200 40% 20%",
|
|
||||||
"muted": "200 20% 92%",
|
|
||||||
"mutedForeground": "200 15% 40%",
|
|
||||||
"accent": "185 85% 32%",
|
|
||||||
"accentForeground": "200 30% 98%",
|
|
||||||
"destructive": "0 70% 45%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "195 20% 78%",
|
|
||||||
"input": "195 20% 84%",
|
|
||||||
"ring": "190 80% 30%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
|
||||||
"background": "200 40% 5%",
|
|
||||||
"foreground": "185 90% 60%",
|
|
||||||
"card": "200 40% 8%",
|
|
||||||
"cardForeground": "185 90% 65%",
|
|
||||||
"popover": "200 40% 7%",
|
|
||||||
"popoverForeground": "185 90% 65%",
|
|
||||||
"primary": "185 90% 50%",
|
|
||||||
"primaryForeground": "200 40% 5%",
|
|
||||||
"secondary": "195 40% 15%",
|
|
||||||
"secondaryForeground": "185 90% 72%",
|
|
||||||
"muted": "200 30% 11%",
|
|
||||||
"mutedForeground": "185 40% 52%",
|
|
||||||
"accent": "175 95% 55%",
|
|
||||||
"accentForeground": "200 40% 5%",
|
|
||||||
"destructive": "0 85% 55%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "190 60% 26%",
|
|
||||||
"input": "190 50% 18%",
|
|
||||||
"ring": "185 90% 50%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "magenta-hacker",
|
|
||||||
"name": "Magenta Hacker",
|
|
||||||
"description": "Ink-on-white with a magenta accent for light, magenta phosphor on black for dark.",
|
|
||||||
"theme": {
|
|
||||||
"mode": "both",
|
|
||||||
"typography": {
|
|
||||||
"fontHeading": "template:terminal:JetBrains Mono",
|
|
||||||
"fontBody": "template:terminal:IBM Plex Mono",
|
|
||||||
"fontMono": "template:terminal:JetBrains Mono"
|
|
||||||
},
|
|
||||||
"lightColors": {
|
|
||||||
"background": "320 20% 98%",
|
|
||||||
"foreground": "320 30% 14%",
|
|
||||||
"card": "320 20% 99%",
|
|
||||||
"cardForeground": "320 30% 14%",
|
|
||||||
"popover": "320 20% 99%",
|
|
||||||
"popoverForeground": "320 30% 14%",
|
|
||||||
"primary": "320 70% 40%",
|
|
||||||
"primaryForeground": "320 20% 98%",
|
|
||||||
"secondary": "315 25% 92%",
|
|
||||||
"secondaryForeground": "320 30% 22%",
|
|
||||||
"muted": "320 15% 93%",
|
|
||||||
"mutedForeground": "320 12% 44%",
|
|
||||||
"accent": "300 75% 43%",
|
|
||||||
"accentForeground": "320 20% 98%",
|
|
||||||
"destructive": "0 70% 45%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "315 18% 80%",
|
|
||||||
"input": "315 18% 85%",
|
|
||||||
"ring": "320 70% 40%"
|
|
||||||
},
|
|
||||||
"darkColors": {
|
|
||||||
"background": "300 25% 5%",
|
|
||||||
"foreground": "320 90% 65%",
|
|
||||||
"card": "300 25% 8%",
|
|
||||||
"cardForeground": "320 90% 70%",
|
|
||||||
"popover": "300 25% 7%",
|
|
||||||
"popoverForeground": "320 90% 70%",
|
|
||||||
"primary": "320 90% 55%",
|
|
||||||
"primaryForeground": "300 25% 5%",
|
|
||||||
"secondary": "310 35% 16%",
|
|
||||||
"secondaryForeground": "320 90% 76%",
|
|
||||||
"muted": "300 20% 12%",
|
|
||||||
"mutedForeground": "320 40% 55%",
|
|
||||||
"accent": "285 90% 62%",
|
|
||||||
"accentForeground": "300 25% 5%",
|
|
||||||
"destructive": "0 85% 58%",
|
|
||||||
"destructiveForeground": "0 0% 100%",
|
|
||||||
"border": "320 55% 28%",
|
|
||||||
"input": "320 45% 20%",
|
|
||||||
"ring": "320 90% 55%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
preview.png
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 70 KiB |
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
@ -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
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 674 KiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 173 KiB |
240
seed/seed.json
@ -1,240 +0,0 @@
|
|||||||
{
|
|
||||||
"demo": true,
|
|
||||||
"settings": {
|
|
||||||
"merge": {
|
|
||||||
"companyName": "grid",
|
|
||||||
"tagline": "A fast parallel task runner and build cache for the terminal."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data_tables": [
|
|
||||||
{
|
|
||||||
"key": "contact_submissions",
|
|
||||||
"name": "Contact Enquiries",
|
|
||||||
"description": "Seeded contact-form submissions for the grid demo. A submission becomes a row here and emails the site admins.",
|
|
||||||
"columns": [
|
|
||||||
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
|
||||||
{ "key": "email", "label": "Email", "type": "email", "required": true },
|
|
||||||
{ "key": "project", "label": "Project", "type": "text", "required": false },
|
|
||||||
{ "key": "message", "label": "Message", "type": "text", "required": true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pages": [
|
|
||||||
{
|
|
||||||
"slug": "/",
|
|
||||||
"title": "grid",
|
|
||||||
"template_key": "landing",
|
|
||||||
"blocks": [
|
|
||||||
{
|
|
||||||
"block_key": "hero",
|
|
||||||
"title": "Hero",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 1,
|
|
||||||
"content": {
|
|
||||||
"template": "centered",
|
|
||||||
"minHeight": "screen",
|
|
||||||
"eyebrow": "v1.4.0",
|
|
||||||
"headline": "Run every task in parallel",
|
|
||||||
"subheadline": "grid is a task runner and build cache for the terminal.",
|
|
||||||
"description": "Define tasks once. grid runs the ones that changed, in parallel, and caches the rest. No config server, no daemon.",
|
|
||||||
"ctaPrimary": { "text": "Install", "url": "/contact" },
|
|
||||||
"ctaSecondary": { "text": "Read the docs", "url": "/blog" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block_key": "feature-grid",
|
|
||||||
"title": "Features",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 2,
|
|
||||||
"content": {
|
|
||||||
"eyebrow": "What it does",
|
|
||||||
"heading": "One binary, three jobs",
|
|
||||||
"intro": "Runs tasks, caches output, and watches files. Nothing else to install.",
|
|
||||||
"layout": "card",
|
|
||||||
"columns": 3,
|
|
||||||
"items": [
|
|
||||||
{ "icon": "lucide:cpu", "title": "Parallel", "text": "Tasks that do not depend on each other run at the same time, up to your core count." },
|
|
||||||
{ "icon": "lucide:database", "title": "Cached", "text": "Every task output is keyed by its inputs. Unchanged inputs skip the work." },
|
|
||||||
{ "icon": "lucide:eye", "title": "Watch", "text": "Point grid at a file glob and it reruns the affected tasks on save." }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block_key": "terminal:code_console",
|
|
||||||
"title": "Install",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 3,
|
|
||||||
"content": {
|
|
||||||
"prompt": "$",
|
|
||||||
"language": "shell",
|
|
||||||
"command": "curl -fsSL https://grid.dev/install.sh | sh",
|
|
||||||
"output": "installing grid v1.4.0 ...\nok: /usr/local/bin/grid\nrun `grid init` in your project to get started"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block_key": "terminal:boot_log",
|
|
||||||
"title": "Run log",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 4,
|
|
||||||
"content": {
|
|
||||||
"cursor": "block",
|
|
||||||
"lines": [
|
|
||||||
"grid run build test lint",
|
|
||||||
"build cached 0.00s",
|
|
||||||
"lint ok 0.31s",
|
|
||||||
"test ok 1.12s",
|
|
||||||
"3 tasks, 1 cached, 0 failed"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block_key": "cta",
|
|
||||||
"title": "CTA",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 5,
|
|
||||||
"content": {
|
|
||||||
"background": "band",
|
|
||||||
"heading": "Try it on your next build",
|
|
||||||
"text": "Install grid, run grid init, and watch your first cached run.",
|
|
||||||
"primaryLabel": "Install",
|
|
||||||
"primaryUrl": "/contact"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/about",
|
|
||||||
"title": "About grid",
|
|
||||||
"template_key": "default",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About grid", "level": 1 } },
|
|
||||||
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>grid started as a shell script that ran build steps in the background. The script grew until it needed a cache. Then it needed a dependency graph. At that point it became a real tool.</p><p>The goal has not changed. Run the work that changed, skip the work that did not, and stay out of the way. grid is open source and ships as a single static binary.</p>" } },
|
|
||||||
{
|
|
||||||
"block_key": "terminal:code_console",
|
|
||||||
"title": "Quickstart",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 3,
|
|
||||||
"content": { "prompt": "$", "language": "shell", "command": "grid init && grid run build", "output": "wrote grid.toml\nbuild ok 0.84s\n1 task, 0 cached, 0 failed" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block_key": "terminal:keybind_table",
|
|
||||||
"title": "Commands",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 4,
|
|
||||||
"content": {
|
|
||||||
"rows": [
|
|
||||||
{ "keys": "grid run", "action": "Run the named tasks and their dependencies" },
|
|
||||||
{ "keys": "grid watch", "action": "Rerun affected tasks when files change" },
|
|
||||||
{ "keys": "grid graph", "action": "Print the task dependency graph" },
|
|
||||||
{ "keys": "grid clean", "action": "Drop the local build cache" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 5, "content": { "variant": "line", "weight": "medium" } },
|
|
||||||
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 6, "content": { "style": "pull", "quote": "Do the work once. Cache the rest.", "attribution": "grid README" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog",
|
|
||||||
"title": "Changelog",
|
|
||||||
"template_key": "blog-index",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "Changelog", "level": 1 } },
|
|
||||||
{ "block_key": "card", "title": "Post: Cache keys", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "How grid keys the cache", "text": "<p>Every task output is keyed by its inputs. Here is how the hash is built and why it is stable across machines.</p>", "link": "/blog/cache-keys", "linkLabel": "Read on" } },
|
|
||||||
{ "block_key": "card", "title": "Post: Watch mode", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Watch mode without a daemon", "text": "<p>grid watch reruns affected tasks on save with no background process. This is how the file watcher stays cheap.</p>", "link": "/blog/watch-mode", "linkLabel": "Read on" } },
|
|
||||||
{ "block_key": "card", "title": "Post: Parallel", "slot": "main", "sort_order": 4, "content": { "layout": "vertical", "title": "Scheduling tasks in parallel", "text": "<p>Independent tasks run together, up to your core count. The scheduler is a topological sort with a worker pool.</p>", "link": "/blog/parallel-scheduling", "linkLabel": "Read on" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog/cache-keys",
|
|
||||||
"title": "How grid keys the cache",
|
|
||||||
"template_key": "article",
|
|
||||||
"parent_slug": "/blog",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "terminal:manpage_header", "title": "Man header", "slot": "main", "sort_order": 1, "content": { "name": "CACHE-KEYS", "section": 7, "version": "grid v1.4.0" } },
|
|
||||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 2, "content": { "text": "How grid keys the cache", "level": 1 } },
|
|
||||||
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 3, "content": { "html": "<p>A cache is only useful if the key is exact. grid hashes the task command, its input files, and the tool versions it declares. Same inputs, same key, cached output.</p><p>The key is content based, not time based. Two machines that check out the same commit build the same key. That makes the cache shareable across a team.</p>" } },
|
|
||||||
{ "block_key": "terminal:code_console", "title": "Example", "slot": "main", "sort_order": 4, "content": { "prompt": "$", "language": "shell", "command": "grid run build --explain", "output": "build key: sha256:9f2c...\ninputs: src/**, grid.toml, go 1.26\nresult: cache hit" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog/watch-mode",
|
|
||||||
"title": "Watch mode without a daemon",
|
|
||||||
"template_key": "article",
|
|
||||||
"parent_slug": "/blog",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "terminal:manpage_header", "title": "Man header", "slot": "main", "sort_order": 1, "content": { "name": "WATCH", "section": 1, "version": "grid v1.4.0" } },
|
|
||||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 2, "content": { "text": "Watch mode without a daemon", "level": 1 } },
|
|
||||||
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 3, "content": { "html": "<p>Most watchers run a background process that never exits. grid does not. grid watch runs in the foreground and exits when you press q.</p><p>On each save it maps the changed file back to the tasks that read it, then reruns only those. The rest stay cached.</p>" } },
|
|
||||||
{ "block_key": "terminal:code_console", "title": "Example", "slot": "main", "sort_order": 4, "content": { "prompt": "$", "language": "shell", "command": "grid watch 'src/**/*.go'", "output": "watching 42 files\nchanged: src/api/handler.go\ntest ok 0.90s" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/blog/parallel-scheduling",
|
|
||||||
"title": "Scheduling tasks in parallel",
|
|
||||||
"template_key": "article",
|
|
||||||
"parent_slug": "/blog",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "terminal:manpage_header", "title": "Man header", "slot": "main", "sort_order": 1, "content": { "name": "SCHEDULER", "section": 7, "version": "grid v1.4.0" } },
|
|
||||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 2, "content": { "text": "Scheduling tasks in parallel", "level": 1 } },
|
|
||||||
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 3, "content": { "html": "<p>grid builds a graph of tasks and their dependencies. It runs a task the moment its dependencies finish. Independent branches run at the same time.</p><p>The worker pool is sized to your core count by default. You can cap it with a flag when a task is memory heavy.</p>" } },
|
|
||||||
{ "block_key": "terminal:code_console", "title": "Example", "slot": "main", "sort_order": 4, "content": { "prompt": "$", "language": "shell", "command": "grid run all --jobs 4", "output": "scheduled 9 tasks across 4 workers\nall ok 2.03s" } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/contact",
|
|
||||||
"title": "Contact",
|
|
||||||
"template_key": "contact",
|
|
||||||
"blocks": [
|
|
||||||
{
|
|
||||||
"block_key": "contact-form",
|
|
||||||
"title": "Contact form",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 1,
|
|
||||||
"content": {
|
|
||||||
"heading": "Get in touch",
|
|
||||||
"description": "Ask a question or report a bug. We reply within one business day.",
|
|
||||||
"submitLabel": "Send",
|
|
||||||
"successMessage": "Message received. We will reply within one business day.",
|
|
||||||
"fields": [
|
|
||||||
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
|
|
||||||
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
|
||||||
{ "name": "project", "label": "Project", "type": "text", "required": false, "width": "full" },
|
|
||||||
{ "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" }
|
|
||||||
],
|
|
||||||
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"block_key": "contact-card",
|
|
||||||
"title": "Reach us",
|
|
||||||
"slot": "main",
|
|
||||||
"sort_order": 2,
|
|
||||||
"content": {
|
|
||||||
"heading": "Other ways in",
|
|
||||||
"columns": 3,
|
|
||||||
"channels": [
|
|
||||||
{ "icon": "lucide:mail", "label": "Email", "value": "team@grid.dev", "link": "mailto:team@grid.dev" },
|
|
||||||
{ "icon": "simple-icons:github", "label": "Source", "value": "github.com/grid-dev/grid", "link": "https://github.com/grid-dev/grid" },
|
|
||||||
{ "icon": "lucide:message-square", "label": "Chat", "value": "Join the Discord", "link": "#" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "/login",
|
|
||||||
"title": "Sign in",
|
|
||||||
"template_key": "auth",
|
|
||||||
"blocks": [
|
|
||||||
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"menu_items": [
|
|
||||||
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
|
|
||||||
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
|
|
||||||
{ "menu": "main", "label": "Docs", "page_slug": "/blog", "sort_order": 3 },
|
|
||||||
{ "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"workflows": [
|
|
||||||
{
|
|
||||||
"key": "contact_notify_admins",
|
|
||||||
"name": "Contact enquiry to admins",
|
|
||||||
"description": "On a contact-form submission, email the site admins.",
|
|
||||||
"enabled": true,
|
|
||||||
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"type": "email",
|
|
||||||
"name": "Email admins",
|
|
||||||
"template": "Form Submission Notification",
|
|
||||||
"recipients": { "mode": "admins", "scope": "all" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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
@ -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
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{% set c_bg = colors.background|default:"#0a0a0a" %}{% set c_fg = colors.foreground|default:"#00ff00" %}{% set c_bd = colors.border|default:"#1a4a1a" %}{% set c_mf = colors.mutedForeground|default:"#5a8c5a" %}<!doctype html><!-- text/plain alternative
|
|
||||||
================================================================================
|
|
||||||
{{ body|safe }}
|
|
||||||
================================================================================
|
|
||||||
--
|
|
||||||
{% if site_name %}{{ site_name }}
|
|
||||||
{% endif %}{% if site_url %}{{ site_url }}
|
|
||||||
{% endif %}{% if unsubscribe_url %}unsubscribe: {{ unsubscribe_url }}
|
|
||||||
{% endif %}
|
|
||||||
--><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{{ site_name }}</title></head><body style="background-color: {{ c_bg }}; margin: 0; padding: 0; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;">{% if preview_text %}<div style="display:none; max-height:0; overflow:hidden;">{{ preview_text }}</div>{% endif %}<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ c_bg }};"><tr><td align="center" style="padding: 32px 12px; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }};"><table role="presentation" width="640" cellspacing="0" cellpadding="0" border="0" style="max-width: 640px; width: 100%; border: 1px solid {{ c_bd }}; background-color: {{ c_bg }};"><tr><td style="padding: 16px 24px 0; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }}; letter-spacing: 0.05em;">================================================================================</td></tr><tr><td style="padding: 16px 24px; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }}; font-size: 14px; line-height: 1.6;">{{ body|safe }}</td></tr><tr><td style="padding: 0 24px; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_fg }}; letter-spacing: 0.05em;">================================================================================</td></tr><tr><td style="padding: 16px 24px; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_mf }}; font-size: 12px;"><pre style="margin: 0; font-family: &#39;JetBrains Mono&#39;, &#39;IBM Plex Mono&#39;, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: {{ c_mf }}; white-space: pre-wrap;">--
|
|
||||||
</pre>{% if site_name %}<div>{{ site_name }}</div>{% endif %}{% if site_url %}<div><a href="{{ site_url }}" style="color: {{ c_fg }}; text-decoration: underline;">{{ site_url }}</a></div>{% endif %}{% if unsubscribe_url %}<div><a href="{{ unsubscribe_url }}" style="color: {{ c_mf }}; text-decoration: underline;">unsubscribe</a></div>{% endif %}</td></tr></table></td></tr></table></body></html>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{# announcement_bar (WO-TF-006): dismissable banner; dismissal persists in localStorage keyed by a content hash (variant + slugified message) so it reappears when the message changes; no-JS fallback is a static non-dismissable strip. Terminal override: mono broadcast readout — data-* + script preserved. #}
|
|
||||||
{% set anncls = "bg-primary/10 text-foreground" %}
|
|
||||||
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
|
|
||||||
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
|
|
||||||
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
|
|
||||||
<p class="terminal-mono uppercase tracking-wide font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
|
|
||||||
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if dismissable %}<script>
|
|
||||||
(function(){
|
|
||||||
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
|
|
||||||
bars.forEach(function(bar){
|
|
||||||
bar.setAttribute('data-bn-ann-wired','1');
|
|
||||||
var key='bnann:'+bar.getAttribute('data-bn-annkey');
|
|
||||||
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
|
|
||||||
var btn=bar.querySelector('[data-bn-ann-dismiss]');
|
|
||||||
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>{% endif %}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{# audio-embed terminal override: neon panel, mono meta. Facade script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
|
|
||||||
<figure class="bn-audio my-6 overflow-hidden rounded-md border border-primary/40 bg-card text-card-foreground tty-frame{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-md border border-primary/30 bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="terminal-heading truncate font-medium text-foreground">{{ title }}</p>{% endif %}{% if artist %}<p class="terminal-mono truncate text-sm text-muted-foreground">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="terminal-mono inline-flex items-center gap-2 rounded-md bg-primary px-4 py-1.5 text-sm font-medium uppercase tracking-wide text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="terminal-mono inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">::lucide:download:sm:: Download</a>{% endif %}</div></div></div></figure><script>
|
|
||||||
(function(){
|
|
||||||
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
|
|
||||||
document.addEventListener('click',function(e){
|
|
||||||
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
|
|
||||||
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
|
|
||||||
e.preventDefault();
|
|
||||||
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
|
|
||||||
if(!src)return;
|
|
||||||
var slot=btn.closest('[data-bn-audio-slot]');
|
|
||||||
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
|
|
||||||
btn.remove();slot.insertBefore(a,slot.firstChild);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
{# auth-form terminal override — reproduces the builtin's security-critical wiring: hardcoded HTMX endpoints /api/auth/login + /api/auth/register (target #auth-message), /forgot-password link, OAuth start URLs gated on context.site.*, and the exact Cap widget markup when captchaEnabled. Only the chrome is themed. #}
|
|
||||||
{% if context.isPublicLoggedIn %}
|
|
||||||
<div class="auth-form-logged-in mx-auto max-w-md text-center py-6">
|
|
||||||
<p class="terminal-mono text-sm uppercase tracking-wide text-muted-foreground">Session active as <strong class="text-primary">{{ context.publicUsername }}</strong></p>
|
|
||||||
<a href="/account" class="terminal-mono mt-3 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium uppercase tracking-wide text-primary-foreground hover:bg-primary/90">My Account</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% set loginActive = "border-primary text-primary" %}
|
|
||||||
{% set registerActive = "border-transparent text-muted-foreground" %}
|
|
||||||
{% set loginHidden = "" %}
|
|
||||||
{% set registerHidden = "hidden" %}
|
|
||||||
{% if default_tab == "register" %}{% set loginActive = "border-transparent text-muted-foreground" %}{% set registerActive = "border-primary text-primary" %}{% set loginHidden = "hidden" %}{% set registerHidden = "" %}{% endif %}
|
|
||||||
{% set tabbase = "flex-1 py-2 text-center terminal-mono text-sm font-medium uppercase tracking-wider border-b-2 transition-colors" %}
|
|
||||||
{% macro captcha() %}<script>
|
|
||||||
window.CAP_CUSTOM_WASM_URL = "/assets/captcha/cap_wasm_bg.wasm";
|
|
||||||
window.CAP_PAKO_URL = "/assets/captcha/pako_inflate.min.js";
|
|
||||||
</script>
|
|
||||||
<cap-widget data-cap-api-endpoint="/api/captcha/" data-cap-worker-count="2"></cap-widget>
|
|
||||||
<script src="/assets/captcha/cap.min.js" defer></script>
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
var s = document.currentScript;
|
|
||||||
var form = s && s.closest ? s.closest('form') : null;
|
|
||||||
if (!form) return;
|
|
||||||
form.addEventListener('htmx:afterRequest', function(){
|
|
||||||
var w = form.querySelector('cap-widget');
|
|
||||||
if (w && typeof w.reset === 'function') w.reset();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>{% endmacro %}
|
|
||||||
{% macro social() %}{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}<div class="auth-social my-4">
|
|
||||||
<div class="relative my-4"><hr class="border-border"/><span class="terminal-mono absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-3 text-xs uppercase tracking-widest text-muted-foreground">or</span></div>
|
|
||||||
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="terminal-mono mb-2 flex w-full items-center justify-center gap-3 rounded-md border border-border px-4 py-2.5 uppercase tracking-wide transition-colors hover:bg-accent/10"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#google"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Google</span></a>{% endif %}
|
|
||||||
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="terminal-mono flex w-full items-center justify-center gap-3 rounded-md border border-border px-4 py-2.5 uppercase tracking-wide transition-colors hover:bg-accent/10"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#facebook"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Facebook</span></a>{% endif %}
|
|
||||||
</div>{% endif %}{% endmacro %}
|
|
||||||
<div class="auth-form tty-corners mx-auto max-w-md rounded-md border border-primary/40 bg-card p-6 text-card-foreground tty-frame">
|
|
||||||
<div id="auth-message"></div>
|
|
||||||
<div class="mb-5 flex border-b border-border">
|
|
||||||
<button type="button" data-auth-tab="login" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ loginActive }}">Login</button>
|
|
||||||
<button type="button" data-auth-tab="register" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ registerActive }}">Register</button>
|
|
||||||
</div>
|
|
||||||
<div id="login-panel" class="{{ loginHidden }}">
|
|
||||||
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
|
||||||
<div class="mb-3"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
|
|
||||||
<div class="mb-4"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Password</label><input type="password" name="password" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
|
|
||||||
{% if captchaEnabled %}{{ captcha() }}{% endif %}
|
|
||||||
<button type="submit" class="terminal-mono w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Login</button>
|
|
||||||
<p class="mt-3 text-center text-sm"><a href="/forgot-password" class="terminal-mono uppercase tracking-wide text-primary hover:underline">Forgot password?</a></p>
|
|
||||||
</form>
|
|
||||||
{{ social() }}
|
|
||||||
</div>
|
|
||||||
<div id="register-panel" class="{{ registerHidden }}">
|
|
||||||
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
|
||||||
<div class="mb-3"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
|
|
||||||
<div class="mb-3"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
|
|
||||||
<div class="mb-4"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
|
|
||||||
{% if captchaEnabled %}{{ captcha() }}{% endif %}
|
|
||||||
<button type="submit" class="terminal-mono w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Create Account</button>
|
|
||||||
</form>
|
|
||||||
{{ social() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
var f=document.currentScript.closest('.auth-form')||document.querySelector('.auth-form');
|
|
||||||
if(!f)return;
|
|
||||||
var tabs=f.querySelectorAll('[data-auth-tab]');
|
|
||||||
tabs.forEach(function(btn){btn.addEventListener('click',function(){
|
|
||||||
var t=btn.getAttribute('data-auth-tab');
|
|
||||||
var lp=f.querySelector('#login-panel'),rp=f.querySelector('#register-panel');
|
|
||||||
if(lp)lp.classList.toggle('hidden',t!=='login');
|
|
||||||
if(rp)rp.classList.toggle('hidden',t!=='register');
|
|
||||||
tabs.forEach(function(b){var on=b===btn;b.className=b.getAttribute('data-tabbase')+(on?' border-primary text-primary':' border-transparent text-muted-foreground');});
|
|
||||||
});});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
{# auth-status terminal override — reproduces the builtin's hardcoded wiring: /login + /login?tab=register when logged out; /account, /account/reviews, and hx-post /api/auth/logout dropdown when logged in; hx-post /api/auth/resend-verification banner when unverified. State from context.*. #}
|
|
||||||
{% if not context.isPublicLoggedIn %}
|
|
||||||
<div class="auth-status flex items-center gap-2">
|
|
||||||
<a href="/login" class="terminal-mono rounded-md px-3 py-1.5 text-sm font-medium uppercase tracking-wide opacity-80 transition-colors hover:bg-primary/10 hover:opacity-100">Login</a>
|
|
||||||
<a href="/login?tab=register" class="terminal-mono rounded-md bg-accent px-3 py-1.5 text-sm font-medium uppercase tracking-wide text-accent-foreground hover:bg-accent/90">Sign Up</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 border-y border-destructive/50 bg-destructive/15 px-4 py-2 text-center text-sm text-destructive">
|
|
||||||
<span class="terminal-mono uppercase tracking-wide">Verify your email address.</span> <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline font-medium">Resend verification email</button>
|
|
||||||
</div>{% endif %}
|
|
||||||
<div class="auth-status relative">
|
|
||||||
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-primary/10 hover:opacity-100">
|
|
||||||
<span class="flex h-7 w-7 items-center justify-center rounded-md bg-accent text-accent-foreground"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#user"></use></svg></span>
|
|
||||||
<span class="terminal-mono uppercase tracking-wide">{{ context.publicDisplayName|default:context.publicUsername }}</span>
|
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
||||||
</button>
|
|
||||||
<div id="auth-dropdown-menu" class="tty-corners hidden absolute right-0 top-full mt-1 w-48 rounded-md border border-primary/40 bg-card py-1 text-card-foreground tty-frame z-50">
|
|
||||||
<a href="/account" class="terminal-mono block px-4 py-2 text-sm uppercase tracking-wide text-card-foreground hover:bg-primary/10">My Profile</a>
|
|
||||||
<a href="/account/reviews" class="terminal-mono block px-4 py-2 text-sm uppercase tracking-wide text-card-foreground hover:bg-primary/10">My Reviews</a>
|
|
||||||
<hr class="my-1 border-border"/>
|
|
||||||
<button hx-post="/api/auth/logout" hx-swap="none" class="terminal-mono block w-full px-4 py-2 text-left text-sm uppercase tracking-wide text-destructive hover:bg-primary/10">Logout</button>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
var menu = document.getElementById('auth-dropdown-menu');
|
|
||||||
var btn = document.querySelector('.auth-status > button');
|
|
||||||
if (!menu || !btn) return;
|
|
||||||
if (!btn.contains(e.target) && !menu.contains(e.target)) {
|
|
||||||
menu.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
{# author-bio-hero (terminal override) — author-page header from the `authors` provider. #}
|
|
||||||
{% if author %}
|
|
||||||
<section class="relative overflow-hidden border-b border-primary/30 bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="tty-scanlines pointer-events-none absolute inset-0"></div>
|
|
||||||
<div class="relative z-10 mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
|
|
||||||
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary tty-frame">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
|
||||||
<h1 class="terminal-heading text-3xl font-bold tracking-tight md:text-4xl" data-text="{{ author.display_name }}">{{ author.display_name }}</h1>
|
|
||||||
{% if author.job_title or author.company %}<p class="terminal-mono mt-1 text-sm uppercase tracking-wide text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
|
||||||
{% if author.bio %}<p class="terminal-page mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
|
|
||||||
<div class="mt-5 flex flex-wrap items-center justify-center gap-4 terminal-mono text-sm uppercase tracking-wide text-accent">
|
|
||||||
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
|
|
||||||
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
|
|
||||||
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
|
|
||||||
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
|
|
||||||
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
|
|
||||||
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
|
||||||
</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
{# author-bio (terminal override) — byline card sourced from the `post` provider's author_* fields. #}
|
|
||||||
{% if post.author_name %}
|
|
||||||
<aside class="tty-corners rounded-md border border-dashed border-primary/40 bg-card p-6 text-card-foreground tty-frame{% if class %} {{ class }}{% endif %}">
|
|
||||||
{% if heading %}<p class="terminal-eyebrow terminal-mono mb-4 text-xs font-semibold uppercase tracking-widest text-accent">{{ heading }}</p>{% endif %}
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full border border-primary/40 bg-muted">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
|
||||||
<div class="min-w-0">
|
|
||||||
<h3 class="terminal-heading text-base font-semibold">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
|
||||||
{% if post.author_bio %}<p class="terminal-page mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
{# blog-hero (terminal override) — blog-index header + featured posts from the `posts` provider. #}
|
|
||||||
<section class="relative overflow-hidden bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="tty-scanlines pointer-events-none absolute inset-0"></div>
|
|
||||||
<div class="relative z-10 mx-auto max-w-6xl px-4">
|
|
||||||
<div class="max-w-2xl">
|
|
||||||
{% if title %}<h1 class="terminal-heading text-4xl font-bold tracking-tight md:text-5xl" data-text="{{ title }}">{{ title }}</h1>{% endif %}
|
|
||||||
{% if subtitle %}<p class="terminal-page mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if posts %}
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
||||||
{% for post in posts %}
|
|
||||||
<article class="tty-corners group flex flex-col rounded-md border border-dashed border-primary/40 bg-card p-4 text-card-foreground tty-frame{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
|
|
||||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-3 block overflow-hidden rounded-md bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
|
||||||
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
|
|
||||||
<h2 class="terminal-heading {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
|
|
||||||
{% if showExcerpt and post.excerpt %}<p class="terminal-page mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
|
||||||
<div class="mt-3 flex flex-wrap items-center gap-x-3 terminal-mono text-xs uppercase tracking-wide text-accent">{% if post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{# breadcrumbs (terminal override) — trail from the `breadcrumbs` provider (page ancestry). #}
|
|
||||||
{# showHome prepends a Home link; separator is text ("/") for default or a chevron #}
|
|
||||||
{# for the arrows template. The trailing is_current item renders as plain text. #}
|
|
||||||
{% set arrows = template == "arrows" %}
|
|
||||||
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
|
|
||||||
<ol class="flex flex-wrap items-center gap-1 terminal-mono text-sm uppercase tracking-wide">
|
|
||||||
{% if showHome %}<li class="flex items-center"><a href="/" class="text-accent transition-colors hover:text-primary">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
|
|
||||||
{% for item in breadcrumbs %}
|
|
||||||
<li class="flex items-center">
|
|
||||||
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-primary">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
|
||||||
{% if item.is_current %}{% if showCurrent %}<span class="font-medium text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-accent transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{# terminal button — keep {% button %} tag intact; terminal chrome on hugging wrapper #}
|
|
||||||
{% with align=align|default:"left" %}<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}"><span class="terminal-mono terminal-mono inline-flex uppercase tracking-wider font-semibold">{% if newTab %}{% button style|default:"primary" text=label|default:"Learn more" href=link|default:"#" size=size|default:"md" icon=icon|default:"" icon_position=iconPosition|default:"left" target="_blank" %}{% else %}{% button style|default:"primary" text=label|default:"Learn more" href=link|default:"#" size=size|default:"md" icon=icon|default:"" icon_position=iconPosition|default:"left" %}{% endif %}</span></div>{% endwith %}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{# terminal card — dashed neon panel; fields layout/link/media/title/text/linkLabel #}
|
|
||||||
{% with layout=layout|default:"vertical" %}<article class="bn-card tty-frame overflow-hidden rounded-md border border-dashed border-primary/40 bg-card text-card-foreground tty-frame {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="text-lg font-semibold tracking-tight text-foreground terminal-heading">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed text-muted-foreground terminal-page">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm font-medium text-primary terminal-mono uppercase tracking-wide">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>{% endwith %}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{# category-list (terminal override) — from the `categories` provider. list or pills. #}
|
|
||||||
{% set pills = style == "pills" %}
|
|
||||||
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
{% if heading %}<h2 class="terminal-eyebrow terminal-mono mb-4 text-xs font-semibold uppercase tracking-widest text-accent">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if pills %}
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{% for cat in categories %}
|
|
||||||
<a href="{{ cat.url }}" class="inline-flex items-center gap-1.5 border border-primary/50 bg-card px-3 py-1 terminal-mono text-sm uppercase tracking-wide text-card-foreground tty-frame transition-colors hover:text-primary">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<ul class="space-y-1">
|
|
||||||
{% for cat in categories %}
|
|
||||||
<li><a href="{{ cat.url }}" class="flex items-center justify-between rounded-md px-3 py-2 terminal-mono text-sm uppercase tracking-wide text-muted-foreground transition-colors hover:bg-accent hover:text-primary"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{# contact-card terminal override: neon channel panels. All field reads + control flow preserved. #}
|
|
||||||
<section class="py-8 md:py-12 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-4xl px-4">
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if description %}<p class="mt-3 max-w-2xl text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
|
||||||
<div class="mt-8 grid grid-cols-1 gap-6{% if columns == 3 %} sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %} sm:grid-cols-2{% endif %}">
|
|
||||||
{% for channel in channels %}
|
|
||||||
<div class="tty-corners flex items-start gap-4 rounded-md border border-primary/40 bg-card p-5 text-card-foreground tty-frame">
|
|
||||||
{% if channel.icon %}<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
|
|
||||||
<div class="min-w-0">
|
|
||||||
{% if channel.label %}<div class="terminal-mono text-sm font-medium uppercase tracking-wide text-accent">{{ channel.label }}</div>{% endif %}
|
|
||||||
{% if channel.value %}<div class="mt-0.5 break-words font-medium">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-primary">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
{# contact-form terminal override: terminal console chrome. The {% form %} block, honeypot, field loop, form.submitted/errors reads and _block_id/bid submit wiring are preserved byte-for-byte — only presentational classes change. #}
|
|
||||||
{% with bid=_block_id|default:context.blockId %}
|
|
||||||
<section id="contact-form-{{ bid }}" class="py-8 md:py-12 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-2xl px-4">
|
|
||||||
{% if form.submitted %}
|
|
||||||
<div class="tty-corners rounded-md border border-primary/50 bg-card p-8 text-center text-card-foreground tty-frame">
|
|
||||||
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-md bg-primary/10 text-primary"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
|
|
||||||
<p class="terminal-heading text-lg font-medium">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if description %}<p class="mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
|
||||||
{% if form.errors._form %}<div class="mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% endif %}
|
|
||||||
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
|
|
||||||
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-5" %}
|
|
||||||
{% for field in fields %}
|
|
||||||
<div class="{% if field.width == "half" %}{% else %}col-span-full{% endif %}">
|
|
||||||
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="terminal-mono mb-1.5 block text-sm font-medium uppercase tracking-wide text-accent">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
|
|
||||||
{% if field.type == "textarea" %}
|
|
||||||
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"></textarea>
|
|
||||||
{% elif field.type == "select" %}
|
|
||||||
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
|
||||||
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
|
|
||||||
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring">
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
|
||||||
<div class="col-span-full">
|
|
||||||
<button type="submit" class="terminal-mono inline-flex items-center justify-center rounded-md bg-primary px-6 py-2.5 text-sm font-medium uppercase tracking-wider text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">{{ submitLabel|default:"Send message" }}</button>
|
|
||||||
</div>
|
|
||||||
{% endform %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endwith %}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
{# countdown terminal override: neon HUD digit boxes, heading. Timer script + data attributes preserved byte-for-byte. #}
|
|
||||||
<section class="relative overflow-hidden py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
|
|
||||||
<div class="relative z-10 mx-auto max-w-4xl px-4 text-center">
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
|
||||||
<div class="mt-10" data-countdown>
|
|
||||||
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
|
|
||||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="days">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
|
|
||||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="hours">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
|
|
||||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="minutes">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
|
|
||||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="seconds">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-8 hidden" data-countdown-expired>
|
|
||||||
<p class="terminal-heading text-xl font-medium">{{ expiredMessage|default:"This has started." }}</p>
|
|
||||||
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="terminal-mono mt-4 inline-flex items-center gap-2 rounded-md bg-primary px-5 py-3 font-semibold uppercase tracking-wider text-primary-foreground transition-opacity hover:opacity-90">{{ expiredLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<noscript><p class="mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
var timers = document.querySelectorAll('[data-countdown-timer]');
|
|
||||||
timers.forEach(function(el){
|
|
||||||
var target = new Date(el.getAttribute('data-target')).getTime();
|
|
||||||
if (isNaN(target)) return;
|
|
||||||
var wrap = el.closest('[data-countdown]');
|
|
||||||
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
|
|
||||||
function pad(n){ return (n < 10 ? '0' : '') + n; }
|
|
||||||
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
|
|
||||||
var interval;
|
|
||||||
function tick(){
|
|
||||||
var diff = target - Date.now();
|
|
||||||
if (diff <= 0){
|
|
||||||
el.style.display = 'none';
|
|
||||||
if (expired) expired.classList.remove('hidden');
|
|
||||||
if (interval) clearInterval(interval);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
set('days', Math.floor(diff / 86400000));
|
|
||||||
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
|
|
||||||
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
|
|
||||||
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
|
|
||||||
}
|
|
||||||
tick();
|
|
||||||
interval = setInterval(tick, 1000);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{# cta terminal override: scanline overlay, heading, CTAs. All field reads preserved. #}
|
|
||||||
<section class="relative overflow-hidden py-12 md:py-20 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
|
|
||||||
<div class="relative z-10 mx-auto max-w-6xl px-4">
|
|
||||||
<div class="tty-corners {% if background == "band" %}rounded-md bg-primary px-6 py-14 text-center text-primary-foreground md:px-12{% elif background == "card" %}rounded-md border border-primary/50 bg-card px-6 py-14 text-center text-card-foreground tty-frame md:px-12{% else %}flex flex-col items-center gap-8 rounded-md border border-primary/30 bg-muted px-6 py-12 text-foreground md:flex-row md:justify-between md:px-12 md:text-left{% endif %}">
|
|
||||||
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if text %}<p class="mt-3 text-lg {% if background == "band" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ text }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-3 {% if background != "split" %}mt-8{% else %}shrink-0{% endif %}">
|
|
||||||
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="terminal-mono inline-flex items-center gap-2 rounded-md px-6 py-3 font-semibold uppercase tracking-wider transition-opacity hover:opacity-90 {% if background == "band" %}bg-background text-foreground{% else %}bg-primary text-primary-foreground{% endif %}">{{ primaryLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
|
||||||
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="terminal-mono inline-flex items-center rounded-md border px-6 py-3 font-semibold uppercase tracking-wider transition-colors {% if background == "band" %}border-primary-foreground/40 text-primary-foreground hover:bg-primary-foreground/10{% else %}border-accent/60 text-accent hover:bg-accent/10{% endif %}">{{ secondaryLabel }}</a>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
{# terminal divider — neon gradient rule / primary-tinted variants #}
|
|
||||||
{% with variant=variant|default:"line" weight=weight|default:"thin" width=width|default:"full" %}{% if variant == "ornament" %}<div class="bn-divider mx-auto my-8 flex items-center gap-4 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="h-px flex-1 bg-gradient-to-r from-transparent via-primary to-transparent"></span><span class="shrink-0 text-primary">::{{ icon|default:"lucide:asterisk" }}:md text-primary::</span><span class="h-px flex-1 bg-gradient-to-l from-transparent via-primary to-transparent"></span></div>{% elif variant == "spacer-rule" %}<div class="bn-divider mx-auto py-10 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><hr class="mx-auto w-16 border-0 border-t {% if weight == "thick" %}border-t-4{% elif weight == "medium" %}border-t-2{% endif %} border-primary/60"></div>{% else %}<hr class="bn-divider mx-auto my-8 border-0 border-t {% if weight == "thick" %}border-t-4{% elif weight == "medium" %}border-t-2{% endif %} border-primary/60 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">{% endif %}{% endwith %}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
{# event-list terminal override: neon cards, mono datetime. All field reads + control flow preserved. #}
|
|
||||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-6xl px-4">
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if intro %}<p class="mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
|
||||||
{% if layout == "cards" %}
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
|
||||||
<article class="tty-corners flex flex-col overflow-hidden rounded-md border border-primary/40 bg-card text-card-foreground tty-frame{% if event.past %} opacity-60{% endif %}">
|
|
||||||
{% if event.media %}<div class="aspect-video overflow-hidden bg-muted">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
|
|
||||||
<div class="flex flex-1 flex-col gap-2 p-5">
|
|
||||||
<div class="terminal-mono text-sm font-medium uppercase tracking-wide text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
|
|
||||||
<h3 class="terminal-heading text-lg font-semibold">{{ event.title }}</h3>
|
|
||||||
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
|
||||||
{% if event.description %}<p class="text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
|
||||||
{% if event.link %}<a href="{{ event.link }}" class="terminal-mono mt-auto inline-flex items-center gap-1 pt-2 text-sm font-medium uppercase tracking-wide text-primary hover:underline">{{ event.linkLabel|default:"Details" }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endif %}{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<ul class="mt-10 divide-y divide-primary/30 border-y border-primary/30">
|
|
||||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
|
||||||
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}">
|
|
||||||
<div class="shrink-0 sm:w-52">
|
|
||||||
<div class="terminal-mono text-sm font-semibold text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
|
|
||||||
{% if event.end %}<div class="terminal-mono text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
|
|
||||||
{% if event.timezone %}<div class="terminal-mono text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<h3 class="terminal-heading text-base font-semibold">{{ event.title }}</h3>
|
|
||||||
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
|
||||||
{% if event.description %}<p class="mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if event.link %}<a href="{{ event.link }}" class="terminal-mono inline-flex shrink-0 items-center rounded-md border border-primary/50 px-3 py-1.5 text-sm font-medium uppercase tracking-wide text-primary hover:bg-primary/10">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endif %}{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{# faq terminal override: heading, neon-tinted dividers, display questions. Chevron svg, {% for %}, and answer prose preserved. #}
|
|
||||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-3xl px-4">
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
|
||||||
<div class="mt-10 divide-y divide-primary/20 border-y border-primary/30">
|
|
||||||
{% for item in items %}
|
|
||||||
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
|
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg font-medium marker:content-none [&::-webkit-details-marker]:hidden">
|
|
||||||
<span class="terminal-heading">{{ item.question }}</span>
|
|
||||||
<svg class="h-5 w-5 shrink-0 text-primary transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
|
||||||
</summary>
|
|
||||||
<div class="pb-4 text-muted-foreground [&_a]:text-primary [&_a]:underline">{{ item.answer|safe }}</div>
|
|
||||||
</details>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{# feature_grid terminal override: scanline hero overlay, mono eyebrow, heading, neon HUD cards. All field reads + control flow preserved. #}
|
|
||||||
<section class="relative overflow-hidden py-12 md:py-20 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
|
|
||||||
<div class="relative z-10 mx-auto max-w-6xl px-4">
|
|
||||||
{% if eyebrow %}<p class="terminal-mono text-center text-xs font-semibold uppercase tracking-widest text-accent">{{ eyebrow }}</p>{% endif %}
|
|
||||||
{% if heading %}<h2 class="terminal-heading mt-2 text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
|
||||||
<div class="mt-10 grid grid-cols-1 sm:grid-cols-2 gap-6{% if columns == 2 %}{% elif columns == 4 %} lg:grid-cols-4{% else %} lg:grid-cols-3{% endif %}">
|
|
||||||
{% for item in items %}
|
|
||||||
<div class="{% if layout == "card" %}tty-corners rounded-md border border-primary/40 bg-card p-6 text-card-foreground tty-frame transition-transform hover:-translate-y-0.5{% elif layout == "centered" %}text-center{% endif %}">
|
|
||||||
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-md bg-primary/10 text-primary{% if layout == "centered" %} mx-auto{% endif %}"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
|
|
||||||
<h3 class="terminal-heading text-lg font-semibold">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-primary">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
|
|
||||||
{% if item.text %}<p class="mt-2 text-muted-foreground">{{ item.text }}</p>{% endif %}
|
|
||||||
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="terminal-mono uppercase tracking-wide mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline">{{ item.linkLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
{# featured-posts (terminal override) — grid of cards from the `posts` provider #}
|
|
||||||
{# (params: limit, featured=true). Neon HUD card: image, title, excerpt, meta. #}
|
|
||||||
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-6xl px-4">
|
|
||||||
{% if heading %}<h2 class="terminal-heading mb-8 text-2xl font-semibold tracking-tight md:text-3xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
|
|
||||||
{% for post in posts %}
|
|
||||||
<article class="tty-corners group flex flex-col overflow-hidden rounded-md border border-dashed border-primary/40 bg-card text-card-foreground tty-frame transition-transform hover:-translate-y-0.5">
|
|
||||||
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
|
||||||
<div class="flex flex-1 flex-col p-5">
|
|
||||||
<h3 class="terminal-heading text-lg font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
|
|
||||||
{% if showExcerpt and post.excerpt %}<p class="terminal-page mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
|
||||||
<div class="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 terminal-mono text-xs uppercase tracking-wide text-accent">
|
|
||||||
{% if showAuthor and post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}
|
|
||||||
{% if showDate and post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
|
|
||||||
{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{# footer (WO-TF-006): layouts columns|minimal|centered; menu columns, legal bar, and social row from the `footer_menus` provider; newsletter section reuses the {% signup_form %} core (no duplicated form). Terminal override: mono column headers, neon top rule, display wordmark. #}
|
|
||||||
{% set fl = layout %}
|
|
||||||
{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
|
|
||||||
{% set fbg = backgroundColor|default:"bg-muted/50" %}
|
|
||||||
{% macro sociallinks(cls) %}{% if social_items %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground hover:text-primary transition-colors"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}::{{ s.icon }}::{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
|
|
||||||
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="terminal-mono uppercase tracking-widest text-xs font-semibold text-accent">{{ newsletterTitle|default:"Stay in the loop" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
|
|
||||||
{% macro legalbar() %}<div class="mt-10 pt-6 border-t border-primary/30 flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-muted-foreground"><p class="terminal-mono">{% if copyright %}© {{ copyright }}{% else %}© {{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="hover:text-primary transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
|
|
||||||
<footer class="w-full border-t border-primary/30 {{ fbg }} {{ class }}">
|
|
||||||
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}max-w-7xl mx-auto px-4 sm:px-6 lg:px-8{% endif %} py-16">
|
|
||||||
{% if fl == "minimal" %}
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
|
|
||||||
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="terminal-heading text-lg font-bold text-foreground">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="text-sm text-muted-foreground hidden sm:inline">{{ tagline }}</span>{% endif %}</div>
|
|
||||||
{{ sociallinks("") }}
|
|
||||||
</div>
|
|
||||||
{{ legalbar() }}
|
|
||||||
{% elif fl == "centered" %}
|
|
||||||
<div class="flex flex-col items-center text-center gap-5">
|
|
||||||
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="terminal-heading text-xl font-bold text-foreground">{{ companyName }}</span>{% endif %}
|
|
||||||
{% if tagline %}<p class="max-w-md text-sm text-muted-foreground">{{ tagline }}</p>{% endif %}
|
|
||||||
{% if columns %}<nav class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">{% for col in columns %}{% for item in col.items %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="terminal-mono uppercase tracking-wide text-xs text-muted-foreground hover:text-primary transition-colors">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
|
|
||||||
{{ newsletter() }}
|
|
||||||
{{ sociallinks("justify-center") }}
|
|
||||||
</div>
|
|
||||||
{{ legalbar() }}
|
|
||||||
{% else %}
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
|
|
||||||
<div class="col-span-2 lg:col-span-2">
|
|
||||||
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="terminal-heading text-xl font-bold text-foreground">{{ companyName }}</span>{% endif %}
|
|
||||||
{% if tagline %}<p class="mt-3 max-w-xs text-sm text-muted-foreground">{{ tagline }}</p>{% endif %}
|
|
||||||
{% if address or email or phone %}<address class="mt-4 not-italic text-sm text-muted-foreground space-y-1">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-primary transition-colors">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-primary transition-colors">{{ phone }}</a></div>{% endif %}</address>{% endif %}
|
|
||||||
{{ sociallinks("mt-5") }}
|
|
||||||
</div>
|
|
||||||
{% for col in columns %}<div><h3 class="terminal-mono uppercase tracking-widest text-xs font-semibold text-accent">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="text-sm text-muted-foreground hover:text-primary transition-colors">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
|
|
||||||
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
{{ legalbar() }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
{# gallery terminal override: bordered TTY tiles. Lightbox script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
|
|
||||||
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}<div class="bn-gallery my-6{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}"><div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="group relative block overflow-hidden rounded-md border border-primary/30 bg-muted tty-frame focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>{% endfor %}</div></div>{% if lightbox %}<script>
|
|
||||||
(function(){
|
|
||||||
if(window.__bnLightbox)return;window.__bnLightbox=true;
|
|
||||||
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
var ov,imgEl,capEl,items=[],idx=0,lastFocus=null;
|
|
||||||
function build(){
|
|
||||||
ov=document.createElement('div');
|
|
||||||
ov.setAttribute('role','dialog');ov.setAttribute('aria-modal','true');ov.setAttribute('aria-label','Image viewer');ov.tabIndex=-1;
|
|
||||||
ov.style.cssText='position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.9);padding:2rem;';
|
|
||||||
var btn='position:absolute;width:2.75rem;height:2.75rem;display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;border:none;background:hsl(var(--background));color:hsl(var(--foreground));cursor:pointer;';
|
|
||||||
ov.innerHTML=
|
|
||||||
'<button type="button" data-lb-close aria-label="Close" style="'+btn+'top:1rem;right:1rem;"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>'+
|
|
||||||
'<button type="button" data-lb-prev aria-label="Previous image" style="'+btn+'left:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-left"/></svg></button>'+
|
|
||||||
'<figure style="margin:0;max-width:92vw;max-height:92vh;display:flex;flex-direction:column;align-items:center;gap:0.75rem;"><img data-lb-img alt="" style="max-width:92vw;max-height:82vh;object-fit:contain;border-radius:0.5rem;'+(reduce?'':'transition:opacity .2s ease;')+'"><figcaption data-lb-cap style="color:hsl(var(--background));opacity:.85;font-size:.875rem;text-align:center;max-width:60ch;"></figcaption></figure>'+
|
|
||||||
'<button type="button" data-lb-next aria-label="Next image" style="'+btn+'right:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"/></svg></button>';
|
|
||||||
document.body.appendChild(ov);
|
|
||||||
imgEl=ov.querySelector('[data-lb-img]');capEl=ov.querySelector('[data-lb-cap]');
|
|
||||||
ov.addEventListener('click',function(e){if(e.target===ov||e.target.closest('[data-lb-close]'))close();});
|
|
||||||
ov.querySelector('[data-lb-prev]').addEventListener('click',function(e){e.stopPropagation();show(idx-1);});
|
|
||||||
ov.querySelector('[data-lb-next]').addEventListener('click',function(e){e.stopPropagation();show(idx+1);});
|
|
||||||
}
|
|
||||||
function show(i){
|
|
||||||
if(!items.length)return;
|
|
||||||
idx=(i+items.length)%items.length;
|
|
||||||
var a=items[idx];var im=a.querySelector('img');
|
|
||||||
imgEl.src=(im&&(im.currentSrc||im.src))||a.getAttribute('href');
|
|
||||||
imgEl.alt=im?im.alt:'';
|
|
||||||
var cap=a.getAttribute('data-caption')||'';capEl.textContent=cap;capEl.style.display=cap?'block':'none';
|
|
||||||
}
|
|
||||||
function open(g,link){
|
|
||||||
if(!ov)build();
|
|
||||||
items=Array.prototype.slice.call(g.querySelectorAll('[data-bn-gallery-item]'));
|
|
||||||
lastFocus=document.activeElement;ov.style.display='flex';
|
|
||||||
var multi=items.length>1;
|
|
||||||
ov.querySelector('[data-lb-prev]').style.display=multi?'inline-flex':'none';
|
|
||||||
ov.querySelector('[data-lb-next]').style.display=multi?'inline-flex':'none';
|
|
||||||
show(items.indexOf(link));ov.focus();
|
|
||||||
}
|
|
||||||
function close(){if(ov)ov.style.display='none';if(lastFocus&&lastFocus.focus)lastFocus.focus();}
|
|
||||||
document.addEventListener('click',function(e){
|
|
||||||
var link=e.target.closest('[data-bn-gallery][data-lightbox="true"] [data-bn-gallery-item]');
|
|
||||||
if(!link)return;e.preventDefault();open(link.closest('[data-bn-gallery]'),link);
|
|
||||||
});
|
|
||||||
document.addEventListener('keydown',function(e){
|
|
||||||
if(!ov||ov.style.display==='none')return;
|
|
||||||
if(e.key==='Escape'){e.preventDefault();close();}
|
|
||||||
else if(e.key==='ArrowLeft')show(idx-1);
|
|
||||||
else if(e.key==='ArrowRight')show(idx+1);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>{% endif %}{% endwith %}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{# terminal heading — terminal-heading everywhere; on h1/h2 only #}
|
|
||||||
{% if level == 1 %}<h1 class="text-4xl font-bold tracking-tight terminal-heading {{ textClass }} flex-1">{{ text }}</h1>
|
|
||||||
{% elif level == 3 %}<h3 class="text-2xl font-semibold terminal-heading {{ textClass }} flex-1">{{ text }}</h3>
|
|
||||||
{% elif level == 4 %}<h4 class="text-xl font-semibold terminal-heading {{ textClass }} flex-1">{{ text }}</h4>
|
|
||||||
{% elif level == 5 %}<h5 class="text-lg font-medium terminal-heading {{ textClass }} flex-1">{{ text }}</h5>
|
|
||||||
{% elif level == 6 %}<h6 class="text-base font-medium terminal-heading {{ textClass }} flex-1">{{ text }}</h6>
|
|
||||||
{% else %}<h2 class="text-3xl font-semibold tracking-tight terminal-heading {{ textClass }} flex-1">{{ text }}</h2>{% endif %}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
{# hero terminal override (GO-BIG showpiece): canvas ASCII glyph-rain background, TTY prompt eyebrow, scanline overlay. All builtin field reads + macro + branch structure preserved from the builtin default. The rain shows when there is no backgroundImage; it lazy-inits on view, honors prefers-reduced-motion, degrades to a static phosphor gradient with JS off, and is dependency-free. #}
|
|
||||||
{% set eyebrowText = eyebrow|get:"text"|default:eyebrow %}
|
|
||||||
{% set headlineText = headline|get:"text"|default:headline %}
|
|
||||||
{% set subText = subheadline|get:"text"|default:subheadline %}
|
|
||||||
{% set descText = description|get:"text"|default:description %}
|
|
||||||
{% set pText = ctaPrimary.text %}
|
|
||||||
{% set pUrl = ctaPrimary.url %}
|
|
||||||
{% set sText = ctaSecondary.text %}
|
|
||||||
{% set sUrl = ctaSecondary.url %}
|
|
||||||
{% set variant = template|default:"centered" %}
|
|
||||||
{% set hasImage = backgroundImage %}
|
|
||||||
{% set showOverlay = hasImage and overlayColor != "none" and overlayColor != "" %}
|
|
||||||
{% macro herocontent(align) %}
|
|
||||||
{% if eyebrowText %}<span class="terminal-eyebrow inline-block mb-4 border border-border bg-muted/40 px-3 py-1">$ {{ eyebrowText }} <span aria-hidden="true" class="caret-blink">_</span></span>{% endif %}
|
|
||||||
{% if headlineText %}<h1 class="terminal-heading text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-foreground">{{ headlineText }}</h1>{% endif %}
|
|
||||||
{% if subText %}<h2 class="mt-4 text-xl sm:text-2xl text-muted-foreground max-w-2xl{% if align == "center" %} mx-auto{% endif %}">{{ subText }}</h2>{% endif %}
|
|
||||||
{% if descText %}<p class="mt-4 max-w-xl leading-relaxed text-muted-foreground{% if align == "center" %} mx-auto{% endif %}">{{ descText }}</p>{% endif %}
|
|
||||||
{% if pText or sText %}<div class="mt-8 flex flex-col sm:flex-row gap-4{% if align == "center" %} justify-center{% endif %}">
|
|
||||||
{% if pText and pUrl %}<a href="{{ pUrl }}" class="terminal-mono inline-flex items-center justify-center gap-2 border border-primary bg-primary px-6 py-3 font-semibold uppercase tracking-wider text-primary-foreground transition-colors hover:bg-transparent hover:text-primary">{{ pText }}</a>{% endif %}
|
|
||||||
{% if sText and sUrl %}<a href="{{ sUrl }}" class="terminal-mono inline-flex items-center justify-center border border-border px-6 py-3 font-semibold uppercase tracking-wider text-foreground transition-colors hover:border-primary hover:text-primary">{{ sText }}</a>{% endif %}
|
|
||||||
</div>{% endif %}
|
|
||||||
{% if trustLogos %}<div class="mt-10">
|
|
||||||
<p class="terminal-mono text-xs uppercase tracking-widest text-muted-foreground mb-3">Trusted by</p>
|
|
||||||
<div class="flex flex-wrap items-center gap-6{% if align == "center" %} justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener noreferrer" class="opacity-60 transition-opacity hover:opacity-100">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}</div>
|
|
||||||
</div>{% endif %}
|
|
||||||
{% endmacro %}
|
|
||||||
{% if variant == "split" %}
|
|
||||||
<section class="hero grid grid-cols-1 lg:grid-cols-2 overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="relative min-h-80 bg-muted {% if mediaPosition == "left" %}order-1 lg:order-1{% else %}order-1 lg:order-2{% endif %}">{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% else %}<div class="tty-rain-wrap"><div class="tty-rain-fallback"></div><canvas class="tty-rain-canvas" aria-hidden="true"></canvas></div>{% endif %}<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div></div>
|
|
||||||
<div class="flex flex-col justify-center px-6 py-16 lg:px-12 {% if mediaPosition == "left" %}order-2 lg:order-2{% else %}order-2 lg:order-1{% endif %}"><div class="max-w-lg text-left">{{ herocontent("left") }}</div></div>
|
|
||||||
</section>
|
|
||||||
{% else %}
|
|
||||||
{% set align = "center" %}
|
|
||||||
{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %}
|
|
||||||
<section class="hero relative flex flex-col overflow-hidden bg-background text-foreground {% if minHeight == "screen" %}min-h-screen{% elif minHeight == "50vh" %}min-h-[50vh]{% elif minHeight == "auto" %}min-h-96{% else %}min-h-[75vh]{% endif %} {% if verticalAlign == "top" %}justify-start pt-20{% elif verticalAlign == "bottom" %}justify-end pb-20{% else %}justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
|
||||||
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}<div class="absolute inset-0 {% if overlayColor == "light" %}bg-background/60{% elif overlayColor == "primary" %}bg-primary/50{% elif overlayOpacity <= 30 %}bg-black/30{% elif overlayOpacity <= 50 %}bg-black/50{% elif overlayOpacity <= 70 %}bg-black/70{% else %}bg-black/80{% endif %}"></div>{% endif %}</div>{% else %}<div class="tty-rain-wrap z-0"><div class="tty-rain-fallback"></div><canvas class="tty-rain-canvas" aria-hidden="true"></canvas></div>{% endif %}
|
|
||||||
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0 z-0"></div>
|
|
||||||
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8 {% if variant == "left-aligned" %}text-left{% else %}text-center{% endif %}">{{ herocontent(align) }}</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
if(window.__terminalRain)return;window.__terminalRain=1;
|
|
||||||
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
function initCanvas(cv){
|
|
||||||
if(cv.__init)return;cv.__init=1;
|
|
||||||
var ctx=cv.getContext&&cv.getContext('2d');if(!ctx)return;
|
|
||||||
var wrap=cv.parentElement;
|
|
||||||
var cs=getComputedStyle(document.documentElement);
|
|
||||||
function raw(n){return cs.getPropertyValue(n).trim();}
|
|
||||||
var fg=raw('--foreground');
|
|
||||||
var col1='hsl('+(raw('--primary')||fg)+')';
|
|
||||||
var col2='hsl('+(raw('--accent')||fg)+')';
|
|
||||||
var fade='hsl('+(raw('--background')||fg)+' / 0.12)';
|
|
||||||
var glyphs='01<>[]{}#$%&*+=/|_~^:;.,abcdefghijklmnopqrstuvwxyz'.split('');
|
|
||||||
var fontSize=14,cols=0,drops=[],W=0,H=0,dpr=1;
|
|
||||||
function resize(){
|
|
||||||
dpr=Math.min(window.devicePixelRatio||1,2);
|
|
||||||
W=wrap.clientWidth;H=wrap.clientHeight;
|
|
||||||
if(W===0||H===0)return;
|
|
||||||
cv.width=W*dpr;cv.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
||||||
cols=Math.max(1,Math.floor(W/fontSize));
|
|
||||||
drops=[];for(var i=0;i<cols;i++)drops[i]=Math.random()*-50;
|
|
||||||
}
|
|
||||||
resize();
|
|
||||||
var last=0,fps=1000/18;
|
|
||||||
function frame(t){
|
|
||||||
requestAnimationFrame(frame);
|
|
||||||
if(t-last<fps)return;last=t;
|
|
||||||
if(W===0||H===0){resize();return;}
|
|
||||||
ctx.fillStyle=fade;ctx.fillRect(0,0,W,H);
|
|
||||||
ctx.font=fontSize+'px monospace';
|
|
||||||
for(var i=0;i<cols;i++){
|
|
||||||
var ch=glyphs[Math.floor(Math.random()*glyphs.length)];
|
|
||||||
var x=i*fontSize,y=drops[i]*fontSize;
|
|
||||||
ctx.fillStyle=Math.random()<0.85?col1:col2;
|
|
||||||
ctx.fillText(ch,x,y);
|
|
||||||
if(y>H&&Math.random()>0.975)drops[i]=0;
|
|
||||||
drops[i]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(window.ResizeObserver){var ro=new ResizeObserver(resize);ro.observe(wrap);}else{window.addEventListener('resize',resize);}
|
|
||||||
requestAnimationFrame(frame);
|
|
||||||
}
|
|
||||||
function boot(){
|
|
||||||
if(reduce)return;
|
|
||||||
var cvs=document.querySelectorAll('.tty-rain-canvas');
|
|
||||||
if(!cvs.length)return;
|
|
||||||
if(!('IntersectionObserver' in window)){cvs.forEach(initCanvas);return;}
|
|
||||||
var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){initCanvas(e.target);io.unobserve(e.target);}});},{threshold:0.05});
|
|
||||||
cvs.forEach(function(c){io.observe(c);});
|
|
||||||
}
|
|
||||||
if(document.readyState!='loading')boot();else document.addEventListener('DOMContentLoaded',boot);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
{# hours-location terminal override: mono readout table, neon map panel. Map lazy-load button + inline handler preserved. #}
|
|
||||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
|
||||||
<div class="mx-auto max-w-5xl px-4">
|
|
||||||
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-10 md:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
{% if hours %}
|
|
||||||
<table class="terminal-mono w-full text-sm">
|
|
||||||
<tbody class="divide-y divide-primary/30">
|
|
||||||
{% for row in hours %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row" class="py-2 pr-4 text-left font-medium uppercase tracking-wide">{{ row.day }}</th>
|
|
||||||
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-primary{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
{% if specialHoursNote %}<p class="mt-4 rounded-md border border-primary/40 bg-muted px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
|
|
||||||
{% if address %}<div class="mt-6"><div class="terminal-mono text-sm font-medium uppercase tracking-wide text-accent">Address</div><address class="mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
|
|
||||||
{% if phone %}<div class="mt-4"><div class="terminal-mono text-sm font-medium uppercase tracking-wide text-accent">Phone</div><a href="tel:{{ phone }}" class="terminal-mono mt-1 inline-block font-medium transition-colors hover:text-primary">{{ phone }}</a></div>{% endif %}
|
|
||||||
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="terminal-mono mt-6 inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium uppercase tracking-wide text-primary-foreground transition-colors hover:bg-primary/90"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if mapEmbedUrl %}
|
|
||||||
<div class="tty-corners aspect-square w-full overflow-hidden rounded-md border border-primary/40 bg-muted tty-frame md:aspect-auto">
|
|
||||||
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="terminal-mono flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm font-medium uppercase tracking-wide transition-colors hover:bg-accent/20 hover:text-foreground"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||