rastrillo / fiesta Public

Hang the same bunting with and without motion, and hold the phone check to every pane

Human

Prompt

Task 4 of Fiesta part 3, fixes after Astra's review of eacecbe: hang the same bunting with and without motion, hold the phone check to the page's content and every sample pane and course, whitelist the stored theme, repaint the bunting when the theme's stylesheet loads, and prove the first-paint drive catches a flash.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

The still bunting is now generated in Go from the geometry bunting.js animates (pennants 36 wide and 44 tall, hung 7 below a rope 4 from the top, 48 apart, six to a swag with 34 clear at each tack, eight swags across a row 96 tall), so a reader with no JS or under reduced motion sees the arrangement the canvas moves, and turning motion off mid-visit changes nothing but the stillness; the template takes it as .Bunting, and TestTheStillBuntingIsTheAnimations reads the script's constants and holds the SVG's row, swags, pennant positions and cloth order to them. TestTheLandingHasNoHorizontalScrollAt320 first requires the page's content, so a stub with a sample and nothing else fails by name, then measures the document and the sample window's own contents, which the window clips, as the page opens and with every pane and every course shown at 320. site.js takes a stored theme only when it is day, plain or signal spelled out, not when it is a name every object inherits; TestABrokenStoredThemeFallsBackToDay covers constructor, __proto__, toString, DAY and the empty string. bunting.js paints its textures again when #site-theme's stylesheet loads, so the rope follows the theme's token as it follows the scheme's; TestTheBuntingFollowsTheTheme reads the rope's colour from the canvas before and after choosing Signal. TestTheThemeComesBackBeforeFirstPaint waits for the steady state before judging the first frame.
Agent notes
TestTheStillBuntingIsTheAnimations was red by assertion against the old SVG (viewBox 2784 for a row of 2848, rotated pennants); TestABrokenStoredThemeFallsBackToDay red on constructor, __proto__ and toString, each marking no tab; TestTheBuntingFollowsTheTheme red with the rope still Day's colour after Signal; TestTheLandingHasNoHorizontalScrollAt320 red by assertion against a stub holding only #sample ("#headline: 0 on the page"). The first-paint drive was proved with a head that writes Day first and swaps after load: it went red on the first frame's --rst-bg, the stylesheet list and the Day request, then the mutation was reverted and it is green. The still pennants hang straight, as the animation's do at rest; the reference preview's alternating tilt was decoration. Both www shards and make ci in a clean worktree were green before this commit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c3fa599e39d3292b95616bbf826c56f9a9140ae9 parent eacecbe
6 files changed, +339 −118
  • internal/www/landing.go +66 −3
  • internal/www/landing_browser_test.go +173 −17
  • internal/www/landing_test.go +84 −0
  • site/landing/page.html.tmpl +1 −93
  • site/static/bunting.js +7 −3
  • site/static/site.js +8 −2

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.173s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.023s
=== suite ===
--- shard 0 of 12 ---
==> shard 0 of 12: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 go test -count=1 ./...
ok  	amadan.net/rastrillo/fiesta	1.323s
ok  	amadan.net/rastrillo/fiesta/check	5.741s
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	7.406s
ok  	amadan.net/rastrillo/fiesta/example	0.271s
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.177s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.480s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.039s
ok  	amadan.net/rastrillo/fiesta/internal/gallery	6.132s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.029s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.371s
ok  	amadan.net/rastrillo/fiesta/internal/site	3.727s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.011s
ok  	amadan.net/rastrillo/fiesta/internal/www	2.857s
--- shard 1 of 12 ---
==> shard 1 of 12: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
--- shard 2 of 12 ---
==> shard 2 of 12: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
--- shard 3 of 12 ---
==> shard 3 of 12: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
7:00PM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
7:01PM INF 1 commits scanned.
7:01PM INF scanned ~5785224 bytes (5.79 MB) in 3.76s
7:01PM INF no leaks found
--- shard 4 of 12 ---
==> shard 4 of 12: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
ok  	amadan.net/rastrillo/fiesta/internal/harness	12.458s
--- shard 5 of 12 ---
==> shard 5 of 12: make browser.check~1of3
./hack/gotest.sh browser 'check~1of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 3 of ./check/ runs 95 of 286 tests
ok  	amadan.net/rastrillo/fiesta/check	198.955s
--- shard 6 of 12 ---
==> shard 6 of 12: make browser.check~2of3
./hack/gotest.sh browser 'check~2of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 3 of ./check/ runs 95 of 286 tests
ok  	amadan.net/rastrillo/fiesta/check	206.865s
--- shard 7 of 12 ---
==> shard 7 of 12: make browser.check~3of3
./hack/gotest.sh browser 'check~3of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 3 of 3 of ./check/ runs 96 of 286 tests
ok  	amadan.net/rastrillo/fiesta/check	209.132s
--- shard 8 of 12 ---
==> shard 8 of 12: make browser.internal.gallery~1of2
./hack/gotest.sh browser 'internal.gallery~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/gallery/ runs 1 of 25 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	84.070s
--- shard 9 of 12 ---
==> shard 9 of 12: make browser.internal.gallery~2of2
./hack/gotest.sh browser 'internal.gallery~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/gallery/ runs 24 of 25 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	42.534s
--- shard 10 of 12 ---
==> shard 10 of 12: make browser.internal.www~1of2
./hack/gotest.sh browser 'internal.www~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/www/ runs 14 of 27 tests
ok  	amadan.net/rastrillo/fiesta/internal/www	29.355s
--- shard 11 of 12 ---
==> shard 11 of 12: make browser.internal.www~2of2
./hack/gotest.sh browser 'internal.www~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/www/ runs 13 of 27 tests
ok  	amadan.net/rastrillo/fiesta/internal/www	14.019s
diff --git a/internal/www/landing.go b/internal/www/landing.go
index 9456af8..fc2003f 100644
--- a/internal/www/landing.go
+++ b/internal/www/landing.go
@@ -5,6 +5,7 @@ import (
"fmt"
"html/template"
"io/fs"
+ "strings"
"amadan.net/rastrillo/fiesta"
)
@@ -14,13 +15,75 @@ import (
const landingTemplate = "landing/page.html.tmpl"
// page is what the template sees: the language and its direction for
-// the html element, the version the footer shows, and the catalog
-// behind T.
+// the html element, the version the footer shows, the still bunting,
+// and the catalog behind T.
type page struct {
Lang, Dir, Version string
+ Bunting template.HTML
strings map[string]string
}
+// buntingGeometry is the bunting's shape in the SVG's own units, one
+// unit a pixel at scale 1: pennants FW wide at the top and FH to the
+// tip, hung Tie below a rope that runs TopY from the top, Pitch apart,
+// Per to a swag with End clear at each tack, Swags swags across a row
+// H tall. Sag is how far the rope's middle dips between two tacks.
+//
+// These are bunting.js's own numbers, so the still picture a reader
+// gets with no JS or under reduced motion is the arrangement the
+// canvas animates; TestTheStillBuntingIsTheAnimations holds the two
+// together.
+type buntingGeometry struct {
+ FW, FH, TopY, Tie, Pitch, Per, End, Swags, H int
+ Sag float64
+}
+
+var buntingShape = buntingGeometry{FW: 36, FH: 44, TopY: 4, Tie: 7, Pitch: 48, Per: 6, End: 34, Swags: 8, H: 96, Sag: 10}
+
+// pennantFills are the six cloths, in the order they hang. They live
+// on the SVG as attributes, and bunting.js reads them from there, so
+// site.css carries Fiesta's tokens only.
+var pennantFills = []string{"#2257df", "#f2b632", "#e8553f", "#16a37f", "#7aa2ff", "#e84f8a"}
+
+// stillBunting draws the bunting at rest as an SVG: one pennant
+// definition per cloth, origin at the middle of its top edge, with a
+// sewn tape and a stitch line along the top; then for each swag the
+// rope between two tacks, and for each pennant its two ties and the
+// pennant itself. The classes are what site.css colours (the rope and
+// ties are a Fiesta token) and what bunting.js reads.
+func stillBunting(g buntingGeometry) template.HTML {
+ span := g.Pitch*g.Per + 2*g.End
+ row := span * g.Swags
+ half := float64(g.FW) / 2
+ var b strings.Builder
+ fmt.Fprintf(&b, `<svg class="site-flags" viewBox="0 0 %d %d" preserveAspectRatio="xMidYMin slice" focusable="false">`+"\n<defs>\n", row, g.H)
+ for k, fill := range pennantFills {
+ fmt.Fprintf(&b, `<g id="site-p%d"><polygon fill="%s" points="%g,0 %g,0 0,%d"/><polygon fill="#000" opacity=".16" points="%g,0 %g,0 %.1f,3.2 %.1f,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M%.1f 3.6H%.1f"/></g>`+"\n",
+ k, fill, -half, half, g.FH, -half, half, half-1.3, -(half - 1.3), -(half - 1.5), half-1.5)
+ }
+ b.WriteString("</defs>\n")
+ // The rope between two tacks is a quadratic curve through its
+ // midpoint, Sag below the tacks; this is its height at x.
+ ropeY := func(x float64, s int) float64 {
+ t := (x - float64(s*span)) / float64(span)
+ return float64(g.TopY) + 2*t*(1-t)*g.Sag
+ }
+ for s := 0; s < g.Swags; s++ {
+ x0 := s * span
+ fmt.Fprintf(&b, `<path class="site-span" d="M%d %d Q%d %g %d %d"/>`+"\n", x0, g.TopY, x0+span/2, float64(g.TopY)+g.Sag, x0+span, g.TopY)
+ for p := 0; p < g.Per; p++ {
+ k := s*g.Per + p
+ cx := x0 + g.End + g.Pitch*p + g.Pitch/2
+ top := ropeY(float64(cx), s) + float64(g.Tie)
+ l, r := float64(cx)-half, float64(cx)+half
+ fmt.Fprintf(&b, `<path class="site-tie" d="M%g %.2fV%.2fM%g %.2fV%.2f"/>`+"\n", l, ropeY(l, s), top, r, ropeY(r, s), top)
+ fmt.Fprintf(&b, `<g class="site-flag" data-c="%d" transform="translate(%d %.2f)"><use href="#site-p%d"/></g>`+"\n", k%len(pennantFills), cx, top, k%len(pennantFills))
+ }
+ }
+ b.WriteString("</svg>")
+ return template.HTML(b.String())
+}
+
// T is one catalog string by key. A key the catalog lacks is an error,
// so a typo in the template fails the render rather than rendering an
// empty element.
@@ -50,7 +113,7 @@ func landing(siteDir fs.FS, lang string) ([]byte, error) {
return nil, fmt.Errorf("www: %w", err)
}
var buf bytes.Buffer
- if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Version: fiesta.Version, strings: strings}); err != nil {
+ if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Version: fiesta.Version, Bunting: stillBunting(buntingShape), strings: strings}); err != nil {
return nil, fmt.Errorf("www: rendering the landing page in %s: %w", lang, err)
}
return buf.Bytes(), nil
diff --git a/internal/www/landing_browser_test.go b/internal/www/landing_browser_test.go
index c4282c4..d545a24 100644
--- a/internal/www/landing_browser_test.go
+++ b/internal/www/landing_browser_test.go
@@ -223,26 +223,105 @@ const measureJS = `(() => {
return JSON.stringify({Doc: de.scrollWidth, Client: w, Body: document.body.scrollWidth, Over: over.slice(0, 8)});
})()`
+// windowJS measures the sample window's own content: the window's box
+// against the viewport, and every shown element inside it against the
+// window's box, since the window clips its overflow and would hide a
+// pane that reached past its edge. An element inside a box that
+// scrolls sideways on purpose (the sidebar's row on a phone) is left
+// to its scroller, which must itself fit.
+const windowJS = `(() => {
+ const w = document.querySelector(".site-window"), wr = w.getBoundingClientRect(), vw = document.documentElement.clientWidth;
+ const scrolls = el => { for (let p = el.parentElement; p && p !== w; p = p.parentElement) { const o = getComputedStyle(p).overflowX; if (o === "auto" || o === "scroll") return true; } return false; };
+ const over = [];
+ w.querySelectorAll("*").forEach(el => {
+ const r = el.getBoundingClientRect();
+ if (r.width === 0 || scrolls(el)) return;
+ if (r.right > wr.right + 1 || r.left < wr.left - 1) {
+ const cls = (el.className || "").toString().trim().split(/\s+/)[0];
+ over.push(el.tagName.toLowerCase() + (cls ? "." + cls : "") + " [" + Math.round(r.left) + "…" + Math.round(r.right) + "]");
+ }
+ });
+ return JSON.stringify({Left: wr.left, Right: wr.right, VW: vw, Over: over.slice(0, 8)});
+})()`
+
// TestTheLandingHasNoHorizontalScrollAt320 is WCAG 1.4.10 Reflow on
// the landing page: at a 320 CSS-pixel viewport nothing scrolls
-// sideways. The bunting is allowed to be wider than the page: it is
-// clipped by its own box, and the check is the document's scroll
-// width, which clipping keeps.
+// sideways, on the page as it opens and with every sample pane and
+// every course shown, and nothing inside the sample window reaches
+// past the window's edge, where its clipping would hide it. The
+// page's content is required first, so a stub with a sample and
+// nothing else cannot pass. The bunting is allowed to be wider than
+// the page: it is clipped by its own box, and the check is the
+// document's scroll width, which clipping keeps.
func TestTheLandingHasNoHorizontalScrollAt320(t *testing.T) {
rig, _ := siteRig(t)
ctx, cancel := context.WithTimeout(rig.Context(), 2*time.Minute)
defer cancel()
mustRun(t, ctx, chromedp.ActionFunc(func(c context.Context) error {
return emulation.SetDeviceMetricsOverride(320, 640, 1, false).Do(c)
- }), chromedp.Navigate(rig.Origin+"/"), soon("#sample"))
- var got struct {
- Doc, Client, Body int
- Over []string
+ }), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitReady("body"),
+ chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise))
+ for _, c := range []struct {
+ sel string
+ want int
+ }{
+ {"#headline", 1},
+ {"#install code", 1},
+ {".site-themebar", 1},
+ {"#sample .site-side a", 5},
+ {"#sample .site-pane", 5},
+ {"#sample .site-pane h2", 5},
+ {"#sample .site-course", 3},
+ {"#reasons [rst-card]", 6},
+ {"#footer nav", 4},
+ {"#bunting svg", 1},
+ } {
+ if s := visible(t, ctx, c.sel); s.Total != c.want {
+ t.Fatalf("%s: %d on the page; want %d. This is not the landing page, and measuring it would prove nothing", c.sel, s.Total, c.want)
+ }
}
- at(t, ctx, measureJS, &got)
- if got.Doc > got.Client || got.Body > got.Client {
- t.Errorf("the landing page scrolls sideways at 320px (WCAG 1.4.10): document %dpx, body %dpx in a %dpx page\n widest: %s",
- got.Doc, got.Body, got.Client, strings.Join(got.Over, "\n "))
+ mustRun(t, ctx, soon(".site-themebar"))
+
+ measure := func(where string) {
+ t.Helper()
+ var page struct {
+ Doc, Client, Body int
+ Over []string
+ }
+ at(t, ctx, measureJS, &page)
+ if page.Doc > page.Client || page.Body > page.Client {
+ t.Errorf("%s: the page scrolls sideways at 320px (WCAG 1.4.10): document %dpx, body %dpx in a %dpx page\n widest: %s",
+ where, page.Doc, page.Body, page.Client, strings.Join(page.Over, "\n "))
+ }
+ var win struct {
+ Left, Right, VW float64
+ Over []string
+ }
+ at(t, ctx, windowJS, &win)
+ if win.Left < -1 || win.Right > win.VW+1 {
+ t.Errorf("%s: the sample window spans %.0f…%.0f in a %.0fpx viewport", where, win.Left, win.Right, win.VW)
+ }
+ if len(win.Over) > 0 {
+ t.Errorf("%s: %d element(s) inside the sample reach past the window's edge, where it clips them:\n %s", where, len(win.Over), strings.Join(win.Over, "\n "))
+ }
+ }
+ measure("as the page opens")
+ for _, pane := range []string{"pane-guests", "pane-menu", "pane-music", "pane-settings", "pane-overview"} {
+ showPane(t, ctx, pane)
+ if p := readPanes(t, ctx); strings.Join(p.Panes, " ") != pane {
+ t.Fatalf("after pressing %s the sample shows %v", pane, p.Panes)
+ }
+ measure(pane)
+ if pane != "pane-menu" {
+ continue
+ }
+ for _, course := range []string{"course-mains", "course-desserts", "course-starters"} {
+ mustRun(t, ctx, chromedp.Click(fmt.Sprintf(`#sample [data-site-course][href="#%s"]`, course), chromedp.ByQuery))
+ if p := readPanes(t, ctx); strings.Join(p.Courses, " ") != course {
+ t.Fatalf("after pressing %s the Menu pane shows %v", course, p.Courses)
+ }
+ measure(course)
+ }
}
rig.Screen("#sample", "the page at 320")
}
@@ -350,7 +429,12 @@ func TestTheThemeComesBackBeforeFirstPaint(t *testing.T) {
open(t, ctx, rig, 1280, 900)
var first firstFrame
mustRun(t, ctx, chromedp.Evaluate(`window.__first`, &first))
+ // The steady state, waited for: a page that came back in Plain
+ // late would otherwise fail here, before the first frame is judged.
plain := rootBg(t, ctx)
+ for deadline := time.Now().Add(3 * time.Second); plain == day && time.Now().Before(deadline); plain = rootBg(t, ctx) {
+ time.Sleep(50 * time.Millisecond)
+ }
if plain == day || plain == "" {
t.Fatalf("after the reload --rst-bg is %q, the same as Day's; Plain did not come back at all", plain)
}
@@ -448,17 +532,20 @@ func themeLink(t *testing.T, ctx context.Context) (href, current string) {
// TestABrokenStoredThemeFallsBackToDay: a stored theme that is not
// day, plain or signal, and a storage that throws, both load Day's
-// stylesheet and mark Day.
+// stylesheet and mark Day. The broken values include the names every
+// object inherits, which a lookup in a plain object would accept.
func TestABrokenStoredThemeFallsBackToDay(t *testing.T) {
rig, _ := siteRig(t)
ctx, cancel := context.WithTimeout(rig.Context(), 2*time.Minute)
defer cancel()
open(t, ctx, rig, 1280, 900)
- mustRun(t, ctx, chromedp.Evaluate(`localStorage.setItem("fiesta-theme", "bogus"); true`, nil))
- open(t, ctx, rig, 1280, 900)
- mustRun(t, ctx, soon(".site-themebar"))
- if href, cur := themeLink(t, ctx); href != "/1/fiesta.css" || cur != "day" {
- t.Errorf("with bogus stored the page loads %q and marks %q; want /1/fiesta.css and day", href, cur)
+ for _, stored := range []string{"bogus", "constructor", "__proto__", "toString", "DAY", ""} {
+ mustRun(t, ctx, chromedp.Evaluate(fmt.Sprintf(`localStorage.setItem("fiesta-theme", %q); true`, stored), nil))
+ open(t, ctx, rig, 1280, 900)
+ mustRun(t, ctx, soon(".site-themebar"))
+ if href, cur := themeLink(t, ctx); href != "/1/fiesta.css" || cur != "day" {
+ t.Errorf("with %q stored the page loads %q and marks %q; want /1/fiesta.css and day", stored, href, cur)
+ }
}
mustRun(t, ctx, chromedp.Evaluate(`localStorage.setItem("fiesta-theme", "plain"); true`, nil))
@@ -812,6 +899,75 @@ func TestTheBuntingMovesOnlyWhenMotionIsAllowed(t *testing.T) {
rig.Screen("#headline", "the page under normal motion")
}
+// ropeJS reads the rope's colour two ways: the most opaque pixel in
+// the canvas's top rows, where only the rope and its ties are drawn,
+// and the token the still SVG's rope is stroked with.
+const ropeJS = `(() => {
+ const c = document.querySelector("#bunting canvas");
+ if (!c) return JSON.stringify({Alpha: -1});
+ const d = c.getContext("2d").getImageData(0, 0, c.width, 6).data;
+ let best = -1, rope = null;
+ for (let i = 0; i < d.length; i += 4) if (d[i + 3] > best) { best = d[i + 3]; rope = [d[i], d[i + 1], d[i + 2]]; }
+ const token = (getComputedStyle(document.querySelector(".site-span")).stroke.match(/\d+/g) || []).slice(0, 3).map(Number);
+ return JSON.stringify({Alpha: best, Rope: rope, Token: token});
+})()`
+
+type rope struct {
+ Alpha int
+ Rope, Token []int
+}
+
+// near is whether two colours are within a few steps per channel, the
+// slack antialiasing and rounding need.
+func near(a, b []int) bool {
+ if len(a) != 3 || len(b) != 3 {
+ return false
+ }
+ for i := range a {
+ if d := a[i] - b[i]; d > 6 || d < -6 {
+ return false
+ }
+ }
+ return true
+}
+
+// ropeMatches polls until the canvas's rope is the token's colour, or
+// fails naming both.
+func ropeMatches(t *testing.T, ctx context.Context, when string) rope {
+ t.Helper()
+ deadline := time.Now().Add(5 * time.Second)
+ for {
+ var r rope
+ at(t, ctx, ropeJS, &r)
+ if r.Alpha > 0 && near(r.Rope, r.Token) {
+ return r
+ }
+ if !time.Now().Before(deadline) {
+ t.Fatalf("%s: the canvas's rope is %v (alpha %d) but the token is %v; the bunting was not repainted", when, r.Rope, r.Alpha, r.Token)
+ }
+ time.Sleep(50 * time.Millisecond)
+ }
+}
+
+// TestTheBuntingFollowsTheTheme: the rope is a Fiesta token, and the
+// three themes give it three colours; choosing Signal repaints the
+// canvas in Signal's once its stylesheet has loaded.
+func TestTheBuntingFollowsTheTheme(t *testing.T) {
+ rig, _ := siteRig(t)
+ ctx, cancel := context.WithTimeout(rig.Context(), 2*time.Minute)
+ defer cancel()
+ open(t, ctx, rig, 1280, 900)
+ waitBunting(t, ctx, "the canvas under normal motion", func(b bunting) bool { return b.Canvas && b.CanvasShown })
+ day := ropeMatches(t, ctx, "in Day")
+ chooseTheme(t, ctx, "signal")
+ signal := ropeMatches(t, ctx, "after choosing Signal")
+ if near(day.Token, signal.Token) {
+ t.Fatalf("Day's rope token %v and Signal's %v are the same colour; this drive proves nothing", day.Token, signal.Token)
+ }
+ mustRun(t, ctx, chromedp.Evaluate(`localStorage.clear(); true`, nil))
+ rig.Screen("#headline", "the page in Signal")
+}
+
// withoutAnchorSupport is what anchor.js's feature test sees in a
// browser without anchor positioning: CSS.supports says no to it.
const withoutAnchorSupport = `(() => { const s = CSS.supports.bind(CSS); CSS.supports = (...a) => !/position-area|position-try/.test(a.join(" ")) && s(...a); })()`
diff --git a/internal/www/landing_test.go b/internal/www/landing_test.go
index 21e6953..a73a9ed 100644
--- a/internal/www/landing_test.go
+++ b/internal/www/landing_test.go
@@ -3,8 +3,10 @@ package www_test
import (
"bytes"
"encoding/json"
+ "fmt"
"io/fs"
"regexp"
+ "strconv"
"strings"
"testing"
"testing/fstest"
@@ -212,6 +214,88 @@ func TestTheLinkBoxShowsTheWholeURL(t *testing.T) {
}
}
+// jsConstant reads one `NAME = <number>` from bunting.js, failing when
+// the script no longer declares it: a constant that moved is a drift
+// this test can no longer see.
+func jsConstant(t *testing.T, js, name string) int {
+ t.Helper()
+ m := regexp.MustCompile(`\b` + name + ` = (\d+)\b`).FindStringSubmatch(js)
+ if m == nil {
+ t.Fatalf("bunting.js no longer declares %s = <number>; the still bunting cannot be held to it", name)
+ }
+ n, _ := strconv.Atoi(m[1])
+ return n
+}
+
+// TestTheStillBuntingIsTheAnimations: the still SVG in the markup,
+// which a reader with no JS or under reduced motion sees, hangs the
+// same bunting bunting.js animates: the same row width and height,
+// the same number of swags, the same pennants at the same places,
+// read from the script's own constants. The pennants' colours cycle
+// in the order the script reads from the markup.
+func TestTheStillBuntingIsTheAnimations(t *testing.T) {
+ js, err := fs.ReadFile(siteSource(t).FS(), "static/bunting.js")
+ if err != nil {
+ t.Fatal(err)
+ }
+ pitch, per, end := jsConstant(t, string(js), "PITCH"), jsConstant(t, string(js), "PER"), jsConstant(t, string(js), "END")
+ fw, fh, h := jsConstant(t, string(js), "FW"), jsConstant(t, string(js), "FH"), jsConstant(t, string(js), "H")
+ m := regexp.MustCompile(`\bROW = SPAN \* (\d+)\b`).FindStringSubmatch(string(js))
+ if m == nil {
+ t.Fatal("bunting.js no longer declares ROW = SPAN * <swags>")
+ }
+ swags, _ := strconv.Atoi(m[1])
+ span := pitch*per + 2*end
+
+ var svg *xhtml.Node
+ walk(parse(t, english(t)), func(n *xhtml.Node) {
+ if n.Data == "svg" && attr(n, "class") == "site-flags" {
+ svg = n
+ }
+ })
+ if svg == nil {
+ t.Fatal("the page has no svg.site-flags")
+ }
+ if want := fmt.Sprintf("0 0 %d %d", span*swags, h); attr(svg, "viewBox") != want {
+ t.Errorf("the still bunting's viewBox is %q; the animation's row is %q", attr(svg, "viewBox"), want)
+ }
+ spans, flags, defs := 0, 0, 0
+ translate := regexp.MustCompile(`^translate\((-?[\d.]+) (-?[\d.]+)\)$`)
+ walk(svg, func(n *xhtml.Node) {
+ switch {
+ case n.Data == "path" && attr(n, "class") == "site-span":
+ spans++
+ case n.Data == "polygon" && strings.Contains(attr(n, "points"), fmt.Sprintf("0,%d", fh)) && strings.HasPrefix(attr(n, "points"), fmt.Sprintf("-%d,0 %d,0", fw/2, fw/2)):
+ defs++
+ case n.Data == "g" && attr(n, "class") == "site-flag":
+ k := flags
+ flags++
+ tm := translate.FindStringSubmatch(attr(n, "transform"))
+ if tm == nil {
+ t.Errorf("pennant %d has transform %q; want a plain translate", k, attr(n, "transform"))
+ return
+ }
+ x, _ := strconv.ParseFloat(tm[1], 64)
+ want := float64((k/per)*span + end + (k%per)*pitch + pitch/2)
+ if x != want {
+ t.Errorf("pennant %d hangs at x=%g; the animation hangs it at %g", k, x, want)
+ }
+ if c, _ := strconv.Atoi(attr(n, "data-c")); c != k%6 {
+ t.Errorf("pennant %d is cloth %d; want %d, cycling through the six", k, c, k%6)
+ }
+ }
+ })
+ if spans != swags {
+ t.Errorf("the still bunting has %d spans; the animation has %d swags", spans, swags)
+ }
+ if flags != swags*per {
+ t.Errorf("the still bunting has %d pennants; the animation hangs %d, %d a swag", flags, swags*per, per)
+ }
+ if defs != 6 {
+ t.Errorf("%d pennant definitions are %d wide and %d tall; want all six", defs, fw, fh)
+ }
+}
+
// marked renders the landing page from siteDir with every English
// string wrapped as ⟦value⟧, so what came from the catalog can be told
// from what did not.
diff --git a/site/landing/page.html.tmpl b/site/landing/page.html.tmpl
index abd301a..8199d8e 100644
--- a/site/landing/page.html.tmpl
+++ b/site/landing/page.html.tmpl
@@ -47,99 +47,7 @@
</header>
<div class="site-bunting" id="bunting" aria-hidden="true">
-<svg class="site-flags" viewBox="0 0 2784 96" preserveAspectRatio="xMidYMin slice" focusable="false">
- <defs>
- <!-- One pennant, origin at the middle of its top edge: a triangle with a sewn tape along the top. -->
- <g id="site-p0"><polygon fill="#2257df" points="-18,0 18,0 0,44"/><polygon fill="#000" opacity=".16" points="-18,0 18,0 16.7,3.2 -16.7,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M-16.9 3.6H16.9"/></g>
- <g id="site-p1"><polygon fill="#f2b632" points="-18,0 18,0 0,44"/><polygon fill="#000" opacity=".16" points="-18,0 18,0 16.7,3.2 -16.7,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M-16.9 3.6H16.9"/></g>
- <g id="site-p2"><polygon fill="#e8553f" points="-18,0 18,0 0,44"/><polygon fill="#000" opacity=".16" points="-18,0 18,0 16.7,3.2 -16.7,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M-16.9 3.6H16.9"/></g>
- <g id="site-p3"><polygon fill="#16a37f" points="-18,0 18,0 0,44"/><polygon fill="#000" opacity=".16" points="-18,0 18,0 16.7,3.2 -16.7,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M-16.9 3.6H16.9"/></g>
- <g id="site-p4"><polygon fill="#7aa2ff" points="-18,0 18,0 0,44"/><polygon fill="#000" opacity=".16" points="-18,0 18,0 16.7,3.2 -16.7,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M-16.9 3.6H16.9"/></g>
- <g id="site-p5"><polygon fill="#e84f8a" points="-18,0 18,0 0,44"/><polygon fill="#000" opacity=".16" points="-18,0 18,0 16.7,3.2 -16.7,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M-16.9 3.6H16.9"/></g>
- </defs>
- <path class="site-span" d="M0 4Q48 16 96 4"/>
- <path class="site-span" d="M96 4Q144 16 192 4"/>
- <path class="site-span" d="M192 4Q240 16 288 4"/>
- <path class="site-span" d="M288 4Q336 16 384 4"/>
- <path class="site-span" d="M384 4Q432 16 480 4"/>
- <path class="site-span" d="M480 4Q528 16 576 4"/>
- <path class="site-span" d="M576 4Q624 16 672 4"/>
- <path class="site-span" d="M672 4Q720 16 768 4"/>
- <path class="site-span" d="M768 4Q816 16 864 4"/>
- <path class="site-span" d="M864 4Q912 16 960 4"/>
- <path class="site-span" d="M960 4Q1008 16 1056 4"/>
- <path class="site-span" d="M1056 4Q1104 16 1152 4"/>
- <path class="site-span" d="M1152 4Q1200 16 1248 4"/>
- <path class="site-span" d="M1248 4Q1296 16 1344 4"/>
- <path class="site-span" d="M1344 4Q1392 16 1440 4"/>
- <path class="site-span" d="M1440 4Q1488 16 1536 4"/>
- <path class="site-span" d="M1536 4Q1584 16 1632 4"/>
- <path class="site-span" d="M1632 4Q1680 16 1728 4"/>
- <path class="site-span" d="M1728 4Q1776 16 1824 4"/>
- <path class="site-span" d="M1824 4Q1872 16 1920 4"/>
- <path class="site-span" d="M1920 4Q1968 16 2016 4"/>
- <path class="site-span" d="M2016 4Q2064 16 2112 4"/>
- <path class="site-span" d="M2112 4Q2160 16 2208 4"/>
- <path class="site-span" d="M2208 4Q2256 16 2304 4"/>
- <path class="site-span" d="M2304 4Q2352 16 2400 4"/>
- <path class="site-span" d="M2400 4Q2448 16 2496 4"/>
- <path class="site-span" d="M2496 4Q2544 16 2592 4"/>
- <path class="site-tie" d="M6 5.41L6.14 12.42M42 9.91L41.86 16.89M54 9.91L54.14 16.89M90 5.41L89.86 12.42M102 5.41L102.14 12.42M138 9.91L137.86 16.89M150 9.91L150.14 16.89M186 5.41L185.86 12.42M198 5.41L198.14 12.42M234 9.91L233.86 16.89M246 9.91L246.14 16.89M282 5.41L281.86 12.42M294 5.41L294.14 12.42M330 9.91L329.86 16.89M342 9.91L342.14 16.89M378 5.41L377.86 12.42M390 5.41L390.14 12.42M426 9.91L425.86 16.89M438 9.91L438.14 16.89M474 5.41L473.86 12.42M486 5.41L486.14 12.42M522 9.91L521.86 16.89M534 9.91L534.14 16.89M570 5.41L569.86 12.42M582 5.41L582.14 12.42M618 9.91L617.86 16.89M630 9.91L630.14 16.89M666 5.41L665.86 12.42M678 5.41L678.14 12.42M714 9.91L713.86 16.89M726 9.91L726.14 16.89M762 5.41L761.86 12.42M774 5.41L774.14 12.42M810 9.91L809.86 16.89M822 9.91L822.14 16.89M858 5.41L857.86 12.42M870 5.41L870.14 12.42M906 9.91L905.86 16.89M918 9.91L918.14 16.89M954 5.41L953.86 12.42M966 5.41L966.14 12.42M1002 9.91L1001.86 16.89M1014 9.91L1014.14 16.89M1050 5.41L1049.86 12.42M1062 5.41L1062.14 12.42M1098 9.91L1097.86 16.89M1110 9.91L1110.14 16.89M1146 5.41L1145.86 12.42M1158 5.41L1158.14 12.42M1194 9.91L1193.86 16.89M1206 9.91L1206.14 16.89M1242 5.41L1241.86 12.42M1254 5.41L1254.14 12.42M1290 9.91L1289.86 16.89M1302 9.91L1302.14 16.89M1338 5.41L1337.86 12.42M1350 5.41L1350.14 12.42M1386 9.91L1385.86 16.89M1398 9.91L1398.14 16.89M1434 5.41L1433.86 12.42M1446 5.41L1446.14 12.42M1482 9.91L1481.86 16.89M1494 9.91L1494.14 16.89M1530 5.41L1529.86 12.42M1542 5.41L1542.14 12.42M1578 9.91L1577.86 16.89M1590 9.91L1590.14 16.89M1626 5.41L1625.86 12.42M1638 5.41L1638.14 12.42M1674 9.91L1673.86 16.89M1686 9.91L1686.14 16.89M1722 5.41L1721.86 12.42M1734 5.41L1734.14 12.42M1770 9.91L1769.86 16.89M1782 9.91L1782.14 16.89M1818 5.41L1817.86 12.42M1830 5.41L1830.14 12.42M1866 9.91L1865.86 16.89M1878 9.91L1878.14 16.89M1914 5.41L1913.86 12.42M1926 5.41L1926.14 12.42M1962 9.91L1961.86 16.89M1974 9.91L1974.14 16.89M2010 5.41L2009.86 12.42M2022 5.41L2022.14 12.42M2058 9.91L2057.86 16.89M2070 9.91L2070.14 16.89M2106 5.41L2105.86 12.42M2118 5.41L2118.14 12.42M2154 9.91L2153.86 16.89M2166 9.91L2166.14 16.89M2202 5.41L2201.86 12.42M2214 5.41L2214.14 12.42M2250 9.91L2249.86 16.89M2262 9.91L2262.14 16.89M2298 5.41L2297.86 12.42M2310 5.41L2310.14 12.42M2346 9.91L2345.86 16.89M2358 9.91L2358.14 16.89M2394 5.41L2393.86 12.42M2406 5.41L2406.14 12.42M2442 9.91L2441.86 16.89M2454 9.91L2454.14 16.89M2490 5.41L2489.86 12.42M2502 5.41L2502.14 12.42M2538 9.91L2537.86 16.89M2550 9.91L2550.14 16.89M2586 5.41L2585.86 12.42"/>
- <g class="site-flag" data-c="0" transform="translate(24 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(72 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(120 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(168 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(216 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(264 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(312 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(360 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(408 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(456 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(504 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(552 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(600 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(648 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(696 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(744 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(792 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(840 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(888 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(936 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(984 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(1032 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(1080 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(1128 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(1176 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(1224 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(1272 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(1320 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(1368 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(1416 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(1464 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(1512 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(1560 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(1608 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(1656 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(1704 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(1752 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(1800 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(1848 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(1896 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(1944 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(1992 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(2040 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(2088 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(2136 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(2184 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(2232 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(2280 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
- <g class="site-flag" data-c="0" transform="translate(2328 14.66) rotate(7.13)"><use href="#site-p0"/></g>
- <g class="site-flag" data-c="1" transform="translate(2376 14.66) rotate(-7.13)"><use href="#site-p1"/></g>
- <g class="site-flag" data-c="2" transform="translate(2424 14.66) rotate(7.13)"><use href="#site-p2"/></g>
- <g class="site-flag" data-c="3" transform="translate(2472 14.66) rotate(-7.13)"><use href="#site-p3"/></g>
- <g class="site-flag" data-c="4" transform="translate(2520 14.66) rotate(7.13)"><use href="#site-p4"/></g>
- <g class="site-flag" data-c="5" transform="translate(2568 14.66) rotate(-7.13)"><use href="#site-p5"/></g>
-</svg>
+{{.Bunting}}
</div>
<main id="main">
diff --git a/site/static/bunting.js b/site/static/bunting.js
index e89e204..f7f2681 100644
--- a/site/static/bunting.js
+++ b/site/static/bunting.js
@@ -413,14 +413,18 @@
rz = setTimeout(function () { if (live) { layout(); draw(); } }, 120);
});
- // The rope's colour and the pennants' fills follow the scheme, so the
- // textures are painted again when it changes: from the page's own
- // menu (rst-scheme, which fiesta.js dispatches) or from the OS.
+ // The rope's colour is a Fiesta token, which each theme and each
+ // scheme sets differently, so the textures are painted again when
+ // either changes: the scheme from the page's own menu (rst-scheme,
+ // which fiesta.js dispatches) or from the OS, and the theme when the
+ // stylesheet site.js swapped into #site-theme has loaded.
function repaint() {
if (live) textures(function () { if (live) draw(); });
}
document.addEventListener("rst-scheme", repaint);
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", repaint);
+ var themeLink = document.getElementById("site-theme");
+ if (themeLink) themeLink.addEventListener("load", repaint);
// The canvas is hung only while motion is allowed. When the
// preference turns on mid-visit the canvas goes and the SVG shows;
diff --git a/site/static/site.js b/site/static/site.js
index a877454..6a2a078 100644
--- a/site/static/site.js
+++ b/site/static/site.js
@@ -22,6 +22,12 @@
"use strict";
var THEMES = { day: "/1/fiesta.css", plain: "/1/fiesta-plain.css", signal: "/1/fiesta-signal.css" };
+ // The three names, spelled out: a lookup in THEMES alone would take
+ // "constructor" or "__proto__", which every object inherits, as a
+ // theme, and mark none of the tabs for it.
+ function isTheme(t) {
+ return t === "day" || t === "plain" || t === "signal";
+ }
function stored() {
try { return localStorage.getItem("fiesta-theme"); } catch (e) { return null; }
}
@@ -38,7 +44,7 @@
var bar = document.querySelector(".site-themebar"), link = document.getElementById("site-theme");
if (bar && link) {
var theme = stored();
- if (!THEMES[theme]) theme = "day";
+ if (!isTheme(theme)) theme = "day";
markTheme(theme);
bar.hidden = false;
bar.addEventListener("click", function (e) {
@@ -46,7 +52,7 @@
if (!a) return;
e.preventDefault();
var t = a.getAttribute("data-site-theme");
- if (!THEMES[t]) return;
+ if (!isTheme(t)) return;
if (link.getAttribute("href") !== THEMES[t]) link.setAttribute("href", THEMES[t]);
remember(t);
markTheme(t);