feat: add Responsive multi-variant layout compositor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-02-17 00:07:38 +00:00
parent 40da0d8b1d
commit e041f7681f
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 98 additions and 0 deletions

39
responsive.go Normal file
View file

@ -0,0 +1,39 @@
package html
import "strings"
// Responsive wraps multiple Layout variants for breakpoint-aware rendering.
// Each variant is rendered inside a container with data-variant for CSS targeting.
type Responsive struct {
variants []responsiveVariant
}
type responsiveVariant struct {
name string
layout *Layout
}
// NewResponsive creates a new multi-variant responsive compositor.
func NewResponsive() *Responsive {
return &Responsive{}
}
// Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
// Variants render in insertion order.
func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
return r
}
// Render produces HTML with each variant in a data-variant container.
func (r *Responsive) Render(ctx *Context) string {
var b strings.Builder
for _, v := range r.variants {
b.WriteString(`<div data-variant="`)
b.WriteString(v.name)
b.WriteString(`">`)
b.WriteString(v.layout.Render(ctx))
b.WriteString(`</div>`)
}
return b.String()
}

59
responsive_test.go Normal file
View file

@ -0,0 +1,59 @@
package html
import (
"strings"
"testing"
)
func TestResponsive_SingleVariant(t *testing.T) {
ctx := NewContext()
r := NewResponsive().
Variant("desktop", NewLayout("HLCRF").
H(Raw("header")).L(Raw("nav")).C(Raw("main")).R(Raw("aside")).F(Raw("footer")))
got := r.Render(ctx)
if !strings.Contains(got, `data-variant="desktop"`) {
t.Errorf("responsive should contain data-variant, got:\n%s", got)
}
if !strings.Contains(got, `data-block="H-0"`) {
t.Errorf("responsive should contain layout content, got:\n%s", got)
}
}
func TestResponsive_MultiVariant(t *testing.T) {
ctx := NewContext()
r := NewResponsive().
Variant("desktop", NewLayout("HLCRF").H(Raw("h")).L(Raw("l")).C(Raw("c")).R(Raw("r")).F(Raw("f"))).
Variant("tablet", NewLayout("HCF").H(Raw("h")).C(Raw("c")).F(Raw("f"))).
Variant("mobile", NewLayout("C").C(Raw("c")))
got := r.Render(ctx)
for _, v := range []string{"desktop", "tablet", "mobile"} {
if !strings.Contains(got, `data-variant="`+v+`"`) {
t.Errorf("responsive missing variant %q in:\n%s", v, got)
}
}
}
func TestResponsive_VariantOrder(t *testing.T) {
ctx := NewContext()
r := NewResponsive().
Variant("desktop", NewLayout("HLCRF").C(Raw("d"))).
Variant("mobile", NewLayout("C").C(Raw("m")))
got := r.Render(ctx)
di := strings.Index(got, `data-variant="desktop"`)
mi := strings.Index(got, `data-variant="mobile"`)
if di < 0 || mi < 0 {
t.Fatalf("missing variants in:\n%s", got)
}
if di >= mi {
t.Errorf("desktop should appear before mobile (insertion order), desktop=%d mobile=%d", di, mi)
}
}
func TestResponsive_ImplementsNode(t *testing.T) {
var _ Node = NewResponsive()
}