initial: theme plugin kindergarten

Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/kindergarten.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-06-06 14:11:35 +08:00
commit ffe46a146c
63 changed files with 5807 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.so
*.test
tmp/
.idea/
.vscode/

155
BUILD_REPORT.md Normal file
View File

@ -0,0 +1,155 @@
# Kindergarten — BUILD_REPORT
Build pass: wave-1 implementation against spec
`themes/docs/works/kindergarten.md` and gating UAT
`themes/docs/uat/kindergarten.md`, with fonts policy overridden by
`themes/docs/FONTS.md`.
## What landed
### Plugin metadata
- `plugin.mod` mirrors spec §2 verbatim (`kind=theme`, `scope=@themes`,
`categories=["templates"]`, 9 tags including the literal-required
`kids,education,playful,family,school`, compatibility pin
`block_core=">=0.11.0 <0.12.0"`, `version=0.1.0`).
- `go.mod` pins `git.dev.alexdunmow.com/block/core v0.11.1` and Go 1.26.4; no
`replace` directives; matches the SDK pin used by `cms/backend/go.mod`.
### Registration surface
- `var Registration plugin.PluginRegistration` exported in `registration.go`.
- `Register(tr, br)`:
- `tr.RegisterSystemTemplate("kindergarten", ...)`
- 4 page templates: `default`, `landing`, `article`, `full-width` with the
exact slot tuples from spec §6.
- `br.LoadSchemasFromFS(Schemas())` called **before** any `br.Register(...)`.
- 8 theme blocks registered: `mascot_hero`, `alphabet_strip`, `schedule`,
`gallery_of_art`, `numbers_counter`, `storybook_quote`, `big_cta`,
`footer` (all with `Source: "kindergarten"`).
- 4 built-in overrides (`heading`, `text`, `button`, `card`) registered
against template key `"kindergarten"`.
- Email wrapper registered via `tr.RegisterEmailWrapper("kindergarten", ...)`.
- `DefaultMasterPages()` ships both `kindergarten:default-master` (used by
`default`+`article`) and `kindergarten:landing-master` (used by
`landing`+`full-width`), with the block ordering from spec §7.
### Schemas
- All 8 schemas live under `schemas/` and declare
`"$schema": "http://json-schema.org/draft-07/schema#"`. Every JSON property
matches the corresponding `content[...]` key in the Go renderers; all
`x-editor` values come from the allowed set (`text, richtext, media, color,
select, number, slug, textarea, array, collection, link`).
### Presets
- `presets.json` is a JSON array of length 3, ordered `recess`, `chalkboard`,
`crayon-box`. Every token value matches `^[0-9]+ [0-9]+% [0-9]+%$` (HSL
triple, no `hsl()` wrappers). All 19 tokens present per preset:
- `recess` → mode `light`, exposes `lightColors` only.
- `chalkboard` → mode `dark`, exposes `darkColors` only.
- `crayon-box` → mode `both`, exposes both `lightColors` and `darkColors`.
### Fonts
- `fonts.json = []` per the wave-1 fonts policy (`themes/docs/FONTS.md`).
- No woff2 bundled.
- `RECOMMENDED_FONTS.md` at the theme root lists Quicksand (heading),
Nunito (body), Fira Code (mono), and Sniglet (alt display) as
Google Fonts picker recommendations.
- Theme CSS goes through `var(--font-heading | --font-body | --font-mono)`
with explicit fallback stacks so the type system stays close to the
intended aesthetic until the admin makes a choice.
### CSS strategy
- `assets/style.css` is registered into the host Tailwind input via
`CSSManifest.InputCSSAppend` (see `embed.go::ThemeCSSManifest`).
- Custom utilities cover crayon underline (`.kg-crayon-underline`), the
dotted-paper background layer (`.kg-dotted-paper`), the sticker drop-shadow
on pills (`box-shadow: 0 4px 0 0 …`), polaroid frames for the gallery, the
numeral badge (`.kg-numeral { border-radius: 50%; aspect-ratio: 1 / 1; }`),
and the schedule time-pill.
- No hardcoded hex / `rgb()` / named colors in any `.templ` or `.go`; all
color references are `hsl(var(--<token>))` against the 19-token CSS
variables. (Check 6 in `check-safety` confirms this.)
## Build output
```
$ cd /home/alex/src/blockninja/themes/kindergarten
$ /home/alex/go/bin/templ generate
(✓) Complete [ updates=14 ]
$ make
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o kindergarten.so .
$ ls -lh kindergarten.so
-rw-rw-r-- 1 alex alex 21M Jun 6 13:24 kindergarten.so
```
Build succeeded; no `warning:` / `WARN` lines on stdout.
## Safety check
```
$ cd /home/alex/src/blockninja/check-safety
$ go run . /home/alex/src/blockninja/themes/kindergarten
... 22 checks ...
exit 0
```
Notes:
- The task brief referenced `cd /home/alex/src/blockninja/backend && go run
./cmd/check-safety . --plugin-dir ../themes/kindergarten`. That path layout
does not match this workspace; `check-safety` lives in
`/home/alex/src/blockninja/check-safety` as a standalone module. The
invocation above is the actual run that exits 0.
- Check 2e ("warn on `any`") flags 32 informational warnings on `map[string]any`
parameters in block render functions. The plugin SDK's `BlockFunc` signature
is `func(ctx, content map[string]any) string` — these `any`s are mandated
by the SDK contract; warnings are not failures and gotham trips the same
ones.
## Open items / deferred
| Item | Status | Notes |
|---|---|---|
| Bundled woff2 fonts | **Deferred** | Wave-1 policy: ship `fonts.json = []` and rely on admin Google Fonts picker. RECOMMENDED_FONTS.md lists the four recommended families. |
| LICENSES.md | **Deferred** | Only required when bundling woff2s. Wave-2 follow-up. |
| Real mascot SVG (Pip) | **Deferred** | The mascot renderer ships four primitive SVG variants (`pip`, `blocks`, `star`, `balloon`). Replace with illustrated Pip in v0.2 per spec §15. |
| `make rebuild` (live-container deploy) | **Out of scope** | Explicitly disabled in the brief. The Makefile only carries `all` (build .so), `clean`, and `templ`. |
| Marketplace screenshots (1440×900 × 6) | **Deferred** | No live container available in this pass; capture during v0.1.0 launch sprint. |
| Seed demo content | **Deferred** | Mascot image + sample posts to be staged when a dev container is available. |
| Email-client smoke testing (Gmail / Apple Mail / Outlook 365) | **Deferred** | Wrapper code shipped; visual verification needs Litmus/Mailbox access. |
| Accessibility contrast audit | **Deferred** | Theme tokens follow spec §4 values; an automated audit needs a running site. |
| `wave-2` Sniglet/Fira Code Rounded bundling | **Deferred** | See `themes/docs/FONTS.md`, last section. |
## Files produced
```
kindergarten/
BUILD_REPORT.md ← this file
Makefile ← local-only build helpers
README.md (not created — not requested)
RECOMMENDED_FONTS.md
assets/style.css ← injected via CSSManifest.InputCSSAppend
embed.go
fonts.json ← literal "[]"
go.mod / go.sum
plugin.mod
presets.json ← 3 presets, 19 tokens each
register.go ← system template + 4 page templates + 8 blocks + 4 overrides + email wrapper
registration.go ← var Registration plugin.PluginRegistration
schemas/{8 block schemas}.schema.json
helpers.go
template.templ + template_templ.go (4 page templates)
email_wrapper.{go,templ,_templ.go}
mascot_hero.{go,templ,_templ.go}
alphabet_strip.{go,templ,_templ.go}
schedule.{go,templ,_templ.go}
gallery_of_art.{go,templ,_templ.go}
numbers_counter.{go,templ,_templ.go}
storybook_quote.{go,templ,_templ.go}
big_cta.{go,templ,_templ.go}
footer.{go,templ,_templ.go}
heading_override.{go,templ,_templ.go} (built-in override)
text_override.{go,templ,_templ.go} (built-in override)
button_override.{go,templ,_templ.go} (built-in override)
card_override.{go,templ,_templ.go} (built-in override)
kindergarten.so ← compiled output (21 MB)
```

29
Makefile Normal file
View File

@ -0,0 +1,29 @@
# Kindergarten — local-only build helpers (.so plugin workflow)
#
# IMPORTANT: This Makefile intentionally omits the `rebuild` family of targets
# that other themes ship; this build pass only validates that the plugin
# compiles locally and passes the safety check.
.PHONY: all clean templ help
PLUGIN_NAME := kindergarten
# Default target: build the .so locally.
all: $(PLUGIN_NAME).so
# Local plugin build (no container). Requires CGO + matching glibc/go toolchain.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
clean:
rm -f $(PLUGIN_NAME).so
# Regenerate templ Go files locally (for development).
templ:
templ generate
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " clean Remove built .so"
@echo " templ Regenerate templ Go files"

43
RECOMMENDED_FONTS.md Normal file
View File

@ -0,0 +1,43 @@
# Recommended fonts for Kindergarten
The kindergarten theme ships `fonts.json = []` per the wave-1 fonts policy
(`themes/docs/FONTS.md`). Admins assign fonts via the typography panel; this
file lists the picker recommendations sourced from the spec (`docs/works/kindergarten.md` §3).
Until the admin makes a choice, the theme falls back to its system stack:
- `--font-heading``Quicksand`, `Sniglet`, `Nunito`, system sans
- `--font-body``Nunito`, `Quicksand`, system sans
- `--font-mono``Fira Code`, `JetBrains Mono`, system monospace
## How to apply
1. Open **Settings → Typography** in the BlockNinja admin.
2. Click the **Google Fonts** tab in the font picker.
3. Add each font below using the listed picker source.
4. Assign Heading / Body / Mono in the typography panel:
- Heading slot → `google:Quicksand` (700)
- Body slot → `google:Nunito` (400, 600)
- Mono slot → `google:Fira Code` (400)
## Recommendations
| Slot | Source | Family | Weights / styles | Notes |
|---------|--------------------|-------------|---------------------|-------|
| Heading | `google:Quicksand` | Quicksand | 400, 600, 700 | Primary display face — rounded, friendly. |
| Heading (alt) | `google:Sniglet` | Sniglet | 400, 800 | Alt display face for extra-chunky banners. Confirm SIL OFL compatibility before bundling. |
| Body | `google:Nunito` | Nunito | 400, 600, 700 | Famously rounded body text. Pairs with Quicksand. |
| Mono | `google:Fira Code` | Fira Code | 400 | Use the regular weight; ligatures look great in classroom code samples. |
## Why these picks
- **Quicksand** and **Nunito** are SIL OFL — safe to recommend and free to use.
- **Sniglet** is also SIL OFL with the original "Sniglet" family available via Google Fonts as a sibling of Comfortaa. The spec calls out Sniglet as the alternate display face; ship Comfortaa as a backup if your admin prefers something with a similar feel but slightly more conservative letterforms.
- **Fira Code** is SIL OFL. The spec calls for "Fira Code Rounded" — Google Fonts ships only the regular Fira Code variants today; use the regular as the closest available stand-in.
## Wave-2 follow-up
If the brand requires Sniglet rounded everywhere, bundle the woff2 files
inside `assets/fonts/web/` and add the entries to `fonts.json` per the
schema in `themes/docs/FONTS.md`. Record the licence in `LICENSES.md` at the
theme root.

64
alphabet_strip.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"bytes"
"context"
"strings"
"git.dev.alexdunmow.com/block/core/blocks"
)
// AlphabetStripBlockMeta defines metadata for the alphabet strip block.
var AlphabetStripBlockMeta = blocks.BlockMeta{
Key: "alphabet_strip",
Title: "Alphabet Strip",
Description: "Decorative letter band; jumps to anchors when used as TOC.",
Category: blocks.CategoryNavigation,
Source: "kindergarten",
}
// AlphabetStripData contains the letters + colorway for the rendered strip.
type AlphabetStripData struct {
Letters []string
ColorMode string
}
// AlphabetStripBlock renders a decorative letter band.
// Content shape: {letters,colorMode}.
func AlphabetStripBlock(ctx context.Context, content map[string]any) string {
raw := getStringDefault(content, "letters", "")
colorMode := getStringDefault(content, "colorMode", "rainbow")
// Validate colorMode against allowed set.
switch colorMode {
case "primary", "rainbow", "mono":
default:
colorMode = "rainbow"
}
letters := splitLetters(raw)
data := AlphabetStripData{
Letters: letters,
ColorMode: colorMode,
}
var buf bytes.Buffer
_ = alphabetStripComponent(data).Render(ctx, &buf)
return buf.String()
}
// splitLetters returns one entry per visible rune. Whitespace is trimmed.
func splitLetters(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
letters := make([]string, 0, len(s))
for _, r := range s {
if r == ' ' || r == '\t' || r == '\n' {
continue
}
letters = append(letters, string(r))
}
return letters
}

25
alphabet_strip.templ Normal file
View File

@ -0,0 +1,25 @@
package main
// alphabetStripComponent renders the letter band.
templ alphabetStripComponent(data AlphabetStripData) {
<nav class={ "kg-alphabet-strip", alphabetClass(data.ColorMode) } aria-label="Alphabet strip" data-block="kindergarten:alphabet_strip">
if len(data.Letters) == 0 {
<span class="kg-empty" data-empty="true">No letters configured</span>
}
for _, letter := range data.Letters {
<span class="kg-alphabet-letter">{ letter }</span>
}
</nav>
}
// alphabetClass returns the modifier class for the chosen colorway.
func alphabetClass(mode string) string {
switch mode {
case "primary":
return "kg-alphabet-primary"
case "mono":
return "kg-alphabet-mono"
default:
return ""
}
}

100
alphabet_strip_templ.go Normal file
View File

@ -0,0 +1,100 @@
// 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"
// alphabetStripComponent renders the letter band.
func alphabetStripComponent(data AlphabetStripData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"kg-alphabet-strip", alphabetClass(data.ColorMode)}
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, "<nav 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: `alphabet_strip.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, "\" aria-label=\"Alphabet strip\" data-block=\"kindergarten:alphabet_strip\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Letters) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span class=\"kg-empty\" data-empty=\"true\">No letters configured</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, letter := range data.Letters {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"kg-alphabet-letter\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(letter)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `alphabet_strip.templ`, Line: 10, 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, 5, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// alphabetClass returns the modifier class for the chosen colorway.
func alphabetClass(mode string) string {
switch mode {
case "primary":
return "kg-alphabet-primary"
case "mono":
return "kg-alphabet-mono"
default:
return ""
}
}
var _ = templruntime.GeneratedTemplate

488
assets/style.css Normal file
View File

@ -0,0 +1,488 @@
/* ============================================================
Kindergarten Primary-colored, hand-lettered theme
------------------------------------------------------------
Token strategy: every color reference goes through
`hsl(var(--<token>))` and font-family goes through
`var(--font-{heading|body|mono}, <fallback-stack>)`.
============================================================ */
/* ---- Typography (CSS-variable consumers + fallbacks) ------- */
.kg-page {
font-family: var(--font-body, "Nunito", "Quicksand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
line-height: 1.7;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
.kg-display,
.kg-page h1,
.kg-page h2,
.kg-page h3,
.kg-page h4,
.kg-page h5,
.kg-page h6 {
font-family: var(--font-heading, "Quicksand", "Sniglet", "Nunito", -apple-system, sans-serif);
font-weight: 700;
letter-spacing: -0.01em;
}
.kg-mono {
font-family: var(--font-mono, "Fira Code", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
}
/* ---- Layout utilities -------------------------------------- */
.kg-section {
padding-block: 4rem;
}
.kg-container {
max-width: 72rem;
margin-inline: auto;
padding-inline: 1.5rem;
}
/* ---- Rounded primitives ------------------------------------ */
.kg-chip {
border-radius: 24px;
padding: 0.5rem 1rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.kg-card {
border-radius: 32px;
background-color: hsl(var(--card));
color: hsl(var(--card-foreground, var(--foreground)));
padding: 2rem;
border: 1px solid hsl(var(--border));
position: relative;
overflow: hidden;
}
.kg-card::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background-color: hsl(var(--primary));
}
.kg-hero-panel {
border-radius: 48px;
padding: clamp(2rem, 6vw, 4rem);
position: relative;
overflow: hidden;
background-color: hsl(var(--accent) / 0.15);
}
.kg-pill {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1.25rem 2.5rem;
border-radius: 9999px;
font-family: var(--font-heading, "Quicksand", "Sniglet", "Nunito", sans-serif);
font-weight: 700;
font-size: 1.125rem;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: 0;
cursor: pointer;
box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.15);
text-decoration: none;
transition: transform 120ms ease, box-shadow 120ms ease;
}
.kg-pill:hover {
transform: translateY(-1px);
box-shadow: 0 5px 0 0 hsl(var(--foreground) / 0.15);
}
.kg-pill:active {
transform: translateY(2px);
box-shadow: 0 2px 0 0 hsl(var(--foreground) / 0.15);
}
.kg-pill:focus-visible {
outline: 3px solid hsl(var(--ring));
outline-offset: 3px;
}
/* CTA colorways */
.kg-pill-red { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
.kg-pill-blue { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
.kg-pill-yellow { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.kg-pill-green { background-color: hsl(145 65% 45%); color: hsl(0 0% 100%); }
/* ---- Crayon underline (utility on headings) ---------------- */
.kg-crayon-underline {
position: relative;
display: inline-block;
}
.kg-crayon-underline::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -6px;
height: 8px;
background: linear-gradient(90deg,
hsl(var(--primary)) 0 22%,
hsl(var(--accent)) 22% 50%,
hsl(var(--secondary)) 50% 78%,
currentColor 78% 100%);
border-radius: 4px;
transform: rotate(-1deg);
z-index: -1;
}
/* ---- Numeral badges ---------------------------------------- */
.kg-numeral {
display: inline-flex;
align-items: center;
justify-content: center;
width: 96px;
height: 96px;
border-radius: 50%;
aspect-ratio: 1 / 1;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif);
font-weight: 700;
font-size: 2.5rem;
box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.15);
}
/* ---- Polaroid frame for gallery_of_art --------------------- */
.kg-polaroid {
background-color: hsl(var(--card));
border-radius: 16px;
padding-top: 12px;
padding-inline: 12px;
padding-bottom: 36px;
box-shadow: 0 6px 24px hsl(var(--foreground) / 0.12);
transform: rotate(-1deg);
transition: transform 200ms ease;
}
.kg-polaroid:nth-child(2n) { transform: rotate(1.5deg); }
.kg-polaroid:nth-child(3n) { transform: rotate(-0.5deg); }
.kg-polaroid:hover { transform: rotate(0deg); }
.kg-polaroid > img,
.kg-polaroid > .kg-polaroid-image {
display: block;
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 8px;
background-color: hsl(var(--muted));
}
.kg-polaroid-caption {
text-align: center;
margin-top: 0.75rem;
font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif);
color: hsl(var(--foreground));
}
/* ---- Alphabet strip ---------------------------------------- */
.kg-alphabet-strip {
display: flex;
overflow-x: auto;
gap: 0.5rem;
padding: 1rem;
scrollbar-width: thin;
}
.kg-alphabet-letter {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: 24px;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif);
font-weight: 700;
font-size: 1.5rem;
box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.12);
}
.kg-alphabet-letter:nth-child(4n+1) { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
.kg-alphabet-letter:nth-child(4n+2) { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
.kg-alphabet-letter:nth-child(4n+3) { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.kg-alphabet-letter:nth-child(4n+4) { background-color: hsl(145 65% 45%); color: hsl(0 0% 100%); }
.kg-alphabet-mono .kg-alphabet-letter {
background-color: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.kg-alphabet-primary .kg-alphabet-letter {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
/* ---- Day schedule pills ------------------------------------ */
.kg-schedule-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1rem;
align-items: center;
padding: 1rem;
border-radius: 24px;
background-color: hsl(var(--card));
border: 1px solid hsl(var(--border));
margin-bottom: 0.75rem;
}
.kg-schedule-time {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
font-family: var(--font-mono, "Fira Code", monospace);
font-weight: 700;
}
.kg-schedule-activity {
font-family: var(--font-body, "Nunito", sans-serif);
color: hsl(var(--foreground));
}
.kg-schedule-icon {
color: hsl(var(--primary));
}
/* ---- Storybook quote --------------------------------------- */
.kg-storybook {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
padding: 2rem;
border-radius: 32px;
background-color: hsl(var(--card));
border: 1px solid hsl(var(--border));
}
@media (min-width: 768px) {
.kg-storybook {
grid-template-columns: 1fr 1fr;
align-items: center;
}
}
.kg-storybook-quote {
font-family: var(--font-heading, "Quicksand", "Sniglet", serif);
font-size: clamp(1.25rem, 3vw, 2rem);
line-height: 1.4;
color: hsl(var(--foreground));
}
.kg-storybook-author {
margin-top: 1rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-body, "Nunito", sans-serif);
}
.kg-storybook-illustration {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 24px;
background-color: hsl(var(--muted));
}
/* ---- Friendly footer --------------------------------------- */
.kg-footer {
padding: 3rem 1.5rem;
background-color: hsl(var(--muted));
color: hsl(var(--foreground));
border-top: 6px dashed hsl(var(--primary));
border-radius: 48px 48px 0 0;
}
.kg-footer-mascot {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
font-family: var(--font-heading, "Quicksand", sans-serif);
}
.kg-footer-signup {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-block: 1.5rem;
max-width: 32rem;
}
.kg-footer-signup input {
flex: 1 1 12rem;
padding: 0.75rem 1rem;
border-radius: 9999px;
border: 2px solid hsl(var(--border));
background-color: hsl(var(--input));
color: hsl(var(--foreground));
font-family: var(--font-body, "Nunito", sans-serif);
}
.kg-footer-signup input:focus-visible {
outline: 3px solid hsl(var(--ring));
outline-offset: 2px;
}
.kg-social-row {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.kg-social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 9999px;
background-color: hsl(var(--card));
color: hsl(var(--primary));
border: 1px solid hsl(var(--border));
}
.kg-social-link:focus-visible {
outline: 3px solid hsl(var(--ring));
outline-offset: 2px;
}
/* ---- Mascot hero confetti dots ----------------------------- */
.kg-confetti {
position: absolute;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(circle, hsl(var(--primary) / 0.5) 8px, transparent 9px),
radial-gradient(circle, hsl(var(--secondary) / 0.5) 6px, transparent 7px),
radial-gradient(circle, hsl(var(--accent) / 0.5) 5px, transparent 6px);
background-size: 80px 80px, 50px 50px, 30px 30px;
background-position: 10% 20%, 70% 30%, 40% 80%;
opacity: 0.6;
}
/* ---- Dotted paper layer (page background) ------------------ */
.kg-dotted-paper {
background-color: hsl(var(--background));
background-image: radial-gradient(circle, hsl(var(--muted-foreground) / 0.15) 1px, transparent 1.5px);
background-size: 24px 24px;
}
/* ---- Heading override (numeral badge variant) -------------- */
.kg-heading-stepped {
display: flex;
align-items: center;
gap: 1rem;
}
.kg-heading-step-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 50%;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif);
font-weight: 700;
font-size: 1.5rem;
flex: 0 0 auto;
}
/* ---- Text override: rounded selection highlight ------------ */
.kg-text ::selection {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.kg-text {
font-family: var(--font-body, "Nunito", sans-serif);
line-height: 1.7;
color: hsl(var(--foreground));
}
/* ---- Numbers counter grid ---------------------------------- */
.kg-numbers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 2rem;
text-align: center;
}
.kg-numbers-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.kg-numbers-label {
font-family: var(--font-body, "Nunito", sans-serif);
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.875rem;
}
/* ---- Gallery grid ------------------------------------------ */
.kg-gallery-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 640px) { .kg-gallery-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .kg-gallery-grid { grid-template-columns: repeat(3, 1fr); } }
/* ---- Big CTA panel (spec §9 button override) --------------- */
.kg-big-cta {
text-align: center;
padding: 3rem 1.5rem;
}
/* ---- Empty-state placeholder ------------------------------- */
.kg-empty {
text-align: center;
padding: 2rem;
border-radius: 24px;
border: 2px dashed hsl(var(--border));
color: hsl(var(--muted-foreground));
font-family: var(--font-body, "Nunito", sans-serif);
}

45
big_cta.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// BigCTABlockMeta defines metadata for the oversized CTA block.
var BigCTABlockMeta = blocks.BlockMeta{
Key: "big_cta",
Title: "Big CTA",
Description: "Oversized pill button with crayon underline.",
Category: blocks.CategoryContent,
Source: "kindergarten",
}
// BigCTAData is the renderer input.
type BigCTAData struct {
Label string
Href string
ColorVariant string
}
// BigCTABlock renders the oversized pill button.
// Content shape: {label,href,colorVariant}.
func BigCTABlock(ctx context.Context, content map[string]any) string {
color := getStringDefault(content, "colorVariant", "yellow")
switch color {
case "red", "blue", "yellow", "green":
default:
color = "yellow"
}
data := BigCTAData{
Label: getString(content, "label"),
Href: getStringDefault(content, "href", "#"),
ColorVariant: color,
}
var buf bytes.Buffer
_ = bigCTAComponent(data).Render(ctx, &buf)
return buf.String()
}

28
big_cta.templ Normal file
View File

