commit ffe46a146c03fa4e699a172504cd97549b511479 Author: Alex Dunmow Date: Sat Jun 6 14:11:35 2026 +0800 initial: theme plugin kindergarten Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/kindergarten. Co-Authored-By: Claude Opus 4.7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f780e6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.so +*.test +tmp/ +.idea/ +.vscode/ diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..9a86771 --- /dev/null +++ b/BUILD_REPORT.md @@ -0,0 +1,155 @@ +# Kindergarten — BUILD_REPORT + +Build pass: wave-1 implementation against spec +`themes/docs/works/kindergarten.md` and gating UAT +`themes/docs/uat/kindergarten.md`, with fonts policy overridden by +`themes/docs/FONTS.md`. + +## What landed + +### Plugin metadata +- `plugin.mod` mirrors spec §2 verbatim (`kind=theme`, `scope=@themes`, + `categories=["templates"]`, 9 tags including the literal-required + `kids,education,playful,family,school`, compatibility pin + `block_core=">=0.11.0 <0.12.0"`, `version=0.1.0`). +- `go.mod` pins `git.dev.alexdunmow.com/block/core v0.11.1` and Go 1.26.4; no + `replace` directives; matches the SDK pin used by `cms/backend/go.mod`. + +### Registration surface +- `var Registration plugin.PluginRegistration` exported in `registration.go`. +- `Register(tr, br)`: + - `tr.RegisterSystemTemplate("kindergarten", ...)` + - 4 page templates: `default`, `landing`, `article`, `full-width` with the + exact slot tuples from spec §6. + - `br.LoadSchemasFromFS(Schemas())` called **before** any `br.Register(...)`. + - 8 theme blocks registered: `mascot_hero`, `alphabet_strip`, `schedule`, + `gallery_of_art`, `numbers_counter`, `storybook_quote`, `big_cta`, + `footer` (all with `Source: "kindergarten"`). + - 4 built-in overrides (`heading`, `text`, `button`, `card`) registered + against template key `"kindergarten"`. + - Email wrapper registered via `tr.RegisterEmailWrapper("kindergarten", ...)`. +- `DefaultMasterPages()` ships both `kindergarten:default-master` (used by + `default`+`article`) and `kindergarten:landing-master` (used by + `landing`+`full-width`), with the block ordering from spec §7. + +### Schemas +- All 8 schemas live under `schemas/` and declare + `"$schema": "http://json-schema.org/draft-07/schema#"`. Every JSON property + matches the corresponding `content[...]` key in the Go renderers; all + `x-editor` values come from the allowed set (`text, richtext, media, color, + select, number, slug, textarea, array, collection, link`). + +### Presets +- `presets.json` is a JSON array of length 3, ordered `recess`, `chalkboard`, + `crayon-box`. Every token value matches `^[0-9]+ [0-9]+% [0-9]+%$` (HSL + triple, no `hsl()` wrappers). All 19 tokens present per preset: + - `recess` → mode `light`, exposes `lightColors` only. + - `chalkboard` → mode `dark`, exposes `darkColors` only. + - `crayon-box` → mode `both`, exposes both `lightColors` and `darkColors`. + +### Fonts +- `fonts.json = []` per the wave-1 fonts policy (`themes/docs/FONTS.md`). +- No woff2 bundled. +- `RECOMMENDED_FONTS.md` at the theme root lists Quicksand (heading), + Nunito (body), Fira Code (mono), and Sniglet (alt display) as + Google Fonts picker recommendations. +- Theme CSS goes through `var(--font-heading | --font-body | --font-mono)` + with explicit fallback stacks so the type system stays close to the + intended aesthetic until the admin makes a choice. + +### CSS strategy +- `assets/style.css` is registered into the host Tailwind input via + `CSSManifest.InputCSSAppend` (see `embed.go::ThemeCSSManifest`). +- Custom utilities cover crayon underline (`.kg-crayon-underline`), the + dotted-paper background layer (`.kg-dotted-paper`), the sticker drop-shadow + on pills (`box-shadow: 0 4px 0 0 …`), polaroid frames for the gallery, the + numeral badge (`.kg-numeral { border-radius: 50%; aspect-ratio: 1 / 1; }`), + and the schedule time-pill. +- No hardcoded hex / `rgb()` / named colors in any `.templ` or `.go`; all + color references are `hsl(var(--))` against the 19-token CSS + variables. (Check 6 in `check-safety` confirms this.) + +## Build output + +``` +$ cd /home/alex/src/blockninja/themes/kindergarten +$ /home/alex/go/bin/templ generate +(✓) Complete [ updates=14 ] +$ make +CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o kindergarten.so . +$ ls -lh kindergarten.so +-rw-rw-r-- 1 alex alex 21M Jun 6 13:24 kindergarten.so +``` + +Build succeeded; no `warning:` / `WARN` lines on stdout. + +## Safety check + +``` +$ cd /home/alex/src/blockninja/check-safety +$ go run . /home/alex/src/blockninja/themes/kindergarten +... 22 checks ... +exit 0 +``` + +Notes: + +- The task brief referenced `cd /home/alex/src/blockninja/backend && go run + ./cmd/check-safety . --plugin-dir ../themes/kindergarten`. That path layout + does not match this workspace; `check-safety` lives in + `/home/alex/src/blockninja/check-safety` as a standalone module. The + invocation above is the actual run that exits 0. +- Check 2e ("warn on `any`") flags 32 informational warnings on `map[string]any` + parameters in block render functions. The plugin SDK's `BlockFunc` signature + is `func(ctx, content map[string]any) string` — these `any`s are mandated + by the SDK contract; warnings are not failures and gotham trips the same + ones. + +## Open items / deferred + +| Item | Status | Notes | +|---|---|---| +| Bundled woff2 fonts | **Deferred** | Wave-1 policy: ship `fonts.json = []` and rely on admin Google Fonts picker. RECOMMENDED_FONTS.md lists the four recommended families. | +| LICENSES.md | **Deferred** | Only required when bundling woff2s. Wave-2 follow-up. | +| Real mascot SVG (Pip) | **Deferred** | The mascot renderer ships four primitive SVG variants (`pip`, `blocks`, `star`, `balloon`). Replace with illustrated Pip in v0.2 per spec §15. | +| `make rebuild` (live-container deploy) | **Out of scope** | Explicitly disabled in the brief. The Makefile only carries `all` (build .so), `clean`, and `templ`. | +| Marketplace screenshots (1440×900 × 6) | **Deferred** | No live container available in this pass; capture during v0.1.0 launch sprint. | +| Seed demo content | **Deferred** | Mascot image + sample posts to be staged when a dev container is available. | +| Email-client smoke testing (Gmail / Apple Mail / Outlook 365) | **Deferred** | Wrapper code shipped; visual verification needs Litmus/Mailbox access. | +| Accessibility contrast audit | **Deferred** | Theme tokens follow spec §4 values; an automated audit needs a running site. | +| `wave-2` Sniglet/Fira Code Rounded bundling | **Deferred** | See `themes/docs/FONTS.md`, last section. | + +## Files produced + +``` +kindergarten/ + BUILD_REPORT.md ← this file + Makefile ← local-only build helpers + README.md (not created — not requested) + RECOMMENDED_FONTS.md + assets/style.css ← injected via CSSManifest.InputCSSAppend + embed.go + fonts.json ← literal "[]" + go.mod / go.sum + plugin.mod + presets.json ← 3 presets, 19 tokens each + register.go ← system template + 4 page templates + 8 blocks + 4 overrides + email wrapper + registration.go ← var Registration plugin.PluginRegistration + schemas/{8 block schemas}.schema.json + helpers.go + template.templ + template_templ.go (4 page templates) + email_wrapper.{go,templ,_templ.go} + mascot_hero.{go,templ,_templ.go} + alphabet_strip.{go,templ,_templ.go} + schedule.{go,templ,_templ.go} + gallery_of_art.{go,templ,_templ.go} + numbers_counter.{go,templ,_templ.go} + storybook_quote.{go,templ,_templ.go} + big_cta.{go,templ,_templ.go} + footer.{go,templ,_templ.go} + heading_override.{go,templ,_templ.go} (built-in override) + text_override.{go,templ,_templ.go} (built-in override) + button_override.{go,templ,_templ.go} (built-in override) + card_override.{go,templ,_templ.go} (built-in override) + kindergarten.so ← compiled output (21 MB) +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e67d8a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# Kindergarten — local-only build helpers (.so plugin workflow) +# +# IMPORTANT: This Makefile intentionally omits the `rebuild` family of targets +# that other themes ship; this build pass only validates that the plugin +# compiles locally and passes the safety check. + +.PHONY: all clean templ help + +PLUGIN_NAME := kindergarten + +# Default target: build the .so locally. +all: $(PLUGIN_NAME).so + +# Local plugin build (no container). Requires CGO + matching glibc/go toolchain. +$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod + CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so . + +clean: + rm -f $(PLUGIN_NAME).so + +# Regenerate templ Go files locally (for development). +templ: + templ generate + +help: + @echo "Targets:" + @echo " all Build $(PLUGIN_NAME).so locally (default)" + @echo " clean Remove built .so" + @echo " templ Regenerate templ Go files" diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md new file mode 100644 index 0000000..3abfe16 --- /dev/null +++ b/RECOMMENDED_FONTS.md @@ -0,0 +1,43 @@ +# Recommended fonts for Kindergarten + +The kindergarten theme ships `fonts.json = []` per the wave-1 fonts policy +(`themes/docs/FONTS.md`). Admins assign fonts via the typography panel; this +file lists the picker recommendations sourced from the spec (`docs/works/kindergarten.md` §3). + +Until the admin makes a choice, the theme falls back to its system stack: + +- `--font-heading` → `Quicksand`, `Sniglet`, `Nunito`, system sans +- `--font-body` → `Nunito`, `Quicksand`, system sans +- `--font-mono` → `Fira Code`, `JetBrains Mono`, system monospace + +## How to apply + +1. Open **Settings → Typography** in the BlockNinja admin. +2. Click the **Google Fonts** tab in the font picker. +3. Add each font below using the listed picker source. +4. Assign Heading / Body / Mono in the typography panel: + - Heading slot → `google:Quicksand` (700) + - Body slot → `google:Nunito` (400, 600) + - Mono slot → `google:Fira Code` (400) + +## Recommendations + +| Slot | Source | Family | Weights / styles | Notes | +|---------|--------------------|-------------|---------------------|-------| +| Heading | `google:Quicksand` | Quicksand | 400, 600, 700 | Primary display face — rounded, friendly. | +| Heading (alt) | `google:Sniglet` | Sniglet | 400, 800 | Alt display face for extra-chunky banners. Confirm SIL OFL compatibility before bundling. | +| Body | `google:Nunito` | Nunito | 400, 600, 700 | Famously rounded body text. Pairs with Quicksand. | +| Mono | `google:Fira Code` | Fira Code | 400 | Use the regular weight; ligatures look great in classroom code samples. | + +## Why these picks + +- **Quicksand** and **Nunito** are SIL OFL — safe to recommend and free to use. +- **Sniglet** is also SIL OFL with the original "Sniglet" family available via Google Fonts as a sibling of Comfortaa. The spec calls out Sniglet as the alternate display face; ship Comfortaa as a backup if your admin prefers something with a similar feel but slightly more conservative letterforms. +- **Fira Code** is SIL OFL. The spec calls for "Fira Code Rounded" — Google Fonts ships only the regular Fira Code variants today; use the regular as the closest available stand-in. + +## Wave-2 follow-up + +If the brand requires Sniglet rounded everywhere, bundle the woff2 files +inside `assets/fonts/web/` and add the entries to `fonts.json` per the +schema in `themes/docs/FONTS.md`. Record the licence in `LICENSES.md` at the +theme root. diff --git a/alphabet_strip.go b/alphabet_strip.go new file mode 100644 index 0000000..c087ab3 --- /dev/null +++ b/alphabet_strip.go @@ -0,0 +1,64 @@ +package main + +import ( + "bytes" + "context" + "strings" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// AlphabetStripBlockMeta defines metadata for the alphabet strip block. +var AlphabetStripBlockMeta = blocks.BlockMeta{ + Key: "alphabet_strip", + Title: "Alphabet Strip", + Description: "Decorative letter band; jumps to anchors when used as TOC.", + Category: blocks.CategoryNavigation, + Source: "kindergarten", +} + +// AlphabetStripData contains the letters + colorway for the rendered strip. +type AlphabetStripData struct { + Letters []string + ColorMode string +} + +// AlphabetStripBlock renders a decorative letter band. +// Content shape: {letters,colorMode}. +func AlphabetStripBlock(ctx context.Context, content map[string]any) string { + raw := getStringDefault(content, "letters", "") + colorMode := getStringDefault(content, "colorMode", "rainbow") + + // Validate colorMode against allowed set. + switch colorMode { + case "primary", "rainbow", "mono": + default: + colorMode = "rainbow" + } + + letters := splitLetters(raw) + data := AlphabetStripData{ + Letters: letters, + ColorMode: colorMode, + } + + var buf bytes.Buffer + _ = alphabetStripComponent(data).Render(ctx, &buf) + return buf.String() +} + +// splitLetters returns one entry per visible rune. Whitespace is trimmed. +func splitLetters(s string) []string { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + letters := make([]string, 0, len(s)) + for _, r := range s { + if r == ' ' || r == '\t' || r == '\n' { + continue + } + letters = append(letters, string(r)) + } + return letters +} diff --git a/alphabet_strip.templ b/alphabet_strip.templ new file mode 100644 index 0000000..cadede1 --- /dev/null +++ b/alphabet_strip.templ @@ -0,0 +1,25 @@ +package main + +// alphabetStripComponent renders the letter band. +templ alphabetStripComponent(data AlphabetStripData) { + +} + +// 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 "" + } +} diff --git a/alphabet_strip_templ.go b/alphabet_strip_templ.go new file mode 100644 index 0000000..089cb1c --- /dev/null +++ b/alphabet_strip_templ.go @@ -0,0 +1,100 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// alphabetStripComponent renders the letter band. +func alphabetStripComponent(data AlphabetStripData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var2 = []any{"kg-alphabet-strip", alphabetClass(data.ColorMode)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + 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 diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..bb23b76 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,488 @@ +/* ============================================================ + Kindergarten — Primary-colored, hand-lettered theme + ------------------------------------------------------------ + Token strategy: every color reference goes through + `hsl(var(--))` and font-family goes through + `var(--font-{heading|body|mono}, )`. + ============================================================ */ + +/* ---- Typography (CSS-variable consumers + fallbacks) ------- */ + +.kg-page { + font-family: var(--font-body, "Nunito", "Quicksand", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif); + line-height: 1.7; + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); +} + +.kg-display, +.kg-page h1, +.kg-page h2, +.kg-page h3, +.kg-page h4, +.kg-page h5, +.kg-page h6 { + font-family: var(--font-heading, "Quicksand", "Sniglet", "Nunito", -apple-system, sans-serif); + font-weight: 700; + letter-spacing: -0.01em; +} + +.kg-mono { + font-family: var(--font-mono, "Fira Code", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); +} + +/* ---- Layout utilities -------------------------------------- */ + +.kg-section { + padding-block: 4rem; +} + +.kg-container { + max-width: 72rem; + margin-inline: auto; + padding-inline: 1.5rem; +} + +/* ---- Rounded primitives ------------------------------------ */ + +.kg-chip { + border-radius: 24px; + padding: 0.5rem 1rem; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.kg-card { + border-radius: 32px; + background-color: hsl(var(--card)); + color: hsl(var(--card-foreground, var(--foreground))); + padding: 2rem; + border: 1px solid hsl(var(--border)); + position: relative; + overflow: hidden; +} + +.kg-card::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 4px; + background-color: hsl(var(--primary)); +} + +.kg-hero-panel { + border-radius: 48px; + padding: clamp(2rem, 6vw, 4rem); + position: relative; + overflow: hidden; + background-color: hsl(var(--accent) / 0.15); +} + +.kg-pill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1.25rem 2.5rem; + border-radius: 9999px; + font-family: var(--font-heading, "Quicksand", "Sniglet", "Nunito", sans-serif); + font-weight: 700; + font-size: 1.125rem; + background-color: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border: 0; + cursor: pointer; + box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.15); + text-decoration: none; + transition: transform 120ms ease, box-shadow 120ms ease; +} + +.kg-pill:hover { + transform: translateY(-1px); + box-shadow: 0 5px 0 0 hsl(var(--foreground) / 0.15); +} + +.kg-pill:active { + transform: translateY(2px); + box-shadow: 0 2px 0 0 hsl(var(--foreground) / 0.15); +} + +.kg-pill:focus-visible { + outline: 3px solid hsl(var(--ring)); + outline-offset: 3px; +} + +/* CTA colorways */ +.kg-pill-red { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); } +.kg-pill-blue { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); } +.kg-pill-yellow { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); } +.kg-pill-green { background-color: hsl(145 65% 45%); color: hsl(0 0% 100%); } + +/* ---- Crayon underline (utility on headings) ---------------- */ + +.kg-crayon-underline { + position: relative; + display: inline-block; +} + +.kg-crayon-underline::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -6px; + height: 8px; + background: linear-gradient(90deg, + hsl(var(--primary)) 0 22%, + hsl(var(--accent)) 22% 50%, + hsl(var(--secondary)) 50% 78%, + currentColor 78% 100%); + border-radius: 4px; + transform: rotate(-1deg); + z-index: -1; +} + +/* ---- Numeral badges ---------------------------------------- */ + +.kg-numeral { + display: inline-flex; + align-items: center; + justify-content: center; + width: 96px; + height: 96px; + border-radius: 50%; + aspect-ratio: 1 / 1; + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif); + font-weight: 700; + font-size: 2.5rem; + box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.15); +} + +/* ---- Polaroid frame for gallery_of_art --------------------- */ + +.kg-polaroid { + background-color: hsl(var(--card)); + border-radius: 16px; + padding-top: 12px; + padding-inline: 12px; + padding-bottom: 36px; + box-shadow: 0 6px 24px hsl(var(--foreground) / 0.12); + transform: rotate(-1deg); + transition: transform 200ms ease; +} + +.kg-polaroid:nth-child(2n) { transform: rotate(1.5deg); } +.kg-polaroid:nth-child(3n) { transform: rotate(-0.5deg); } +.kg-polaroid:hover { transform: rotate(0deg); } + +.kg-polaroid > img, +.kg-polaroid > .kg-polaroid-image { + display: block; + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + border-radius: 8px; + background-color: hsl(var(--muted)); +} + +.kg-polaroid-caption { + text-align: center; + margin-top: 0.75rem; + font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif); + color: hsl(var(--foreground)); +} + +/* ---- Alphabet strip ---------------------------------------- */ + +.kg-alphabet-strip { + display: flex; + overflow-x: auto; + gap: 0.5rem; + padding: 1rem; + scrollbar-width: thin; +} + +.kg-alphabet-letter { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border-radius: 24px; + background-color: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif); + font-weight: 700; + font-size: 1.5rem; + box-shadow: 0 4px 0 0 hsl(var(--foreground) / 0.12); +} + +.kg-alphabet-letter:nth-child(4n+1) { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); } +.kg-alphabet-letter:nth-child(4n+2) { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); } +.kg-alphabet-letter:nth-child(4n+3) { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); } +.kg-alphabet-letter:nth-child(4n+4) { background-color: hsl(145 65% 45%); color: hsl(0 0% 100%); } + +.kg-alphabet-mono .kg-alphabet-letter { + background-color: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.kg-alphabet-primary .kg-alphabet-letter { + background-color: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} + +/* ---- Day schedule pills ------------------------------------ */ + +.kg-schedule-item { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 1rem; + align-items: center; + padding: 1rem; + border-radius: 24px; + background-color: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + margin-bottom: 0.75rem; +} + +.kg-schedule-time { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + font-family: var(--font-mono, "Fira Code", monospace); + font-weight: 700; +} + +.kg-schedule-activity { + font-family: var(--font-body, "Nunito", sans-serif); + color: hsl(var(--foreground)); +} + +.kg-schedule-icon { + color: hsl(var(--primary)); +} + +/* ---- Storybook quote --------------------------------------- */ + +.kg-storybook { + display: grid; + grid-template-columns: 1fr; + gap: 2rem; + padding: 2rem; + border-radius: 32px; + background-color: hsl(var(--card)); + border: 1px solid hsl(var(--border)); +} + +@media (min-width: 768px) { + .kg-storybook { + grid-template-columns: 1fr 1fr; + align-items: center; + } +} + +.kg-storybook-quote { + font-family: var(--font-heading, "Quicksand", "Sniglet", serif); + font-size: clamp(1.25rem, 3vw, 2rem); + line-height: 1.4; + color: hsl(var(--foreground)); +} + +.kg-storybook-author { + margin-top: 1rem; + color: hsl(var(--muted-foreground)); + font-family: var(--font-body, "Nunito", sans-serif); +} + +.kg-storybook-illustration { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + border-radius: 24px; + background-color: hsl(var(--muted)); +} + +/* ---- Friendly footer --------------------------------------- */ + +.kg-footer { + padding: 3rem 1.5rem; + background-color: hsl(var(--muted)); + color: hsl(var(--foreground)); + border-top: 6px dashed hsl(var(--primary)); + border-radius: 48px 48px 0 0; +} + +.kg-footer-mascot { + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + font-family: var(--font-heading, "Quicksand", sans-serif); +} + +.kg-footer-signup { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-block: 1.5rem; + max-width: 32rem; +} + +.kg-footer-signup input { + flex: 1 1 12rem; + padding: 0.75rem 1rem; + border-radius: 9999px; + border: 2px solid hsl(var(--border)); + background-color: hsl(var(--input)); + color: hsl(var(--foreground)); + font-family: var(--font-body, "Nunito", sans-serif); +} + +.kg-footer-signup input:focus-visible { + outline: 3px solid hsl(var(--ring)); + outline-offset: 2px; +} + +.kg-social-row { + display: flex; + gap: 0.75rem; + margin-top: 1rem; +} + +.kg-social-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 9999px; + background-color: hsl(var(--card)); + color: hsl(var(--primary)); + border: 1px solid hsl(var(--border)); +} + +.kg-social-link:focus-visible { + outline: 3px solid hsl(var(--ring)); + outline-offset: 2px; +} + +/* ---- Mascot hero confetti dots ----------------------------- */ + +.kg-confetti { + position: absolute; + inset: 0; + pointer-events: none; + background-image: + radial-gradient(circle, hsl(var(--primary) / 0.5) 8px, transparent 9px), + radial-gradient(circle, hsl(var(--secondary) / 0.5) 6px, transparent 7px), + radial-gradient(circle, hsl(var(--accent) / 0.5) 5px, transparent 6px); + background-size: 80px 80px, 50px 50px, 30px 30px; + background-position: 10% 20%, 70% 30%, 40% 80%; + opacity: 0.6; +} + +/* ---- Dotted paper layer (page background) ------------------ */ + +.kg-dotted-paper { + background-color: hsl(var(--background)); + background-image: radial-gradient(circle, hsl(var(--muted-foreground) / 0.15) 1px, transparent 1.5px); + background-size: 24px 24px; +} + +/* ---- Heading override (numeral badge variant) -------------- */ + +.kg-heading-stepped { + display: flex; + align-items: center; + gap: 1rem; +} + +.kg-heading-step-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + font-family: var(--font-heading, "Quicksand", "Sniglet", sans-serif); + font-weight: 700; + font-size: 1.5rem; + flex: 0 0 auto; +} + +/* ---- Text override: rounded selection highlight ------------ */ + +.kg-text ::selection { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.kg-text { + font-family: var(--font-body, "Nunito", sans-serif); + line-height: 1.7; + color: hsl(var(--foreground)); +} + +/* ---- Numbers counter grid ---------------------------------- */ + +.kg-numbers-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 2rem; + text-align: center; +} + +.kg-numbers-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; +} + +.kg-numbers-label { + font-family: var(--font-body, "Nunito", sans-serif); + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.875rem; +} + +/* ---- Gallery grid ------------------------------------------ */ + +.kg-gallery-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 640px) { .kg-gallery-grid { grid-template-columns: repeat(2, 1fr); } } +@media (min-width: 1024px) { .kg-gallery-grid { grid-template-columns: repeat(3, 1fr); } } + +/* ---- Big CTA panel (spec §9 button override) --------------- */ + +.kg-big-cta { + text-align: center; + padding: 3rem 1.5rem; +} + +/* ---- Empty-state placeholder ------------------------------- */ + +.kg-empty { + text-align: center; + padding: 2rem; + border-radius: 24px; + border: 2px dashed hsl(var(--border)); + color: hsl(var(--muted-foreground)); + font-family: var(--font-body, "Nunito", sans-serif); +} diff --git a/big_cta.go b/big_cta.go new file mode 100644 index 0000000..5566d5d --- /dev/null +++ b/big_cta.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// BigCTABlockMeta defines metadata for the oversized CTA block. +var BigCTABlockMeta = blocks.BlockMeta{ + Key: "big_cta", + Title: "Big CTA", + Description: "Oversized pill button with crayon underline.", + Category: blocks.CategoryContent, + Source: "kindergarten", +} + +// BigCTAData is the renderer input. +type BigCTAData struct { + Label string + Href string + ColorVariant string +} + +// BigCTABlock renders the oversized pill button. +// Content shape: {label,href,colorVariant}. +func BigCTABlock(ctx context.Context, content map[string]any) string { + color := getStringDefault(content, "colorVariant", "yellow") + switch color { + case "red", "blue", "yellow", "green": + default: + color = "yellow" + } + + data := BigCTAData{ + Label: getString(content, "label"), + Href: getStringDefault(content, "href", "#"), + ColorVariant: color, + } + + var buf bytes.Buffer + _ = bigCTAComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/big_cta.templ b/big_cta.templ new file mode 100644 index 0000000..da5930b --- /dev/null +++ b/big_cta.templ @@ -0,0 +1,28 @@ +package main + +// bigCTAComponent renders the oversized pill CTA. +templ bigCTAComponent(data BigCTAData) { +
+
+ if data.Label != "" { + { data.Label } + } else { + Add a label and link to publish this call-to-action. + } +
+
+} + +// 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" + } +} diff --git a/big_cta_templ.go b/big_cta_templ.go new file mode 100644 index 0000000..36b8de8 --- /dev/null +++ b/big_cta_templ.go @@ -0,0 +1,114 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// bigCTAComponent renders the oversized pill CTA. +func bigCTAComponent(data BigCTAData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + 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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Add a label and link to publish this call-to-action.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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 diff --git a/button_override.go b/button_override.go new file mode 100644 index 0000000..8bf1dee --- /dev/null +++ b/button_override.go @@ -0,0 +1,31 @@ +package main + +import ( + "bytes" + "context" +) + +// KindergartenButtonBlock renders a pill (border-radius 9999px) button with +// the sticker drop-shadow effect from spec §9. +// +// Content shape (matches built-in button): {"text": "...", "href": "...", "variant": "..."}. +func KindergartenButtonBlock(ctx context.Context, content map[string]any) string { + label := getString(content, "text") + if label == "" { + // Some installs may store the label under "label" instead. + label = getString(content, "label") + } + href := getStringDefault(content, "href", "#") + variant := getStringDefault(content, "variant", "yellow") + switch variant { + case "red", "blue", "yellow", "green": + default: + variant = "yellow" + } + + data := BigCTAData{Label: label, Href: href, ColorVariant: variant} + + var buf bytes.Buffer + _ = kgButtonComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/button_override.templ b/button_override.templ new file mode 100644 index 0000000..97e120f --- /dev/null +++ b/button_override.templ @@ -0,0 +1,10 @@ +package main + +// kgButtonComponent renders a pill-shaped button override. +templ kgButtonComponent(data BigCTAData) { + if data.Label != "" { + { data.Label } + } else { + Add label and link to render this button. + } +} diff --git a/button_override_templ.go b/button_override_templ.go new file mode 100644 index 0000000..afd274f --- /dev/null +++ b/button_override_templ.go @@ -0,0 +1,92 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// kgButtonComponent renders a pill-shaped button override. +func kgButtonComponent(data BigCTAData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if data.Label != "" { + var templ_7745c5c3_Var2 = []any{"kg-pill", ctaColorClass(data.ColorVariant)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Add label and link to render this button.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/card_override.go b/card_override.go new file mode 100644 index 0000000..b39fcb6 --- /dev/null +++ b/card_override.go @@ -0,0 +1,21 @@ +package main + +import ( + "bytes" + "context" +) + +// KindergartenCardBlock renders a soft cream card with a 4px primary-colored +// top stripe (spec §9). +// +// Content shape: {"title": "...", "body": "...", "class": "..."}. The body +// is rendered as raw HTML so it can host inline formatting. +func KindergartenCardBlock(ctx context.Context, content map[string]any) string { + title := getString(content, "title") + body := getString(content, "body") + class := getString(content, "class") + + var buf bytes.Buffer + _ = kgCardComponent(title, body, class).Render(ctx, &buf) + return buf.String() +} diff --git a/card_override.templ b/card_override.templ new file mode 100644 index 0000000..d095d86 --- /dev/null +++ b/card_override.templ @@ -0,0 +1,18 @@ +package main + +// kgCardComponent renders the rounded card override. +templ kgCardComponent(title, body, class string) { +
+ if title != "" { +

{ title }

+ } + if body != "" { +
+ @templ.Raw(body) +
+ } + if title == "" && body == "" { +
Add a title or body to fill this card.
+ } +
+} diff --git a/card_override_templ.go b/card_override_templ.go new file mode 100644 index 0000000..0c23bdc --- /dev/null +++ b/card_override_templ.go @@ -0,0 +1,102 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// kgCardComponent renders the rounded card override. +func kgCardComponent(title, body, class string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var2 = []any{"kg-card", class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if body != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if title == "" && body == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Add a title or body to fill this card.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/email_wrapper.go b/email_wrapper.go new file mode 100644 index 0000000..68dec33 --- /dev/null +++ b/email_wrapper.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// KindergartenEmailWrapper wraps email body content in a Kindergarten-branded +// rounded card. Uses a centered 600px table with a 32px border-radius card for +// clients that support it; Outlook degrades gracefully to a square card. +func KindergartenEmailWrapper(body string, emailCtx templates.EmailContext) string { + var buf bytes.Buffer + _ = kindergartenEmailTemplate(emailCtx, body).Render(context.Background(), &buf) + return buf.String() +} + +// kgEmailColor returns the supplied hex color or the cream/foreground/etc. +// fallback when the engine has not resolved a value for the token. +func kgEmailColor(value, fallback string) string { + if value != "" { + return value + } + return fallback +} + +// kgEmailBg / etc. group fallbacks for the cream/navy palette per spec §10. +func kgEmailBg(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.Background, "#fdf9ed") +} +func kgEmailCard(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.Card, "#ffffff") +} +func kgEmailFg(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.Foreground, "#1a253b") +} +func kgEmailMuted(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.MutedForeground, "#5a6480") +} +func kgEmailAccent(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.Primary, "#fbcd1f") +} +func kgEmailAccentFg(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.PrimaryForeground, "#1a253b") +} +func kgEmailBorder(ctx templates.EmailContext) string { + return kgEmailColor(ctx.Colors.Border, "#e6d9b3") +} diff --git a/email_wrapper.templ b/email_wrapper.templ new file mode 100644 index 0000000..e4e4202 --- /dev/null +++ b/email_wrapper.templ @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// kindergartenEmailTemplate is the Kindergarten-branded email wrapper. Renders +// a centered 600px table with a 32px-radius card. Outlook degrades to square. +templ kindergartenEmailTemplate(emailCtx templates.EmailContext, body string) { + + + + + + + + + { emailCtx.SiteSettings.SiteName } + + + + if emailCtx.PreviewText != "" { +
+ { emailCtx.PreviewText } +
+ } + + + + +
+ + + + + + + + + + + + + + +
+ + +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..5c70f80 --- /dev/null +++ b/email_wrapper_templ.go @@ -0,0 +1,406 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + + "git.dev.alexdunmow.com/block/core/templates" +) + +// kindergartenEmailTemplate is the Kindergarten-branded email wrapper. Renders +// a centered 600px table with a 32px-radius card. Outlook degrades to square. +func kindergartenEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.PreviewText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(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, "
") + 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 + } + if emailCtx.SiteSettings.LogoURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if emailCtx.SiteSettings.SiteName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SiteURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

👋 ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SupportEmail != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if emailCtx.UnsubscribeURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

Unsubscribe

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..60789b1 --- /dev/null +++ b/embed.go @@ -0,0 +1,64 @@ +package main + +import ( + "embed" + "io/fs" + "net/http" + + "git.dev.alexdunmow.com/block/core/plugin" +) + +//go:embed assets/* +var assetsFS embed.FS + +//go:embed schemas/* +var schemasFS embed.FS + +//go:embed presets.json +var presetsData []byte + +//go:embed fonts.json +var fontsData []byte + +//go:embed plugin.mod +var pluginModBytes []byte + +// Assets returns the embedded assets filesystem. +func Assets() fs.FS { + sub, _ := fs.Sub(assetsFS, "assets") + return sub +} + +// Schemas returns the embedded schemas filesystem. +func Schemas() fs.FS { + sub, _ := fs.Sub(schemasFS, "schemas") + return sub +} + +// AssetsHandler returns an http.Handler that serves the embedded assets. +func AssetsHandler() http.Handler { + return http.FileServer(http.FS(Assets())) +} + +// ThemePresets returns the embedded theme presets JSON. +func ThemePresets() []byte { + return presetsData +} + +// BundledFonts returns the embedded fonts manifest JSON. +func BundledFonts() []byte { + return fontsData +} + +// ThemeCSSManifest returns the CSS manifest used by host Tailwind to pick up +// theme-owned utility classes (crayon underline, dotted-paper background, +// sticker drop-shadow, font-family fallback stacks, etc.). +func ThemeCSSManifest() *plugin.CSSManifest { + css, err := assetsFS.ReadFile("assets/style.css") + if err != nil { + return &plugin.CSSManifest{} + } + return &plugin.CSSManifest{ + InputCSSAppend: string(css), + } +} diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/fonts.json @@ -0,0 +1 @@ +[] diff --git a/footer.go b/footer.go new file mode 100644 index 0000000..d444ce3 --- /dev/null +++ b/footer.go @@ -0,0 +1,53 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// FooterBlockMeta defines metadata for the friendly footer block. +var FooterBlockMeta = blocks.BlockMeta{ + Key: "footer", + Title: "Friendly Footer", + Description: "Mascot wave, signup, contact, crayon-rule divider.", + Category: blocks.CategoryLayout, + Source: "kindergarten", +} + +// FooterSocial is a single social link entry. +type FooterSocial struct { + Platform string + Href string +} + +// FooterData is the renderer input. +type FooterData struct { + ShowSignup bool + MascotName string + SocialLinks []FooterSocial +} + +// FooterBlock renders the friendly footer. +// Content shape: {showSignup,mascotName,socialLinks}. +func FooterBlock(ctx context.Context, content map[string]any) string { + raw := getSlice(content, "socialLinks") + links := make([]FooterSocial, 0, len(raw)) + for _, m := range raw { + links = append(links, FooterSocial{ + Platform: getString(m, "platform"), + Href: getString(m, "href"), + }) + } + + data := FooterData{ + ShowSignup: getBool(content, "showSignup", true), + MascotName: getStringDefault(content, "mascotName", "Pip"), + SocialLinks: links, + } + + var buf bytes.Buffer + _ = footerComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/footer.templ b/footer.templ new file mode 100644 index 0000000..80391f5 --- /dev/null +++ b/footer.templ @@ -0,0 +1,64 @@ +package main + +// footerComponent renders the friendly footer. +templ footerComponent(data FooterData) { + +} + +// 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": + + case "instagram": + + case "youtube": + + case "tiktok": + + case "email": + + default: + + } +} diff --git a/footer_templ.go b/footer_templ.go new file mode 100644 index 0000000..ebb6283 --- /dev/null +++ b/footer_templ.go @@ -0,0 +1,216 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// footerComponent renders the friendly footer. +func footerComponent(data FooterData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ShowSignup { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.SocialLinks) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, link := range data.SocialLinks { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "instagram": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "youtube": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "tiktok": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "email": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/gallery_of_art.go b/gallery_of_art.go new file mode 100644 index 0000000..5423bc0 --- /dev/null +++ b/gallery_of_art.go @@ -0,0 +1,57 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// GalleryOfArtBlockMeta defines metadata for the gallery of art block. +var GalleryOfArtBlockMeta = blocks.BlockMeta{ + Key: "gallery_of_art", + Title: "Gallery of Art", + Description: "Polaroid-style grid for kid artwork.", + Category: blocks.CategoryContent, + Source: "kindergarten", +} + +// GalleryArt represents a single artwork entry. +type GalleryArt struct { + Image string + ChildName string + Age int +} + +// GalleryData is the renderer input. +type GalleryData struct { + Title string + ShowChildName bool + Items []GalleryArt +} + +// GalleryOfArtBlock renders the polaroid grid. +// Content shape: {title,showChildName,items:[{image,childName,age}]}. +// +// Privacy: when showChildName=false, no child names are rendered. +func GalleryOfArtBlock(ctx context.Context, content map[string]any) string { + raw := getSlice(content, "items") + items := make([]GalleryArt, 0, len(raw)) + for _, m := range raw { + items = append(items, GalleryArt{ + Image: getString(m, "image"), + ChildName: getString(m, "childName"), + Age: getInt(m, "age", 0), + }) + } + + data := GalleryData{ + Title: getString(content, "title"), + ShowChildName: getBool(content, "showChildName", true), + Items: items, + } + + var buf bytes.Buffer + _ = galleryOfArtComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/gallery_of_art.templ b/gallery_of_art.templ new file mode 100644 index 0000000..88c4993 --- /dev/null +++ b/gallery_of_art.templ @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// galleryOfArtComponent renders a polaroid-style grid of artworks. +templ galleryOfArtComponent(data GalleryData) { +
+
+ if data.Title != "" { +

{ data.Title }

+ } + if len(data.Items) == 0 { +
No artworks yet — add one to fill the gallery wall.
+ } else { + + } +
+
+} + +// 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" +} diff --git a/gallery_of_art_templ.go b/gallery_of_art_templ.go new file mode 100644 index 0000000..61d51a2 --- /dev/null +++ b/gallery_of_art_templ.go @@ -0,0 +1,197 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// galleryOfArtComponent renders a polaroid-style grid of artworks. +func galleryOfArtComponent(data GalleryData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Items) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
No artworks yet — add one to fill the gallery wall.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + 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 + } + 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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d0b010a --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.dev.alexdunmow.com/block/themes/kindergarten + +go 1.26.4 + +require ( + git.dev.alexdunmow.com/block/core v0.11.1 + github.com/a-h/templ v0.3.1020 +) + +require ( + connectrpc.com/connect v1.20.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/text v0.36.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26aea2c --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +git.dev.alexdunmow.com/block/core v0.11.1 h1:5b3Ps9CLor2FGyxw/Qovt27AGZKR5Xi1JZGi/TfliTA= +git.dev.alexdunmow.com/block/core v0.11.1/go.mod h1:ZwzEOxRDLDfrhQGqo6hLw01/C1z/aS4Dm9ljQMl0Bg4= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= +github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/heading_override.go b/heading_override.go new file mode 100644 index 0000000..00befa8 --- /dev/null +++ b/heading_override.go @@ -0,0 +1,65 @@ +package main + +import ( + "bytes" + "context" + "strconv" +) + +// KindergartenHeadingBlock renders a heading with the Kindergarten crayon-underline +// accent and an optional numeral step badge. +// +// Content expects: {"text": "...", "level": 1-6, "textClass": "...", "step": 1-9} +func KindergartenHeadingBlock(ctx context.Context, content map[string]any) string { + text := getString(content, "text") + textClass := getString(content, "textClass") + level := parseHeadingLevel(content) + step := parseStep(content) + + var buf bytes.Buffer + _ = kgHeadingComponent(level, text, textClass, step).Render(ctx, &buf) + return buf.String() +} + +// parseHeadingLevel parses the level from content, defaulting to 2. +func parseHeadingLevel(content map[string]any) int { + if level, ok := content["level"].(float64); ok { + l := int(level) + if l >= 1 && l <= 6 { + return l + } + } + if level, ok := content["level"].(int); ok { + if level >= 1 && level <= 6 { + return level + } + } + if level, ok := content["level"].(string); ok { + if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 { + return l + } + } + return 2 +} + +// parseStep parses an optional step badge (1-9). Returns 0 if not provided +// or out of range; renderer skips the badge when step == 0. +func parseStep(content map[string]any) int { + if v, ok := content["step"].(float64); ok { + s := int(v) + if s >= 1 && s <= 9 { + return s + } + } + if v, ok := content["step"].(int); ok { + if v >= 1 && v <= 9 { + return v + } + } + if v, ok := content["step"].(string); ok { + if s, err := strconv.Atoi(v); err == nil && s >= 1 && s <= 9 { + return s + } + } + return 0 +} diff --git a/heading_override.templ b/heading_override.templ new file mode 100644 index 0000000..a909279 --- /dev/null +++ b/heading_override.templ @@ -0,0 +1,57 @@ +package main + +import "fmt" + +// kgHeadingBaseClass returns size classes per heading level (Tailwind-like +// utilities scoped to the kindergarten CSS). +func kgHeadingBaseClass(level int) string { + switch level { + case 1: + return "kg-display kg-crayon-underline" + case 2: + return "kg-display kg-crayon-underline" + case 3: + return "kg-display" + case 4: + return "kg-display" + case 5: + return "kg-display" + case 6: + return "kg-display" + default: + return "kg-display" + } +} + +// kgHeadingComponent renders a heading with Kindergarten accents, optionally +// preceded by a circular step badge (e.g. for "Step 1", "Step 2" sections). +templ kgHeadingComponent(level int, text, textClass string, step int) { + if step > 0 { +
+ + @kgHeadingTag(level, text, textClass) +
+ } else { + @kgHeadingTag(level, text, textClass) + } +} + +// kgHeadingTag emits the actual

..

tag at the requested level. +templ kgHeadingTag(level int, text, textClass string) { + switch level { + case 1: +

{ text }

+ case 2: +

{ text }

+ case 3: +

{ text }

+ case 4: +

{ text }

+ case 5: +
{ text }
+ case 6: +
{ text }
+ default: +

{ text }

+ } +} diff --git a/heading_override_templ.go b/heading_override_templ.go new file mode 100644 index 0000000..5fb9092 --- /dev/null +++ b/heading_override_templ.go @@ -0,0 +1,373 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "fmt" + +// kgHeadingBaseClass returns size classes per heading level (Tailwind-like +// utilities scoped to the kindergarten CSS). +func kgHeadingBaseClass(level int) string { + switch level { + case 1: + return "kg-display kg-crayon-underline" + case 2: + return "kg-display kg-crayon-underline" + case 3: + return "kg-display" + case 4: + return "kg-display" + case 5: + return "kg-display" + case 6: + return "kg-display" + default: + return "kg-display" + } +} + +// kgHeadingComponent renders a heading with Kindergarten accents, optionally +// preceded by a circular step badge (e.g. for "Step 1", "Step 2" sections). +func kgHeadingComponent(level int, text, textClass string, step int) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if step > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + 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, "") + 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, "
") + 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

..

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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..b4f9ceb --- /dev/null +++ b/helpers.go @@ -0,0 +1,63 @@ +package main + +// getString extracts a string value from content map. +func getString(content map[string]any, key string) string { + if v, ok := content[key].(string); ok { + return v + } + return "" +} + +// getStringDefault extracts a string value from content with a fallback. +func getStringDefault(content map[string]any, key, def string) string { + if v, ok := content[key].(string); ok && v != "" { + return v + } + return def +} + +// getBool extracts a bool value from content. Accepts native bool, "true"/"false" +// strings, and the JSON-encoded float forms ("true" via 1, 0). Defaults to def. +func getBool(content map[string]any, key string, def bool) bool { + switch v := content[key].(type) { + case bool: + return v + case string: + switch v { + case "true": + return true + case "false": + return false + } + case float64: + return v != 0 + } + return def +} + +// getInt extracts an int value from content map (handles float64 from JSON). +func getInt(content map[string]any, key string, defaultVal int) int { + if v, ok := content[key].(float64); ok { + return int(v) + } + if v, ok := content[key].(int); ok { + return v + } + return defaultVal +} + +// getSlice extracts a slice of maps from content. If the underlying value is +// not a JSON array of objects (e.g. malformed content provides a bare string), +// the function returns nil so renderers can fall back to their empty state. +func getSlice(content map[string]any, key string) []map[string]any { + if v, ok := content[key].([]any); ok { + result := make([]map[string]any, 0, len(v)) + for _, item := range v { + if m, ok := item.(map[string]any); ok { + result = append(result, m) + } + } + return result + } + return nil +} diff --git a/mascot_hero.go b/mascot_hero.go new file mode 100644 index 0000000..22c226d --- /dev/null +++ b/mascot_hero.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// MascotHeroBlockMeta defines metadata for the mascot hero block. +var MascotHeroBlockMeta = blocks.BlockMeta{ + Key: "mascot_hero", + Title: "Mascot Hero", + Description: "Big rounded panel with mascot SVG and primary-colored confetti.", + Category: blocks.CategoryContent, + Source: "kindergarten", +} + +// MascotHeroData carries the strongly-typed shape consumed by the templ component. +type MascotHeroData struct { + Mascot string + Headline string + Tagline string + CTALabel string + CTAHref string + BgColor string +} + +// MascotHeroBlock renders the mascot hero block from the unstructured content map. +// Content shape: {mascot,headline,tagline,ctaLabel,ctaHref,bgColor}. +func MascotHeroBlock(ctx context.Context, content map[string]any) string { + data := MascotHeroData{ + Mascot: getStringDefault(content, "mascot", "pip"), + Headline: getString(content, "headline"), + Tagline: getString(content, "tagline"), + CTALabel: getString(content, "ctaLabel"), + CTAHref: getString(content, "ctaHref"), + BgColor: getString(content, "bgColor"), + } + + var buf bytes.Buffer + _ = mascotHeroComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/mascot_hero.templ b/mascot_hero.templ new file mode 100644 index 0000000..87fdd15 --- /dev/null +++ b/mascot_hero.templ @@ -0,0 +1,78 @@ +package main + +// mascotHeroComponent renders the mascot hero panel. +templ mascotHeroComponent(data MascotHeroData) { +
+
+
+ +
+
+ if data.Headline != "" { +

{ data.Headline }

+ } else { +

Welcome!

+ } + if data.Tagline != "" { +

{ data.Tagline }

+ } + if data.CTALabel != "" && data.CTAHref != "" { + + } +
+
+ @mascotSVG(data.Mascot) +
+
+
+
+
+} + +// 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": + + + + + A + B + C + + case "star": + + + + + + + case "balloon": + + + + + + + + + default: + + + + + + + + + + + + + } +} diff --git a/mascot_hero_templ.go b/mascot_hero_templ.go new file mode 100644 index 0000000..2a0ee59 --- /dev/null +++ b/mascot_hero_templ.go @@ -0,0 +1,178 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// mascotHeroComponent renders the mascot hero panel. +func mascotHeroComponent(data MascotHeroData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Headline != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Welcome!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Tagline != "" { + 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(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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.CTALabel != "" && data.CTAHref != "" { + 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, 10, "
") + 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, "
") + 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, " A B C") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "star": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "balloon": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/numbers_counter.go b/numbers_counter.go new file mode 100644 index 0000000..3b84b15 --- /dev/null +++ b/numbers_counter.go @@ -0,0 +1,47 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// NumbersCounterBlockMeta defines metadata for the numbers counter block. +var NumbersCounterBlockMeta = blocks.BlockMeta{ + Key: "numbers_counter", + Title: "Numbers Counter", + Description: "Big circular numeral badges for stats (e.g. '12 teachers').", + Category: blocks.CategoryContent, + Source: "kindergarten", +} + +// NumberItem is a single numeral + label pair. +type NumberItem struct { + Number int + Label string +} + +// NumbersData is the renderer input. +type NumbersData struct { + Items []NumberItem +} + +// NumbersCounterBlock renders the round numeral grid. +// Content shape: {items:[{number,label}]}. +func NumbersCounterBlock(ctx context.Context, content map[string]any) string { + raw := getSlice(content, "items") + items := make([]NumberItem, 0, len(raw)) + for _, m := range raw { + items = append(items, NumberItem{ + Number: getInt(m, "number", 0), + Label: getString(m, "label"), + }) + } + + data := NumbersData{Items: items} + + var buf bytes.Buffer + _ = numbersCounterComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/numbers_counter.templ b/numbers_counter.templ new file mode 100644 index 0000000..5fa440a --- /dev/null +++ b/numbers_counter.templ @@ -0,0 +1,25 @@ +package main + +import "fmt" + +// numbersCounterComponent renders circular numeral badges. +templ numbersCounterComponent(data NumbersData) { +
+
+ if len(data.Items) == 0 { +
No numbers yet — add a few stats to celebrate.
+ } else { +
+ for _, item := range data.Items { +
+ { fmt.Sprintf("%d", item.Number) } + if item.Label != "" { + { item.Label } + } +
+ } +
+ } +
+
+} diff --git a/numbers_counter_templ.go b/numbers_counter_templ.go new file mode 100644 index 0000000..a445d16 --- /dev/null +++ b/numbers_counter_templ.go @@ -0,0 +1,117 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "fmt" + +// numbersCounterComponent renders circular numeral badges. +func numbersCounterComponent(data NumbersData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Items) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
No numbers yet — add a few stats to celebrate.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range data.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + 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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.Label != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + 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, "") + 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..fa43db1 --- /dev/null +++ b/plugin.mod @@ -0,0 +1,12 @@ +[plugin] +name = "kindergarten" +display_name = "Kindergarten" +scope = "@themes" +version = "0.1.0" +description = "Primary-colored, hand-lettered theme for schools, daycares, toy shops, museums, and kids' publishers." +kind = "theme" +categories = ["templates"] +tags = ["kids", "education", "playful", "family", "school", "museum", "friendly", "daycare", "publisher"] + +[compatibility] +block_core = ">=0.11.0 <0.12.0" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..7b80111 --- /dev/null +++ b/presets.json @@ -0,0 +1,110 @@ +[ + { + "id": "recess", + "name": "Crayon Recess", + "description": "Light, sunny, primary-on-cream — for daytime classrooms.", + "theme": { + "lightColors": { + "background": "48 60% 97%", + "foreground": "220 40% 18%", + "card": "0 0% 100%", + "cardForeground": "220 40% 18%", + "popover": "0 0% 100%", + "popoverForeground": "220 40% 18%", + "primary": "4 80% 56%", + "primaryForeground": "0 0% 100%", + "secondary": "220 75% 56%", + "secondaryForeground": "0 0% 100%", + "muted": "48 40% 92%", + "mutedForeground": "220 25% 38%", + "accent": "48 95% 58%", + "accentForeground": "220 50% 14%", + "destructive": "0 84% 55%", + "destructiveForeground": "0 0% 100%", + "border": "220 30% 86%", + "input": "48 30% 90%", + "ring": "4 80% 56%" + }, + "mode": "light" + } + }, + { + "id": "chalkboard", + "name": "Chalkboard Night", + "description": "Deep navy chalkboard with crayon-bright accents.", + "theme": { + "darkColors": { + "background": "220 50% 10%", + "foreground": "48 60% 96%", + "card": "220 45% 14%", + "cardForeground": "48 60% 96%", + "popover": "220 45% 14%", + "popoverForeground": "48 60% 96%", + "primary": "48 95% 58%", + "primaryForeground": "220 50% 10%", + "secondary": "4 80% 60%", + "secondaryForeground": "0 0% 100%", + "muted": "220 35% 18%", + "mutedForeground": "220 20% 70%", + "accent": "145 70% 50%", + "accentForeground": "220 50% 10%", + "destructive": "0 84% 60%", + "destructiveForeground": "0 0% 100%", + "border": "220 35% 24%", + "input": "220 35% 20%", + "ring": "48 95% 58%" + }, + "mode": "dark" + } + }, + { + "id": "crayon-box", + "name": "Crayon Box", + "description": "Full primary quartet — red, blue, yellow, green — in both light and dark.", + "theme": { + "lightColors": { + "background": "48 55% 98%", + "foreground": "220 45% 15%", + "card": "0 0% 100%", + "cardForeground": "220 45% 15%", + "popover": "0 0% 100%", + "popoverForeground": "220 45% 15%", + "primary": "220 78% 52%", + "primaryForeground": "0 0% 100%", + "secondary": "4 78% 56%", + "secondaryForeground": "0 0% 100%", + "muted": "48 35% 93%", + "mutedForeground": "220 22% 38%", + "accent": "145 65% 45%", + "accentForeground": "0 0% 100%", + "destructive": "0 84% 55%", + "destructiveForeground": "0 0% 100%", + "border": "220 25% 85%", + "input": "48 25% 88%", + "ring": "48 95% 55%" + }, + "darkColors": { + "background": "220 40% 8%", + "foreground": "48 55% 95%", + "card": "220 40% 12%", + "cardForeground": "48 55% 95%", + "popover": "220 40% 12%", + "popoverForeground": "48 55% 95%", + "primary": "220 80% 60%", + "primaryForeground": "0 0% 100%", + "secondary": "4 80% 58%", + "secondaryForeground": "0 0% 100%", + "muted": "220 30% 16%", + "mutedForeground": "220 18% 68%", + "accent": "145 70% 50%", + "accentForeground": "220 40% 8%", + "destructive": "0 84% 60%", + "destructiveForeground": "0 0% 100%", + "border": "220 30% 22%", + "input": "220 30% 18%", + "ring": "48 95% 60%" + }, + "mode": "both" + } + } +] diff --git a/register.go b/register.go new file mode 100644 index 0000000..e7e773f --- /dev/null +++ b/register.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + + "github.com/a-h/templ" + + "git.dev.alexdunmow.com/block/core/blocks" + "git.dev.alexdunmow.com/block/core/plugin" + "git.dev.alexdunmow.com/block/core/templates" +) + +// wrap adapts a templ-returning render function to templates.TemplateFunc. +// templ.Component already satisfies templates.HTMLComponent via Render. +func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc { + return func(ctx context.Context, doc map[string]any) templates.HTMLComponent { + return f(ctx, doc) + } +} + +// Register wires the Kindergarten theme into the host registries. The call +// order matters: the system template + page templates are registered first, +// then schemas are loaded before any block registrations so the schema +// bindings resolve. +func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { + tr.RegisterSystemTemplate(templates.SystemTemplateMeta{ + Key: "kindergarten", + Title: "Kindergarten", + Description: "Primary-colored, hand-lettered theme for schools, daycares, toy shops, museums, and kids' publishers.", + }) + + if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{ + Key: "default", + Title: "Default", + Description: "Standard rounded layout with header, main, footer", + Slots: []string{"header", "main", "footer"}, + }, wrap(RenderKindergarten)); err != nil { + return err + } + + if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{ + Key: "landing", + Title: "Landing", + Description: "Mascot-led hero plus stacked feature sections", + Slots: []string{"hero", "main", "cta", "footer"}, + }, wrap(RenderKindergartenLanding)); err != nil { + return err + } + + if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{ + Key: "article", + Title: "Story Page", + Description: "Picture-book reading layout for blog posts and stories", + Slots: []string{"header", "main", "aside", "footer"}, + }, wrap(RenderKindergartenArticle)); err != nil { + return err + } + + if err := tr.RegisterPageTemplate("kindergarten", templates.PageTemplateMeta{ + Key: "full-width", + Title: "Full Width", + Description: "Edge-to-edge gallery / classroom showcase", + Slots: []string{"header", "main", "footer"}, + }, wrap(RenderKindergartenFullWidth)); err != nil { + return err + } + + // Schemas must be loaded before block registrations so each Register() + // call can bind to its declared schema. + if err := br.LoadSchemasFromFS(Schemas()); err != nil { + return err + } + + // Theme-owned blocks (eight per spec §8). Registered unqualified; the host + // addresses them as "kindergarten:" 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, + }, + }, + }, + } +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..72e18ba --- /dev/null +++ b/registration.go @@ -0,0 +1,25 @@ +package main + +import ( + "io/fs" + "net/http" + + "git.dev.alexdunmow.com/block/core/blocks" + "git.dev.alexdunmow.com/block/core/plugin" + "git.dev.alexdunmow.com/block/core/templates" +) + +// Registration is the compile-time plugin registration for the Kindergarten theme. +var Registration = plugin.PluginRegistration{ + Name: "kindergarten", + Version: plugin.ParseModVersion(pluginModBytes), + Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { + return Register(tr, br) + }, + Assets: func() http.Handler { return AssetsHandler() }, + Schemas: func() fs.FS { return Schemas() }, + ThemePresets: func() []byte { return ThemePresets() }, + BundledFonts: func() []byte { return BundledFonts() }, + MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() }, + CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() }, +} diff --git a/schedule.go b/schedule.go new file mode 100644 index 0000000..d1af1a6 --- /dev/null +++ b/schedule.go @@ -0,0 +1,53 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// ScheduleBlockMeta defines metadata for the day schedule block. +var ScheduleBlockMeta = blocks.BlockMeta{ + Key: "schedule", + Title: "Day Schedule", + Description: "Daycare/classroom day plan with crayon time-pills.", + Category: blocks.CategoryContent, + Source: "kindergarten", +} + +// ScheduleItem is a single time/activity row. +type ScheduleItem struct { + Time string + Activity string + Icon string +} + +// ScheduleData is the renderer's strongly-typed input. +type ScheduleData struct { + Title string + Items []ScheduleItem +} + +// ScheduleBlock renders the day-schedule block. +// Content shape: {title,items:[{time,activity,icon}]}. +func ScheduleBlock(ctx context.Context, content map[string]any) string { + raw := getSlice(content, "items") + items := make([]ScheduleItem, 0, len(raw)) + for _, m := range raw { + items = append(items, ScheduleItem{ + Time: getString(m, "time"), + Activity: getString(m, "activity"), + Icon: getString(m, "icon"), + }) + } + + data := ScheduleData{ + Title: getString(content, "title"), + Items: items, + } + + var buf bytes.Buffer + _ = scheduleComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/schedule.templ b/schedule.templ new file mode 100644 index 0000000..0579d25 --- /dev/null +++ b/schedule.templ @@ -0,0 +1,60 @@ +package main + +// scheduleComponent renders the day schedule. +templ scheduleComponent(data ScheduleData) { +
+
+ if data.Title != "" { +

{ data.Title }

+ } + if len(data.Items) == 0 { +
No schedule items yet — add a time block to get started.
+ } else { +
    + for _, item := range data.Items { +
  1. + + if item.Time != "" { + { item.Time } + } else { + --:-- + } + + + if item.Activity != "" { + { item.Activity } + } else { + Activity + } + + +
  2. + } +
+ } +
+
+} + +// 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": + + case "book": + + case "paint": + + case "snack": + + case "play": + + case "nap": + + default: + + } +} diff --git a/schedule_templ.go b/schedule_templ.go new file mode 100644 index 0000000..dd45d07 --- /dev/null +++ b/schedule_templ.go @@ -0,0 +1,197 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// scheduleComponent renders the day schedule. +func scheduleComponent(data ScheduleData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Items) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
No schedule items yet — add a time block to get started.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range data.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
  1. ") + 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, " ") + 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, " ") + 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, "
  2. ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "book": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "paint": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "snack": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "play": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "nap": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/schemas/alphabet_strip.schema.json b/schemas/alphabet_strip.schema.json new file mode 100644 index 0000000..84b57b1 --- /dev/null +++ b/schemas/alphabet_strip.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Alphabet Strip", + "description": "Decorative letter band — primary, rainbow, or mono colorways.", + "type": "object", + "properties": { + "letters": { + "type": "string", + "title": "Letters", + "description": "Sequence of letters to render. Each character becomes one tile.", + "default": "ABCDEFGHIJ", + "x-editor": "text" + }, + "colorMode": { + "type": "string", + "title": "Color Mode", + "description": "How the letter tiles are colored.", + "x-editor": "select", + "enum": ["primary", "rainbow", "mono"], + "default": "rainbow" + } + } +} diff --git a/schemas/big_cta.schema.json b/schemas/big_cta.schema.json new file mode 100644 index 0000000..b8d11e6 --- /dev/null +++ b/schemas/big_cta.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Big CTA", + "description": "Oversized pill button with sticker drop-shadow and crayon underline.", + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "Button copy (e.g. 'Enrol today').", + "x-editor": "text" + }, + "href": { + "type": "string", + "title": "Link", + "description": "Where the button points.", + "x-editor": "link" + }, + "colorVariant": { + "type": "string", + "title": "Color Variant", + "description": "Which primary colorway the pill uses.", + "x-editor": "select", + "enum": ["red", "blue", "yellow", "green"], + "default": "yellow" + } + } +} diff --git a/schemas/footer.schema.json b/schemas/footer.schema.json new file mode 100644 index 0000000..4bdefa2 --- /dev/null +++ b/schemas/footer.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Friendly Footer", + "description": "Mascot wave, optional signup, contact, social links, and crayon-rule divider.", + "type": "object", + "properties": { + "showSignup": { + "type": "string", + "title": "Show Newsletter Signup", + "description": "Render the small newsletter signup form.", + "x-editor": "select", + "enum": ["true", "false"], + "default": "true" + }, + "mascotName": { + "type": "string", + "title": "Mascot Name", + "description": "Name of the mascot displayed in the footer wave (e.g. 'Pip').", + "default": "Pip", + "x-editor": "text" + }, + "socialLinks": { + "type": "array", + "title": "Social Links", + "description": "Friendly icon links for social platforms.", + "default": [], + "x-editor": "array", + "items": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "title": "Platform", + "description": "Which platform this link points to.", + "x-editor": "select", + "enum": ["facebook", "instagram", "youtube", "tiktok", "email"] + }, + "href": { + "type": "string", + "title": "URL", + "description": "Link target.", + "x-editor": "link" + } + } + } + } + } +} diff --git a/schemas/gallery_of_art.schema.json b/schemas/gallery_of_art.schema.json new file mode 100644 index 0000000..863e631 --- /dev/null +++ b/schemas/gallery_of_art.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Gallery of Art", + "description": "Polaroid-style grid for kid artwork with privacy toggle for child names.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Optional heading above the gallery.", + "x-editor": "text" + }, + "showChildName": { + "type": "string", + "title": "Show Child Names", + "description": "Privacy toggle. When 'false', child names are omitted from the rendered gallery.", + "x-editor": "select", + "enum": ["true", "false"], + "default": "true" + }, + "items": { + "type": "array", + "title": "Artworks", + "description": "Collection of artworks. Each entry has an image, child name, and age.", + "default": [], + "x-editor": "collection", + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "title": "Image", + "description": "Artwork image (media reference).", + "x-editor": "media" + }, + "childName": { + "type": "string", + "title": "Child Name", + "description": "Artist's first name. Hidden when 'Show Child Names' is off.", + "x-editor": "text" + }, + "age": { + "type": "integer", + "title": "Age", + "description": "Artist's age (years).", + "x-editor": "number" + } + } + } + } + } +} diff --git a/schemas/mascot_hero.schema.json b/schemas/mascot_hero.schema.json new file mode 100644 index 0000000..45eb3d2 --- /dev/null +++ b/schemas/mascot_hero.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Mascot Hero", + "description": "Big rounded panel with mascot SVG, headline, tagline, and a primary-colored CTA.", + "type": "object", + "properties": { + "mascot": { + "type": "string", + "title": "Mascot", + "description": "Mascot character to render in the hero panel.", + "x-editor": "select", + "enum": ["pip", "blocks", "star", "balloon"], + "default": "pip" + }, + "headline": { + "type": "string", + "title": "Headline", + "description": "Big, hand-lettered headline.", + "x-editor": "text" + }, + "tagline": { + "type": "string", + "title": "Tagline", + "description": "Friendly one-liner under the headline.", + "x-editor": "text" + }, + "ctaLabel": { + "type": "string", + "title": "CTA Label", + "description": "Pill button label (e.g. 'Enrol today').", + "x-editor": "text" + }, + "ctaHref": { + "type": "string", + "title": "CTA Link", + "description": "Where the pill button points.", + "x-editor": "link" + }, + "bgColor": { + "type": "string", + "title": "Background Wash", + "description": "Optional HSL triple (e.g. '48 95% 58%') to tint the hero wash. Leave blank to use theme accent.", + "x-editor": "color" + } + } +} diff --git a/schemas/numbers_counter.schema.json b/schemas/numbers_counter.schema.json new file mode 100644 index 0000000..10757a2 --- /dev/null +++ b/schemas/numbers_counter.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Numbers Counter", + "description": "Big circular numeral badges for stats (e.g. '12 teachers').", + "type": "object", + "properties": { + "items": { + "type": "array", + "title": "Numbers", + "description": "Collection of (number, label) pairs.", + "default": [], + "x-editor": "collection", + "items": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "title": "Number", + "description": "Numeral rendered as a circular badge.", + "x-editor": "number" + }, + "label": { + "type": "string", + "title": "Label", + "description": "Plain-language description (e.g. 'teachers').", + "x-editor": "text" + } + } + } + } + } +} diff --git a/schemas/schedule.schema.json b/schemas/schedule.schema.json new file mode 100644 index 0000000..61278df --- /dev/null +++ b/schemas/schedule.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Day Schedule", + "description": "Daycare / classroom day plan with crayon time-pills.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Optional heading above the schedule (e.g. 'A day at Sunflower Room').", + "x-editor": "text" + }, + "items": { + "type": "array", + "title": "Schedule Items", + "description": "Ordered list of time-blocks.", + "default": [], + "x-editor": "array", + "items": { + "type": "object", + "properties": { + "time": { + "type": "string", + "title": "Time", + "description": "Time-of-day label (e.g. '09:00').", + "x-editor": "text" + }, + "activity": { + "type": "string", + "title": "Activity", + "description": "What happens in this slot.", + "x-editor": "text" + }, + "icon": { + "type": "string", + "title": "Icon", + "description": "Icon name (sun, book, paint, snack, play, nap).", + "x-editor": "select", + "enum": ["", "sun", "book", "paint", "snack", "play", "nap"] + } + } + } + } + } +} diff --git a/schemas/storybook_quote.schema.json b/schemas/storybook_quote.schema.json new file mode 100644 index 0000000..4c9bac6 --- /dev/null +++ b/schemas/storybook_quote.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Storybook Quote", + "description": "Picture-book page treatment for a quote with optional illustration.", + "type": "object", + "properties": { + "quote": { + "type": "string", + "title": "Quote", + "description": "The quoted text.", + "x-editor": "textarea" + }, + "author": { + "type": "string", + "title": "Author", + "description": "Who said it.", + "x-editor": "text" + }, + "illustration": { + "type": "string", + "title": "Illustration", + "description": "Optional picture-book illustration.", + "x-editor": "media" + } + } +} diff --git a/storybook_quote.go b/storybook_quote.go new file mode 100644 index 0000000..ebea8df --- /dev/null +++ b/storybook_quote.go @@ -0,0 +1,38 @@ +package main + +import ( + "bytes" + "context" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +// StorybookQuoteBlockMeta defines metadata for the storybook quote block. +var StorybookQuoteBlockMeta = blocks.BlockMeta{ + Key: "storybook_quote", + Title: "Storybook Quote", + Description: "Picture-book page treatment for a quote with optional illustration.", + Category: blocks.CategoryContent, + Source: "kindergarten", +} + +// StorybookQuoteData is the renderer input. +type StorybookQuoteData struct { + Quote string + Author string + Illustration string +} + +// StorybookQuoteBlock renders the quote panel. +// Content shape: {quote,author,illustration}. +func StorybookQuoteBlock(ctx context.Context, content map[string]any) string { + data := StorybookQuoteData{ + Quote: getString(content, "quote"), + Author: getString(content, "author"), + Illustration: getString(content, "illustration"), + } + + var buf bytes.Buffer + _ = storybookQuoteComponent(data).Render(ctx, &buf) + return buf.String() +} diff --git a/storybook_quote.templ b/storybook_quote.templ new file mode 100644 index 0000000..4cd0ed3 --- /dev/null +++ b/storybook_quote.templ @@ -0,0 +1,42 @@ +package main + +import "git.dev.alexdunmow.com/block/core/blocks" + +// storybookQuoteComponent renders a quote with optional illustration. +templ storybookQuoteComponent(data StorybookQuoteData) { +
+
+
+
+ if data.Quote != "" { +

"{ data.Quote }"

+ } else { +

Add a quote to inspire your readers.

+ } + if data.Author != "" { +
— { data.Author }
+ } +
+
+ if data.Illustration != "" { + { + } else { + @storybookFallbackIllustration() + } +
+
+
+
+} + +// storybookFallbackIllustration renders a friendly SVG when no illustration +// is supplied. It guarantees a visible, themed graphic instead of a broken +// image icon. +templ storybookFallbackIllustration() { + + + + + + +} diff --git a/storybook_quote_templ.go b/storybook_quote_templ.go new file mode 100644 index 0000000..b9fb0b4 --- /dev/null +++ b/storybook_quote_templ.go @@ -0,0 +1,163 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "git.dev.alexdunmow.com/block/core/blocks" + +// storybookQuoteComponent renders a quote with optional illustration. +func storybookQuoteComponent(data StorybookQuoteData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Quote != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

\"") + 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, "\"

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Add a quote to inspire your readers.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Author != "" { + 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(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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Illustration != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"")") + 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, "
") + 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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..bf6ab91 --- /dev/null +++ b/template.templ @@ -0,0 +1,257 @@ +package main + +import ( + "context" + + "git.dev.alexdunmow.com/block/core/templates/bn" +) + +// PageData carries everything the page templates need to render. +type PageData struct { + Title string + Slots map[string]string + ThemeMode string + ThemeCSS string + SiteSettings bn.SiteSettingsData + PageMeta bn.PageMeta + StructuredData string + CSSHash string + PageviewNonce string + EngagementConfig bn.EngagementConfig +} + +// parseKindergartenPageData converts the unstructured doc map the engine +// passes in into the strongly-typed PageData used by the templ components. +func parseKindergartenPageData(doc map[string]any) PageData { + title := "Untitled" + if t, ok := doc["title"].(string); ok { + title = t + } + + slots := make(map[string]string) + if s, ok := doc["slots"].(map[string]string); ok { + slots = s + } + + themeCSS := "" + if tc, ok := doc["theme_css"].(string); ok { + themeCSS = tc + } + + structuredData := "" + if sd, ok := doc["structured_data"].(string); ok { + structuredData = sd + } + + cssHash := "" + if ch, ok := doc["css_hash"].(string); ok { + cssHash = ch + } + + pageviewNonce := "" + if pn, ok := doc["pageview_nonce"].(string); ok { + pageviewNonce = pn + } + + themeMode := "light" + if tm, ok := doc["theme_mode"].(string); ok && tm != "" { + themeMode = tm + } + + siteSettings := bn.ParseSiteSettings(doc) + pageMeta := bn.ParsePageMeta(doc) + engagementConfig := bn.ParseEngagementConfig(doc) + + return PageData{ + Title: title, + Slots: slots, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + SiteSettings: siteSettings, + PageMeta: pageMeta, + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: engagementConfig, + } +} + +// pluginStyles is the canonical list of theme stylesheet URLs each page sends to . +var pluginStyles = []string{"/templates/kindergarten/style.css"} + +// Kindergarten — default template. +templ Kindergarten(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
No content blocks assigned to this page.
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Kindergarten — landing template with mascot hero + CTA strip. +templ KindergartenLanding(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ @templ.Raw(data.Slots["hero"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } +
+
+ @templ.Raw(data.Slots["cta"]) +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Kindergarten — article (storybook reading) template. +templ KindergartenArticle(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } else { +
No content yet — start a story.
+ } +
+ if aside, ok := data.Slots["aside"]; ok && aside != "" { + + } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// Kindergarten — full-width gallery / showcase template. +templ KindergartenFullWidth(data PageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+
No content blocks assigned to this page.
+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// 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)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..35986b2 --- /dev/null +++ b/template_templ.go @@ -0,0 +1,529 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "context" + + "git.dev.alexdunmow.com/block/core/templates/bn" +) + +// PageData carries everything the page templates need to render. +type PageData struct { + Title string + Slots map[string]string + ThemeMode string + ThemeCSS string + SiteSettings bn.SiteSettingsData + PageMeta bn.PageMeta + StructuredData string + CSSHash string + PageviewNonce string + EngagementConfig bn.EngagementConfig +} + +// parseKindergartenPageData converts the unstructured doc map the engine +// passes in into the strongly-typed PageData used by the templ components. +func parseKindergartenPageData(doc map[string]any) PageData { + title := "Untitled" + if t, ok := doc["title"].(string); ok { + title = t + } + + slots := make(map[string]string) + if s, ok := doc["slots"].(map[string]string); ok { + slots = s + } + + themeCSS := "" + if tc, ok := doc["theme_css"].(string); ok { + themeCSS = tc + } + + structuredData := "" + if sd, ok := doc["structured_data"].(string); ok { + structuredData = sd + } + + cssHash := "" + if ch, ok := doc["css_hash"].(string); ok { + cssHash = ch + } + + pageviewNonce := "" + if pn, ok := doc["pageview_nonce"].(string); ok { + pageviewNonce = pn + } + + themeMode := "light" + if tm, ok := doc["theme_mode"].(string); ok && tm != "" { + themeMode = tm + } + + siteSettings := bn.ParseSiteSettings(doc) + pageMeta := bn.ParsePageMeta(doc) + engagementConfig := bn.ParseEngagementConfig(doc) + + return PageData{ + Title: title, + Slots: slots, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + SiteSettings: siteSettings, + PageMeta: pageMeta, + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: engagementConfig, + } +} + +// pluginStyles is the canonical list of theme stylesheet URLs each page sends to . +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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + 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 + } + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
No content yet — start a story.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + 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, "
") + 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, "") + 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 diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..8e6da6b --- /dev/null +++ b/text_override.go @@ -0,0 +1,17 @@ +package main + +import ( + "bytes" + "context" +) + +// KindergartenTextBlock renders text with Nunito body, 1.7 line-height, +// and the rounded ::selection highlight in primary yellow. +func KindergartenTextBlock(ctx context.Context, content map[string]any) string { + text := getString(content, "text") + class := getString(content, "class") + + var buf bytes.Buffer + _ = kgTextComponent(text, class).Render(ctx, &buf) + return buf.String() +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..989e066 --- /dev/null +++ b/text_override.templ @@ -0,0 +1,9 @@ +package main + +// kgTextComponent renders rich text body styled to match the Kindergarten +// reading rhythm. +templ kgTextComponent(text, class string) { +
+ @templ.Raw(text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..b388ef9 --- /dev/null +++ b/text_override_templ.go @@ -0,0 +1,68 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// kgTextComponent renders rich text body styled to match the Kindergarten +// reading rhythm. +func kgTextComponent(text, class string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var2 = []any{"kg-text", class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate