Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a mechanical block/core/X -> block/pluginsdk/X import rewrite. - abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms authoring source (cms/backend/abi/proto/v1) with go_package retargeted to git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept cms/core proto duplication (audit gap 4). - abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1 save the embedded go_package path. - plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim, caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the guest-facing type packages: blocks (+builtin/shared/tags), templates (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai, subscriptions, menus, datasources. Internal imports rewritten core -> pluginsdk; zero block/core references remain. - README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one binding; no replace directives; templates/bn is a synced copy authored in cms. Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
package blocks
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// BlockFunc renders a block given its content data.
|
|
type BlockFunc func(ctx context.Context, content map[string]any) string
|
|
|
|
// BlockCategory represents categories for organizing blocks in the palette.
|
|
type BlockCategory string
|
|
|
|
const (
|
|
CategoryContent BlockCategory = "content"
|
|
CategoryLayout BlockCategory = "layout"
|
|
CategoryNavigation BlockCategory = "navigation"
|
|
CategoryBlog BlockCategory = "blog"
|
|
CategoryTheme BlockCategory = "theme"
|
|
)
|
|
|
|
// BlockMeta provides metadata about a block type for admin UI and validation.
|
|
type BlockMeta struct {
|
|
Key string
|
|
Title string
|
|
Description string
|
|
Category BlockCategory
|
|
HasInternalSlot bool
|
|
Source string
|
|
Hidden bool
|
|
EditorJS string
|
|
}
|
|
|
|
// BlockNode represents a block with its content and children for rendering.
|
|
type BlockNode struct {
|
|
ID uuid.UUID
|
|
BlockKey string
|
|
Title string
|
|
Content map[string]any
|
|
HtmlContent string
|
|
Children []*BlockNode
|
|
SortOrder int
|
|
}
|
|
|
|
// SlotRenderer is the interface for rendering container slots.
|
|
type SlotRenderer interface {
|
|
RenderContainerSlot(ctx context.Context, containerID uuid.UUID) string
|
|
}
|
|
|
|
// HumanProofBannerData holds data for the public human proof banner.
|
|
type HumanProofBannerData struct {
|
|
ActiveTimeMinutes int
|
|
KeystrokeCount int
|
|
SessionCount int
|
|
PostSlug string
|
|
}
|
|
|
|
// RenderHumanProofBanner renders the public-facing banner HTML.
|
|
func RenderHumanProofBanner(hp *HumanProofBannerData) string {
|
|
return fmt.Sprintf(`<div id="hp-banner-%[4]s" data-human-proof-banner class="rounded-lg overflow-hidden my-6">`+
|
|
`<div class="flex items-center justify-between py-3 px-5 bg-muted border border-border rounded-lg">`+
|
|
`<div class="flex items-center gap-3">`+
|
|
`<div class="w-8 h-8 bg-primary text-primary-foreground rounded-md flex items-center justify-center text-xs font-bold">HP</div>`+
|
|
`<div>`+
|
|
`<div class="font-semibold text-sm text-foreground">Human Proof</div>`+
|
|
`<div class="text-xs text-muted-foreground">%[1]d min active · %[2]s keystrokes · %[3]d sessions</div>`+
|
|
`</div></div>`+
|
|
`<button class="px-4 py-1.5 bg-primary text-primary-foreground rounded-md text-sm font-medium cursor-pointer hover:bg-primary/90 border-0" data-action="watch">Watch this post being written</button>`+
|
|
`</div></div>`+
|
|
`<script src="/assets/human-proof-player.js" defer></script>`+
|
|
`<script>document.addEventListener("DOMContentLoaded",function(){if(window.HumanProof){window.HumanProof.init({postSlug:%[4]q,bannerId:"hp-banner-%[4]s",contentId:"hp-content-%[4]s"})}});</script>`,
|
|
hp.ActiveTimeMinutes,
|
|
formatThousands(hp.KeystrokeCount),
|
|
hp.SessionCount,
|
|
hp.PostSlug,
|
|
)
|
|
}
|
|
|
|
func formatThousands(n int) string {
|
|
if n < 1000 {
|
|
return fmt.Sprintf("%d", n)
|
|
}
|
|
s := fmt.Sprintf("%d", n)
|
|
var result []byte
|
|
for i, c := range s {
|
|
if i > 0 && (len(s)-i)%3 == 0 {
|
|
result = append(result, ',')
|
|
}
|
|
result = append(result, byte(c))
|
|
}
|
|
return string(result)
|
|
}
|
|
|
|
// ResolveMediaPath converts a media: prefixed path to a /media/ URL path.
|
|
func ResolveMediaPath(path string) string {
|
|
if after, ok := strings.CutPrefix(path, "media:"); ok {
|
|
return "/media/" + after
|
|
}
|
|
if _, err := uuid.Parse(path); err == nil {
|
|
return "/media/" + path
|
|
}
|
|
return path
|
|
}
|