Compare commits

..

17 Commits
v0.1.1 ... main

Author SHA1 Message Date
Alex Dunmow
e15e53f161 bump to 0.4.0 2026-07-06 21:21:14 +08:00
Alex Dunmow
e650b6d978 feat: Wave B retina reshoot (home/about/article × light+dark, 2880x1800) with bundled Baloo 2 + Quicksand
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:21:14 +08:00
Alex Dunmow
0b357c267c bump to 0.3.1 2026-07-06 21:16:19 +08:00
Alex Dunmow
860b422b9a feat(fonts): bundle Baloo 2 + Quicksand via @font-face + preset typography; align alphabet strip to content column
Host does not consume LoadedPlugin.BundledFonts (CMS dead code), so
declare @font-face against the served /templates/kindergarten/fonts/*.woff2
paths and set typography.fontHeading/fontBody in all 5 presets so
--font-heading/--font-body resolve to the bundled families. Also constrain
.kg-alphabet-strip to the .kg-container max-width/inset so its left edge
aligns with every other block (design-review cosmetic note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:16:09 +08:00
Alex Dunmow
0ae327ea56 bump to 0.3.0 2026-07-06 09:32:07 +08:00
Alex Dunmow
0d4ddd7f4c feat: Wave B gallery screenshots (home/about/article × light+dark) 2026-07-06 09:31:53 +08:00
Alex Dunmow
955688daf6 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
d7d7e1a7a3 refactor: replace hand-rolled utility CSS with Tailwind classes (host build now scans theme templates)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:54:28 +08:00
Alex Dunmow
ca4e132f41 fix(theme): drive responsive layout from theme-owned CSS
Host Tailwind JIT does not scan theme templates, so responsive grid/hero
utilities (sm:/md:/lg:grid-cols-*, min-h-screen, arbitrary values) are
dropped and multi-column layouts collapse to one column. Ship the
load-bearing responsive layout as compiled theme CSS and point the
override templates at it. Layout only — colours stay on semantic tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:40:22 +08:00
Alex Dunmow
85d95d0545 fix(kindergarten): repair hero, grids, empty media + mobile whitespace
Remediate broken layout on the demo home page (persona intact):

- Hero rendered with no height: it relied on min-h-screen /
  responsive Tailwind that the host JIT drops on a cold provision.
  Drive hero sizing + type through compiled .kg-hero* CSS classes
  (input_css_append is compiled every build). Mascot repositioned to
  bottom-center behind readable content; confetti/ABC/reduced-motion
  fallback preserved. All builtin field reads unchanged.
- Landing header was position:absolute and overlapped the feature
  grid when the hero collapsed -> switch to the default's in-flow
  sticky header (collision gone).
- feature-grid columns never formed (lg:grid-cols-3 not compiled) ->
  route through compiled .kg-cols/.kg-cols-3 media-query grid.
- Empty art-wall polaroids ballooned into large blank squares ->
  graceful 4:3 capped placeholder with a playful crayon wash.
- Define .kg-section (was only in the dead assets/style.css, so
  persona sections had no padding) and add a consistent .kg-sect
  rhythm; trim oversized section padding.
- Alphabet strip now a single scrollable row (was flex-wrap) so the
  sticky header stays compact on mobile.
- Mobile: all layout-critical grids are compiled media-query CSS
  (mobile-first, 1 col), no fixed widths > viewport, tap targets ok.

No .reveal/scroll-hide pattern in this theme; whitespace was real
layout void. Colors stay semantic tokens; check-safety 32 checks OK.
No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:26:20 +08:00
Alex Dunmow
0e2119ae8d bump to 0.2.1 2026-07-05 16:44:48 +08:00
Alex Dunmow
223d4bcf89 docs: publishing README 2026-07-05 13:43:43 +08:00
Alex Dunmow
df6ed78281 feat(kindergarten): Wave A theme lane (WO-TF-019) — 49 overrides, 7 templates, GO-BIG mascot hero
Preschool / daycare identity: crayon borders, sticker badges, confetti, and a
dependency-free bouncing-mascot + confetti-canvas hero showpiece (hero/landing
only, lazy-init, prefers-reduced-motion honored, static no-JS fallback).

- All 49 mandatory builtin overrides (OVERRIDE-CONTRACT §1), field-correct
  against the frozen cms builtins, re-skinned to the crayon design system.
  Legacy button (label/link/style) and card (text/media) field reads reconciled.
- 7 page templates: default, full-width, landing, article, blog-index, contact,
  auth — standard header/main/footer slots, dotted-paper chrome, dual-mode.
- Blocks reworked: mascot_hero/big_cta/gallery_of_art/numbers_counter/
  storybook_quote/footer converted to builtin overrides; 4 persona blocks kept/
  added — alphabet_strip (array-shaped, gotcha-safe), schedule, art_wall,
  story_time.
- 5 presets, all mode "both", bright-classroom light + quiet-time navy dark.
- Bundled OFL woff2 (Baloo 2 700, Quicksand 400/700) in assets/fonts + LICENSES;
  all font-family via var(--font-*); RECOMMENDED_FONTS/ICONS updated;
  required_icon_packs declared.
- Themed email-safe wrapper; master pages (site + auth); seed demo site
  (home/about/blog+3 posts/contact form+row_inserted email workflow/login),
  preschool voice.

Known platform limit (OVERRIDE-CONTRACT §11): auth-form/auth-status/
password-reset-form/page-suggestions overrides are authored correct-to-model but
do not dispatch (compiled BlockFuncs). page-suggestions also omits the dynamic
suggestion list (no FTS in codeless).

Verified: `make` (ninja plugin build) exit 0; check-safety exit 0. Visual quality
UNVERIFIED until Wave B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:25:52 +08:00
Alex Dunmow
dccd269ab2 feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:17:12 +08:00
Alex Dunmow
b38c0d3a57 chore: repo hygiene for the wasm era (WO-WZ-015 fix round)
Replace the retired .so Makefile with the wasm/.bnp convention (build /
verify / archive-check / publish via ninja plugin build), gitignore *.so
and *.bnp artifacts, and refresh the stale [compatibility] block_core
constraint to the v0.15 era.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:15:12 +08:00
Alex Dunmow
dc9b68c083 wasm port (WO-WZ-015): reactor-mode wasip1 guest, core v0.15.2
Add wasmguest.Serve(Registration) boilerplate and bump block/core to v0.15.2
for the .so->wasm migration. Built + published as .bnp to the dev registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:11:12 +08:00
Alex Dunmow
6200982d80 bump to 0.1.2 2026-07-03 22:09:59 +08:00
142 changed files with 2718 additions and 5048 deletions

1
.gitignore vendored
View File

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

View File

@ -1,29 +1,55 @@
# Kindergarten — local-only build helpers (.so plugin workflow) # kindergarten — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
# #
# IMPORTANT: This Makefile intentionally omits the `rebuild` family of targets # This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# that other themes ship; this build pass only validates that the plugin # (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# compiles locally and passes the safety check. # an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
#
# Usage:
# make build # pack kindergarten-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry)
# make clean # remove build artefacts
.PHONY: all clean templ help .PHONY: build verify archive-check publish clean help
PLUGIN_NAME := kindergarten PLUGIN_NAME := kindergarten
NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Default target: build the .so locally. # Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
all: $(PLUGIN_NAME).so # + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build:
$(NINJA) plugin build --dir .
# Local plugin build (no container). Requires CGO + matching glibc/go toolchain. # Validate the built .bnp exactly as the registry/loader will.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod verify:
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so . $(NINJA) plugin verify $(BNP)
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK"
# Publish the prebuilt .bnp to the active registry.
# Pass --host https://my.blockninja.dev for the dev orchestrator.
publish: build
$(NINJA) plugin publish --bnp $(BNP)
# Remove build artefacts.
clean: clean:
rm -f $(PLUGIN_NAME).so rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
# Regenerate templ Go files locally (for development).
templ:
templ generate
help: help:
@echo "Targets:" @echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)" @echo " build Pack the codeless .bnp (no wasm)"
@echo " clean Remove built .so" @echo " verify Validate the built .bnp (loader-identical checks)"
@echo " templ Regenerate templ Go files" @echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry"
@echo " clean Remove build artefacts"

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# Kindergarten
A bright, hand-lettered theme built on primary colors and crayon. Wobbly cut-out shapes, sticker badges, and dotted paper give it the warmth of a classroom wall. It feels playful and safe, the kind of page a parent trusts and a child recognizes. Rounded type keeps everything friendly and easy to read.
## Good for
We built Kindergarten with early learning in mind. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Preschools and daycare centres
- After-school and holiday programs
- Children's activity classes
- Family and parenting groups
- Kids' party and play venues
## What you get
- Five color presets, each with a tuned light mode and a tuned dark mode: Crayon Box, Sunny Recess, Bluebird, Meadow Play, and Bubblegum.
- Two bundled fonts, ready to assign: Baloo 2 for rounded headings and Quicksand for body text. Fonts stay admin-controlled, so you can swap them from the typography settings.
- A GO BIG hero with an animated canvas: a bouncing mascot, wiggling letters, and a confetti burst. It honors reduced-motion settings and falls back to a static scene when scripts are off.
- Four persona blocks: an alphabet strip, a daily class schedule, an art wall of child-artist polaroids, and a story-time open book.
- A demo site that installs when you activate the theme: a home page, an about page, a blog with three posts, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.

View File

@ -1,43 +1,39 @@
# Recommended fonts for Kindergarten # Recommended fonts Kindergarten
The kindergarten theme ships `fonts.json = []` per the wave-1 fonts policy This theme **bundles** two OFL (SIL Open Font License 1.1) latin-subset webfonts in
(`themes/docs/FONTS.md`). Admins assign fonts via the typography panel; this `assets/fonts/` and declares them in `fonts.json`. On install they become
file lists the picker recommendations sourced from the spec (`docs/works/kindergarten.md` §3). `source=template` rows in the `fonts` table, so their `@font-face` declarations are
auto-emitted and they are selectable in the admin typography picker. No manual font setup
is required to get the intended look.
Until the admin makes a choice, the theme falls back to its system stack: | Slot | Bundled family | File | Fallback stack (before assignment) |
|---|---|---|---|
| Heading (`--font-heading`) | **Baloo 2** | `fonts/baloo2-700.woff2` | `"Baloo 2", "Fredoka", "Quicksand", system-ui, sans-serif` |
| Body (`--font-body`) | **Quicksand** | `fonts/quicksand-400.woff2`, `fonts/quicksand-700.woff2` | `"Quicksand", "Nunito", system-ui, -apple-system, "Segoe UI", sans-serif` |
| Mono (`--font-mono`) | *(not bundled — system fallback)* | — | `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace` |
- `--font-heading``Quicksand`, `Sniglet`, `Nunito`, system sans ## How the theme consumes them
- `--font-body``Nunito`, `Quicksand`, system sans
- `--font-mono``Fira Code`, `JetBrains Mono`, system monospace
## How to apply Every `font-family` in the theme flows through a CSS variable with a bundled-family
fallback — nothing is hardcoded, so the admin picker always wins:
1. Open **Settings → Typography** in the BlockNinja admin. - `.kg-display` -> `var(--font-heading, "Baloo 2", ...)` — headlines, hero, section titles, sticker numerals.
2. Click the **Google Fonts** tab in the font picker. - `.kg-body` / `.kg-prose` -> `var(--font-body, "Quicksand", ...)` — body copy, forms, cards.
3. Add each font below using the listed picker source. - `.kg-mono` -> `var(--font-mono, ...)` — schedule times and small numeric readouts.
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 ## To assign (optional)
| Slot | Source | Family | Weights / styles | Notes | Open the typography panel and assign **Baloo 2 -> Heading** and **Quicksand -> Body**. Both
|---------|--------------------|-------------|---------------------|-------| appear in the picker under the theme's bundled fonts.
| 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 ## Alternate direction
- **Quicksand** and **Nunito** are SIL OFL — safe to recommend and free to use. The locked WO-TF-019 direction pairs **Baloo 2** (rounded display) with **Quicksand**
- **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. (body). **Fredoka** (OFL) is an equally good rounded display alternative to Baloo 2 — both
- **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. are in the curated Google Fonts tab. **Nunito** (OFL) is a warmer-weight body alternative to
Quicksand.
## Wave-2 follow-up ## Licensing
If the brand requires Sniglet rounded everywhere, bundle the woff2 files Both are OFL 1.1 and cleared for redistribution inside this artifact. Details and
inside `assets/fonts/web/` and add the entries to `fonts.json` per the attribution: `assets/fonts/LICENSES.md`.
schema in `themes/docs/FONTS.md`. Record the licence in `LICENSES.md` at the
theme root.

26
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,26 @@
# Recommended icons — Kindergarten
Icons render through the CMS sprite pipeline: `<svg><use href="/icons/<pack>.svg#<name>"/></svg>`
(or the `::pack:name:size::` shorthand). The theme's overrides reference two bundled packs.
## Required packs
Declared in `plugin.mod` as `required_icon_packs = ["lucide", "simple-icons"]`
(forward-declared — the loader does not yet auto-install; install manually until it does).
| Pack | Used for | Install |
|---|---|---|
| **lucide** | UI glyphs the builtin/override templates use: `arrow-right`, `check`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `star`, `flag`, `navigation`, `map-pin`, `download`, `play`, `sun`, `book-open`, `palette`, `user`, `globe` | Ships bundled with the CMS — no action needed. |
| **simple-icons** | Author/social brand glyphs: `x` (Twitter), `linkedin`, `github`, `facebook`, `instagram`, `youtube` | Ships bundled with the CMS — no action needed. |
Both packs ship bundled with the CMS, so a fresh instance renders every override icon with
no manual step. If you author blocks that reference other packs (e.g. `tabler`, `phosphor`),
install them from `/admin/icons` (Browse -> install -> enable) before activation.
## Note
Icon values in block content are stored as `"pack:name"` strings and split with
`|split:":"|first` / `|last`. Never inline raw SVG path data for content icons — that
bypasses the sprite system and the admin pack manager. (The theme's decorative mascot,
schedule glyphs, and social marks in persona blocks are intrinsic illustrations, not
content-icon fields.)

View File

@ -1,64 +0,0 @@
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
}

View File

@ -1,25 +0,0 @@
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 ""
}
}

View File

@ -1,100 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

20
assets/fonts/LICENSES.md Normal file
View File

@ -0,0 +1,20 @@
# Bundled font licenses — Kindergarten theme
Both families are bundled as latin-subset `.woff2` and are licensed under the
SIL Open Font License 1.1 (OFL). Redistribution inside this theme artifact is permitted.
| File | Family | License | Source |
|---|---|---|---|
| `baloo2-700.woff2` | Baloo 2 (700) | OFL 1.1 | Google Fonts (fonts.gstatic.com, v23), latin subset |
| `quicksand-400.woff2` | Quicksand (400) | OFL 1.1 | Google Fonts (fonts.gstatic.com, v37), latin subset |
| `quicksand-700.woff2` | Quicksand (700) | OFL 1.1 | Google Fonts (fonts.gstatic.com, v37), latin subset |
OFL 1.1 full text: https://openfontlicense.org
- Baloo 2 © The Baloo 2 Project Authors (ektype).
- Quicksand © The Quicksand Project Authors (Andrew Paglinawan).
These are the Google-hosted latin subsets (Latin + Latin-1 punctuation), chosen to keep
the bundle small (~50 KB total). They cover English content; sites needing extended Latin,
Devanagari, Cyrillic, Greek, or Vietnamese glyphs should assign the full family from the
admin Google Fonts picker instead.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,45 +0,0 @@
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()
}

View File

@ -1,28 +0,0 @@
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"
}
}

View File

@ -1,114 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -0,0 +1 @@
<nav class="kg-alphabet-strip {% if colorMode == "primary" %}kg-alphabet-primary{% elif colorMode == "mono" %}kg-alphabet-mono{% endif %}" aria-label="Alphabet strip" data-block="kindergarten:alphabet_strip">{% if not letters %}<span class="kg-empty" data-empty="true">Add a few letters to build the strip.</span>{% endif %}{% for item in letters %}<span class="kg-alphabet-letter">{{ item.letter }}</span>{% endfor %}</nav>

View File

@ -5,11 +5,22 @@
"type": "object", "type": "object",
"properties": { "properties": {
"letters": { "letters": {
"type": "string", "type": "array",
"title": "Letters", "title": "Letters",
"description": "Sequence of letters to render. Each character becomes one tile.", "description": "Ordered list of letter tiles.",
"default": "ABCDEFGHIJ", "default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"letter": {
"type": "string",
"title": "Letter",
"description": "A single letter or character for this tile.",
"x-editor": "text" "x-editor": "text"
}
}
}
}, },
"colorMode": { "colorMode": {
"type": "string", "type": "string",

1
blocks/art_wall.ninjatpl Normal file
View File

@ -0,0 +1 @@
<section class="kg-section" data-block="kindergarten:art_wall"><div class="kg-container">{% set showNames = true %}{% if showChildName == false %}{% set showNames = false %}{% endif %}{% if title %}<h2 class="kg-display mb-6" style="font-size: 2rem;"><span class="kg-crayon-underline">{{ title }}</span></h2>{% endif %}{% if not items %}<div class="kg-empty" data-empty="true">No artworks yet — pin a few pieces to fill the wall.</div>{% else %}<div class="kg-gallery-grid grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">{% for art in items %}<figure class="kg-polaroid">{% if art.image %}<img src="{{ art.image|mediaURL }}" alt="{% if showNames and art.childName %}Artwork by {{ art.childName }}{% else %}Children's artwork{% endif %}">{% else %}<span class="kg-polaroid-image" role="img" aria-label="Placeholder for a child's artwork"></span>{% endif %}<figcaption class="kg-polaroid-caption">{% if showNames and art.childName %}<span>{{ art.childName }}</span>{% endif %}{% if showNames and art.childName and art.age %}, {% endif %}{% if art.age %}<span>age {{ art.age }}</span>{% endif %}</figcaption></figure>{% endfor %}</div>{% endif %}</div></section>

View File

@ -0,0 +1,51 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Art Wall",
"description": "Polaroid-style wall of children's artwork with artist names.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Optional heading above the wall (e.g. 'Our art wall').",
"x-editor": "text"
},
"showChildName": {
"type": "boolean",
"title": "Show artist name",
"description": "Show each child's name and age under their artwork.",
"default": true,
"x-editor": "select"
},
"items": {
"type": "array",
"title": "Artworks",
"description": "Each piece of artwork.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Artwork image",
"description": "The scanned or photographed artwork.",
"x-editor": "media"
},
"childName": {
"type": "string",
"title": "Artist name",
"description": "First name of the child who made it.",
"x-editor": "text"
},
"age": {
"type": "string",
"title": "Age",
"description": "The child's age (optional).",
"x-editor": "text"
}
}
}
}
}
}

34
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,34 @@
# Kindergarten theme persona block definitions (codeless .bnp, WO-TF-019).
# Keys are fully qualified (kindergarten:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the data-block attributes and the master_pages.json / seed references.
#
# Builtin-duplicating blocks (hero, cta, footer, gallery, stats, quote) are NOT
# theme blocks — they are template overrides in templates/overrides/kindergarten/.
# Only true preschool furniture lives here.
blocks:
- key: kindergarten:alphabet_strip
title: Alphabet Strip
description: A-Z learning band of colorful crayon letter tiles.
category: navigation
schema: alphabet_strip.schema.json
template: alphabet_strip.ninjatpl
- key: kindergarten:schedule
title: Day Schedule
description: Daily class plan with crayon time-pills and activity icons.
category: content
schema: schedule.schema.json
template: schedule.ninjatpl
- key: kindergarten:art_wall
title: Art Wall
description: Polaroid wall of children's artwork with artist names.
category: content
schema: art_wall.schema.json
template: art_wall.ninjatpl
- key: kindergarten:story_time
title: Story Time
description: Picture-book read-aloud panel — cover, title, and a line to read together.
category: content
schema: story_time.schema.json
template: story_time.ninjatpl

1
blocks/schedule.ninjatpl Normal file
View File

@ -0,0 +1 @@
<section class="kg-section" data-block="kindergarten:schedule"><div class="kg-container">{% if title %}<h2 class="kg-display kg-crayon-underline" style="font-size: 2rem; margin-bottom: 1.5rem;">{{ title }}</h2>{% endif %}{% if not items %}<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 in items %}<li class="kg-schedule-item"><span class="kg-schedule-time">{% if item.time %}{{ item.time }}{% else %}--:--{% endif %}</span><span class="kg-schedule-activity">{% if item.activity %}{{ item.activity }}{% else %}Activity{% endif %}</span><span class="kg-schedule-icon" aria-hidden="true">{% if item.icon == "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"></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>{% elif item.icon == "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><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>{% elif item.icon == "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"></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>{% elif item.icon == "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"></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>{% elif item.icon == "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"></polygon></svg>{% elif item.icon == "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"></path></svg>{% else %}<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>{% endif %}</span></li>{% endfor %}</ol>{% endif %}</div></section>

View File

@ -0,0 +1 @@
<section class="kg-section" data-block="kindergarten:story_time"><div class="kg-container"><figure class="kg-storybook md:grid-cols-2 md:items-center">{% if cover %}<img class="kg-storybook-illustration" src="{{ cover|mediaURL }}" alt="Cover of {{ bookTitle|default:'the story' }}">{% else %}<svg viewBox="0 0 200 150" width="100%" height="auto" class="kg-storybook-illustration" role="img" aria-label="Open storybook"><rect width="200" height="150" rx="18" fill="hsl(var(--muted))"></rect><path d="M100 28 C 78 20 46 20 30 28 L 30 122 C 46 114 78 114 100 122 C 122 114 154 114 170 122 L 170 28 C 154 20 122 20 100 28 Z" fill="hsl(var(--card))" stroke="hsl(var(--foreground))" stroke-width="4" stroke-linejoin="round"></path><line x1="100" y1="28" x2="100" y2="122" stroke="hsl(var(--foreground))" stroke-width="4"></line><circle cx="60" cy="60" r="12" fill="hsl(var(--accent))"></circle><rect x="46" y="82" width="30" height="6" rx="3" fill="hsl(var(--primary))"></rect><rect x="122" y="52" width="34" height="6" rx="3" fill="hsl(var(--secondary))"></rect><rect x="122" y="70" width="26" height="6" rx="3" fill="hsl(var(--primary))"></rect></svg>{% endif %}<div><span class="kg-sticker">Story time</span>{% if bookTitle %}<h2 class="kg-display mt-3" style="font-size: clamp(1.5rem, 3.5vw, 2.25rem);">{{ bookTitle }}</h2>{% endif %}{% if line %}<p class="kg-storybook-quote mt-3">"{{ line }}"</p>{% else %}<p class="kg-storybook-quote mt-3" data-empty="true">Add a line from the story to read together.</p>{% endif %}{% if author %}<figcaption class="kg-body mt-3 text-muted-foreground">by {{ author }}</figcaption>{% endif %}</div></figure></div></section>

View File

@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Story Time",
"description": "Picture-book read-aloud panel — cover, book title, and a line to read together.",
"type": "object",
"properties": {
"bookTitle": {
"type": "string",
"title": "Book title",
"description": "The name of the book or story.",
"x-editor": "text"
},
"author": {
"type": "string",
"title": "Author",
"description": "Who wrote it (optional).",
"x-editor": "text"
},
"line": {
"type": "string",
"title": "A line to read",
"description": "A short line from the story to read together.",
"x-editor": "textarea"
},
"cover": {
"type": "string",
"title": "Cover image",
"description": "The book cover or an illustration (optional — a friendly open book shows if empty).",
"x-editor": "media"
}
}
}

View File

@ -1,31 +0,0 @@
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()
}

View File

@ -1,10 +0,0 @@
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>
}
}

View File

@ -1,92 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,21 +0,0 @@
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()
}

View File

@ -1,18 +0,0 @@
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>
}

View File

@ -1,102 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,49 +0,0 @@
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")
}

View File

@ -1,108 +0,0 @@
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>
}

View File

@ -1,406 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"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

View File

@ -1,64 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
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),
}
}

View File

@ -1 +1,18 @@
[] [
{
"name": "Baloo 2",
"family": "Baloo 2",
"variants": [
{ "weight": "700", "style": "normal", "file": "fonts/baloo2-700.woff2" }
]
},
{
"name": "Quicksand",
"family": "Quicksand",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/quicksand-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/quicksand-400.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/quicksand-700.woff2" }
]
}
]

View File

@ -1,53 +0,0 @@
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()
}

View File

@ -1,64 +0,0 @@
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>
}
}

View File

@ -1,216 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,57 +0,0 @@
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()
}

View File

@ -1,52 +0,0 @@
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"
}

View File

@ -1,197 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"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
View File

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

44
go.sum
View File

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

View File

@ -1,65 +0,0 @@
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
}

View File

@ -1,57 +0,0 @@
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>
}
}

View File

@ -1,373 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
// 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

View File

@ -1,63 +0,0 @@
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
}

584
manifest.yaml Normal file
View File

@ -0,0 +1,584 @@
# Kindergarten theme — codeless .bnp manifest (WO-TF-019, Wave A).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs: ["lucide", "simple-icons"]
system_templates:
- key: kindergarten
title: Kindergarten
description: "Preschool / daycare theme — crayon-drawn borders, sticker badges, confetti, and a bouncing-mascot canvas hero. Warm, safe, and playful, in a bright classroom by day and a quiet-time navy by night."
page_templates:
- system: kindergarten
key: default
title: Default
description: "Standard rounded page — dotted-paper background, header, main, footer"
slots: [header, main, footer]
- system: kindergarten
key: full-width
title: Full Width
description: "Edge-to-edge classroom / gallery showcase, no max-width"
slots: [header, main, footer]
- system: kindergarten
key: landing
title: Landing
description: "Bouncing-mascot canvas hero, sticker feature cards, big confetti CTA"
slots: [header, main, footer]
- system: kindergarten
key: article
title: Story Page
description: "Picture-book reading layout — prose column plus a sticker note aside"
slots: [header, main, aside, footer]
- system: kindergarten
key: blog-index
title: Blog Index
description: "Story-wall feed of rounded post cards"
slots: [header, main, footer]
- system: kindergarten
key: contact
title: Contact
description: "Friendly enrol / contact page — form card plus a contact sticker card"
slots: [header, main, footer]
- system: kindergarten
key: auth
title: Auth
description: "Centered rounded sign-in card for login / register / reset"
slots: [header, main, footer]
# All 49 mandatory-tier builtin overrides (OVERRIDE-CONTRACT §1), dashed keys.
template_overrides:
# Chrome (4)
- { template: kindergarten, block: navbar }
- { template: kindergarten, block: footer }
- { template: kindergarten, block: utility-bar }
- { template: kindergarten, block: announcement-bar }
# Marketing (12)
- { template: kindergarten, block: hero }
- { template: kindergarten, block: feature-grid }
- { template: kindergarten, block: rich-section }
- { template: kindergarten, block: stats }
- { template: kindergarten, block: testimonial }
- { template: kindergarten, block: logo-strip }
- { template: kindergarten, block: pricing }
- { template: kindergarten, block: team }
- { template: kindergarten, block: faq }
- { template: kindergarten, block: timeline }
- { template: kindergarten, block: cta }
- { template: kindergarten, block: countdown }
# Primitives (11)
- { template: kindergarten, block: heading }
- { template: kindergarten, block: text }
- { template: kindergarten, block: rich-text }
- { template: kindergarten, block: image }
- { template: kindergarten, block: quote }
- { template: kindergarten, block: divider }
- { template: kindergarten, block: button }
- { template: kindergarten, block: card }
- { template: kindergarten, block: gallery }
- { template: kindergarten, block: video-embed }
- { template: kindergarten, block: audio-embed }
# Commerce / local (6)
- { template: kindergarten, block: menu-list }
- { template: kindergarten, block: event-list }
- { template: kindergarten, block: contact-form }
- { template: kindergarten, block: contact-card }
- { template: kindergarten, block: hours-location }
- { template: kindergarten, block: social-links }
# Blog family (12)
- { template: kindergarten, block: blog-hero }
- { template: kindergarten, block: featured-posts }
- { template: kindergarten, block: related-posts }
- { template: kindergarten, block: popular-posts }
- { template: kindergarten, block: category-list }
- { template: kindergarten, block: post-hero }
- { template: kindergarten, block: post-metadata }
- { template: kindergarten, block: author-bio }
- { template: kindergarten, block: author-bio-hero }
- { template: kindergarten, block: newsletter-signup }
- { template: kindergarten, block: breadcrumbs }
- { template: kindergarten, block: sidebar-nav }
# Auth surface (3)
- { template: kindergarten, block: auth-form }
- { template: kindergarten, block: password-reset-form }
- { template: kindergarten, block: auth-status }
# System (1)
- { template: kindergarten, block: page-suggestions }
email_wrappers:
- kindergarten
css:
input_css_append: |
/* ============================================================
Kindergarten — Preschool / daycare theme (WO-TF-019, Wave A).
Crayon borders, sticker badges, confetti, primary-colour joy,
and a GO-BIG bouncing-mascot canvas hero (hero/landing only).
Every colour goes through hsl(var(--token)); every font-family
goes through var(--font-{heading|body|mono}, <fallback>).
No hardcoded hex / rgb / named colours.
============================================================ */
/* ---- Typography -------------------------------------------- */
/* Families flow through the admin-bound CSS variables; the
fallback leads with the bundled OFL family so the intended look
survives before the admin assigns a slot.
Bundled display faces. The host does not yet emit @font-face for
bundled_fonts (LoadedPlugin.BundledFonts is unconsumed — CMS dead
code), so the theme declares its own faces against the served
artifact paths (/templates/kindergarten/fonts/...). Presets set
typography.fontHeading/fontBody to template:kindergarten:<Family>
so --font-heading/--font-body resolve to these families. Weights
match fonts.json exactly (Baloo 2 700; Quicksand 400/500 share the
400 file, 700 its own). */
@font-face { font-family: 'Baloo 2'; src: url('/templates/kindergarten/fonts/baloo2-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Quicksand'; src: url('/templates/kindergarten/fonts/quicksand-400.woff2') format('woff2'); font-weight: 400 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Quicksand'; src: url('/templates/kindergarten/fonts/quicksand-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
.kg-body {
font-family: var(--font-body, "Quicksand", "Nunito", system-ui, -apple-system, "Segoe UI", sans-serif);
}
.kg-display {
font-family: var(--font-heading, "Baloo 2", "Fredoka", "Quicksand", system-ui, sans-serif);
font-weight: 700;
letter-spacing: -0.01em;
}
.kg-mono {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
}
.kg-prose,
.kg-prose p,
.kg-prose li {
font-family: var(--font-body, "Quicksand", "Nunito", sans-serif);
line-height: 1.75;
}
.kg-prose code {
color: hsl(var(--primary));
background-color: hsl(var(--muted));
padding: 0.125rem 0.4rem;
border-radius: 0.5rem;
font-family: var(--font-mono, ui-monospace, Menlo, monospace);
font-size: 0.875em;
}
/* Friendly kicker: rounded, bold, primary — carries enough on its
own that it is never a silent no-op used alone. */
.kg-eyebrow {
font-family: var(--font-heading, "Baloo 2", "Quicksand", sans-serif);
font-weight: 700;
font-size: 0.85rem;
letter-spacing: 0.02em;
color: hsl(var(--primary));
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
/* ---- Crayon underline (decorative heading accent) ---------- */
.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 26%,
hsl(var(--accent)) 26% 52%,
hsl(var(--secondary)) 52% 78%,
hsl(var(--primary)) 78% 100%);
border-radius: 6px;
transform: rotate(-1deg);
z-index: -1;
}
/* ---- Crayon / sticker primitives (drop-in surface chrome) -- */
/* kg-crayon-shadow: chunky hand-cut offset shadow + playful lift.
Pairs with Tailwind border-* / rounded-* on the same element. */
.kg-crayon-shadow {
box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.12);
transition: transform 160ms ease, box-shadow 160ms ease;
}
.kg-crayon-shadow:hover,
.kg-crayon-shadow:focus-within {
transform: translateY(-2px);
box-shadow: 0 8px 0 0 hsl(var(--foreground) / 0.14);
}
/* kg-drop: chunky flat button shadow (the crayon "sticker" edge). */
.kg-drop {
box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.15);
}
/* kg-pop: playful hover bounce for buttons / interactive chips. */
.kg-pop {
transition: transform 140ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.kg-pop:hover,
.kg-pop:focus-visible {
transform: translateY(-2px) scale(1.03);
}
.kg-pop:active {
transform: translateY(1px) scale(0.98);
}
/* kg-sticker: peel-sticker badge — rounded, chunky, slight tilt. */
.kg-sticker {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.85rem;
border-radius: 9999px;
font-family: var(--font-heading, "Baloo 2", "Quicksand", sans-serif);
font-weight: 700;
font-size: 0.8rem;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
box-shadow: 0 3px 0 0 hsl(var(--foreground) / 0.12);
transform: rotate(-2deg);
}
/* Global focus-visible ring (accessibility). */
.kg-page a:focus-visible,
.kg-page button:focus-visible,
.kg-page input:focus-visible,
.kg-page textarea:focus-visible,
.kg-page select:focus-visible {
outline: 3px solid hsl(var(--ring));
outline-offset: 2px;
}
/* ---- Page shell ------------------------------------------- */
.kg-page {
font-family: var(--font-body, "Quicksand", "Nunito", system-ui, sans-serif);
line-height: 1.7;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
.kg-page h1, .kg-page h2, .kg-page h3,
.kg-page h4, .kg-page h5, .kg-page h6 {
font-family: var(--font-heading, "Baloo 2", "Quicksand", sans-serif);
font-weight: 700;
letter-spacing: -0.01em;
}
.kg-container {
max-width: 72rem;
margin-inline: auto;
padding-inline: 1.5rem;
}
.kg-empty {
text-align: center;
padding: 2rem;
border-radius: 1.5rem;
border: 3px dashed hsl(var(--border));
color: hsl(var(--muted-foreground));
font-family: var(--font-body, "Quicksand", sans-serif);
}
/* Dotted-paper page background layer. */
.kg-dotted-paper {
background-color: hsl(var(--background));
background-image: radial-gradient(circle, hsl(var(--muted-foreground) / 0.14) 1px, transparent 1.6px);
background-size: 26px 26px;
}
/* ---- Persona-block furniture ------------------------------ */
.kg-hero-panel {
border-radius: 2.5rem;
padding: clamp(2rem, 6vw, 4rem);
position: relative;
overflow: hidden;
background-color: hsl(var(--accent) / 0.15);
}
.kg-card {
border-radius: 2rem;
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
padding: 2rem;
border: 3px solid hsl(var(--border));
position: relative;
overflow: hidden;
}
/* ---- Robust responsive layout ------------------------------ */
/* The host Tailwind JIT scans rendered BUILT-IN html and lags on
theme-only classes, so every layout-critical grid, section
rhythm, and hero size is driven by these compiled-every-build
rules — never by responsive Tailwind utilities in the theme
templates (which are dropped on a cold provision). All are
mobile-first: 1 column by default, widening at breakpoints. */
.kg-section { padding-block: clamp(2rem, 5vw, 3rem); }
.kg-sect { padding-block: clamp(2.25rem, 5vw, 3.5rem); position: relative; }
/* Hero — full sizing + type in CSS so it never depends on the
(unreliable) min-h-screen / responsive Tailwind in the template. */
.kg-hero {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
min-height: clamp(24rem, 58vh, 38rem);
padding-block: clamp(2.5rem, 7vw, 4.5rem);
background-color: hsl(var(--accent) / 0.15);
color: hsl(var(--foreground));
}
.kg-hero-media { position: absolute; inset: 0; z-index: 0; }
.kg-hero-media > img { width: 100%; height: 100%; object-fit: cover; }
.kg-hero-overlay { position: absolute; inset: 0; background-color: hsl(var(--foreground) / 0.4); }
.kg-hero-mascot {
position: absolute;
left: 50%;
bottom: -0.25rem;
transform: translateX(-50%);
width: clamp(88px, 16vw, 150px);
pointer-events: none;
}
.kg-hero-mascot svg { width: 100%; height: auto; display: block; }
.kg-hero-inner {
position: relative;
z-index: 10;
width: 100%;
max-width: 56rem;
margin-inline: auto;
padding-inline: 1.5rem;
}
.kg-hero--center { text-align: center; }
.kg-hero-title { margin-top: 1rem; font-size: clamp(2.25rem, 6vw, 3.75rem); line-height: 1.06; }
.kg-hero-abc { display: inline-flex; gap: 0.35rem; margin-top: 1.1rem; font-size: clamp(1.5rem, 4vw, 2rem); }
.kg-hero-abc .kg-wiggle-letter:nth-child(1) { color: hsl(var(--primary)); }
.kg-hero-abc .kg-wiggle-letter:nth-child(2) { color: hsl(var(--secondary)); }
.kg-hero-abc .kg-wiggle-letter:nth-child(3) { color: hsl(var(--accent)); }
.kg-hero-sub { margin-top: 1rem; font-size: clamp(1.125rem, 2.5vw, 1.5rem); color: hsl(var(--muted-foreground)); max-width: 42rem; }
.kg-hero-desc { margin-top: 0.75rem; color: hsl(var(--muted-foreground)); max-width: 36rem; line-height: 1.6; }
.kg-hero--center .kg-hero-sub,
.kg-hero--center .kg-hero-desc { margin-inline: auto; }
.kg-hero-cta { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2rem; }
.kg-hero--center .kg-hero-cta { justify-content: center; }
.kg-hero-trust { margin-top: 2.5rem; }
.kg-hero-logos { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; margin-top: 0.75rem; }
.kg-hero--center .kg-hero-logos { justify-content: center; }
/* Chunky rounded buttons (crayon sticker edge). */
.kg-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 9999px;
padding: 0.85rem 1.75rem;
font-family: var(--font-heading, "Baloo 2", "Quicksand", sans-serif);
font-weight: 700;
font-size: 1.05rem;
line-height: 1.1;
text-decoration: none;
}
.kg-btn-primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.15); }
.kg-btn-outline { background-color: hsl(var(--background)); color: hsl(var(--primary)); border: 3px solid hsl(var(--primary)); }
/* Alphabet strip (persona block). Aligned to the content column
(same max-width + inline padding as .kg-container) so its left
edge lines up with every other block instead of sitting flush to
the viewport edge; still horizontally scrollable when narrow. */
.kg-alphabet-strip {
display: flex;
flex-wrap: nowrap;
gap: 0.6rem;
max-width: 72rem;
margin-inline: auto;
padding: 0.6rem 1.5rem;
overflow-x: auto;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.kg-alphabet-letter {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: 1.25rem;
font-family: var(--font-heading, "Baloo 2", "Quicksand", 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(var(--destructive)); color: hsl(var(--destructive-foreground)); }
.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 (persona block). */
.kg-schedule-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1rem;
align-items: center;
padding: 1rem 1.25rem;
border-radius: 1.5rem;
background-color: hsl(var(--card));
border: 3px solid hsl(var(--border));
margin-bottom: 0.75rem;
}
.kg-schedule-time {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.85rem;
border-radius: 9999px;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
font-family: var(--font-mono, ui-monospace, monospace);
font-weight: 700;
}
.kg-schedule-activity { color: hsl(var(--foreground)); }
.kg-schedule-icon { color: hsl(var(--primary)); }
/* Art-wall polaroid (persona block). */
.kg-polaroid {
background-color: hsl(var(--card));
border-radius: 1rem;
padding: 12px 12px 36px;
box-shadow: 0 6px 24px hsl(var(--foreground) / 0.12);
transform: rotate(-1.5deg);
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 {
display: block;
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 0.5rem;
background-color: hsl(var(--muted));
}
/* Empty-media graceful state: a compact, playful crayon wash — NOT a
ballooning blank square. Shorter aspect + capped height so three
imageless polaroids never dominate the page. */
.kg-polaroid > .kg-polaroid-image {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
max-height: 220px;
border-radius: 0.5rem;
background-color: hsl(var(--accent) / 0.16);
background-image:
radial-gradient(circle at 50% 46%, hsl(var(--primary) / 0.38) 0 9%, transparent 10%),
radial-gradient(circle, hsl(var(--foreground) / 0.10) 1.5px, transparent 2px);
background-size: 100% 100%, 20px 20px;
background-position: center center, center center;
background-repeat: no-repeat, repeat;
}
.kg-polaroid-caption {
text-align: center;
margin-top: 0.75rem;
font-family: var(--font-heading, "Baloo 2", "Quicksand", sans-serif);
color: hsl(var(--foreground));
}
/* Story-time open book (persona block). */
.kg-storybook {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
padding: 2rem;
border-radius: 2rem;
background-color: hsl(var(--card));
border: 3px solid hsl(var(--border));
}
.kg-storybook-quote {
font-family: var(--font-heading, "Baloo 2", "Quicksand", sans-serif);
font-size: clamp(1.25rem, 3vw, 2rem);
line-height: 1.4;
color: hsl(var(--foreground));
}
.kg-storybook-illustration {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 1.25rem;
background-color: hsl(var(--muted));
}
/* ---- GO-BIG motion: bouncing mascot + wiggling letters ----- */
/* The canvas confetti/mascot showpiece lives ONLY on the hero
override (hero/landing). It lazy-inits on view, honours
prefers-reduced-motion, and degrades to a static confetti wash
with JS off. No canvas anywhere else in the theme. */
.kg-mascot {
display: inline-block;
transform-origin: center bottom;
animation: kg-bounce 2.4s ease-in-out infinite;
}
@keyframes kg-bounce {
0%, 100% { transform: translateY(0) rotate(-1deg); }
50% { transform: translateY(-16px) rotate(1deg); }
}
.kg-wiggle-letter {
display: inline-block;
animation: kg-wiggle 2.2s ease-in-out infinite;
}
.kg-wiggle-letter:nth-child(2n) { animation-delay: 0.15s; }
.kg-wiggle-letter:nth-child(3n) { animation-delay: 0.3s; }
.kg-wiggle-letter:nth-child(4n) { animation-delay: 0.45s; }
@keyframes kg-wiggle {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(5deg); }
}
/* Confetti canvas + static no-JS fallback. */
.kg-confetti-wrap {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.kg-confetti-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.kg-confetti-fallback {
position: absolute;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(circle, hsl(var(--primary) / 0.55) 8px, transparent 9px),
radial-gradient(circle, hsl(var(--secondary) / 0.55) 6px, transparent 7px),
radial-gradient(circle, hsl(var(--accent) / 0.6) 5px, transparent 6px);
background-size: 90px 90px, 60px 60px, 40px 40px;
background-position: 12% 18%, 72% 28%, 42% 78%;
opacity: 0.55;
}
/* ---- Reduced motion: freeze everything -------------------- */
@media (prefers-reduced-motion: reduce) {
.kg-mascot,
.kg-wiggle-letter,
.kg-confetti-canvas {
animation: none !important;
}
.kg-confetti-canvas { display: none; }
.kg-pop:hover,
.kg-pop:focus-visible,
.kg-crayon-shadow:hover,
.kg-crayon-shadow:focus-within { transform: none; }
}

View File

@ -1,44 +0,0 @@
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()
}

View File

@ -1,78 +0,0 @@
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>
}
}