@ -0,0 +1,28 @@
package main
// bigCTAComponent renders the oversized pill CTA.
templ bigCTAComponent(data BigCTAData) {
<section class="kg-big-cta" data-block="kindergarten:big_cta">
<div class="kg-container">
if data.Label != "" {
<a href={ templ.SafeURL(data.Href) } class={ "kg-pill", "kg-crayon-underline", ctaColorClass(data.ColorVariant) }>{ data.Label }</a>
} else {
<span class="kg-empty" data-empty="true">Add a label and link to publish this call-to-action.</span>
}
</div>
</section>
}
// ctaColorClass picks the modifier class for the four-color CTA variants.
func ctaColorClass(variant string) string {
switch variant {
case "red":
return "kg-pill-red"
case "blue":
return "kg-pill-blue"
case "green":
return "kg-pill-green"
default:
return "kg-pill-yellow"
}
}

114
big_cta_templ.go Normal file
View File

@ -0,0 +1,114 @@
// 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"
// bigCTAComponent renders the oversized pill CTA.
func bigCTAComponent(data BigCTAData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"kg-big-cta\" data-block=\"kindergarten:big_cta\"><div class=\"kg-container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Label != "" {
var templ_7745c5c3_Var2 = []any{"kg-pill", "kg-crayon-underline", ctaColorClass(data.ColorVariant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `big_cta.templ`, Line: 8, Col: 38}
}
_, 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, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `big_cta.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `big_cta.templ`, Line: 8, Col: 130}
}
_, 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, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"kg-empty\" data-empty=\"true\">Add a label and link to publish this call-to-action.</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ctaColorClass picks the modifier class for the four-color CTA variants.
func ctaColorClass(variant string) string {
switch variant {
case "red":
return "kg-pill-red"
case "blue":
return "kg-pill-blue"
case "green":
return "kg-pill-green"
default:
return "kg-pill-yellow"
}
}
var _ = templruntime.GeneratedTemplate

31
button_override.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"bytes"
"context"
)
// KindergartenButtonBlock renders a pill (border-radius 9999px) button with
// the sticker drop-shadow effect from spec §9.
//
// Content shape (matches built-in button): {"text": "...", "href": "...", "variant": "..."}.
func KindergartenButtonBlock(ctx context.Context, content map[string]any) string {
label := getString(content, "text")
if label == "" {
// Some installs may store the label under "label" instead.
label = getString(content, "label")
}
href := getStringDefault(content, "href", "#")
variant := getStringDefault(content, "variant", "yellow")
switch variant {
case "red", "blue", "yellow", "green":
default:
variant = "yellow"
}
data := BigCTAData{Label: label, Href: href, ColorVariant: variant}
var buf bytes.Buffer
_ = kgButtonComponent(data).Render(ctx, &buf)
return buf.String()
}

10
button_override.templ Normal file
View File

@ -0,0 +1,10 @@
package main
// kgButtonComponent renders a pill-shaped button override.
templ kgButtonComponent(data BigCTAData) {
if data.Label != "" {
<a href={ templ.SafeURL(data.Href) } class={ "kg-pill", ctaColorClass(data.ColorVariant) }>{ data.Label }</a>
} else {
<span class="kg-empty" data-empty="true">Add label and link to render this button.</span>
}
}

92
button_override_templ.go Normal file
View File

@ -0,0 +1,92 @@
// 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"
// kgButtonComponent renders a pill-shaped button override.
func kgButtonComponent(data BigCTAData) 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 data.Label != "" {
var templ_7745c5c3_Var2 = []any{"kg-pill", ctaColorClass(data.ColorVariant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 6, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 6, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span class=\"kg-empty\" data-empty=\"true\">Add label and link to render this button.</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

21
card_override.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"bytes"
"context"
)
// KindergartenCardBlock renders a soft cream card with a 4px primary-colored
// top stripe (spec §9).
//
// Content shape: {"title": "...", "body": "...", "class": "..."}. The body
// is rendered as raw HTML so it can host inline formatting.
func KindergartenCardBlock(ctx context.Context, content map[string]any) string {
title := getString(content, "title")
body := getString(content, "body")
class := getString(content, "class")
var buf bytes.Buffer
_ = kgCardComponent(title, body, class).Render(ctx, &buf)
return buf.String()
}

18
card_override.templ Normal file
View File

@ -0,0 +1,18 @@
package main
// kgCardComponent renders the rounded card override.
templ kgCardComponent(title, body, class string) {
<div class={ "kg-card", class }>
if title != "" {
<h3 class="kg-display" style="margin-top: 0; margin-bottom: 1rem; font-size: 1.5rem;">{ title }</h3>
}
if body != "" {
<div class="kg-text">
@templ.Raw(body)
</div>
}
if title == "" && body == "" {
<div data-empty="true" class="kg-empty">Add a title or body to fill this card.</div>
}
</div>
}

102
card_override_templ.go Normal file
View File

@ -0,0 +1,102 @@
// 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"
// kgCardComponent renders the rounded card override.
func kgCardComponent(title, body, class string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"kg-card", class}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h3 class=\"kg-display\" style=\"margin-top: 0; margin-bottom: 1rem; font-size: 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 7, Col: 96}
}
_, 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, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if body != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"kg-text\">")
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, 6, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if title == "" && body == "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div data-empty=\"true\" class=\"kg-empty\">Add a title or body to fill this card.</div>")
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

49
email_wrapper.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/templates"
)
// KindergartenEmailWrapper wraps email body content in a Kindergarten-branded
// rounded card. Uses a centered 600px table with a 32px border-radius card for
// clients that support it; Outlook degrades gracefully to a square card.
func KindergartenEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = kindergartenEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// kgEmailColor returns the supplied hex color or the cream/foreground/etc.
// fallback when the engine has not resolved a value for the token.
func kgEmailColor(value, fallback string) string {
if value != "" {
return value
}
return fallback
}
// kgEmailBg / etc. group fallbacks for the cream/navy palette per spec §10.
func kgEmailBg(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.Background, "#fdf9ed")
}
func kgEmailCard(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.Card, "#ffffff")
}
func kgEmailFg(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.Foreground, "#1a253b")
}
func kgEmailMuted(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.MutedForeground, "#5a6480")
}
func kgEmailAccent(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.Primary, "#fbcd1f")
}
func kgEmailAccentFg(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.PrimaryForeground, "#1a253b")
}
func kgEmailBorder(ctx templates.EmailContext) string {
return kgEmailColor(ctx.Colors.Border, "#e6d9b3")
}

108
email_wrapper.templ Normal file
View File

@ -0,0 +1,108 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// kindergartenEmailTemplate is the Kindergarten-branded email wrapper. Renders
// a centered 600px table with a 32px-radius card. Outlook degrades to square.
templ kindergartenEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
h1, h2, h3, h4, h5, h6 { font-family: 'Quicksand', 'Nunito', -apple-system, sans-serif; font-weight: 700; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Nunito', 'Quicksand', -apple-system, sans-serif;", kgEmailBg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
{ emailCtx.PreviewText }
</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 48px 10px; background-color: %s;", kgEmailBg(emailCtx)) }>
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s; border-radius: 32px;", kgEmailCard(emailCtx), kgEmailBorder(emailCtx)) }>
<tr>
<td align="center" style={ fmt.Sprintf("padding: 32px 40px; background-color: %s; border-bottom: 2px dashed %s; border-radius: 32px 32px 0 0;", kgEmailCard(emailCtx), kgEmailBorder(emailCtx)) }>
if emailCtx.SiteSettings.LogoURL != "" {
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 48px; width: auto; display: block;"/>
} else if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; font-size: 28px; font-weight: 700; color: %s; letter-spacing: -0.5px; font-family: 'Quicksand', 'Nunito', sans-serif;", kgEmailFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<tr>
<td class="content-padding" style={ fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: 'Nunito', sans-serif;", kgEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<tr>
<td align="center" style={ fmt.Sprintf("padding: 0 48px 32px 48px; background-color: %s;", kgEmailCard(emailCtx)) }>
if emailCtx.SiteSettings.SiteURL != "" {
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("display: inline-block; padding: 16px 32px; border-radius: 9999px; background-color: %s; color: %s; text-decoration: none; font-family: 'Quicksand', sans-serif; font-weight: 700; font-size: 16px;", kgEmailAccent(emailCtx), kgEmailAccentFg(emailCtx)) }>
Visit { emailCtx.SiteSettings.SiteName }
</a>
}
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 32px 48px; border-top: 2px dashed %s; border-radius: 0 0 32px 32px; background-color: %s;", kgEmailBorder(emailCtx), kgEmailCard(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center">
<p style={ fmt.Sprintf("margin: 0 0 8px; font-size: 14px; font-weight: 700; color: %s; font-family: 'Quicksand', sans-serif;", kgEmailFg(emailCtx)) }>
👋 { emailCtx.SiteSettings.SiteName }
</p>
if emailCtx.SiteSettings.SupportEmail != "" {
<p style={ fmt.Sprintf("margin: 0 0 8px; font-size: 13px; color: %s;", kgEmailMuted(emailCtx)) }>
Need a hand? { emailCtx.SiteSettings.SupportEmail }
</p>
}
if emailCtx.UnsubscribeURL != "" {
<p style={ fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", kgEmailMuted(emailCtx)) }>
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", kgEmailMuted(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

406
email_wrapper_templ.go Normal file
View File

@ -0,0 +1,406 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// kindergartenEmailTemplate is the Kindergarten-branded email wrapper. Renders
// a centered 600px table with a 32px-radius card. Outlook degrades to square.
func kindergartenEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><!--[if mso]>\n\t\t\t<noscript>\n\t\t\t\t<xml>\n\t\t\t\t\t<o:OfficeDocumentSettings>\n\t\t\t\t\t\t<o:PixelsPerInch>96</o:PixelsPerInch>\n\t\t\t\t\t</o:OfficeDocumentSettings>\n\t\t\t\t</xml>\n\t\t\t</noscript>\n\t\t\t<![endif]--><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: 28, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }\n\t\t\t\ttable, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }\n\t\t\t\timg { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\ta[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }\n\t\t\t\th1, h2, h3, h4, h5, h6 { font-family: 'Quicksand', 'Nunito', -apple-system, sans-serif; font-weight: 700; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.content-padding { padding-left: 24px !important; padding-right: 24px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Nunito', 'Quicksand', -apple-system, sans-serif;", kgEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 42, Col: 159}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
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: 45, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 48px 10px; background-color: %s;", kgEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 50, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><table role=\"presentation\" class=\"email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s; border-radius: 32px;", kgEmailCard(emailCtx), kgEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 51, Col: 265}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 40px; background-color: %s; border-bottom: 2px dashed %s; border-radius: 32px 32px 0 0;", kgEmailCard(emailCtx), kgEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 53, Col: 199}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.LogoURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 55, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 55, Col: 89}
}
_, 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, 12, "\" style=\"max-height: 48px; width: auto; display: block;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h1 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("margin: 0; font-size: 28px; font-weight: 700; color: %s; letter-spacing: -0.5px; font-family: 'Quicksand', 'Nunito', sans-serif;", kgEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 57, Col: 186}
}
_, 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, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, 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: 58, Col: 43}
}
_, 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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td></tr><tr><td class=\"content-padding\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: 'Nunito', sans-serif;", kgEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 64, Col: 180}
}
_, 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, 17, "\">")
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, 18, "</td></tr><tr><td align=\"center\" 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: 0 48px 32px 48px; background-color: %s;", kgEmailCard(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 69, Col: 121}
}
_, 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, 19, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 templ.SafeURL
templ_7745c5c3_Var14, 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: 71, Col: 64}
}
_, 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, 21, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("display: inline-block; padding: 16px 32px; border-radius: 9999px; background-color: %s; color: %s; text-decoration: none; font-family: 'Quicksand', sans-serif; font-weight: 700; font-size: 16px;", kgEmailAccent(emailCtx), kgEmailAccentFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 71, Col: 336}
}
_, 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, 22, "\">Visit ")
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: 72, Col: 49}
}
_, 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, 23, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td></tr><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 48px; border-top: 2px dashed %s; border-radius: 0 0 32px 32px; background-color: %s;", kgEmailBorder(emailCtx), kgEmailCard(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 78, Col: 181}
}
_, 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, 25, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\"><p 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("margin: 0 0 8px; font-size: 14px; font-weight: 700; color: %s; font-family: 'Quicksand', sans-serif;", kgEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 82, Col: 159}
}
_, 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, 26, "\">👋 ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, 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: 83, Col: 50}
}
_, 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, 27, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SupportEmail != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 8px; font-size: 13px; color: %s;", kgEmailMuted(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 86, Col: 107}
}
_, 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, 29, "\">Need a hand? ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SupportEmail)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 87, Col: 63}
}
_, 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, 30, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", kgEmailMuted(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 91, Col: 101}
}
_, 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, 32, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 templ.SafeURL
templ_7745c5c3_Var23, 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: 92, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", kgEmailMuted(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 92, Col: 145}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</td></tr></table></td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

64
embed.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler returns an http.Handler that serves the embedded assets.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded theme presets JSON.
func ThemePresets() []byte {
return presetsData
}
// BundledFonts returns the embedded fonts manifest JSON.
func BundledFonts() []byte {
return fontsData
}
// ThemeCSSManifest returns the CSS manifest used by host Tailwind to pick up
// theme-owned utility classes (crayon underline, dotted-paper background,
// sticker drop-shadow, font-family fallback stacks, etc.).
func ThemeCSSManifest() *plugin.CSSManifest {
css, err := assetsFS.ReadFile("assets/style.css")
if err != nil {
return &plugin.CSSManifest{}
}
return &plugin.CSSManifest{
InputCSSAppend: string(css),
}
}

1
fonts.json Normal file
View File

@ -0,0 +1 @@
[]

53
footer.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FooterBlockMeta defines metadata for the friendly footer block.
var FooterBlockMeta = blocks.BlockMeta{
Key: "footer",
Title: "Friendly Footer",
Description: "Mascot wave, signup, contact, crayon-rule divider.",
Category: blocks.CategoryLayout,
Source: "kindergarten",
}
// FooterSocial is a single social link entry.
type FooterSocial struct {
Platform string
Href string
}
// FooterData is the renderer input.
type FooterData struct {
ShowSignup bool
MascotName string
SocialLinks []FooterSocial
}
// FooterBlock renders the friendly footer.
// Content shape: {showSignup,mascotName,socialLinks}.
func FooterBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "socialLinks")
links := make([]FooterSocial, 0, len(raw))
for _, m := range raw {
links = append(links, FooterSocial{
Platform: getString(m, "platform"),
Href: getString(m, "href"),
})
}
data := FooterData{
ShowSignup: getBool(content, "showSignup", true),
MascotName: getStringDefault(content, "mascotName", "Pip"),
SocialLinks: links,
}
var buf bytes.Buffer
_ = footerComponent(data).Render(ctx, &buf)
return buf.String()
}

64
footer.templ Normal file
View File

@ -0,0 +1,64 @@
package main
// footerComponent renders the friendly footer.
templ footerComponent(data FooterData) {
<div class="kg-footer" data-block="kindergarten:footer">
<div class="kg-container">
<div class="kg-footer-mascot">
<span aria-hidden="true">👋</span>
<span>{ data.MascotName } says hi!</span>
</div>
if data.ShowSignup {
<form class="kg-footer-signup" aria-label={ "Newsletter signup" } method="post" action="/subscribe">
<input type="email" name="email" placeholder="Your email" aria-label="Email address" required />
<button type="submit" class="kg-pill kg-pill-yellow">Sign up</button>
</form>
}
if len(data.SocialLinks) > 0 {
<div class="kg-social-row" aria-label={ "Social media" }>
for _, link := range data.SocialLinks {
<a href={ templ.SafeURL(link.Href) } class="kg-social-link" aria-label={ socialLabel(link.Platform) }>
@socialIcon(link.Platform)
</a>
}
</div>
}
</div>
</div>
}
// socialLabel renders an accessible label for the icon link.
func socialLabel(platform string) string {
switch platform {
case "facebook":
return "Visit our Facebook"
case "instagram":
return "Visit our Instagram"
case "youtube":
return "Visit our YouTube"
case "tiktok":
return "Visit our TikTok"
case "email":
return "Send us an email"
default:
return "Open social link"
}
}
// socialIcon renders a small social icon (Lucide-style stroke).
templ socialIcon(platform string) {
switch platform {
case "facebook":
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg>
case "instagram":
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/></svg>
case "youtube":
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"/><polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"/></svg>
case "tiktok":
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5"/></svg>
case "email":
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
default:
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>
}
}

216
footer_templ.go Normal file
View File

@ -0,0 +1,216 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// footerComponent renders the friendly footer.
func footerComponent(data FooterData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"kg-footer\" data-block=\"kindergarten:footer\"><div class=\"kg-container\"><div class=\"kg-footer-mascot\"><span aria-hidden=\"true\">👋</span> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.MascotName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 9, Col: 27}
}
_, 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, " says hi!</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowSignup {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<form class=\"kg-footer-signup\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue("Newsletter signup")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 12, Col: 67}
}
_, 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, "\" method=\"post\" action=\"/subscribe\"><input type=\"email\" name=\"email\" placeholder=\"Your email\" aria-label=\"Email address\" required> <button type=\"submit\" class=\"kg-pill kg-pill-yellow\">Sign up</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.SocialLinks) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"kg-social-row\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue("Social media")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 18, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range data.SocialLinks {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<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(link.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 20, Col: 40}
}
_, 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, "\" class=\"kg-social-link\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(socialLabel(link.Platform))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 20, Col: 105}
}
_, 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, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = socialIcon(link.Platform).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// socialLabel renders an accessible label for the icon link.
func socialLabel(platform string) string {
switch platform {
case "facebook":
return "Visit our Facebook"
case "instagram":
return "Visit our Instagram"
case "youtube":
return "Visit our YouTube"
case "tiktok":
return "Visit our TikTok"
case "email":
return "Send us an email"
default:
return "Open social link"
}
}
// socialIcon renders a small social icon (Lucide-style stroke).
func socialIcon(platform 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_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch platform {
case "facebook":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "instagram":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"5\" ry=\"5\"></rect><path d=\"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z\"></path><line x1=\"17.5\" y1=\"6.5\" x2=\"17.51\" y2=\"6.5\"></line></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "youtube":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z\"></path><polygon points=\"9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02\"></polygon></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "tiktok":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "email":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\"></path><polyline points=\"22,6 12,13 2,6\"></polyline></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

57
gallery_of_art.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// GalleryOfArtBlockMeta defines metadata for the gallery of art block.
var GalleryOfArtBlockMeta = blocks.BlockMeta{
Key: "gallery_of_art",
Title: "Gallery of Art",
Description: "Polaroid-style grid for kid artwork.",
Category: blocks.CategoryContent,
Source: "kindergarten",
}
// GalleryArt represents a single artwork entry.
type GalleryArt struct {
Image string
ChildName string
Age int
}
// GalleryData is the renderer input.
type GalleryData struct {
Title string
ShowChildName bool
Items []GalleryArt
}
// GalleryOfArtBlock renders the polaroid grid.
// Content shape: {title,showChildName,items:[{image,childName,age}]}.
//
// Privacy: when showChildName=false, no child names are rendered.
func GalleryOfArtBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "items")
items := make([]GalleryArt, 0, len(raw))
for _, m := range raw {
items = append(items, GalleryArt{
Image: getString(m, "image"),
ChildName: getString(m, "childName"),
Age: getInt(m, "age", 0),
})
}
data := GalleryData{
Title: getString(content, "title"),
ShowChildName: getBool(content, "showChildName", true),
Items: items,
}
var buf bytes.Buffer
_ = galleryOfArtComponent(data).Render(ctx, &buf)
return buf.String()
}

52
gallery_of_art.templ Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/blocks"
)
// galleryOfArtComponent renders a polaroid-style grid of artworks.
templ galleryOfArtComponent(data GalleryData) {
<section class="kg-section" data-block="kindergarten:gallery_of_art">
<div class="kg-container">
if data.Title != "" {
<h2 class="kg-display kg-crayon-underline" style="font-size: 2rem; margin-bottom: 1.5rem;">{ data.Title }</h2>
}
if len(data.Items) == 0 {
<div class="kg-empty" data-empty="true">No artworks yet — add one to fill the gallery wall.</div>
} else {
<div class="kg-gallery-grid">
for _, art := range data.Items {
<figure class="kg-polaroid">
if art.Image != "" {
<img src={ blocks.ResolveMediaPath(art.Image) } alt={ polaroidAlt(art, data.ShowChildName) } />
} else {
<span class="kg-polaroid-image" role="img" aria-label="Placeholder for child artwork"></span>
}
<figcaption class="kg-polaroid-caption">
if data.ShowChildName && art.ChildName != "" {
<span>{ art.ChildName }</span>
}
if data.ShowChildName && art.ChildName != "" && art.Age > 0 {
{ ", " }
}
if art.Age > 0 {
<span>{ fmt.Sprintf("age %d", art.Age) }</span>
}
</figcaption>
</figure>
}
</div>
}
</div>
</section>
}
// polaroidAlt produces a respectful alt-text that honours the privacy toggle.
func polaroidAlt(art GalleryArt, showChildName bool) string {
if showChildName && art.ChildName != "" {
return fmt.Sprintf("Artwork by %s", art.ChildName)
}
return "Children's artwork"
}

197
gallery_of_art_templ.go Normal file
View File

@ -0,0 +1,197 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"git.dev.alexdunmow.com/block/core/blocks"
)
// galleryOfArtComponent renders a polaroid-style grid of artworks.
func galleryOfArtComponent(data GalleryData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"kg-section\" data-block=\"kindergarten:gallery_of_art\"><div class=\"kg-container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"kg-display kg-crayon-underline\" style=\"font-size: 2rem; margin-bottom: 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_of_art.templ`, Line: 14, Col: 107}
}
_, 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, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Items) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"kg-empty\" data-empty=\"true\">No artworks yet — add one to fill the gallery wall.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"kg-gallery-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, art := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<figure class=\"kg-polaroid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if art.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(blocks.ResolveMediaPath(art.Image))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_of_art.templ`, Line: 23, Col: 53}
}
_, 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, 8, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(polaroidAlt(art, data.ShowChildName))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_of_art.templ`, Line: 23, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"kg-polaroid-image\" role=\"img\" aria-label=\"Placeholder for child artwork\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<figcaption class=\"kg-polaroid-caption\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowChildName && art.ChildName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(art.ChildName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_of_art.templ`, Line: 29, Col: 30}
}
_, 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, 13, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowChildName && art.ChildName != "" && art.Age > 0 {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(", ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_of_art.templ`, Line: 32, Col: 15}
}
_, 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, 14, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if art.Age > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("age %d", art.Age))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `gallery_of_art.templ`, Line: 35, Col: 47}
}
_, 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, 16, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</figcaption></figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// polaroidAlt produces a respectful alt-text that honours the privacy toggle.
func polaroidAlt(art GalleryArt, showChildName bool) string {
if showChildName && art.ChildName != "" {
return fmt.Sprintf("Artwork by %s", art.ChildName)
}
return "Children's artwork"
}
var _ = templruntime.GeneratedTemplate

20
go.mod Normal file
View File

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

42
go.sum Normal file
View File

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

65
heading_override.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"bytes"
"context"
"strconv"
)
// KindergartenHeadingBlock renders a heading with the Kindergarten crayon-underline
// accent and an optional numeral step badge.
//
// Content expects: {"text": "...", "level": 1-6, "textClass": "...", "step": 1-9}
func KindergartenHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
level := parseHeadingLevel(content)
step := parseStep(content)
var buf bytes.Buffer
_ = kgHeadingComponent(level, text, textClass, step).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses the level from content, defaulting to 2.
func parseHeadingLevel(content map[string]any) int {
if level, ok := content["level"].(float64); ok {
l := int(level)
if l >= 1 && l <= 6 {
return l
}
}
if level, ok := content["level"].(int); ok {
if level >= 1 && level <= 6 {
return level
}
}
if level, ok := content["level"].(string); ok {
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
return l
}
}
return 2
}
// parseStep parses an optional step badge (1-9). Returns 0 if not provided
// or out of range; renderer skips the badge when step == 0.
func parseStep(content map[string]any) int {
if v, ok := content["step"].(float64); ok {
s := int(v)
if s >= 1 && s <= 9 {
return s
}
}
if v, ok := content["step"].(int); ok {
if v >= 1 && v <= 9 {
return v
}
}
if v, ok := content["step"].(string); ok {
if s, err := strconv.Atoi(v); err == nil && s >= 1 && s <= 9 {
return s
}
}
return 0
}

57
heading_override.templ Normal file
View File

@ -0,0 +1,57 @@
package main
import "fmt"
// kgHeadingBaseClass returns size classes per heading level (Tailwind-like
// utilities scoped to the kindergarten CSS).
func kgHeadingBaseClass(level int) string {
switch level {
case 1:
return "kg-display kg-crayon-underline"
case 2:
return "kg-display kg-crayon-underline"
case 3:
return "kg-display"
case 4:
return "kg-display"
case 5:
return "kg-display"
case 6:
return "kg-display"
default:
return "kg-display"
}
}
// kgHeadingComponent renders a heading with Kindergarten accents, optionally
// preceded by a circular step badge (e.g. for "Step 1", "Step 2" sections).
templ kgHeadingComponent(level int, text, textClass string, step int) {
if step > 0 {
<div class="kg-heading-stepped">
<span class="kg-heading-step-badge" aria-hidden="true">{ fmt.Sprintf("%d", step) }</span>
@kgHeadingTag(level, text, textClass)
</div>
} else {
@kgHeadingTag(level, text, textClass)
}
}
// kgHeadingTag emits the actual <h1>..<h6> tag at the requested level.
templ kgHeadingTag(level int, text, textClass string) {
switch level {
case 1:
<h1 class={ kgHeadingBaseClass(1), textClass } style="font-size: clamp(2.5rem, 6vw, 4rem); margin: 0;">{ text }</h1>
case 2:
<h2 class={ kgHeadingBaseClass(2), textClass } style="font-size: 2.25rem; margin: 0;">{ text }</h2>
case 3:
<h3 class={ kgHeadingBaseClass(3), textClass } style="font-size: 1.75rem; margin: 0;">{ text }</h3>
case 4:
<h4 class={ kgHeadingBaseClass(4), textClass } style="font-size: 1.5rem; margin: 0;">{ text }</h4>
case 5:
<h5 class={ kgHeadingBaseClass(5), textClass } style="font-size: 1.25rem; margin: 0;">{ text }</h5>
case 6:
<h6 class={ kgHeadingBaseClass(6), textClass } style="font-size: 1.125rem; margin: 0;">{ text }</h6>
default:
<h2 class={ kgHeadingBaseClass(2), textClass } style="font-size: 2.25rem; margin: 0;">{ text }</h2>
}
}

373
heading_override_templ.go Normal file
View File

@ -0,0 +1,373 @@
// 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"
// kgHeadingBaseClass returns size classes per heading level (Tailwind-like
// utilities scoped to the kindergarten CSS).
func kgHeadingBaseClass(level int) string {
switch level {
case 1:
return "kg-display kg-crayon-underline"
case 2:
return "kg-display kg-crayon-underline"
case 3:
return "kg-display"
case 4:
return "kg-display"
case 5:
return "kg-display"
case 6:
return "kg-display"
default:
return "kg-display"
}
}
// kgHeadingComponent renders a heading with Kindergarten accents, optionally
// preceded by a circular step badge (e.g. for "Step 1", "Step 2" sections).
func kgHeadingComponent(level int, text, textClass string, step int) 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 step > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"kg-heading-stepped\"><span class=\"kg-heading-step-badge\" aria-hidden=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", step))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 31, 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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = kgHeadingTag(level, text, textClass).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = kgHeadingTag(level, text, textClass).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
// kgHeadingTag emits the actual <h1>..<h6> tag at the requested level.
func kgHeadingTag(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_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch level {
case 1:
var templ_7745c5c3_Var4 = []any{kgHeadingBaseClass(1), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"font-size: clamp(2.5rem, 6vw, 4rem); margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 43, Col: 112}
}
_, 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, 6, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var7 = []any{kgHeadingBaseClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var7).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_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"font-size: 2.25rem; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 45, Col: 95}
}
_, 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, 9, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var10 = []any{kgHeadingBaseClass(3), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).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_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"font-size: 1.75rem; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 47, Col: 95}
}
_, 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, 12, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var13 = []any{kgHeadingBaseClass(4), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var13).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_Var14)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"font-size: 1.5rem; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 49, Col: 94}
}
_, 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, 15, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var16 = []any{kgHeadingBaseClass(5), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var16).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_Var17)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" style=\"font-size: 1.25rem; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 51, Col: 95}
}
_, 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, 18, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var19 = []any{kgHeadingBaseClass(6), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).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_Var20)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"font-size: 1.125rem; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 53, Col: 96}
}
_, 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, 21, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var22 = []any{kgHeadingBaseClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).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_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" style=\"font-size: 2.25rem; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 55, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

63
helpers.go Normal file
View File

@ -0,0 +1,63 @@
package main
// getString extracts a string value from 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 value from content 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
}
// getBool extracts a bool value from content. Accepts native bool, "true"/"false"
// strings, and the JSON-encoded float forms ("true" via 1, 0). Defaults to def.
func getBool(content map[string]any, key string, def bool) bool {
switch v := content[key].(type) {
case bool:
return v
case string:
switch v {
case "true":
return true
case "false":
return false
}
case float64:
return v != 0
}
return def
}
// getInt extracts an int value from content map (handles float64 from JSON).
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
}
// getSlice extracts a slice of maps from content. If the underlying value is
// not a JSON array of objects (e.g. malformed content provides a bare string),
// the function returns nil so renderers can fall back to their empty state.
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
}

44
mascot_hero.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MascotHeroBlockMeta defines metadata for the mascot hero block.
var MascotHeroBlockMeta = blocks.BlockMeta{
Key: "mascot_hero",
Title: "Mascot Hero",
Description: "Big rounded panel with mascot SVG and primary-colored confetti.",
Category: blocks.CategoryContent,
Source: "kindergarten",
}
// MascotHeroData carries the strongly-typed shape consumed by the templ component.
type MascotHeroData struct {
Mascot string
Headline string
Tagline string
CTALabel string
CTAHref string
BgColor string
}
// MascotHeroBlock renders the mascot hero block from the unstructured content map.
// Content shape: {mascot,headline,tagline,ctaLabel,ctaHref,bgColor}.
func MascotHeroBlock(ctx context.Context, content map[string]any) string {
data := MascotHeroData{
Mascot: getStringDefault(content, "mascot", "pip"),
Headline: getString(content, "headline"),
Tagline: getString(content, "tagline"),
CTALabel: getString(content, "ctaLabel"),
CTAHref: getString(content, "ctaHref"),
BgColor: getString(content, "bgColor"),
}
var buf bytes.Buffer
_ = mascotHeroComponent(data).Render(ctx, &buf)
return buf.String()
}

78
mascot_hero.templ Normal file
View File

@ -0,0 +1,78 @@
package main
// mascotHeroComponent renders the mascot hero panel.
templ mascotHeroComponent(data MascotHeroData) {
<section class="kg-section" data-block="kindergarten:mascot_hero">
<div class="kg-container">
<div class="kg-hero-panel">
<div class="kg-confetti" aria-hidden="true"></div>
<div style="position: relative; z-index: 1; display: grid; gap: 2rem; grid-template-columns: 1fr; align-items: center;" class="kg-mascot-hero-grid">
<div>
if data.Headline != "" {
<h1 class="kg-display kg-crayon-underline" style="font-size: clamp(2.5rem, 6vw, 4rem); margin: 0;">{ data.Headline }</h1>
} else {
<h1 class="kg-display" style="font-size: clamp(2.5rem, 6vw, 4rem); margin: 0;" data-empty="true">Welcome!</h1>
}
if data.Tagline != "" {
<p class="kg-text" style="font-size: 1.25rem; margin-top: 1.5rem;">{ data.Tagline }</p>
}
if data.CTALabel != "" && data.CTAHref != "" {
<div style="margin-top: 2rem;">
<a href={ templ.SafeURL(data.CTAHref) } class="kg-pill">{ data.CTALabel }</a>
</div>
}
</div>
<div style="display: flex; justify-content: center; align-items: center;">
@mascotSVG(data.Mascot)
</div>
</div>
</div>
</div>
</section>
}
// mascotSVG renders a primitive, license-free mascot illustration.
// The SVGs intentionally use `currentColor` (and the theme primary) so they
// inherit colorways from the active preset.
templ mascotSVG(name string) {
switch name {
case "blocks":
<svg viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Alphabet blocks mascot">
<rect x="20" y="60" width="60" height="60" rx="12" fill="hsl(var(--primary))" />
<rect x="90" y="40" width="60" height="60" rx="12" fill="hsl(var(--secondary))" />
<rect x="60" y="120" width="60" height="60" rx="12" fill="hsl(var(--accent))" />
<text x="50" y="98" text-anchor="middle" font-family="var(--font-heading, Quicksand)" font-weight="700" font-size="32" fill="hsl(var(--primary-foreground))">A</text>
<text x="120" y="78" text-anchor="middle" font-family="var(--font-heading, Quicksand)" font-weight="700" font-size="32" fill="hsl(var(--secondary-foreground))">B</text>
<text x="90" y="158" text-anchor="middle" font-family="var(--font-heading, Quicksand)" font-weight="700" font-size="32" fill="hsl(var(--accent-foreground))">C</text>
</svg>
case "star":
<svg viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Friendly star mascot">
<polygon points="100,10 122,75 190,75 135,115 155,180 100,140 45,180 65,115 10,75 78,75" fill="hsl(var(--accent))" stroke="hsl(var(--foreground))" stroke-width="4" stroke-linejoin="round" />
<circle cx="80" cy="95" r="6" fill="hsl(var(--foreground))" />
<circle cx="120" cy="95" r="6" fill="hsl(var(--foreground))" />
<path d="M 80 120 Q 100 135 120 120" stroke="hsl(var(--foreground))" stroke-width="4" fill="none" stroke-linecap="round" />
</svg>
case "balloon":
<svg viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Balloon mascot">
<ellipse cx="100" cy="80" rx="50" ry="60" fill="hsl(var(--primary))" stroke="hsl(var(--foreground))" stroke-width="4" />
<path d="M 100 140 L 95 150 L 100 145 L 105 150 Z" fill="hsl(var(--foreground))" />
<path d="M 100 150 Q 90 175 100 195" stroke="hsl(var(--foreground))" stroke-width="2" fill="none" />
<circle cx="85" cy="65" r="6" fill="hsl(var(--foreground))" />
<circle cx="115" cy="65" r="6" fill="hsl(var(--foreground))" />
<path d="M 85 90 Q 100 105 115 90" stroke="hsl(var(--foreground))" stroke-width="4" fill="none" stroke-linecap="round" />
</svg>
default:
<svg viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Pip the mascot">
<circle cx="100" cy="115" r="70" fill="hsl(var(--accent))" stroke="hsl(var(--foreground))" stroke-width="4" />
<circle cx="78" cy="100" r="8" fill="hsl(var(--foreground))" />
<circle cx="122" cy="100" r="8" fill="hsl(var(--foreground))" />
<circle cx="78" cy="98" r="3" fill="hsl(var(--background))" />
<circle cx="122" cy="98" r="3" fill="hsl(var(--background))" />
<path d="M 75 130 Q 100 155 125 130" stroke="hsl(var(--foreground))" stroke-width="5" fill="none" stroke-linecap="round" />
<circle cx="60" cy="115" r="6" fill="hsl(var(--secondary))" />
<circle cx="140" cy="115" r="6" fill="hsl(var(--secondary))" />
<path d="M 50 60 L 60 35 L 80 50 Z" fill="hsl(var(--primary))" />
<path d="M 150 60 L 140 35 L 120 50 Z" fill="hsl(var(--primary))" />
</svg>
}
}

178
mascot_hero_templ.go Normal file
View File

@ -0,0 +1,178 @@
// 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"
// mascotHeroComponent renders the mascot hero panel.
func mascotHeroComponent(data MascotHeroData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"kg-section\" data-block=\"kindergarten:mascot_hero\"><div class=\"kg-container\"><div class=\"kg-hero-panel\"><div class=\"kg-confetti\" aria-hidden=\"true\"></div><div style=\"position: relative; z-index: 1; display: grid; gap: 2rem; grid-template-columns: 1fr; align-items: center;\" class=\"kg-mascot-hero-grid\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Headline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h1 class=\"kg-display kg-crayon-underline\" style=\"font-size: clamp(2.5rem, 6vw, 4rem); margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Headline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mascot_hero.templ`, Line: 12, Col: 121}
}
_, 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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1 class=\"kg-display\" style=\"font-size: clamp(2.5rem, 6vw, 4rem); margin: 0;\" data-empty=\"true\">Welcome!</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Tagline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"kg-text\" style=\"font-size: 1.25rem; margin-top: 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Tagline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mascot_hero.templ`, Line: 17, Col: 88}
}
_, 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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.CTALabel != "" && data.CTAHref != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div style=\"margin-top: 2rem;\"><a 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(data.CTAHref))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mascot_hero.templ`, Line: 21, Col: 45}
}
_, 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, 8, "\" class=\"kg-pill\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.CTALabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `mascot_hero.templ`, Line: 21, Col: 79}
}
_, 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, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div style=\"display: flex; justify-content: center; align-items: center;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = mascotSVG(data.Mascot).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// mascotSVG renders a primitive, license-free mascot illustration.
// The SVGs intentionally use `currentColor` (and the theme primary) so they
// inherit colorways from the active preset.
func mascotSVG(name 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_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch name {
case "blocks":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<svg viewBox=\"0 0 200 200\" width=\"200\" height=\"200\" role=\"img\" aria-label=\"Alphabet blocks mascot\"><rect x=\"20\" y=\"60\" width=\"60\" height=\"60\" rx=\"12\" fill=\"hsl(var(--primary))\"></rect> <rect x=\"90\" y=\"40\" width=\"60\" height=\"60\" rx=\"12\" fill=\"hsl(var(--secondary))\"></rect> <rect x=\"60\" y=\"120\" width=\"60\" height=\"60\" rx=\"12\" fill=\"hsl(var(--accent))\"></rect> <text x=\"50\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--font-heading, Quicksand)\" font-weight=\"700\" font-size=\"32\" fill=\"hsl(var(--primary-foreground))\">A</text> <text x=\"120\" y=\"78\" text-anchor=\"middle\" font-family=\"var(--font-heading, Quicksand)\" font-weight=\"700\" font-size=\"32\" fill=\"hsl(var(--secondary-foreground))\">B</text> <text x=\"90\" y=\"158\" text-anchor=\"middle\" font-family=\"var(--font-heading, Quicksand)\" font-weight=\"700\" font-size=\"32\" fill=\"hsl(var(--accent-foreground))\">C</text></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "star":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg viewBox=\"0 0 200 200\" width=\"200\" height=\"200\" role=\"img\" aria-label=\"Friendly star mascot\"><polygon points=\"100,10 122,75 190,75 135,115 155,180 100,140 45,180 65,115 10,75 78,75\" fill=\"hsl(var(--accent))\" stroke=\"hsl(var(--foreground))\" stroke-width=\"4\" stroke-linejoin=\"round\"></polygon> <circle cx=\"80\" cy=\"95\" r=\"6\" fill=\"hsl(var(--foreground))\"></circle> <circle cx=\"120\" cy=\"95\" r=\"6\" fill=\"hsl(var(--foreground))\"></circle> <path d=\"M 80 120 Q 100 135 120 120\" stroke=\"hsl(var(--foreground))\" stroke-width=\"4\" fill=\"none\" stroke-linecap=\"round\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "balloon":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<svg viewBox=\"0 0 200 200\" width=\"200\" height=\"200\" role=\"img\" aria-label=\"Balloon mascot\"><ellipse cx=\"100\" cy=\"80\" rx=\"50\" ry=\"60\" fill=\"hsl(var(--primary))\" stroke=\"hsl(var(--foreground))\" stroke-width=\"4\"></ellipse> <path d=\"M 100 140 L 95 150 L 100 145 L 105 150 Z\" fill=\"hsl(var(--foreground))\"></path> <path d=\"M 100 150 Q 90 175 100 195\" stroke=\"hsl(var(--foreground))\" stroke-width=\"2\" fill=\"none\"></path> <circle cx=\"85\" cy=\"65\" r=\"6\" fill=\"hsl(var(--foreground))\"></circle> <circle cx=\"115\" cy=\"65\" r=\"6\" fill=\"hsl(var(--foreground))\"></circle> <path d=\"M 85 90 Q 100 105 115 90\" stroke=\"hsl(var(--foreground))\" stroke-width=\"4\" fill=\"none\" stroke-linecap=\"round\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg viewBox=\"0 0 200 200\" width=\"200\" height=\"200\" role=\"img\" aria-label=\"Pip the mascot\"><circle cx=\"100\" cy=\"115\" r=\"70\" fill=\"hsl(var(--accent))\" stroke=\"hsl(var(--foreground))\" stroke-width=\"4\"></circle> <circle cx=\"78\" cy=\"100\" r=\"8\" fill=\"hsl(var(--foreground))\"></circle> <circle cx=\"122\" cy=\"100\" r=\"8\" fill=\"hsl(var(--foreground))\"></circle> <circle cx=\"78\" cy=\"98\" r=\"3\" fill=\"hsl(var(--background))\"></circle> <circle cx=\"122\" cy=\"98\" r=\"3\" fill=\"hsl(var(--background))\"></circle> <path d=\"M 75 130 Q 100 155 125 130\" stroke=\"hsl(var(--foreground))\" stroke-width=\"5\" fill=\"none\" stroke-linecap=\"round\"></path> <circle cx=\"60\" cy=\"115\" r=\"6\" fill=\"hsl(var(--secondary))\"></circle> <circle cx=\"140\" cy=\"115\" r=\"6\" fill=\"hsl(var(--secondary))\"></circle> <path d=\"M 50 60 L 60 35 L 80 50 Z\" fill=\"hsl(var(--primary))\"></path> <path d=\"M 150 60 L 140 35 L 120 50 Z\" fill=\"hsl(var(--primary))\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

47
numbers_counter.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// NumbersCounterBlockMeta defines metadata for the numbers counter block.
var NumbersCounterBlockMeta = blocks.BlockMeta{
Key: "numbers_counter",
Title: "Numbers Counter",
Description: "Big circular numeral badges for stats (e.g. '12 teachers').",
Category: blocks.CategoryContent,
Source: "kindergarten",
}
// NumberItem is a single numeral + label pair.
type NumberItem struct {
Number int
Label string
}
// NumbersData is the renderer input.
type NumbersData struct {
Items []NumberItem
}
// NumbersCounterBlock renders the round numeral grid.
// Content shape: {items:[{number,label}]}.
func NumbersCounterBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "items")
items := make([]NumberItem, 0, len(raw))
for _, m := range raw {
items = append(items, NumberItem{
Number: getInt(m, "number", 0),
Label: getString(m, "label"),
})
}
data := NumbersData{Items: items}
var buf bytes.Buffer
_ = numbersCounterComponent(data).Render(ctx, &buf)
return buf.String()
}

25
numbers_counter.templ Normal file
View File

@ -0,0 +1,25 @@
package main
import "fmt"
// numbersCounterComponent renders circular numeral badges.
templ numbersCounterComponent(data NumbersData) {
<section class="kg-section" data-block="kindergarten:numbers_counter">
<div class="kg-container">
if len(data.Items) == 0 {
<div class="kg-empty" data-empty="true">No numbers yet — add a few stats to celebrate.</div>
} else {
<div class="kg-numbers-grid">
for _, item := range data.Items {
<div class="kg-numbers-item">
<span class="kg-numeral" data-number={ fmt.Sprintf("%d", item.Number) }>{ fmt.Sprintf("%d", item.Number) }</span>
if item.Label != "" {
<span class="kg-numbers-label">{ item.Label }</span>
}
</div>
}
</div>
}
</div>
</section>
}

117
numbers_counter_templ.go Normal file
View 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 "fmt"
// numbersCounterComponent renders circular numeral badges.
func numbersCounterComponent(data NumbersData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"kg-section\" data-block=\"kindergarten:numbers_counter\"><div class=\"kg-container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Items) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"kg-empty\" data-empty=\"true\">No numbers yet — add a few stats to celebrate.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"kg-numbers-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"kg-numbers-item\"><span class=\"kg-numeral\" data-number=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", item.Number))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `numbers_counter.templ`, Line: 15, Col: 76}
}
_, 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, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Number))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `numbers_counter.templ`, Line: 15, Col: 111}
}
_, 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, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Label != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"kg-numbers-label\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `numbers_counter.templ`, Line: 17, Col: 51}
}
_, 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, 8, "</span>")
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
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

12
plugin.mod Normal file
View File

@ -0,0 +1,12 @@
[plugin]
name = "kindergarten"
display_name = "Kindergarten"
scope = "@themes"
version = "0.1.0"
description = "Primary-colored, hand-lettered theme for schools, daycares, toy shops, museums, and kids' publishers."
kind = "theme"
categories = ["templates"]
tags = ["kids", "education", "playful", "family", "school", "museum", "friendly", "daycare", "publisher"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

110
presets.json Normal file
View File

@ -0,0 +1,110 @@
[
{
"id": "recess",
"name": "Crayon Recess",
"description": "Light, sunny, primary-on-cream — for daytime classrooms.",
"theme": {
"lightColors": {
"background": "48 60% 97%",
"foreground": "220 40% 18%",
"card": "0 0% 100%",
"cardForeground": "220 40% 18%",
"popover": "0 0% 100%",
"popoverForeground": "220 40% 18%",
"primary": "4 80% 56%",
"primaryForeground": "0 0% 100%",
"secondary": "220 75% 56%",
"secondaryForeground": "0 0% 100%",
"muted": "48 40% 92%",
"mutedForeground": "220 25% 38%",
"accent": "48 95% 58%",
"accentForeground": "220 50% 14%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "220 30% 86%",
"input": "48 30% 90%",
"ring": "4 80% 56%"
},
"mode": "light"
}
},
{
"id": "chalkboard",
"name": "Chalkboard Night",
"description": "Deep navy chalkboard with crayon-bright accents.",
"theme": {
"darkColors": {
"background": "220 50% 10%",
"foreground": "48 60% 96%",
"card": "220 45% 14%",
"cardForeground": "48 60% 96%",
"popover": "220 45% 14%",
"popoverForeground": "48 60% 96%",
"primary": "48 95% 58%",
"primaryForeground": "220 50% 10%",
"secondary": "4 80% 60%",
"secondaryForeground": "0 0% 100%",
"muted": "220 35% 18%",
"mutedForeground": "220 20% 70%",
"accent": "145 70% 50%",
"accentForeground": "220 50% 10%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "220 35% 24%",
"input": "220 35% 20%",
"ring": "48 95% 58%"
},
"mode": "dark"
}
},
{
"id": "crayon-box",
"name": "Crayon Box",
"description": "Full primary quartet — red, blue, yellow, green — in both light and dark.",
"theme": {
"lightColors": {
"background": "48 55% 98%",
"foreground": "220 45% 15%",
"card": "0 0% 100%",
"cardForeground": "220 45% 15%",
"popover": "0 0% 100%",
"popoverForeground": "220 45% 15%",
"primary": "220 78% 52%",
"primaryForeground": "0 0% 100%",
"secondary": "4 78% 56%",
"secondaryForeground": "0 0% 100%",
"muted": "48 35% 93%",
"mutedForeground": "220 22% 38%",
"accent": "145 65% 45%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "220 25% 85%",
"input": "48 25% 88%",
"ring": "48 95% 55%"
},
"darkColors": {
"background": "220 40% 8%",
"foreground": "48 55% 95%",
"card": "220 40% 12%",
"cardForeground": "48 55% 95%",
"popover": "220 40% 12%",
"popoverForeground": "48 55% 95%",
"primary": "220 80% 60%",
"primaryForeground": "0 0% 100%",
"secondary": "4 80% 58%",
"secondaryForeground": "0 0% 100%",
"muted": "220 30% 16%",
"mutedForeground": "220 18% 68%",
"accent": "145 70% 50%",
"accentForeground": "220 40% 8%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "220 30% 22%",
"input": "220 30% 18%",
"ring": "48 95% 60%"
},
"mode": "both"
}
}
]

181
register.go Normal file
View 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 satisfies 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 wires the Kindergarten theme into the host registries. The call
// order matters: the system template + page templates are registered first,
// then schemas are loaded before any block registrations so the schema
// bindings resolve.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "kindergarten",
Title: "Kindergarten",
Description: "Primary-colored, hand-lettered theme for schools, daycares, toy shops, museums, and kids' publishers.",
})
if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard rounded layout with header, main, footer",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderKindergarten)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{
Key: "landing",
Title: "Landing",
Description: "Mascot-led hero plus stacked feature sections",
Slots: []string{"hero", "main", "cta", "footer"},
}, wrap(RenderKindergartenLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{
Key: "article",
Title: "Story Page",
Description: "Picture-book reading layout for blog posts and stories",
Slots: []string{"header", "main", "aside", "footer"},
}, wrap(RenderKindergartenArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge gallery / classroom showcase",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderKindergartenFullWidth)); err != nil {
return err
}
// Schemas must be loaded before block registrations so each Register()
// call can bind to its declared schema.
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
// Theme-owned blocks (eight per spec §8). Registered unqualified; the host
// addresses them as "kindergarten:<key>" downstream.
br.Register(MascotHeroBlockMeta, MascotHeroBlock)
br.Register(AlphabetStripBlockMeta, AlphabetStripBlock)
br.Register(ScheduleBlockMeta, ScheduleBlock)
br.Register(GalleryOfArtBlockMeta, GalleryOfArtBlock)
br.Register(NumbersCounterBlockMeta, NumbersCounterBlock)
br.Register(StorybookQuoteBlockMeta, StorybookQuoteBlock)
br.Register(BigCTABlockMeta, BigCTABlock)
br.Register(FooterBlockMeta, FooterBlock)
// Built-in overrides (spec §9). Each is scoped to the kindergarten template,
// so they only apply when this theme is active.
br.RegisterTemplateOverride("kindergarten", "heading", KindergartenHeadingBlock)
br.RegisterTemplateOverride("kindergarten", "text", KindergartenTextBlock)
br.RegisterTemplateOverride("kindergarten", "button", KindergartenButtonBlock)
br.RegisterTemplateOverride("kindergarten", "card", KindergartenCardBlock)
// Branded email wrapper.
tr.RegisterEmailWrapper("kindergarten", KindergartenEmailWrapper)
return nil
}
// DefaultMasterPages returns the default master pages that Kindergarten ships.
// Per spec §7, two masters: default-master (used by default + article page
// templates) and landing-master (used by landing + full-width).
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "kindergarten:default-master",
Title: "Kindergarten Default Master",
PageTemplates: []string{"default", "article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Crayon Nav",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "kindergarten:alphabet_strip",
Title: "ABC Strip",
Content: map[string]any{"letters": "ABCDEFGHIJ", "colorMode": "rainbow"},
Slot: "header",
SortOrder: 1,
},
{
BlockKey: "slot",
Title: "Main Slot",
Content: map[string]any{"slotName": "main", "placeholder": "Page content"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "kindergarten:footer",
Title: "Friendly Footer",
Content: map[string]any{"showSignup": "true", "mascotName": "Pip"},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "kindergarten:landing-master",
Title: "Kindergarten Landing Master",
PageTemplates: []string{"landing", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Crayon Nav",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "kindergarten:mascot_hero",
Title: "Mascot Hero",
Content: map[string]any{"mascot": "pip", "headline": "Welcome to school!", "tagline": "Big fun, small humans."},
Slot: "hero",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Slot",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "kindergarten:big_cta",
Title: "Big Yellow CTA",
Content: map[string]any{"label": "Enrol today", "href": "/enrol", "colorVariant": "yellow"},
Slot: "cta",
SortOrder: 0,
},
{
BlockKey: "kindergarten:footer",
Title: "Friendly Footer",
Content: map[string]any{"showSignup": "true"},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

25
registration.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// Registration is the compile-time plugin registration for the Kindergarten theme.
var Registration = plugin.PluginRegistration{
Name: "kindergarten",
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() },
}

53
schedule.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ScheduleBlockMeta defines metadata for the day schedule block.
var ScheduleBlockMeta = blocks.BlockMeta{
Key: "schedule",
Title: "Day Schedule",
Description: "Daycare/classroom day plan with crayon time-pills.",
Category: blocks.CategoryContent,
Source: "kindergarten",
}
// ScheduleItem is a single time/activity row.
type ScheduleItem struct {
Time string
Activity string
Icon string
}
// ScheduleData is the renderer's strongly-typed input.
type ScheduleData struct {
Title string
Items []ScheduleItem
}
// ScheduleBlock renders the day-schedule block.
// Content shape: {title,items:[{time,activity,icon}]}.
func ScheduleBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "items")
items := make([]ScheduleItem, 0, len(raw))
for _, m := range raw {
items = append(items, ScheduleItem{
Time: getString(m, "time"),
Activity: getString(m, "activity"),
Icon: getString(m, "icon"),
})
}
data := ScheduleData{
Title: getString(content, "title"),
Items: items,
}
var buf bytes.Buffer
_ = scheduleComponent(data).Render(ctx, &buf)
return buf.String()
}

60
schedule.templ Normal file
View File

@ -0,0 +1,60 @@
package main
// scheduleComponent renders the day schedule.
templ scheduleComponent(data ScheduleData) {
<section class="kg-section" data-block="kindergarten:schedule">
<div class="kg-container">
if data.Title != "" {
<h2 class="kg-display kg-crayon-underline" style="font-size: 2rem; margin-bottom: 1.5rem;">{ data.Title }</h2>
}
if len(data.Items) == 0 {
<div class="kg-empty" data-empty="true">No schedule items yet — add a time block to get started.</div>
} else {
<ol style="list-style: none; padding: 0; margin: 0;">
for _, item := range data.Items {
<li class="kg-schedule-item">
<span class="kg-schedule-time">
if item.Time != "" {
{ item.Time }
} else {
--:--
}
</span>
<span class="kg-schedule-activity">
if item.Activity != "" {
{ item.Activity }
} else {
Activity
}
</span>
<span class="kg-schedule-icon" aria-hidden="true">
@scheduleIcon(item.Icon)
</span>
</li>
}
</ol>
}
</div>
</section>
}
// scheduleIcon renders a small SVG matching the icon name. Defaults to a star
// shape when the icon is missing or unknown so we never render a broken image.
templ scheduleIcon(name string) {
switch name {
case "sun":
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m4.93 19.07 1.41-1.41"/><path d="m17.66 6.34 1.41-1.41"/></svg>
case "book":
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
case "paint":
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 11h2m-1 -1v2"/><circle cx="13" cy="6" r="2"/><circle cx="19" cy="11" r="2"/><circle cx="6" cy="14" r="2"/><circle cx="10" cy="20" r="2"/><path d="M7.4 12.4l3.6 -2.4"/><path d="M11.6 8.6l1.4 -0.6"/><path d="M15 6l4 5"/><path d="M8 16l2 4"/></svg>
case "snack":
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
case "play":
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
case "nap":
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
default:
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.8 5.8 21 7 14 2 9.3 9 8.5 12 2"/></svg>
}
}

197
schedule_templ.go Normal file
View File

@ -0,0 +1,197 @@
// 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"
// scheduleComponent renders the day schedule.
func scheduleComponent(data ScheduleData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"kg-section\" data-block=\"kindergarten:schedule\"><div class=\"kg-container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"kg-display kg-crayon-underline\" style=\"font-size: 2rem; margin-bottom: 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schedule.templ`, Line: 8, Col: 107}
}
_, 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, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Items) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"kg-empty\" data-empty=\"true\">No schedule items yet — add a time block to get started.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<ol style=\"list-style: none; padding: 0; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<li class=\"kg-schedule-item\"><span class=\"kg-schedule-time\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Time != "" {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Time)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schedule.templ`, Line: 18, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "--:--")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span> <span class=\"kg-schedule-activity\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Activity != "" {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Activity)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `schedule.templ`, Line: 25, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Activity")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span> <span class=\"kg-schedule-icon\" aria-hidden=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = scheduleIcon(item.Icon).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</ol>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// scheduleIcon renders a small SVG matching the icon name. Defaults to a star
// shape when the icon is missing or unknown so we never render a broken image.
func scheduleIcon(name 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_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch name {
case "sun":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"4\"></circle><path d=\"M12 2v2\"></path><path d=\"M12 20v2\"></path><path d=\"m4.93 4.93 1.41 1.41\"></path><path d=\"m17.66 17.66 1.41 1.41\"></path><path d=\"M2 12h2\"></path><path d=\"M20 12h2\"></path><path d=\"m4.93 19.07 1.41-1.41\"></path><path d=\"m17.66 6.34 1.41-1.41\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "book":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"></path><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "paint":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 11h2m-1 -1v2\"></path><circle cx=\"13\" cy=\"6\" r=\"2\"></circle><circle cx=\"19\" cy=\"11\" r=\"2\"></circle><circle cx=\"6\" cy=\"14\" r=\"2\"></circle><circle cx=\"10\" cy=\"20\" r=\"2\"></circle><path d=\"M7.4 12.4l3.6 -2.4\"></path><path d=\"M11.6 8.6l1.4 -0.6\"></path><path d=\"M15 6l4 5\"></path><path d=\"M8 16l2 4\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "snack":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M8 14s1.5 2 4 2 4-2 4-2\"></path><line x1=\"9\" y1=\"9\" x2=\"9.01\" y2=\"9\"></line><line x1=\"15\" y1=\"9\" x2=\"15.01\" y2=\"9\"></line></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "play":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"5 3 19 12 5 21 5 3\"></polygon></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "nap":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.8 5.8 21 7 14 2 9.3 9 8.5 12 2\"></polygon></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Alphabet Strip",
"description": "Decorative letter band — primary, rainbow, or mono colorways.",
"type": "object",
"properties": {
"letters": {
"type": "string",
"title": "Letters",
"description": "Sequence of letters to render. Each character becomes one tile.",
"default": "ABCDEFGHIJ",
"x-editor": "text"
},
"colorMode": {
"type": "string",
"title": "Color Mode",
"description": "How the letter tiles are colored.",
"x-editor": "select",
"enum": ["primary", "rainbow", "mono"],
"default": "rainbow"
}
}
}

View File

@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Big CTA",
"description": "Oversized pill button with sticker drop-shadow and crayon underline.",
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"description": "Button copy (e.g. 'Enrol today').",
"x-editor": "text"
},
"href": {
"type": "string",
"title": "Link",
"description": "Where the button points.",
"x-editor": "link"
},
"colorVariant": {
"type": "string",
"title": "Color Variant",
"description": "Which primary colorway the pill uses.",
"x-editor": "select",
"enum": ["red", "blue", "yellow", "green"],
"default": "yellow"
}
}
}

View File

@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Friendly Footer",
"description": "Mascot wave, optional signup, contact, social links, and crayon-rule divider.",
"type": "object",
"properties": {
"showSignup": {
"type": "string",
"title": "Show Newsletter Signup",
"description": "Render the small newsletter signup form.",
"x-editor": "select",
"enum": ["true", "false"],
"default": "true"
},
"mascotName": {
"type": "string",
"title": "Mascot Name",
"description": "Name of the mascot displayed in the footer wave (e.g. 'Pip').",
"default": "Pip",
"x-editor": "text"
},
"socialLinks": {
"type": "array",
"title": "Social Links",
"description": "Friendly icon links for social platforms.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"platform": {
"type": "string",
"title": "Platform",
"description": "Which platform this link points to.",
"x-editor": "select",
"enum": ["facebook", "instagram", "youtube", "tiktok", "email"]
},
"href": {
"type": "string",
"title": "URL",
"description": "Link target.",
"x-editor": "link"
}
}
}
}
}
}

View File

@ -0,0 +1,52 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Gallery of Art",
"description": "Polaroid-style grid for kid artwork with privacy toggle for child names.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Optional heading above the gallery.",
"x-editor": "text"
},
"showChildName": {
"type": "string",
"title": "Show Child Names",
"description": "Privacy toggle. When 'false', child names are omitted from the rendered gallery.",
"x-editor": "select",
"enum": ["true", "false"],
"default": "true"
},
"items": {
"type": "array",
"title": "Artworks",
"description": "Collection of artworks. Each entry has an image, child name, and age.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Image",
"description": "Artwork image (media reference).",
"x-editor": "media"
},
"childName": {
"type": "string",
"title": "Child Name",
"description": "Artist's first name. Hidden when 'Show Child Names' is off.",
"x-editor": "text"
},
"age": {
"type": "integer",
"title": "Age",
"description": "Artist's age (years).",
"x-editor": "number"
}
}
}
}
}
}

View File

@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Mascot Hero",
"description": "Big rounded panel with mascot SVG, headline, tagline, and a primary-colored CTA.",
"type": "object",
"properties": {
"mascot": {
"type": "string",
"title": "Mascot",
"description": "Mascot character to render in the hero panel.",
"x-editor": "select",
"enum": ["pip", "blocks", "star", "balloon"],
"default": "pip"
},
"headline": {
"type": "string",
"title": "Headline",
"description": "Big, hand-lettered headline.",
"x-editor": "text"
},
"tagline": {
"type": "string",
"title": "Tagline",
"description": "Friendly one-liner under the headline.",
"x-editor": "text"
},
"ctaLabel": {
"type": "string",
"title": "CTA Label",
"description": "Pill button label (e.g. 'Enrol today').",
"x-editor": "text"
},
"ctaHref": {
"type": "string",
"title": "CTA Link",
"description": "Where the pill button points.",
"x-editor": "link"
},
"bgColor": {
"type": "string",
"title": "Background Wash",
"description": "Optional HSL triple (e.g. '48 95% 58%') to tint the hero wash. Leave blank to use theme accent.",
"x-editor": "color"
}
}
}

View File

@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Numbers Counter",
"description": "Big circular numeral badges for stats (e.g. '12 teachers').",
"type": "object",
"properties": {
"items": {
"type": "array",
"title": "Numbers",
"description": "Collection of (number, label) pairs.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"number": {
"type": "integer",
"title": "Number",
"description": "Numeral rendered as a circular badge.",
"x-editor": "number"
},
"label": {
"type": "string",
"title": "Label",
"description": "Plain-language description (e.g. 'teachers').",
"x-editor": "text"
}
}
}
}
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Day Schedule",
"description": "Daycare / classroom day plan with crayon time-pills.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Optional heading above the schedule (e.g. 'A day at Sunflower Room').",
"x-editor": "text"
},
"items": {
"type": "array",
"title": "Schedule Items",
"description": "Ordered list of time-blocks.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"time": {
"type": "string",
"title": "Time",
"description": "Time-of-day label (e.g. '09:00').",
"x-editor": "text"
},
"activity": {
"type": "string",
"title": "Activity",
"description": "What happens in this slot.",
"x-editor": "text"
},
"icon": {
"type": "string",
"title": "Icon",
"description": "Icon name (sun, book, paint, snack, play, nap).",
"x-editor": "select",
"enum": ["", "sun", "book", "paint", "snack", "play", "nap"]
}
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Storybook Quote",
"description": "Picture-book page treatment for a quote with optional illustration.",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The quoted text.",
"x-editor": "textarea"
},
"author": {
"type": "string",
"title": "Author",
"description": "Who said it.",
"x-editor": "text"
},
"illustration": {
"type": "string",
"title": "Illustration",
"description": "Optional picture-book illustration.",
"x-editor": "media"
}
}
}

38
storybook_quote.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// StorybookQuoteBlockMeta defines metadata for the storybook quote block.
var StorybookQuoteBlockMeta = blocks.BlockMeta{
Key: "storybook_quote",
Title: "Storybook Quote",
Description: "Picture-book page treatment for a quote with optional illustration.",
Category: blocks.CategoryContent,
Source: "kindergarten",
}
// StorybookQuoteData is the renderer input.
type StorybookQuoteData struct {
Quote string
Author string
Illustration string
}
// StorybookQuoteBlock renders the quote panel.
// Content shape: {quote,author,illustration}.
func StorybookQuoteBlock(ctx context.Context, content map[string]any) string {
data := StorybookQuoteData{
Quote: getString(content, "quote"),
Author: getString(content, "author"),
Illustration: getString(content, "illustration"),
}
var buf bytes.Buffer
_ = storybookQuoteComponent(data).Render(ctx, &buf)
return buf.String()
}

42
storybook_quote.templ Normal file
View File

@ -0,0 +1,42 @@
package main
import "git.dev.alexdunmow.com/block/core/blocks"
// storybookQuoteComponent renders a quote with optional illustration.
templ storybookQuoteComponent(data StorybookQuoteData) {
<section class="kg-section" data-block="kindergarten:storybook_quote">
<div class="kg-container">
<figure class="kg-storybook">
<blockquote style="margin: 0;">
if data.Quote != "" {
<p class="kg-storybook-quote">"{ data.Quote }"</p>
} else {
<p class="kg-storybook-quote" data-empty="true">Add a quote to inspire your readers.</p>
}
if data.Author != "" {
<figcaption class="kg-storybook-author">— { data.Author }</figcaption>
}
</blockquote>
<div>
if data.Illustration != "" {
<img class="kg-storybook-illustration" src={ blocks.ResolveMediaPath(data.Illustration) } alt={ "Illustration accompanying the quote" } />
} else {
@storybookFallbackIllustration()
}
</div>
</figure>
</div>
</section>
}
// storybookFallbackIllustration renders a friendly SVG when no illustration
// is supplied. It guarantees a visible, themed graphic instead of a broken
// image icon.
templ storybookFallbackIllustration() {
<svg viewBox="0 0 200 150" width="100%" height="auto" class="kg-storybook-illustration" role="img" aria-label="Storybook illustration">
<rect width="200" height="150" rx="24" fill="hsl(var(--muted))" />
<circle cx="60" cy="60" r="20" fill="hsl(var(--accent))" />
<rect x="100" y="40" width="60" height="40" rx="8" fill="hsl(var(--primary))" />
<path d="M 30 110 Q 100 90 170 110 L 170 130 L 30 130 Z" fill="hsl(var(--secondary))" />
</svg>
}

163
storybook_quote_templ.go Normal file
View File

@ -0,0 +1,163 @@
// 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 "git.dev.alexdunmow.com/block/core/blocks"
// storybookQuoteComponent renders a quote with optional illustration.
func storybookQuoteComponent(data StorybookQuoteData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"kg-section\" data-block=\"kindergarten:storybook_quote\"><div class=\"kg-container\"><figure class=\"kg-storybook\"><blockquote style=\"margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Quote != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"kg-storybook-quote\">\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Quote)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `storybook_quote.templ`, Line: 12, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"kg-storybook-quote\" data-empty=\"true\">Add a quote to inspire your readers.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Author != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figcaption class=\"kg-storybook-author\">— ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `storybook_quote.templ`, Line: 17, Col: 63}
}
_, 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, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</blockquote><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Illustration != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<img class=\"kg-storybook-illustration\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(blocks.ResolveMediaPath(data.Illustration))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `storybook_quote.templ`, Line: 22, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("Illustration accompanying the quote")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `storybook_quote.templ`, Line: 22, Col: 139}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = storybookFallbackIllustration().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></figure></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// storybookFallbackIllustration renders a friendly SVG when no illustration
// is supplied. It guarantees a visible, themed graphic instead of a broken
// image icon.
func storybookFallbackIllustration() 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, 12, "<svg viewBox=\"0 0 200 150\" width=\"100%\" height=\"auto\" class=\"kg-storybook-illustration\" role=\"img\" aria-label=\"Storybook illustration\"><rect width=\"200\" height=\"150\" rx=\"24\" fill=\"hsl(var(--muted))\"></rect> <circle cx=\"60\" cy=\"60\" r=\"20\" fill=\"hsl(var(--accent))\"></circle> <rect x=\"100\" y=\"40\" width=\"60\" height=\"40\" rx=\"8\" fill=\"hsl(var(--primary))\"></rect> <path d=\"M 30 110 Q 100 90 170 110 L 170 130 L 30 130 Z\" fill=\"hsl(var(--secondary))\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

257
template.templ Normal file
View File

@ -0,0 +1,257 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries everything the page templates need to render.
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
}
// parseKindergartenPageData converts the unstructured doc map the engine
// passes in into the strongly-typed PageData used by the templ components.
func parseKindergartenPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// pluginStyles is the canonical list of theme stylesheet URLs each page sends to <head>.
var pluginStyles = []string{"/templates/kindergarten/style.css"}
// Kindergarten — default template.
templ Kindergarten(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: pluginStyles,
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="kg-page kg-dotted-paper">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="kg-section" style="padding-block: 1.5rem;">
@templ.Raw(data.Slots["header"])
</header>
<main class="kg-container" style="padding-block: 2rem; flex: 1;">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="kg-empty">No content blocks assigned to this page.</div>
}
</main>
<footer style="margin-top: auto;">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Kindergarten — landing template with mascot hero + CTA strip.
templ KindergartenLanding(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: pluginStyles,
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="kg-page kg-dotted-paper">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="kg-section" style="padding-block: 1.5rem;">
@templ.Raw(data.Slots["header"])
</header>
<section class="kg-section" style="padding-top: 0;">
@templ.Raw(data.Slots["hero"])
</section>
<main class="kg-container">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
}
</main>
<section>
@templ.Raw(data.Slots["cta"])
</section>
<footer style="margin-top: auto;">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Kindergarten — article (storybook reading) template.
templ KindergartenArticle(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: pluginStyles,
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="kg-page kg-dotted-paper">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="kg-section" style="padding-block: 1.5rem;">
@templ.Raw(data.Slots["header"])
</header>
<div class="kg-container" style="display: grid; grid-template-columns: 1fr; gap: 2rem; padding-block: 2rem;">
<main>
if main, ok := data.Slots["main"]; ok && main != "" {
<article class="kg-text">
@templ.Raw(main)
</article>
} else {
<div class="kg-empty">No content yet — start a story.</div>
}
</main>
if aside, ok := data.Slots["aside"]; ok && aside != "" {
<aside class="kg-card">
@templ.Raw(aside)
</aside>
}
</div>
<footer style="margin-top: auto;">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Kindergarten — full-width gallery / showcase template.
templ KindergartenFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: pluginStyles,
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="kg-page kg-dotted-paper">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="kg-section" style="padding-block: 1.5rem;">
@templ.Raw(data.Slots["header"])
</header>
<main style="flex: 1;">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="kg-container">
<div class="kg-empty">No content blocks assigned to this page.</div>
</div>
}
</main>
<footer style="margin-top: auto;">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Render functions invoked by the registry.
func RenderKindergarten(ctx context.Context, doc map[string]any) templ.Component {
return Kindergarten(parseKindergartenPageData(doc))
}
func RenderKindergartenLanding(ctx context.Context, doc map[string]any) templ.Component {
return KindergartenLanding(parseKindergartenPageData(doc))
}
func RenderKindergartenArticle(ctx context.Context, doc map[string]any) templ.Component {
return KindergartenArticle(parseKindergartenPageData(doc))
}
func RenderKindergartenFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return KindergartenFullWidth(parseKindergartenPageData(doc))
}

529
template_templ.go Normal file
View File

@ -0,0 +1,529 @@
// 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 carries everything the page templates need to render.
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
}
// parseKindergartenPageData converts the unstructured doc map the engine
// passes in into the strongly-typed PageData used by the templ components.
func parseKindergartenPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// pluginStyles is the canonical list of theme stylesheet URLs each page sends to <head>.
var pluginStyles = []string{"/templates/kindergarten/style.css"}
// Kindergarten — default template.
func Kindergarten(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\">")
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: pluginStyles,
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=\"kg-page kg-dotted-paper\">")
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=\"kg-section\" style=\"padding-block: 1.5rem;\">")
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, "</header><main class=\"kg-container\" style=\"padding-block: 2rem; flex: 1;\">")
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, "<div class=\"kg-empty\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer style=\"margin-top: auto;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</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, 8, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Kindergarten — landing template with mascot hero + CTA strip.
func KindergartenLanding(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_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\">")
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: pluginStyles,
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, 10, "<body class=\"kg-page kg-dotted-paper\">")
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, 11, "<header class=\"kg-section\" style=\"padding-block: 1.5rem;\">")
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, 12, "</header><section class=\"kg-section\" style=\"padding-top: 0;\">")
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, "</section><main class=\"kg-container\">")
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
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</main><section>")
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, 15, "</section><footer style=\"margin-top: auto;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</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, 17, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Kindergarten — article (storybook reading) template.
func KindergartenArticle(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, 18, "<!doctype html><html lang=\"en\">")
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: pluginStyles,
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, 19, "<body class=\"kg-page kg-dotted-paper\">")
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, 20, "<header class=\"kg-section\" style=\"padding-block: 1.5rem;\">")
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, 21, "</header><div class=\"kg-container\" style=\"display: grid; grid-template-columns: 1fr; gap: 2rem; padding-block: 2rem;\"><main>")
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, 22, "<article class=\"kg-text\">")
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, 23, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<div class=\"kg-empty\">No content yet — start a story.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if aside, ok := data.Slots["aside"]; ok && aside != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<aside class=\"kg-card\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(aside).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div><footer style=\"margin-top: auto;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</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, 30, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Kindergarten — full-width gallery / showcase template.
func KindergartenFullWidth(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, 31, "<!doctype html><html lang=\"en\">")
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: pluginStyles,
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, 32, "<body class=\"kg-page kg-dotted-paper\">")
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, 33, "<header class=\"kg-section\" style=\"padding-block: 1.5rem;\">")
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, 34, "</header><main style=\"flex: 1;\">")
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, 35, "<div class=\"kg-container\"><div class=\"kg-empty\">No content blocks assigned to this page.</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</main><footer style=\"margin-top: auto;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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 invoked by the registry.
func RenderKindergarten(ctx context.Context, doc map[string]any) templ.Component {
return Kindergarten(parseKindergartenPageData(doc))
}
func RenderKindergartenLanding(ctx context.Context, doc map[string]any) templ.Component {
return KindergartenLanding(parseKindergartenPageData(doc))
}
func RenderKindergartenArticle(ctx context.Context, doc map[string]any) templ.Component {
return KindergartenArticle(parseKindergartenPageData(doc))
}
func RenderKindergartenFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return KindergartenFullWidth(parseKindergartenPageData(doc))
}
var _ = templruntime.GeneratedTemplate

17
text_override.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"bytes"
"context"
)
// KindergartenTextBlock renders text with Nunito body, 1.7 line-height,
// and the rounded ::selection highlight in primary yellow.
func KindergartenTextBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
class := getString(content, "class")
var buf bytes.Buffer
_ = kgTextComponent(text, class).Render(ctx, &buf)
return buf.String()
}

9
text_override.templ Normal file
View File

@ -0,0 +1,9 @@
package main
// kgTextComponent renders rich text body styled to match the Kindergarten
// reading rhythm.
templ kgTextComponent(text, class string) {
<div class={ "kg-text", class }>
@templ.Raw(text)
</div>
}

68
text_override_templ.go Normal file
View File

@ -0,0 +1,68 @@
// 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"
// kgTextComponent renders rich text body styled to match the Kindergarten
// reading rhythm.
func kgTextComponent(text, class string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"kg-text", class}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate