fix(html): align left slot with navigation semantics
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 00:16:01 +01:00
parent 09b48e00dc
commit 61adbd55c5
6 changed files with 22 additions and 7 deletions

View file

@ -73,7 +73,7 @@ The `Layout` type is a compositor for five named slots:
| Slot Letter | Semantic Element | ARIA Role | Accessor |
|-------------|-----------------|-----------|----------|
| H | `<header>` | `banner` | `layout.H(...)` |
| L | `<aside>` | `complementary` | `layout.L(...)` |
| L | `<nav>` | `navigation` | `layout.L(...)` |
| C | `<main>` | `main` | `layout.C(...)` |
| R | `<aside>` | `complementary` | `layout.R(...)` |
| F | `<footer>` | `contentinfo` | `layout.F(...)` |

View file

@ -54,7 +54,7 @@ This builds a Header-Content-Footer layout with semantic HTML elements (`<header
**Node tree** -- All renderable units implement `Node`, a single-method interface: `Render(ctx *Context) string`. The library composes nodes into trees using `El()` for elements, `Text()` for translated text, control-flow constructors (`If`, `Unless`, `Each`, `Switch`, `Entitled`), and accessibility helpers (`AriaLabel`, `AltText`, `TabIndex`, `AutoFocus`, `Role`).
**HLCRF Layout** -- A five-slot compositor that maps to semantic HTML: `<header>` (H), `<aside>` (L/R), `<main>` (C), `<footer>` (F). The variant string controls which slots render: `"HLCRF"` for all five, `"HCF"` for three, `"C"` for content only. Layouts nest: placing a `Layout` inside another layout's slot produces hierarchical `data-block` paths like `L-0-C-0`.
**HLCRF Layout** -- A five-slot compositor that maps to semantic HTML: `<header>` (H), `<nav>` (L), `<main>` (C), `<aside>` (R), `<footer>` (F). The variant string controls which slots render: `"HLCRF"` for all five, `"HCF"` for three, `"C"` for content only. Layouts nest: placing a `Layout` inside another layout's slot produces hierarchical `data-block` paths like `L-0-C-0`.
**Responsive variants** -- `Responsive` wraps multiple `Layout` instances with named breakpoints (e.g. `"desktop"`, `"mobile"`). Each variant renders inside a `<div data-variant="name">` container for CSS or JavaScript targeting. `VariantSelector(name)` returns a ready-made attribute selector for styling these containers from CSS.

View file

@ -24,7 +24,7 @@ type slotMeta struct {
// slotRegistry maps slot letters to their semantic HTML elements and ARIA roles.
var slotRegistry = map[byte]slotMeta{
'H': {tag: "header", role: "banner"},
'L': {tag: "aside", role: "complementary"},
'L': {tag: "nav", role: "navigation"},
'C': {tag: "main", role: "main"},
'R': {tag: "aside", role: "complementary"},
'F': {tag: "footer", role: "contentinfo"},
@ -148,7 +148,7 @@ func (l *Layout) H(nodes ...Node) *Layout {
return l
}
// L appends nodes to the Left aside slot.
// L appends nodes to the Left navigation slot.
// Usage example: NewLayout("LC").L(Text("nav"))
func (l *Layout) L(nodes ...Node) *Layout {
if l == nil {

View file

@ -11,14 +11,14 @@ func TestLayout_HLCRF_Good(t *testing.T) {
got := layout.Render(ctx)
// Must contain semantic elements
for _, want := range []string{"<header", "<aside", "<main", "<footer"} {
for _, want := range []string{"<header", "<nav", "<main", "<footer"} {
if !containsText(got, want) {
t.Errorf("HLCRF layout missing %q in:\n%s", want, got)
}
}
// Must contain ARIA roles
for _, want := range []string{`role="banner"`, `role="complementary"`, `role="main"`, `role="contentinfo"`} {
for _, want := range []string{`role="banner"`, `role="navigation"`, `role="main"`, `role="contentinfo"`} {
if !containsText(got, want) {
t.Errorf("HLCRF layout missing role %q in:\n%s", want, got)
}

15
path.go
View file

@ -15,9 +15,22 @@ func ParseBlockID(id string) []byte {
return nil
}
// Accept both the current "{slot}-0" path format and the dot notation
// used in the RFC prose examples. A plain single-slot ID such as "H" is
// also valid.
normalized := strings.ReplaceAll(id, ".", "-")
if !strings.Contains(normalized, "-") {
if len(normalized) == 1 {
if _, ok := slotRegistry[normalized[0]]; ok {
return []byte{normalized[0]}
}
}
return nil
}
// Valid IDs are exact sequences of "{slot}-0" segments, e.g.
// "H-0" or "L-0-C-0". Any malformed segment invalidates the whole ID.
parts := strings.Split(id, "-")
parts := strings.Split(normalized, "-")
if len(parts)%2 != 0 {
return nil
}

View file

@ -65,6 +65,8 @@ func TestParseBlockID_ExtractsSlots_Good(t *testing.T) {
want []byte
}{
{"L-0-C-0", []byte{'L', 'C'}},
{"L.0.C.0", []byte{'L', 'C'}},
{"H", []byte{'H'}},
{"H-0", []byte{'H'}},
{"C-0-C-0-C-0", []byte{'C', 'C', 'C'}},
{"", nil},