View File

@ -1,178 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

23
master_pages.json Normal file
View File

@ -0,0 +1,23 @@
[
{
"key": "kindergarten:site-master",
"title": "Kindergarten Site Master",
"page_templates": ["default", "full-width", "landing", "article", "blog-index", "contact"],
"blocks": [
{ "block_key": "navbar", "title": "Crayon Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Little Sprouts" }, "companyName": "Little Sprouts", "stickyBehavior": "sticky", "cta": { "text": "Book a visit", "url": "/contact" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "kindergarten:alphabet_strip", "title": "ABC Band", "content": { "colorMode": "rainbow", "letters": [ { "letter": "A" }, { "letter": "B" }, { "letter": "C" }, { "letter": "D" }, { "letter": "E" }, { "letter": "F" }, { "letter": "G" }, { "letter": "H" } ] }, "slot": "header", "sort_order": 1 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Friendly Footer", "content": { "layout": "columns", "companyName": "Little Sprouts", "tagline": "A warm, playful place to learn and grow.", "copyright": "Little Sprouts Preschool", "showNewsletter": true, "newsletterTitle": "Stay in the loop", "newsletterDescription": "Notes home, event dates, and the odd finger-painting photo." }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "kindergarten:auth-master",
"title": "Kindergarten Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "navbar", "title": "Crayon Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Little Sprouts" }, "companyName": "Little Sprouts" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Auth Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Friendly Footer", "content": { "layout": "minimal", "companyName": "Little Sprouts", "tagline": "A warm, playful place to learn and grow.", "copyright": "Little Sprouts Preschool" }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -1,47 +0,0 @@
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()
}

View File

@ -1,25 +0,0 @@
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>
}

View File

@ -1,117 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "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

View File

@ -2,11 +2,8 @@
name = "kindergarten" name = "kindergarten"
display_name = "Kindergarten" display_name = "Kindergarten"
scope = "@themes" scope = "@themes"
version = "0.1.1" version = "0.4.0"
description = "Primary-colored, hand-lettered theme for schools, daycares, toy shops, museums, and kids' publishers." description = "Primary-colored, hand-lettered theme for preschools and daycares — crayon borders, sticker badges, confetti, and a bouncing mascot showpiece."
kind = "theme" kind = "theme"
categories = ["templates"] categories = ["templates"]
tags = ["kids", "education", "playful", "family", "school", "museum", "friendly", "daycare", "publisher"] tags = ["kids", "education", "playful", "family", "school", "daycare", "preschool", "friendly", "colorful"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

View File

@ -1,110 +1,272 @@
[ [
{
"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", "id": "crayon-box",
"name": "Crayon Box", "name": "Crayon Box",
"description": "Full primary quartet — red, blue, yellow, green — in both light and dark.", "description": "The default — crayon blue, red, and yellow. Bright classroom by day, quiet-time navy by night.",
"theme": { "theme": {
"mode": "both",
"typography": {
"fontHeading": "template:kindergarten:Baloo 2",
"fontBody": "template:kindergarten:Quicksand"
},
"lightColors": { "lightColors": {
"background": "48 55% 98%", "background": "48 55% 98%",
"foreground": "220 45% 15%", "foreground": "222 47% 16%",
"card": "0 0% 100%", "card": "0 0% 100%",
"cardForeground": "220 45% 15%", "cardForeground": "222 47% 16%",
"popover": "0 0% 100%", "popover": "0 0% 100%",
"popoverForeground": "220 45% 15%", "popoverForeground": "222 47% 16%",
"primary": "220 78% 52%", "primary": "222 80% 54%",
"primaryForeground": "0 0% 100%", "primaryForeground": "0 0% 100%",
"secondary": "4 78% 56%", "secondary": "4 78% 57%",
"secondaryForeground": "0 0% 100%", "secondaryForeground": "0 0% 100%",
"muted": "48 35% 93%", "muted": "48 40% 93%",
"mutedForeground": "220 22% 38%", "mutedForeground": "222 20% 40%",
"accent": "145 65% 45%", "accent": "45 96% 56%",
"accentForeground": "222 50% 14%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "222 25% 86%",
"input": "48 30% 90%",
"ring": "222 80% 54%"
},
"darkColors": {
"background": "224 46% 9%",
"foreground": "48 55% 95%",
"card": "224 42% 13%",
"cardForeground": "48 55% 95%",
"popover": "224 42% 13%",
"popoverForeground": "48 55% 95%",
"primary": "218 90% 66%",
"primaryForeground": "224 46% 9%",
"secondary": "4 84% 64%",
"secondaryForeground": "0 0% 100%",
"muted": "224 34% 17%",
"mutedForeground": "220 20% 70%",
"accent": "45 96% 62%",
"accentForeground": "224 46% 9%",
"destructive": "0 84% 62%",
"destructiveForeground": "0 0% 100%",
"border": "224 32% 22%",
"input": "224 32% 19%",
"ring": "45 96% 62%"
}
}
},
{
"id": "sunny-recess",
"name": "Sunny Recess",
"description": "Warm red-and-sun classroom by day, soft navy with a glow-star yellow for nap time.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:kindergarten:Baloo 2",
"fontBody": "template:kindergarten:Quicksand"
},
"lightColors": {
"background": "40 70% 97%",
"foreground": "20 40% 18%",
"card": "0 0% 100%",
"cardForeground": "20 40% 18%",
"popover": "0 0% 100%",
"popoverForeground": "20 40% 18%",
"primary": "8 82% 56%",
"primaryForeground": "0 0% 100%",
"secondary": "205 75% 52%",
"secondaryForeground": "0 0% 100%",
"muted": "40 45% 92%",
"mutedForeground": "20 20% 40%",
"accent": "42 96% 55%",
"accentForeground": "20 45% 16%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "32 35% 85%",
"input": "40 35% 90%",
"ring": "8 82% 56%"
},
"darkColors": {
"background": "222 45% 9%",
"foreground": "40 60% 95%",
"card": "222 40% 13%",
"cardForeground": "40 60% 95%",
"popover": "222 40% 13%",
"popoverForeground": "40 60% 95%",
"primary": "10 88% 64%",
"primaryForeground": "0 0% 100%",
"secondary": "205 82% 62%",
"secondaryForeground": "222 45% 9%",
"muted": "222 32% 17%",
"mutedForeground": "40 22% 72%",
"accent": "44 96% 62%",
"accentForeground": "222 45% 9%",
"destructive": "0 84% 62%",
"destructiveForeground": "0 0% 100%",
"border": "222 30% 22%",
"input": "222 30% 19%",
"ring": "44 96% 62%"
}
}
},
{
"id": "bluebird",
"name": "Bluebird",
"description": "Calm blue-and-teal for reading corners. Deep quiet navy with a teal glow after dark.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:kindergarten:Baloo 2",
"fontBody": "template:kindergarten:Quicksand"
},
"lightColors": {
"background": "205 60% 98%",
"foreground": "218 45% 16%",
"card": "0 0% 100%",
"cardForeground": "218 45% 16%",
"popover": "0 0% 100%",
"popoverForeground": "218 45% 16%",
"primary": "212 84% 52%",
"primaryForeground": "0 0% 100%",
"secondary": "45 95% 55%",
"secondaryForeground": "218 50% 14%",
"muted": "205 40% 93%",
"mutedForeground": "218 22% 42%",
"accent": "175 70% 42%",
"accentForeground": "0 0% 100%", "accentForeground": "0 0% 100%",
"destructive": "0 84% 55%", "destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%", "destructiveForeground": "0 0% 100%",
"border": "220 25% 85%", "border": "210 30% 86%",
"input": "48 25% 88%", "input": "205 30% 90%",
"ring": "48 95% 55%" "ring": "212 84% 52%"
}, },
"darkColors": { "darkColors": {
"background": "220 40% 8%", "background": "220 48% 9%",
"foreground": "48 55% 95%", "foreground": "205 55% 95%",
"card": "220 40% 12%", "card": "220 44% 13%",
"cardForeground": "48 55% 95%", "cardForeground": "205 55% 95%",
"popover": "220 40% 12%", "popover": "220 44% 13%",
"popoverForeground": "48 55% 95%", "popoverForeground": "205 55% 95%",
"primary": "220 80% 60%", "primary": "210 92% 66%",
"primaryForeground": "0 0% 100%", "primaryForeground": "220 48% 9%",
"secondary": "4 80% 58%", "secondary": "45 96% 62%",
"secondaryForeground": "0 0% 100%", "secondaryForeground": "220 48% 9%",
"muted": "220 30% 16%", "muted": "220 34% 17%",
"mutedForeground": "220 18% 68%", "mutedForeground": "205 20% 72%",
"accent": "145 70% 50%", "accent": "172 80% 52%",
"accentForeground": "220 40% 8%", "accentForeground": "220 48% 9%",
"destructive": "0 84% 60%", "destructive": "0 84% 62%",
"destructiveForeground": "0 0% 100%", "destructiveForeground": "0 0% 100%",
"border": "220 30% 22%", "border": "220 32% 22%",
"input": "220 30% 18%", "input": "220 32% 19%",
"ring": "48 95% 60%" "ring": "172 80% 52%"
}
}
}, },
"mode": "both" {
"id": "meadow",
"name": "Meadow Play",
"description": "Green-and-orange nature play by day, deep teal-navy with a warm glow for winding down.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:kindergarten:Baloo 2",
"fontBody": "template:kindergarten:Quicksand"
},
"lightColors": {
"background": "90 45% 97%",
"foreground": "140 35% 15%",
"card": "0 0% 100%",
"cardForeground": "140 35% 15%",
"popover": "0 0% 100%",
"popoverForeground": "140 35% 15%",
"primary": "145 62% 40%",
"primaryForeground": "0 0% 100%",
"secondary": "25 88% 55%",
"secondaryForeground": "0 0% 100%",
"muted": "90 30% 92%",
"mutedForeground": "140 18% 38%",
"accent": "48 95% 55%",
"accentForeground": "140 40% 14%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "110 25% 84%",
"input": "90 25% 89%",
"ring": "145 62% 40%"
},
"darkColors": {
"background": "200 40% 9%",
"foreground": "90 45% 94%",
"card": "200 36% 13%",
"cardForeground": "90 45% 94%",
"popover": "200 36% 13%",
"popoverForeground": "90 45% 94%",
"primary": "145 66% 52%",
"primaryForeground": "200 40% 9%",
"secondary": "25 90% 62%",
"secondaryForeground": "0 0% 100%",
"muted": "200 30% 17%",
"mutedForeground": "90 20% 70%",
"accent": "48 96% 62%",
"accentForeground": "200 40% 9%",
"destructive": "0 84% 62%",
"destructiveForeground": "0 0% 100%",
"border": "200 28% 22%",
"input": "200 28% 19%",
"ring": "48 96% 62%"
}
}
},
{
"id": "bubblegum",
"name": "Bubblegum",
"description": "Playful pink-and-purple for toy shops and party rooms. Plum-navy with a soft glow at night.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:kindergarten:Baloo 2",
"fontBody": "template:kindergarten:Quicksand"
},
"lightColors": {
"background": "330 50% 98%",
"foreground": "300 35% 18%",
"card": "0 0% 100%",
"cardForeground": "300 35% 18%",
"popover": "0 0% 100%",
"popoverForeground": "300 35% 18%",
"primary": "330 80% 58%",
"primaryForeground": "0 0% 100%",
"secondary": "265 70% 60%",
"secondaryForeground": "0 0% 100%",
"muted": "330 35% 94%",
"mutedForeground": "300 18% 42%",
"accent": "42 95% 56%",
"accentForeground": "300 40% 16%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "320 30% 88%",
"input": "330 30% 92%",
"ring": "330 80% 58%"
},
"darkColors": {
"background": "285 40% 9%",
"foreground": "330 45% 95%",
"card": "285 36% 13%",
"cardForeground": "330 45% 95%",
"popover": "285 36% 13%",
"popoverForeground": "330 45% 95%",
"primary": "330 88% 68%",
"primaryForeground": "285 40% 9%",
"secondary": "265 82% 72%",
"secondaryForeground": "285 40% 9%",
"muted": "285 30% 17%",
"mutedForeground": "330 20% 72%",
"accent": "45 96% 64%",
"accentForeground": "285 40% 9%",
"destructive": "0 84% 62%",
"destructiveForeground": "0 0% 100%",
"border": "285 28% 23%",
"input": "285 28% 20%",
"ring": "45 96% 64%"
}
} }
} }
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 288 KiB

View File

@ -1,181 +0,0 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function to templates.TemplateFunc.
// templ.Component already 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,
},
},
},
}
}

View File

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

View File

@ -1,53 +0,0 @@
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()
}

View File

@ -1,60 +0,0 @@
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>
}
}

View File

@ -1,197 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

@ -1,28 +0,0 @@
{
"$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

@ -1,48 +0,0 @@
{
"$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

@ -1,52 +0,0 @@
{
"$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

@ -1,46 +0,0 @@
{
"$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

@ -1,32 +0,0 @@
{
"$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

@ -1,26 +0,0 @@
{
"$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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

228
seed/seed.json Normal file
View File

@ -0,0 +1,228 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "Little Sprouts",
"tagline": "A warm, playful place to learn and grow."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Visit Requests",
"description": "Seeded contact-form submissions for the Little Sprouts demo. A submission becomes a row here and emails the site admins.",
"columns": [
{ "key": "name", "label": "Parent name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "childAge", "label": "Child's age", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Little Sprouts",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Hero",
"slot": "main",
"sort_order": 1,
"content": {
"template": "centered",
"minHeight": "screen",
"eyebrow": "Now enrolling",
"headline": "A happy place to start",
"subheadline": "Play-based preschool for ages 2 to 5.",
"description": "Small groups, warm teachers, and a garden to dig in. Come and see a normal day.",
"ctaPrimary": { "text": "Book a visit", "url": "/contact" },
"ctaSecondary": { "text": "See our day", "url": "/about" }
}
},
{
"block_key": "feature-grid",
"title": "Why families choose us",
"slot": "main",
"sort_order": 2,
"content": {
"eyebrow": "Why families choose us",
"heading": "What your child gets",
"intro": "Every day has room to move, make, and rest.",
"layout": "card",
"columns": 3,
"items": [
{ "icon": "lucide:sun", "title": "Time outside every day", "text": "Rain or shine, we head to the garden and the yard." },
{ "icon": "lucide:book-open", "title": "Stories and songs", "text": "We read together, sing together, and make up our own tales." },
{ "icon": "lucide:palette", "title": "Room to make a mess", "text": "Paint, clay, glue, and glitter. Aprons on, big ideas out." }
]
}
},
{
"block_key": "kindergarten:schedule",
"title": "A day in the Sunflower Room",
"slot": "main",
"sort_order": 3,
"content": {
"title": "A day in the Sunflower Room",
"items": [
{ "time": "09:00", "activity": "Arrival and free play", "icon": "play" },
{ "time": "10:00", "activity": "Morning circle and songs", "icon": "book" },
{ "time": "10:30", "activity": "Snack and fruit", "icon": "snack" },
{ "time": "11:00", "activity": "Garden and outside play", "icon": "sun" },
{ "time": "12:00", "activity": "Lunch together", "icon": "snack" },
{ "time": "13:00", "activity": "Quiet time and rest", "icon": "nap" },
{ "time": "14:00", "activity": "Painting and making", "icon": "paint" }
]
}
},
{
"block_key": "kindergarten:art_wall",
"title": "Fresh off the easel",
"slot": "main",
"sort_order": 4,
"content": {
"title": "Fresh off the easel",
"showChildName": true,
"items": [
{ "childName": "Mia", "age": "4" },
{ "childName": "Noah", "age": "3" },
{ "childName": "Ada", "age": "5" }
]
}
},
{
"block_key": "cta",
"title": "CTA",
"slot": "main",
"sort_order": 5,
"content": {
"background": "band",
"heading": "Come and see for yourself",
"text": "Visits run most mornings. We will show you around and answer everything.",
"primaryLabel": "Book a visit",
"primaryUrl": "/contact"
}
}
]
},
{
"slug": "/about",
"title": "About us",
"template_key": "default",
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About Little Sprouts", "level": 1 } },
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>Little Sprouts started in a church hall with six children and a box of blocks. Families kept asking for more days, so we grew into a proper little school with a garden out the back.</p><p>Our rooms are small on purpose. Teachers learn every child by name in the first week. We follow the children's lead, keep the day gentle, and send them home tired and happy.</p>" } },
{
"block_key": "kindergarten:story_time",
"title": "Story time",
"slot": "main",
"sort_order": 3,
"content": {
"bookTitle": "The Very Sleepy Bear",
"author": "our reading corner",
"line": "The bear yawned, and the whole forest yawned back."
}
},
{ "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 4, "content": { "variant": "line", "weight": "medium" } },
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "content": { "style": "pull", "quote": "My daughter runs to the door every morning.", "attribution": "A Little Sprouts parent" } }
]
},
{
"slug": "/blog",
"title": "Stories",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Post: Settling in", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "Settling in: the first week", "text": "<p>Little tips that help a new child feel at home, from the drop-off wave to the goodbye routine.</p>", "link": "/blog/settling-in", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: What to pack", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "What to pack for a preschool day", "text": "<p>A short list of what to bring, what to label, and what to leave at home.</p>", "link": "/blog/what-to-pack", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: Reading at home", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Ways to read at home", "text": "<p>Reading together does not need to be fancy. Here is how we keep it easy and fun.</p>", "link": "/blog/reading-at-home", "linkLabel": "Read on" } }
]
},
{
"slug": "/blog/settling-in",
"title": "Settling in: the first week",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Settling in: the first week", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The first week is a big change for everyone. Some children walk straight in. Others hold on tight at the door. Both are normal.</p><p>Keep goodbyes short and warm. Tell your child when you will be back, then go. We will send a photo once they settle, and most do within minutes.</p>" } }
]
},
{
"slug": "/blog/what-to-pack",
"title": "What to pack for a preschool day",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "What to pack for a preschool day", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Pack a spare set of clothes, a hat, and a water bottle. Label everything with your child's name.</p><p>Leave toys from home in the car. They tend to go missing, and sharing them is hard on a new friend. We have plenty here to play with.</p>" } }
]
},
{
"slug": "/blog/reading-at-home",
"title": "Ways to read at home",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Ways to read at home", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Read the same book as often as they ask. Repetition is how young children learn the shape of a story.</p><p>Point at the pictures and ask what happens next. Let them turn the pages. The goal is a warm few minutes together, not a perfect reading.</p>" } }
]
},
{
"slug": "/contact",
"title": "Book a visit",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Book a visit",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Book a visit",
"description": "Tell us a little about your family. We will call to set up a time.",
"submitLabel": "Send",
"successMessage": "Thanks. We will be in touch very soon.",
"fields": [
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
{ "name": "childAge", "label": "Your child's age", "type": "text", "required": false, "width": "full" },
{ "name": "message", "label": "Anything you would like us to know", "type": "textarea", "required": true, "width": "full" }
],
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
}
},
{
"block_key": "contact-card",
"title": "Find us",
"slot": "main",
"sort_order": 2,
"content": {
"heading": "Find us",
"columns": 3,
"channels": [
{ "icon": "lucide:mail", "label": "Email", "value": "hello@littlesprouts.example", "link": "mailto:hello@littlesprouts.example" },
{ "icon": "lucide:phone", "label": "Phone", "value": "(02) 5550 1234", "link": "tel:+61255501234" },
{ "icon": "lucide:map-pin", "label": "Visit", "value": "14 Garden Lane, Riverside" }
]
}
}
]
},
{
"slug": "/login",
"title": "Sign in",
"template_key": "auth",
"blocks": [
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "Stories", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Visit", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Visit request to admins",
"description": "On a contact-form submission, email the site admins.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,38 +0,0 @@
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()
}

View File

@ -1,42 +0,0 @@
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>
}

View File

@ -1,163 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "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

View File

@ -1,257 +0,0 @@
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))
}

View File

@ -1,529 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData 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

View File

@ -0,0 +1,68 @@
<!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>{{ site_name }}</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="background-color: {{ colors.background|default:'#fdf9ed' }}; margin: 0; padding: 0; font-family: &amp;#39;Nunito&amp;#39;, &amp;#39;Quicksand&amp;#39;, -apple-system, sans-serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 48px 10px; background-color: {{ colors.background|default:'#fdf9ed' }};">
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:'#ffffff' }}; border: 1px solid {{ colors.border|default:'#e6d9b3' }}; border-radius: 32px;">
<tr>
<td align="center" style="padding: 32px 40px; background-color: {{ colors.card|default:'#ffffff' }}; border-bottom: 2px dashed {{ colors.border|default:'#e6d9b3' }}; border-radius: 32px 32px 0 0;">
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 28px; font-weight: 700; color: {{ colors.foreground|default:'#1a253b' }}; letter-spacing: -0.5px; font-family: &amp;#39;Quicksand&amp;#39;, &amp;#39;Nunito&amp;#39;, sans-serif;">{{ site_name }}</h1>{% endif %}
</td>
</tr>
<tr>
<td class="content-padding" style="padding: 40px 48px; color: {{ colors.foreground|default:'#1a253b' }}; font-size: 16px; line-height: 1.7; font-family: &amp;#39;Nunito&amp;#39;, sans-serif;">{{ body|safe }}</td>
</tr>
<tr>
<td align="center" style="padding: 0 48px 32px 48px; background-color: {{ colors.card|default:'#ffffff' }};">
{% if site_url %}<a href="{{ site_url }}" style="display: inline-block; padding: 16px 32px; border-radius: 9999px; background-color: {{ colors.primary|default:'#fbcd1f' }}; color: {{ colors.primaryForeground|default:'#1a253b' }}; text-decoration: none; font-family: &amp;#39;Quicksand&amp;#39;, sans-serif; font-weight: 700; font-size: 16px;">Visit {{ site_name }}</a>{% endif %}
</td>
</tr>
<tr>
<td style="padding: 32px 48px; border-top: 2px dashed {{ colors.border|default:'#e6d9b3' }}; border-radius: 0 0 32px 32px; background-color: {{ colors.card|default:'#ffffff' }};">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center">
<p style="margin: 0 0 8px; font-size: 14px; font-weight: 700; color: {{ colors.foreground|default:'#1a253b' }}; font-family: &amp;#39;Quicksand&amp;#39;, sans-serif;">👋 {{ site_name }}</p>
{% if support_email %}<p style="margin: 0 0 8px; font-size: 13px; color: {{ colors.mutedForeground|default:'#5a6480' }};">Need a hand? {{ support_email }}</p>{% endif %}
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:'#5a6480' }};"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:'#5a6480' }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.9); backdrop-filter: blur(6px); border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full max-w-5xl mx-auto px-4 py-12 grid gap-10 lg:grid-cols-[1fr_280px]">
<article class="kg-prose prose max-w-none min-w-0">{{ slots.main|safe }}</article>
{% if slots.aside %}<aside class="kg-crayon-shadow rounded-3xl border-[3px] border-primary/30 bg-card text-card-foreground p-6 text-sm h-max">{{ slots.aside|safe }}</aside>{% endif %}
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full" style="border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full flex items-center justify-center px-4 py-16">
<div class="w-full max-w-md">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="kg-body text-center text-muted-foreground">Sign-in form goes here.</p>{% endif %}</div>
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.9); backdrop-filter: blur(6px); border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full max-w-6xl mx-auto px-4 py-12">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="kg-empty">No stories yet — check back soon.</div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.9); backdrop-filter: blur(6px); border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full max-w-5xl mx-auto px-4 py-12">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="kg-empty">Add a contact form to this page.</div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.9); backdrop-filter: blur(6px); border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full max-w-6xl mx-auto px-4 py-12">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="kg-empty">No content blocks assigned to this page yet.</div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.9); backdrop-filter: blur(6px); border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-6xl mx-auto px-4 py-12"><div class="kg-empty">No content blocks assigned to this page yet.</div></div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="kg-page kg-dotted-paper min-h-screen flex flex-col antialiased">
{{ admin_banner_html|safe }}
<header class="w-full sticky top-0 z-40" style="background-color: hsl(var(--background) / 0.9); backdrop-filter: blur(6px); border-bottom: 3px solid hsl(var(--primary) / 0.25);">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-6xl mx-auto px-4 py-12"><div class="kg-empty">No content blocks assigned to this page yet.</div></div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,21 @@
{# announcement_bar (WO-TF-006): dismissable banner; dismissal persists in localStorage keyed by a content hash (variant + slugified message) so it reappears when the message changes; no-JS fallback is a static non-dismissable strip. Kindergarten override: mono broadcast readout — data-* + script preserved. #}
{% set anncls = "bg-primary/10 text-foreground" %}
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
<p class="kg-mono font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed kindergarten override: neon panel, mono meta. Facade script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
<figure class="bn-audio my-6 overflow-hidden rounded-2xl border border-primary/40 bg-card text-card-foreground kg-crayon-shadow{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-2xl border border-primary/30 bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="kg-display truncate font-medium text-foreground">{{ title }}</p>{% endif %}{% if artist %}<p class="kg-mono truncate text-sm text-muted-foreground">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="kg-mono inline-flex items-center gap-2 rounded-2xl bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="kg-mono inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">::lucide:download:sm:: Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,77 @@
{# auth-form kindergarten override — reproduces the builtin's security-critical wiring: hardcoded HTMX endpoints /api/auth/login + /api/auth/register (target #auth-message), /forgot-password link, OAuth start URLs gated on context.site.*, and the exact Cap widget markup when captchaEnabled. Only the chrome is themed. #}
{% if context.isPublicLoggedIn %}
<div class="auth-form-logged-in mx-auto max-w-md text-center py-6">
<p class="kg-mono text-sm text-muted-foreground">Session active as <strong class="text-primary">{{ context.publicUsername }}</strong></p>
<a href="/account" class="kg-mono mt-3 inline-flex items-center rounded-2xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">My Account</a>
</div>
{% else %}
{% set loginActive = "border-primary text-primary" %}
{% set registerActive = "border-transparent text-muted-foreground" %}
{% set loginHidden = "" %}
{% set registerHidden = "hidden" %}
{% if default_tab == "register" %}{% set loginActive = "border-transparent text-muted-foreground" %}{% set registerActive = "border-primary text-primary" %}{% set loginHidden = "hidden" %}{% set registerHidden = "" %}{% endif %}
{% set tabbase = "flex-1 py-2 text-center kg-mono text-sm font-medium border-b-2 transition-colors" %}
{% macro captcha() %}<script>
window.CAP_CUSTOM_WASM_URL = "/assets/captcha/cap_wasm_bg.wasm";
window.CAP_PAKO_URL = "/assets/captcha/pako_inflate.min.js";
</script>
<cap-widget data-cap-api-endpoint="/api/captcha/" data-cap-worker-count="2"></cap-widget>
<script src="/assets/captcha/cap.min.js" defer></script>
<script>
(function(){
var s = document.currentScript;
var form = s && s.closest ? s.closest('form') : null;
if (!form) return;
form.addEventListener('htmx:afterRequest', function(){
var w = form.querySelector('cap-widget');
if (w && typeof w.reset === 'function') w.reset();
});
})();
</script>{% endmacro %}
{% macro social() %}{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}<div class="auth-social my-4">
<div class="relative my-4"><hr class="border-border"/><span class="kg-mono absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-3 text-xs text-muted-foreground">or</span></div>
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="kg-mono mb-2 flex w-full items-center justify-center gap-3 rounded-2xl border border-border px-4 py-2.5 transition-colors hover:bg-accent/10"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#google"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Google</span></a>{% endif %}
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="kg-mono flex w-full items-center justify-center gap-3 rounded-2xl border border-border px-4 py-2.5 transition-colors hover:bg-accent/10"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#facebook"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Facebook</span></a>{% endif %}
</div>{% endif %}{% endmacro %}
<div class="auth-form mx-auto max-w-md rounded-2xl border border-primary/40 bg-card p-6 text-card-foreground kg-crayon-shadow">
<div id="auth-message"></div>
<div class="mb-5 flex border-b border-border">
<button type="button" data-auth-tab="login" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ loginActive }}">Login</button>
<button type="button" data-auth-tab="register" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ registerActive }}">Register</button>
</div>
<div id="login-panel" class="{{ loginHidden }}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="kg-mono mb-1 block text-xs font-medium text-accent">Email</label><input type="email" name="email" required class="w-full rounded-2xl border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-4"><label class="kg-mono mb-1 block text-xs font-medium text-accent">Password</label><input type="password" name="password" required class="w-full rounded-2xl border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
{% if captchaEnabled %}{{ captcha() }}{% endif %}
<button type="submit" class="kg-mono kg-pop w-full rounded-2xl bg-primary px-4 py-2.5 font-semibold text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Login</button>
<p class="mt-3 text-center text-sm"><a href="/forgot-password" class="kg-mono text-primary hover:underline">Forgot password?</a></p>
</form>
{{ social() }}
</div>
<div id="register-panel" class="{{ registerHidden }}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="kg-mono mb-1 block text-xs font-medium text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-2xl border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-3"><label class="kg-mono mb-1 block text-xs font-medium text-accent">Email</label><input type="email" name="email" required class="w-full rounded-2xl border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-4"><label class="kg-mono mb-1 block text-xs font-medium text-accent">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-2xl border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
{% if captchaEnabled %}{{ captcha() }}{% endif %}
<button type="submit" class="kg-mono kg-pop w-full rounded-2xl bg-primary px-4 py-2.5 font-semibold text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Create Account</button>
</form>
{{ social() }}
</div>
</div>
<script>
(function(){
var f=document.currentScript.closest('.auth-form')||document.querySelector('.auth-form');
if(!f)return;
var tabs=f.querySelectorAll('[data-auth-tab]');
tabs.forEach(function(btn){btn.addEventListener('click',function(){
var t=btn.getAttribute('data-auth-tab');
var lp=f.querySelector('#login-panel'),rp=f.querySelector('#register-panel');
if(lp)lp.classList.toggle('hidden',t!=='login');
if(rp)rp.classList.toggle('hidden',t!=='register');
tabs.forEach(function(b){var on=b===btn;b.className=b.getAttribute('data-tabbase')+(on?' border-primary text-primary':' border-transparent text-muted-foreground');});
});});
})();
</script>
{% endif %}

View File

@ -0,0 +1,36 @@
{# auth-status kindergarten override — reproduces the builtin's hardcoded wiring: /login + /login?tab=register when logged out; /account, /account/reviews, and hx-post /api/auth/logout dropdown when logged in; hx-post /api/auth/resend-verification banner when unverified. State from context.*. #}
{% if not context.isPublicLoggedIn %}
<div class="auth-status flex items-center gap-2">
<a href="/login" class="kg-mono rounded-2xl px-3 py-1.5 text-sm font-medium opacity-80 transition-colors hover:bg-primary/10 hover:opacity-100">Login</a>
<a href="/login?tab=register" class="kg-mono rounded-2xl bg-accent px-3 py-1.5 text-sm font-medium text-accent-foreground hover:bg-accent/90">Sign Up</a>
</div>
{% else %}
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 border-y border-destructive/50 bg-destructive/15 px-4 py-2 text-center text-sm text-destructive">
<span class="kg-mono">Verify your email address.</span> <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline font-medium">Resend verification email</button>
</div>{% endif %}
<div class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 rounded-2xl px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-primary/10 hover:opacity-100">
<span class="flex h-7 w-7 items-center justify-center rounded-2xl bg-accent text-accent-foreground"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#user"></use></svg></span>
<span class="kg-mono">{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 rounded-2xl border border-primary/40 bg-card py-1 text-card-foreground kg-crayon-shadow z-50">
<a href="/account" class="kg-mono block px-4 py-2 text-sm text-card-foreground hover:bg-primary/10">My Profile</a>
<a href="/account/reviews" class="kg-mono block px-4 py-2 text-sm text-card-foreground hover:bg-primary/10">My Reviews</a>
<hr class="my-1 border-border"/>
<button hx-post="/api/auth/logout" hx-swap="none" class="kg-mono block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-primary/10">Logout</button>
</div>
<script>
(function(){
document.addEventListener('click', function(e) {
var menu = document.getElementById('auth-dropdown-menu');
var btn = document.querySelector('.auth-status > button');
if (!menu || !btn) return;
if (!btn.contains(e.target) && !menu.contains(e.target)) {
menu.classList.add('hidden');
}
});
})();
</script>
</div>
{% endif %}

View File

@ -0,0 +1,21 @@
{# author-bio-hero (kindergarten override) — author-page header from the `authors` provider. #}
{% if author %}
<section class="relative overflow-hidden border-b border-primary/30 bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="relative z-10 mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary kg-crayon-shadow">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<h1 class="kg-display text-3xl font-bold tracking-tight md:text-4xl">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="kg-mono mt-1 text-sm text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="kg-body mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
<div class="mt-5 flex flex-wrap items-center justify-center gap-4 kg-mono text-sm text-accent">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,13 @@
{# author-bio (kindergarten override) — byline card sourced from the `post` provider's author_* fields. #}
{% if post.author_name %}
<aside class="rounded-2xl border border-dashed border-primary/40 bg-card p-6 text-card-foreground kg-crayon-shadow{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="kg-eyebrow kg-mono mb-4 text-xs font-semibold text-accent">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full border border-primary/40 bg-muted">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="kg-display text-base font-semibold">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="kg-body mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,23 @@
{# blog-hero (kindergarten override) — blog-index header + featured posts from the `posts` provider. #}
<section class="relative overflow-hidden bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="relative z-10 mx-auto max-w-6xl px-4">
<div class="max-w-2xl">
{% if title %}<h1 class="kg-display text-4xl font-bold tracking-tight md:text-5xl">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="kg-body mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
{% for post in posts %}
<article class="group flex flex-col rounded-2xl border border-dashed border-primary/40 bg-card p-4 text-card-foreground kg-crayon-shadow{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-3 block overflow-hidden rounded-2xl bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="kg-display {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="kg-body mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="mt-3 flex flex-wrap items-center gap-x-3 kg-mono text-xs text-accent">{% if post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,15 @@
{# breadcrumbs (kindergarten override) — trail from the `breadcrumbs` provider (page ancestry). #}
{# showHome prepends a Home link; separator is text ("/") for default or a chevron #}
{# for the arrows template. The trailing is_current item renders as plain text. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="flex flex-wrap items-center gap-1 kg-mono text-sm">
{% if showHome %}<li class="flex items-center"><a href="/" class="text-accent transition-colors hover:text-primary">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
{% for item in breadcrumbs %}
<li class="flex items-center">
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-primary">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="font-medium text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-accent transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -0,0 +1,2 @@
{# kindergarten button — keep the {% button %} tag intact (new fields: label / link / style / size / icon / iconPosition / newTab); wrap in a rounded kg-pop bounce so the host-rendered button gets the playful hover. #}
{% with align=align|default:"left" %}<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}"><span class="kg-btn kg-display kg-pop inline-flex [&_a]:rounded-full [&_button]:rounded-full">{% if newTab %}{% button style|default:"primary" text=label|default:"Let's go" href=link|default:"#" size=size|default:"md" icon=icon|default:"" icon_position=iconPosition|default:"left" target="_blank" %}{% else %}{% button style|default:"primary" text=label|default:"Let's go" href=link|default:"#" size=size|default:"md" icon=icon|default:"" icon_position=iconPosition|default:"left" %}{% endif %}</span></div>{% endwith %}

View File

@ -0,0 +1,2 @@
{# kindergarten card — crayon-bordered sticker panel; new builtin fields layout / link / media / title / text / linkLabel (reconciled from legacy body/image). #}
{% with layout=layout|default:"vertical" %}<article class="bn-card kg-crayon-shadow overflow-hidden rounded-3xl border-[3px] border-primary/40 bg-card text-card-foreground {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="kg-display text-lg font-bold tracking-tight text-foreground">{{ title }}</h3>{% endif %}{% if text %}<div class="kg-body text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="kg-display mt-2 inline-flex items-center gap-1 text-sm font-bold text-primary">{{ linkLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></span>{% endif %}</div>{% if link %}</a>{% endif %}</article>{% endwith %}

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