rastrillo / fiesta Public

The column shell's language and scheme menus move to a footer row at the foot of the page

A menu above the page's title pushed the title down and read as the
page's first action. The column now opens with its page header, and
its language and colour-scheme menus sit in rst-page-foot, the
column's last child: muted, one type step down, under a hairline,
packed at the inline end, and wrapping on a phone rather than
scrolling sideways.

The row's summaries are flat, as a shell tail's are. The four
summary-face rules, in both spellings, list the row's dropdowns in
their :not(); the new argument weighs what the shell tail's does, so
no summary 1.0 styles changes weight. The eight selectors are in
widenedSelectors, each with exactly what it adds.

Where nothing anchors a panel, it hangs from its <details>, and
Language is not at the row's end: its panel, at least 176px wide,
ran past the opposite edge of the page (to -78px at 320 with scripts
off). The row is the containing block instead, relative with its
dropdowns static, so every panel opens under the row and inside it,
with anchor.js and with no script. A row left holding only template
whitespace is hidden by :not(:has(> *)), since :empty counts the
whitespace.

The example: the language menu moves into the footer unchanged; a
scheme menu of three one-field forms posting to /_theme follows it,
with System current, the shape Rastrillo's column layout renders;
/_theme joins sampleURLs; the blurb is the approved English. Its
eleven translations are machine drafts from the approved English,
not reviewed, recorded in drafts.json. The frame heights grow by what
the sweep names, because the language menu now opens down under the
page instead of over it: Desktop 590 to 920, Mobile 980 to 1330, ga
1040 to 1390, ru 1050 to 1400.

The spelling fixture gains the row, with two menus and with only
whitespace, since TestBothSpellingsComputeTheSameStyles proves a class
pairs with its attribute only where the fixture renders it.
TestTheFootersMenusOpenOnScreen is weighed at 4 s for the slice deal.

New tests, each red by assertion before the change:
TestTheColumnShellEndsWithItsFooterRow, TestTheFooterRowSitsUnderThePage,
TestTheFootersMenusOpenOnScreen (its no-anchor legs),
TestAnEmptyFooterRowDrawsNothing and
TestTheColumnDemosMenusAreInItsFooter. TestTheFootersMenusWearNoFace
could not run before (no class for the row); taking the row out of the
face rules turns it red in every state and spelling. The anchored leg
of TestTheFootersMenusOpenOnScreen already passed, since 1.0's anchor
rules hold; position-try-fallbacks cut to flip-inline turns it red.
Its left-to-right labels are the widest pair in the gallery's sample
catalogs, the words the column demo shows (Portuguese's); the
catalogs have no gallery.scheme.label.

Size, gzip level 6, before and after: fiesta.css 23,018 to 23,146
(+128, allotment 150), fiesta-plain.css 22,303 to 22,431,
fiesta-signal.css 22,414 to 22,542.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b5dcf46e164a5aa7168dea285975a9b6f2f57967 parent e2207ed
23 files changed, +666 −24
  • check/baseline_test.go +48 −1
  • check/footer_browser_test.go +369 −0
  • check/footer_test.go +115 −0
  • check/testdata/fixtures/spelling-class.html +2 −0
  • css/tokens.css +48 −5
  • examples/examples.json +1 −0
  • examples/layout-column/default.html +11 −1
  • examples/prose.json +1 −1
  • hack/test-weights.txt +1 −0
  • internal/gallery/frame_test.go +54 −0
  • internal/gallery/heights.go +4 −4
  • internal/gallery/i18n/ar.json +1 −1
  • internal/gallery/i18n/bn.json +1 −1
  • internal/gallery/i18n/drafts.json +1 −1
  • internal/gallery/i18n/es.json +1 −1
  • internal/gallery/i18n/ga.json +1 −1
  • internal/gallery/i18n/hi.json +1 −1
  • internal/gallery/i18n/ja.json +1 −1
  • internal/gallery/i18n/pt-PT.json +1 −1
  • internal/gallery/i18n/ru.json +1 −1
  • internal/gallery/i18n/vi.json +1 −1
  • internal/gallery/i18n/yue.json +1 −1
  • internal/gallery/i18n/zh-Hans.json +1 −1

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.209s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 153737 bytes, 22431 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 153832 bytes, 22542 bytes gzipped (22.0 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 156651 bytes, 23146 bytes gzipped (22.6 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.024s
=== history ===
history: 19 commits; release tags: v1.0.0
=== suite ===
--- shard 0 of 48 ---
==> shard 0 of 48: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh root 'internal/gallery@3 cmd/fiesta@2 internal/www@2'
ok  	amadan.net/rastrillo/fiesta	2.658s
ok  	amadan.net/rastrillo/fiesta/check	7.514s
ok  	amadan.net/rastrillo/fiesta/example	0.108s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.102s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.069s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.010s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.008s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.398s
ok  	amadan.net/rastrillo/fiesta/internal/site	2.390s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.006s
suite: root took 15s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m11.540000s 0m1.950000s
--- shard 1 of 48 ---
==> shard 1 of 48: make test.internal.gallery~1of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~1of3'
gotest: slice 1 of 3 of ./internal/gallery/ runs 35 of 106 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	62.824s
suite: test.internal.gallery~1of3 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.860000s 0m1.230000s
--- shard 2 of 48 ---
==> shard 2 of 48: make test.internal.gallery~2of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~2of3'
gotest: slice 2 of 3 of ./internal/gallery/ runs 35 of 106 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.647s
suite: test.internal.gallery~2of3 took 71s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.440000s 0m1.240000s
--- shard 3 of 48 ---
==> shard 3 of 48: make test.internal.gallery~3of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~3of3'
gotest: slice 3 of 3 of ./internal/gallery/ runs 36 of 106 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	54.905s
suite: test.internal.gallery~3of3 took 61s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.290000s 0m1.130000s
--- shard 4 of 48 ---
==> shard 4 of 48: make test.cmd.fiesta~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~1of2'
gotest: slice 1 of 2 of ./cmd/fiesta/ runs 7 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	49.186s
suite: test.cmd.fiesta~1of2 took 53s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.260000s 0m2.120000s
--- shard 5 of 48 ---
==> shard 5 of 48: make test.cmd.fiesta~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~2of2'
gotest: slice 2 of 2 of ./cmd/fiesta/ runs 5 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	52.714s
suite: test.cmd.fiesta~2of2 took 57s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.060000s 0m2.180000s
--- shard 6 of 48 ---
==> shard 6 of 48: make test.internal.www~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~1of2'
gotest: slice 1 of 2 of ./internal/www/ runs 11 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	59.930s
suite: test.internal.www~1of2 took 66s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.140000s 0m1.580000s
--- shard 7 of 48 ---
==> shard 7 of 48: make test.internal.www~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~2of2'
gotest: slice 2 of 2 of ./internal/www/ runs 14 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	57.984s
suite: test.internal.www~2of2 took 64s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.300000s 0m1.740000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
suite: staticcheck took 12s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.000000s 0m0.830000s
--- shard 9 of 48 ---
==> shard 9 of 48: 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.
suite: govulncheck took 8s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m3.220000s 0m0.380000s
--- shard 10 of 48 ---
==> shard 10 of 48: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
3:09AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
3:10AM INF 19 commits scanned.
3:10AM INF scanned ~8247241 bytes (8.25 MB) in 7.02s
3:10AM INF no leaks found
suite: gitleaks took 8s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m6.880000s 0m0.230000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: ./internal/harness/ runs its 10 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/harness	5.013s
suite: browser.internal.harness took 11s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m1.640000s 0m1.300000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 16 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	86.722s
suite: browser.check~1of13 took 96s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.680000s 0m13.170000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	90.417s
suite: browser.check~2of13 took 100s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.540000s 0m13.280000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 15 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	88.942s
suite: browser.check~3of13 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.670000s 0m10.850000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 15 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	84.859s
suite: browser.check~4of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.160000s 0m15.480000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	76.341s
suite: browser.check~5of13 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.230000s 0m12.800000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	76.149s
suite: browser.check~6of13 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.520000s 0m14.540000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	83.655s
suite: browser.check~7of13 took 93s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.310000s 0m14.270000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	86.310s
suite: browser.check~8of13 took 96s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.890000s 0m14.380000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	77.742s
suite: browser.check~9of13 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m28.720000s 0m8.960000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.072s
suite: browser.check~10of13 took 92s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.950000s 0m11.870000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	79.941s
suite: browser.check~11of13 took 90s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.020000s 0m14.410000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.629s
suite: browser.check~12of13 took 92s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.580000s 0m15.390000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	79.805s
suite: browser.check~13of13 took 90s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.960000s 0m10.370000s
--- shard 25 of 48 ---
==> shard 25 of 48: make browser.internal.gallery~1of18
./hack/gotest.sh browser 'internal.gallery~1of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	34.236s
suite: browser.internal.gallery~1of18 took 43s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.550000s 0m3.050000s
--- shard 26 of 48 ---
==> shard 26 of 48: make browser.internal.gallery~2of18
./hack/gotest.sh browser 'internal.gallery~2of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	51.183s
suite: browser.internal.gallery~2of18 took 60s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.330000s 0m3.650000s
--- shard 27 of 48 ---
==> shard 27 of 48: make browser.internal.gallery~3of18
./hack/gotest.sh browser 'internal.gallery~3of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.005s
suite: browser.internal.gallery~3of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.240000s 0m4.060000s
--- shard 28 of 48 ---
==> shard 28 of 48: make browser.internal.gallery~4of18
./hack/gotest.sh browser 'internal.gallery~4of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.231s
suite: browser.internal.gallery~4of18 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.680000s 0m5.260000s
--- shard 29 of 48 ---
==> shard 29 of 48: make browser.internal.gallery~5of18
./hack/gotest.sh browser 'internal.gallery~5of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.427s
suite: browser.internal.gallery~5of18 took 70s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.840000s 0m4.480000s
--- shard 30 of 48 ---
==> shard 30 of 48: make browser.internal.gallery~6of18
./hack/gotest.sh browser 'internal.gallery~6of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	54.596s
suite: browser.internal.gallery~6of18 took 63s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.490000s 0m3.580000s
--- shard 31 of 48 ---
==> shard 31 of 48: make browser.internal.gallery~7of18
./hack/gotest.sh browser 'internal.gallery~7of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	54.937s
suite: browser.internal.gallery~7of18 took 64s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.860000s 0m2.700000s
--- shard 32 of 48 ---
==> shard 32 of 48: make browser.internal.gallery~8of18
./hack/gotest.sh browser 'internal.gallery~8of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	56.874s
suite: browser.internal.gallery~8of18 took 65s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.160000s 0m5.020000s
--- shard 33 of 48 ---
==> shard 33 of 48: make browser.internal.gallery~9of18
./hack/gotest.sh browser 'internal.gallery~9of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	64.269s
suite: browser.internal.gallery~9of18 took 73s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m22.040000s 0m6.770000s
--- shard 34 of 48 ---
==> shard 34 of 48: make browser.internal.gallery~10of18
./hack/gotest.sh browser 'internal.gallery~10of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.317s
suite: browser.internal.gallery~10of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.020000s 0m2.620000s
--- shard 35 of 48 ---
==> shard 35 of 48: make browser.internal.gallery~11of18
./hack/gotest.sh browser 'internal.gallery~11of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.825s
suite: browser.internal.gallery~11of18 took 71s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.860000s 0m2.730000s
--- shard 36 of 48 ---
==> shard 36 of 48: make browser.internal.gallery~12of18
./hack/gotest.sh browser 'internal.gallery~12of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	64.257s
suite: browser.internal.gallery~12of18 took 73s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.450000s 0m4.710000s
--- shard 37 of 48 ---
==> shard 37 of 48: make browser.internal.gallery~13of18
./hack/gotest.sh browser 'internal.gallery~13of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 3 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.835s
suite: browser.internal.gallery~13of18 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.890000s 0m5.240000s
--- shard 38 of 48 ---
==> shard 38 of 48: make browser.internal.gallery~14of18
./hack/gotest.sh browser 'internal.gallery~14of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.859s
suite: browser.internal.gallery~14of18 took 76s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.230000s 0m5.250000s
--- shard 39 of 48 ---
==> shard 39 of 48: make browser.internal.gallery~15of18
./hack/gotest.sh browser 'internal.gallery~15of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.965s
suite: browser.internal.gallery~15of18 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.220000s 0m5.450000s
--- shard 40 of 48 ---
==> shard 40 of 48: make browser.internal.gallery~16of18
./hack/gotest.sh browser 'internal.gallery~16of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	62.187s
suite: browser.internal.gallery~16of18 took 71s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.930000s 0m4.000000s
--- shard 41 of 48 ---
==> shard 41 of 48: make browser.internal.gallery~17of18
./hack/gotest.sh browser 'internal.gallery~17of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.887s
suite: browser.internal.gallery~17of18 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.610000s 0m5.750000s
--- shard 42 of 48 ---
==> shard 42 of 48: make browser.internal.gallery~18of18
./hack/gotest.sh browser 'internal.gallery~18of18'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.994s
suite: browser.internal.gallery~18of18 took 76s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.680000s 0m5.290000s
--- shard 43 of 48 ---
==> shard 43 of 48: make browser.internal.gallery.history
./hack/gotest.sh browser 'internal.gallery.history'
gotest: on ip-172-31-27-122, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: ./internal/gallery/history/ runs its 3 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery/history	32.062s
suite: browser.internal.gallery.history took 42s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.160000s 0m2.010000s
--- shard 44 of 48 ---
==> shard 44 of 48: make browser.internal.www~1of4
./hack/gotest.sh browser 'internal.www~1of4'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 4 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	70.643s
suite: browser.internal.www~1of4 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.250000s 0m2.100000s
--- shard 45 of 48 ---
==> shard 45 of 48: make browser.internal.www~2of4
./hack/gotest.sh browser 'internal.www~2of4'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	66.679s
suite: browser.internal.www~2of4 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.400000s 0m2.720000s
--- shard 46 of 48 ---
==> shard 46 of 48: make browser.internal.www~3of4
./hack/gotest.sh browser 'internal.www~3of4'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	80.698s
suite: browser.internal.www~3of4 took 88s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.670000s 0m2.810000s
--- shard 47 of 48 ---
==> shard 47 of 48: make browser.internal.www~4of4
./hack/gotest.sh browser 'internal.www~4of4'
gotest: on ip-172-31-27-122, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	76.456s
suite: browser.internal.www~4of4 took 85s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.220000s 0m3.010000s
diff --git a/check/baseline_test.go b/check/baseline_test.go
index bba871b..aacb8f4 100644
--- a/check/baseline_test.go
+++ b/check/baseline_test.go
@@ -56,10 +56,57 @@ type widening struct {
Why string
}
+// The footer row's menus are flat, as a shell tail's are, so each of the
+// four summary-face rules, in each spelling, excludes them in its :not().
+// The new argument weighs what the shell tail's does, so no summary
+// the rule already styles changes weight.
+const footFlat = "the footer row's menus are flat, as a shell tail's are"
+
// widenedSelectors is keyed by the 1.0 selector. Each entry must be
// used: an entry for a selector the CSS no longer widens is a promise
// about nothing.
-var widenedSelectors = map[string]widening{}
+var widenedSelectors = map[string]widening{
+ ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn)": {
+ Now: ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn)",
+ Added: []string{".rst-page__foot .rst-dropdown"},
+ Why: footFlat,
+ },
+ "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn])": {
+ Now: "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn])",
+ Added: []string{"[rst-page-foot] [rst-dropdown]"},
+ Why: footFlat,
+ },
+ ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):active": {
+ Now: ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):active",
+ Added: []string{".rst-page__foot .rst-dropdown"},
+ Why: footFlat,
+ },
+ "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):active": {
+ Now: "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):active",
+ Added: []string{"[rst-page-foot] [rst-dropdown]"},
+ Why: footFlat,
+ },
+ ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover": {
+ Now: ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover",
+ Added: []string{".rst-page__foot .rst-dropdown"},
+ Why: footFlat,
+ },
+ "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover": {
+ Now: "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover",
+ Added: []string{"[rst-page-foot] [rst-dropdown]"},
+ Why: footFlat,
+ },
+ ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover:active": {
+ Now: ".rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover:active",
+ Added: []string{".rst-page__foot .rst-dropdown"},
+ Why: footFlat,
+ },
+ "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover:active": {
+ Now: "[rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover:active",
+ Added: []string{"[rst-page-foot] [rst-dropdown]"},
+ Why: footFlat,
+ },
+}
// newOnOldMarkup is the few new rules that do style markup 1.0 could
// already carry, each an exact selector with why: a button in a
diff --git a/check/footer_browser_test.go b/check/footer_browser_test.go
new file mode 100644
index 0000000..d3ab8d3
--- /dev/null
+++ b/check/footer_browser_test.go
@@ -0,0 +1,369 @@
+//go:build browser
+
+// The column's footer row as a reader meets it: under the page, quiet,
+// its menus flat, and each menu's panel on the screen however the
+// browser places it.
+package check
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "math"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/chromedp/cdproto/emulation"
+ cdppage "github.com/chromedp/cdproto/page"
+ "github.com/chromedp/chromedp"
+
+ "amadan.net/rastrillo/fiesta"
+ "amadan.net/rastrillo/fiesta/internal/harness"
+)
+
+// columnDoc is the column example as the rig serves it, its assets
+// flattened beside the page.
+func columnDoc(t *testing.T) string {
+ t.Helper()
+ return parityLinks(defaultMarkup(t, "layout-column"))
+}
+
+// inArabic is a document re-rooted right to left, as an Arabic page
+// of the same app would be.
+func inArabic(t *testing.T, doc string) string {
+ t.Helper()
+ const root = `<html lang="en" dir="ltr">`
+ if !strings.Contains(doc, root) {
+ t.Fatalf("the document has no %s to turn right to left", root)
+ }
+ return strings.Replace(doc, root, `<html lang="ar" dir="rtl">`, 1)
+}
+
+// TestTheFooterRowSitsUnderThePage: the page's title is the first
+// thing on screen and the menus come after the content, at the inline
+// end of the column (the right in English, the left in Arabic), side by
+// side where they fit and wrapping without a sideways scrollbar where
+// they do not; the row is muted, under a hairline in the line colour.
+func TestTheFooterRowSitsUnderThePage(t *testing.T) {
+ doc := columnDoc(t)
+ rig := sizingRig(t, false, map[string]string{"/": doc, "/rtl": inArabic(t, doc)})
+ ctx, cancel := context.WithTimeout(rig.Context(), 90*time.Second)
+ defer cancel()
+ for _, size := range [][2]int64{{1280, 800}, {390, 844}, {320, 640}} {
+ for _, path := range []string{"/", "/rtl"} {
+ where := fmt.Sprintf("%s at %d", path, size[0])
+ mustRun(t, ctx, chromedp.EmulateViewport(size[0], size[1]), chromedp.Navigate(rig.Origin+path), chromedp.WaitVisible("[rst-page-header] h1", chromedp.ByQuery))
+ var foot bool
+ at(t, ctx, `JSON.stringify(!!document.querySelector("main[rst-page] > [rst-page-foot]"))`, &foot)
+ if !foot {
+ t.Fatalf("%s: the column page has no footer row in its <main rst-page>", where)
+ }
+ var g struct {
+ Last, RTL bool
+ TitleTop, CountBottom, FootTop float64
+ SummaryTops []float64
+ SchemeEnd, ContentEnd float64
+ ScrollW, ClientW float64
+ Colour, Muted string
+ BorderWidth, BorderStyle, Border, Line string
+ }
+ at(t, ctx, `(() => {
+ const main = document.querySelector("main[rst-page]"), foot = document.querySelector("[rst-page-foot]");
+ const last = main.lastElementChild === foot, f = foot.getBoundingClientRect(), cs = getComputedStyle(foot), rtl = cs.direction === "rtl";
+ const end = rtl ? f.left + parseFloat(cs.borderLeftWidth) + parseFloat(cs.paddingLeft) : f.right - parseFloat(cs.borderRightWidth) - parseFloat(cs.paddingRight);
+ const scheme = foot.querySelector("[rst-scheme] > summary").getBoundingClientRect();
+ const probe = document.createElement("span"); probe.style.color = "var(--rst-text-muted)"; probe.style.borderTop = "1px solid var(--rst-line)"; main.append(probe);
+ const p = getComputedStyle(probe), muted = p.color, line = p.borderTopColor; probe.remove();
+ return JSON.stringify({Last: last, RTL: rtl,
+ TitleTop: document.querySelector("[rst-page-header] h1").getBoundingClientRect().top,
+ CountBottom: document.querySelector("[rst-count-line]").getBoundingClientRect().bottom, FootTop: f.top,
+ SummaryTops: [...document.querySelectorAll("[rst-dropdown] > summary")].map(s => s.getBoundingClientRect().top),
+ SchemeEnd: rtl ? scheme.left : scheme.right, ContentEnd: end,
+ ScrollW: document.documentElement.scrollWidth, ClientW: document.documentElement.clientWidth,
+ Colour: cs.color, Muted: muted, BorderWidth: cs.borderTopWidth, BorderStyle: cs.borderTopStyle, Border: cs.borderTopColor, Line: line}); })()`, &g)
+ if g.RTL != (path == "/rtl") {
+ t.Fatalf("%s: the footer reads direction rtl=%v; this leg is not the direction it claims", where, g.RTL)
+ }
+ if len(g.SummaryTops) != 2 {
+ t.Fatalf("%s: %d dropdown summaries on the page, want the footer's two", where, len(g.SummaryTops))
+ }
+ for _, top := range g.SummaryTops {
+ if top <= g.TitleTop {
+ t.Errorf("%s: a menu's summary starts at %.0f and the title at %.0f; the title must be the first thing on screen", where, top, g.TitleTop)
+ }
+ }
+ if !g.Last {
+ t.Errorf("%s: the footer row is not the page's last element", where)
+ }
+ if g.FootTop < g.CountBottom-0.5 {
+ t.Errorf("%s: the footer starts at %.1f, above the count line's foot at %.1f", where, g.FootTop, g.CountBottom)
+ }
+ if math.Abs(g.SchemeEnd-g.ContentEnd) > 1 {
+ t.Errorf("%s: the scheme menu ends at %.1f, the footer's content box at %.1f; the menus belong at the row's inline end", where, g.SchemeEnd, g.ContentEnd)
+ }
+ if size[0] != 320 && math.Abs(g.SummaryTops[0]-g.SummaryTops[1]) > 0.5 {
+ t.Errorf("%s: the two summaries start at %.1f and %.1f; at this width they fit on one line", where, g.SummaryTops[0], g.SummaryTops[1])
+ }
+ if size[0] == 320 && g.ScrollW > g.ClientW {
+ t.Errorf("%s: the page is %.0f wide in a %.0f viewport; the row must wrap, not scroll sideways", where, g.ScrollW, g.ClientW)
+ }
+ if g.Colour != g.Muted {
+ t.Errorf("%s: the footer's text is %s, want the muted text colour %s", where, g.Colour, g.Muted)
+ }
+ if g.BorderWidth != "1px" || g.BorderStyle != "solid" || g.Border != g.Line {
+ t.Errorf("%s: the footer's top border is %s %s %s, want 1px solid %s, the line colour", where, g.BorderWidth, g.BorderStyle, g.Border, g.Line)
+ }
+ }
+ }
+}
+
+// TestTheFootersMenusWearNoFace: in day, light, the footer row's
+// summaries stay flat in every state, as the menus in a shell's tail
+// do: a quiet row of muted words, not two buttons at the foot of the
+// page. The same dropdown in a page of its own wears the summary face,
+// so the check can see a face when there is one. Both spellings.
+func TestTheFootersMenusWearNoFace(t *testing.T) {
+ doc := columnDoc(t)
+ i, j := strings.Index(doc, "<footer rst-page-foot>"), strings.Index(doc, "</footer>")
+ if i < 0 || j < i {
+ t.Fatal("the column example has no footer row to spell with classes")
+ }
+ classFoot := classSpelling(t, `<main rst-page>`+doc[i:j+len("</footer>")]+`</main>`)
+ if !strings.Contains(classFoot, `class="rst-page__foot"`) {
+ t.Fatalf("the class-spelled footer is %.120q; it does not carry .rst-page__foot", classFoot)
+ }
+ pages := map[string]string{
+ "/": doc,
+ "/class": sizingDoc("footer, classes", strings.Replace(classFoot, `class="rst-page__foot"`, `class="rst-page__foot" id="class-foot"`, 1)),
+ "/alone": sizingDoc("a dropdown alone", `<div id="alone">`+dropdownSample(t)+`</div>`),
+ }
+ for _, width := range []int64{1280, 390} {
+ rig := sizingRig(t, false, pages, harness.WithHover())
+ ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
+ mustRun(t, ctx, chromedp.EmulateViewport(width, 844), emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "prefers-color-scheme", Value: "light"}}))
+ mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/alone"), chromedp.WaitVisible("#alone", chromedp.ByQuery))
+ still(t, ctx)
+ if bg, sh := computed(t, ctx, "#alone > [rst-dropdown] > summary", "background-color"), computed(t, ctx, "#alone > [rst-dropdown] > summary", "box-shadow"); bg == "rgba(0, 0, 0, 0)" && sh == "none" {
+ t.Errorf("%d: CONTROL: the dropdown alone is %s with box-shadow %q, no face; the check below cannot see one", width, bg, sh)
+ }
+ for _, page := range []struct{ path, sel string }{
+ {"/", "[rst-page-foot] [rst-locale] > summary"},
+ {"/", "[rst-page-foot] [rst-scheme] > summary"},
+ {"/class", "#class-foot .rst-scheme > summary"},
+ } {
+ mustRun(t, ctx, chromedp.Navigate(rig.Origin+page.path), chromedp.WaitVisible(page.sel, chromedp.ByQuery))
+ still(t, ctx)
+ var hov bool
+ at(t, ctx, `JSON.stringify(matchMedia("(hover: hover)").matches)`, &hov)
+ if !hov {
+ t.Fatalf("%d: (hover: hover) is false; the face's hover rules cannot be read here", width)
+ }
+ for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}, {"active"}} {
+ forceState(t, ctx, page.sel, states...)
+ if bg, sh := computed(t, ctx, page.sel, "background-color"), computed(t, ctx, page.sel, "box-shadow"); bg != "rgba(0, 0, 0, 0)" || sh != "none" {
+ t.Errorf("%d: footer summary %s %v is %s with box-shadow %q; want no face (rgba(0, 0, 0, 0), none)", width, page.sel, states, bg, sh)
+ }
+ forceState(t, ctx, page.sel)
+ }
+ }
+ cancel()
+ }
+}
+
+// longestLabels is the pair of footer labels, Language and Colour
+// scheme, of the left-to-right locale whose two summaries are widest
+// on screen, read from the gallery's sample catalogs (the words the
+// column demo shows in each language), measured in ctx's browser.
+func longestLabels(t *testing.T, ctx context.Context, origin string) (lang, language, scheme string) {
+ t.Helper()
+ var pairs []struct{ Lang, Language, Scheme string }
+ for _, locale := range []string{"bn", "es", "ga", "hi", "ja", "pt-PT", "ru", "vi", "yue", "zh-Hans"} {
+ language, scheme := sampleWord(t, locale, "Language"), sampleWord(t, locale, "Colour scheme")
+ pairs = append(pairs, struct{ Lang, Language, Scheme string }{locale, language, scheme})
+ }
+ raw, _ := json.Marshal(pairs)
+ var widest int
+ mustRun(t, ctx, chromedp.EmulateViewport(1280, 800), chromedp.Navigate(origin+"/measure"), chromedp.WaitReady("[rst-page]", chromedp.ByQuery))
+ at(t, ctx, `(() => { const pairs = `+string(raw)+`, page = document.querySelector("[rst-page]"); let best = -1, at = 0;
+ pairs.forEach((p, i) => { const f = document.createElement("footer"); f.setAttribute("rst-page-foot", ""); f.lang = p.Lang;
+ for (const w of [p.Language, p.Scheme]) { const d = document.createElement("details"); d.setAttribute("rst-dropdown", ""); const s = document.createElement("summary"); s.textContent = w; d.append(s); f.append(d); }
+ page.append(f); const w = [...f.querySelectorAll("summary")].reduce((n, s) => n + s.getBoundingClientRect().width, 0); f.remove();
+ if (w > best) { best = w; at = i; } });
+ return JSON.stringify(at); })()`, &widest)
+ return pairs[widest].Lang, pairs[widest].Language, pairs[widest].Scheme
+}
+
+// sampleWord is word in locale's sample catalog, the translation the
+// gallery puts on its pages.
+func sampleWord(t *testing.T, locale, word string) string {
+ t.Helper()
+ raw, err := os.ReadFile("../internal/gallery/i18n/samples." + locale + ".json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var catalog map[string]string
+ if err := json.Unmarshal(raw, &catalog); err != nil {
+ t.Fatalf("samples.%s.json: %v", locale, err)
+ }
+ got, ok := catalog[word]
+ if !ok || got == "" {
+ t.Fatalf("samples.%s.json has no %q", locale, word)
+ }
+ return got
+}
+
+// labelled is the column document with its two footer summaries
+// reading language and scheme, in lang, in dir.
+func labelled(t *testing.T, doc, lang, dir, language, scheme string) string {
+ t.Helper()
+ for _, swap := range [][2]string{
+ {`<html lang="en" dir="ltr">`, `<html lang="` + lang + `" dir="` + dir + `">`},
+ {`<summary>Language<`, `<summary>` + language + `<`},
+ {`<summary>Colour scheme<`, `<summary>` + scheme + `<`},
+ } {
+ if !strings.Contains(doc, swap[0]) {
+ t.Fatalf("the column document has no %s to relabel", swap[0])
+ }
+ doc = strings.Replace(doc, swap[0], swap[1], 1)
+ }
+ return doc
+}
+
+// TestTheFootersMenusOpenOnScreen: on a phone, scrolled to the foot of
+// the page, each footer menu opens on the screen, with the longest
+// labels a left-to-right translation gives the row and with Arabic's,
+// right to left. Where the browser anchors a panel to its summary it
+// flips it up and over as it must. Where it cannot, a panel hangs from
+// the box that holds it, and in the row Language is not at the end:
+// hung from its own <details>, its panel, at least 176px wide, runs
+// past the opposite edge at 320. So the row holds every panel, under
+// it and inside it, with anchor.js running and with no script at all,
+// and the page grows to hold the panel so it can be scrolled to.
+func TestTheFootersMenusOpenOnScreen(t *testing.T) {
+ doc := columnDoc(t)
+ noAnchor := strings.ReplaceAll(string(fiesta.TokensCSS()), "@supports (position-area: block-end) and (position-try-fallbacks: flip-block)", "@supports (position-area: rastrillo-no-such-value)")
+ if noAnchor == string(fiesta.TokensCSS()) {
+ t.Fatal("tokens.css has no anchor-positioning block to take away; the no-anchor legs would test nothing")
+ }
+ measure := sizingRig(t, false, map[string]string{"/measure": sizingDoc("measure", `<main rst-page></main>`)})
+ mctx, mcancel := context.WithTimeout(measure.Context(), 30*time.Second)
+ lang, language, scheme := longestLabels(t, mctx, measure.Origin)
+ mcancel()
+ t.Logf("the widest left-to-right labels are %s's: %q and %q", lang, language, scheme)
+ ltr := labelled(t, doc, lang, "ltr", language, scheme)
+ rtl := labelled(t, doc, "ar", "rtl", sampleWord(t, "ar", "Language"), sampleWord(t, "ar", "Colour scheme"))
+ for _, leg := range []struct {
+ name string
+ anchor, scripts bool
+ }{
+ {"anchor positioning", true, true},
+ {"no anchor positioning, anchor.js", false, true},
+ {"no anchor positioning, no scripts", false, false},
+ } {
+ t.Run(leg.name, func(t *testing.T) {
+ pages := map[string]string{"/ltr": ltr, "/rtl": rtl}
+ if !leg.anchor {
+ for path, page := range pages {
+ pages[path] = strings.Replace(page, `href="tokens.css"`, `href="tokens-noanchor.css"`, 1)
+ }
+ }
+ rig := harness.New(t, func(string) http.Handler {
+ mux := sizingMux(t, pages)
+ mux.HandleFunc("GET /tokens-noanchor.css", func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/css")
+ fmt.Fprint(w, noAnchor)
+ })
+ return mux
+ }, harness.WithCoarsePointer())
+ ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
+ defer cancel()
+ if !leg.anchor && leg.scripts {
+ mustRun(t, ctx, chromedp.ActionFunc(func(ctx context.Context) error {
+ _, err := cdppage.AddScriptToEvaluateOnNewDocument(withoutAnchorSupport).Do(ctx)
+ return err
+ }))
+ }
+ if !leg.scripts {
+ mustRun(t, ctx, emulation.SetScriptExecutionDisabled(true))
+ }
+ for _, size := range [][2]int64{{390, 640}, {320, 568}} {
+ for _, path := range []string{"/ltr", "/rtl"} {
+ for _, menu := range []string{"rst-locale", "rst-scheme"} {
+ where := fmt.Sprintf("%s at %dx%d, %s", path, size[0], size[1], menu)
+ details := "[rst-page-foot] > [" + menu + "]"
+ mustRun(t, ctx, chromedp.EmulateViewport(size[0], size[1]), chromedp.Navigate(rig.Origin+path), chromedp.WaitVisible(details, chromedp.ByQuery))
+ var flips bool
+ at(t, ctx, `JSON.stringify(document.documentElement.hasAttribute("data-rst-flip-menus"))`, &flips)
+ if want := !leg.anchor && leg.scripts; flips != want {
+ t.Fatalf("%s: <html> data-rst-flip-menus is %v, want %v; anchor.js is not doing what this leg needs", where, flips, want)
+ }
+ RequireAnchorPositioning(t, ctx, where, details+" > [rst-dropdown-menu]", leg.anchor)
+ mustRun(t, ctx, chromedp.Evaluate(`window.scrollTo(0, document.documentElement.scrollHeight)`, nil),
+ chromedp.Click(details+" > summary", chromedp.ByQuery))
+ if leg.scripts {
+ // anchor.js places a panel on toggle, which
+ // fires after the click.
+ mustRun(t, ctx, chromedp.Evaluate(`new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))`, nil, awaitPromise))
+ }
+ var g struct {
+ Open bool
+ L, R, T, B, FL, FR, FB, VW float64
+ VH, SY, SH float64
+ }
+ at(t, ctx, `(() => { const d = document.querySelector(`+jsStr(details)+`), p = d.querySelector(":scope > [rst-dropdown-menu]").getBoundingClientRect(), f = d.parentElement.getBoundingClientRect();
+ return JSON.stringify({Open: d.open, L: p.left, R: p.right, T: p.top, B: p.bottom, FL: f.left, FR: f.right, FB: f.bottom,
+ VW: document.documentElement.clientWidth, VH: innerHeight, SY: scrollY, SH: document.documentElement.scrollHeight}); })()`, &g)
+ if !g.Open {
+ t.Fatalf("%s: the menu did not open; this leg proves nothing", where)
+ }
+ if g.L < -0.5 || g.R > g.VW+0.5 {
+ t.Errorf("%s: the panel spans %.1f to %.1f of a %.0fpx viewport; it must open on the screen", where, g.L, g.R, g.VW)
+ }
+ if leg.anchor {
+ if g.T < -0.5 || g.B > g.VH+0.5 {
+ t.Errorf("%s: the panel runs from %.1f to %.1f of a %.0fpx viewport; anchored, it must flip to stay on the screen", where, g.T, g.B, g.VH)
+ }
+ continue
+ }
+ if g.L < g.FL-0.5 || g.R > g.FR+0.5 {
+ t.Errorf("%s: the panel spans %.1f to %.1f and the footer %.1f to %.1f; the row must hold its panel", where, g.L, g.R, g.FL, g.FR)
+ }
+ if g.T < g.FB-0.5 {
+ t.Errorf("%s: the panel starts at %.1f, above the footer's foot at %.1f; it must open under the row", where, g.T, g.FB)
+ }
+ if g.B+g.SY > g.SH+0.5 {
+ t.Errorf("%s: the panel ends at %.1f of the document, which is %.0f tall; the page must grow to hold it", where, g.B+g.SY, g.SH)
+ }
+ }
+ }
+ }
+ })
+ }
+}
+
+// TestAnEmptyFooterRowDrawsNothing: an app with one language and no
+// scheme menu renders the row with nothing in it but the template's
+// whitespace. That row is not drawn: no hairline and no gap under the
+// page. :empty would miss it, since whitespace is a child. The control
+// is the same row with one menu in it, which is drawn. Both spellings.
+func TestAnEmptyFooterRowDrawsNothing(t *testing.T) {
+ menu := `<details rst-dropdown><summary>Language</summary><div rst-dropdown-menu><a href="#">English</a></div></details>`
+ body := `<main rst-page><p>Content</p>` +
+ `<footer rst-page-foot id="empty">` + "\n \n" + `</footer>` +
+ `<footer rst-page-foot id="held">` + menu + `</footer></main>` +
+ `<main class="rst-page"><p>Content</p>` +
+ `<footer class="rst-page__foot" id="empty-c">` + "\n \n" + `</footer>` +
+ `<footer class="rst-page__foot" id="held-c">` + strings.NewReplacer("rst-dropdown-menu", `class="rst-dropdown__menu"`, "rst-dropdown", `class="rst-dropdown"`).Replace(menu) + `</footer></main>`
+ rig := sizingRig(t, false, map[string]string{"/": sizingDoc("empty footer", body)})
+ ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
+ defer cancel()
+ mustRun(t, ctx, chromedp.EmulateViewport(1280, 800), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#held", chromedp.ByQuery))
+ for sel, want := range map[string]string{"#empty": "none", "#held": "flex", "#empty-c": "none", "#held-c": "flex"} {
+ if got := computed(t, ctx, sel, "display"); got != want {
+ t.Errorf("footer %s is display %s, want %s", sel, got, want)
+ }
+ }
+}
diff --git a/check/footer_test.go b/check/footer_test.go
new file mode 100644
index 0000000..85ebbad
--- /dev/null
+++ b/check/footer_test.go
@@ -0,0 +1,115 @@
+package check
+
+import (
+ "regexp"
+ "strings"
+ "testing"
+
+ "amadan.net/rastrillo/fiesta"
+)
+
+// The column's footer row: the column shell ends with it, and it holds
+// the language and colour-scheme menus that used to stand above the
+// page's title. A menu there pushed the title down and read as the
+// page's first action; at the foot of the column it is out of the way
+// and still one tap from every page.
+
+// summaryFace matches the four selectors, one per state, of the rules
+// that give a dropdown's summary its face, in the attribute spelling:
+// the :not() list of the dropdowns that stay flat, then the summary
+// and its state.
+var summaryFace = regexp.MustCompile(`^\[rst-dropdown\]:not\((.*)\) > summary:not\(\[rst-btn\]\)(|:active|:hover|:hover:active)$`)
+
+// summaryFaceClass is summaryFace in the class spelling.
+var summaryFaceClass = regexp.MustCompile(`^\.rst-dropdown:not\((.*)\) > summary:not\(\.rst-btn\)(|:active|:hover|:hover:active)$`)
+
+// TestTheColumnShellEndsWithItsFooterRow: the column example opens its
+// page with the slot, so the page header is the first thing in it, and
+// closes it with the footer row holding the language menu and then the
+// scheme menu; no menu is left above the slot. The stylesheet keeps
+// the row's menus flat in every state of the summary face, hides a row
+// that holds only template whitespace, and makes the row, not each
+// menu, the box a panel hangs from where nothing can anchor it.
+func TestTheColumnShellEndsWithItsFooterRow(t *testing.T) {
+ doc := defaultMarkup(t, "layout-column")
+ const open, slot, slotEnd = `<main rst-page id="main">`, "<!-- fiesta:slot -->", "<!-- /fiesta:slot -->"
+ i, j, k := strings.Index(doc, open), strings.Index(doc, slot), strings.Index(doc, slotEnd)
+ if i < 0 || j < i || k < j {
+ t.Fatalf("layout-column has no %s followed by its slot; the checks below read nothing", open)
+ }
+ if before := doc[i+len(open) : j]; strings.TrimSpace(before) != "" {
+ t.Errorf("layout-column: between %s and the slot there is %.80q; the page header must be the first thing in the page", open, before)
+ }
+ if strings.Contains(doc[i:j], "<details") {
+ t.Error("layout-column: a <details> sits between <main> and the slot")
+ }
+ rest := doc[k+len(slotEnd):]
+ for _, want := range []string{
+ `<footer rst-page-foot>`,
+ `<details rst-dropdown rst-locale name="rst-menus">`, `</details>`,
+ `<details rst-dropdown rst-scheme name="rst-menus">`, `</details>`,
+ `</footer>`, `</main>`,
+ } {
+ rest = strings.TrimLeft(rest, " \t\n")
+ if strings.HasPrefix(want, "</details>") {
+ // The menu's own markup comes before its closing tag.
+ end := strings.Index(rest, want)
+ if end < 0 || strings.Contains(rest[:end], "<details") {
+ t.Fatalf("layout-column: after the slot, a menu is not closed before the next one opens")
+ }
+ rest = rest[end+len(want):]
+ continue
+ }
+ if !strings.HasPrefix(rest, want) {
+ t.Fatalf("layout-column: after the slot comes %.60q, want %s next: the footer row, the language menu, the scheme menu, then </footer> and </main>", rest, want)
+ }
+ rest = rest[len(want):]
+ }
+
+ decls, _ := parseDeclarations(t, "tokens.css", string(fiesta.TokensCSS()))
+ faces := map[string]bool{}
+ has := map[string]bool{}
+ for _, d := range decls {
+ has[d.Selector+" { "+d.Property+": "+d.Value+" }"] = true
+ for _, spelling := range []struct {
+ re *regexp.Regexp
+ flat string
+ }{{summaryFace, "[rst-page-foot] [rst-dropdown]"}, {summaryFaceClass, ".rst-page__foot .rst-dropdown"}} {
+ m := spelling.re.FindStringSubmatch(d.Selector)
+ if m == nil || d.Property != "box-shadow" {
+ continue
+ }
+ state := m[2]
+ if state == "" {
+ state = "at rest"
+ }
+ key := "rule " + state + " does not list " + spelling.flat
+ if _, seen := faces[key]; !seen {
+ faces[key] = false
+ }
+ for _, arg := range splitSelectorList(m[1]) {
+ if arg == spelling.flat {
+ faces[key] = true
+ }
+ }
+ }
+ }
+ if len(faces) != 8 {
+ t.Fatalf("read %d summary-face rules by spelling and state, want 8 (four states, two spellings): %v", len(faces), faces)
+ }
+ for _, key := range sortedKeys(faces) {
+ if !faces[key] {
+ t.Errorf("the summary face's %s in its :not(); a footer menu would wear a face in that state", key)
+ }
+ }
+ for _, want := range []string{
+ "[rst-page-foot]:not(:has(> *)) { display: none }",
+ ".rst-page__foot:not(:has(> *)) { display: none }",
+ "[rst-page-foot] [rst-dropdown] { position: static }",
+ ".rst-page__foot .rst-dropdown { position: static }",
+ } {
+ if !has[want] {
+ t.Errorf("tokens.css does not carry %s", want)
+ }
+ }
+}
diff --git a/check/testdata/fixtures/spelling-class.html b/check/testdata/fixtures/spelling-class.html
index ad6a95b..4f21350 100644
--- a/check/testdata/fixtures/spelling-class.html
+++ b/check/testdata/fixtures/spelling-class.html
@@ -110,6 +110,8 @@
<div class="rst-shell-topbar" ><div class="rst-shell__bar" ><a class="rst-shell__brand" href="#a">Brand</a><nav class="rst-shell__nav" ><a href="#a" aria-current="page">Home</a><a href="#a">Away</a></nav><details class="rst-shell__menu" name="rst-fixture-menus3"><summary>Menu</summary></details><div class="rst-shell__tail" ><nav class="rst-shell__nav" ><a href="#a">Tail</a></nav><details class="rst-shell__account rst-dropdown" name="rst-fixture-menus4"><summary class="rst-btn" >Ana</summary><div class="rst-dropdown__menu" ><a href="#a">Sign out</a></div></details></div></div><div class="rst-shell__foot" ></div></div>
<div class="rst-shell-sidebar" ><details class="rst-shell__chrome" name="rst-fixture-menus5"><summary>Menu</summary></details><div class="rst-shell__rail" ><div class="rst-shell__group" >Group</div><nav class="rst-shell__nav" ><a href="#a" aria-current="page">Home</a></nav><div class="rst-shell__rail-foot" ><details class="rst-dropdown" name="rst-fixture-menus6"><summary class="rst-btn" >Ana</summary><div class="rst-dropdown__menu" ><a href="#a">Sign out</a></div></details></div></div><main class="rst-shell__main" >Main</main></div>
<div class="rst-shell-console" ><div class="rst-shell__bar" ><a class="rst-shell__brand" href="#a">Brand</a><details class="rst-shell__menu" name="rst-fixture-menus7"><summary>Menu</summary></details><div class="rst-shell__tail" ><details class="rst-shell__account rst-dropdown" name="rst-fixture-menus8"><summary class="rst-btn" >Ana</summary><div class="rst-dropdown__menu" ><a href="#a">Sign out</a></div></details></div></div><div class="rst-shell__rail" ><nav class="rst-shell__nav" ><a href="#a" aria-current="page">Home</a><a href="#a">Away</a></nav></div><main class="rst-shell__main" >Main</main><div class="rst-shell__foot" >Foot</div></div>
+ <div class="rst-page" ><p>Content</p><footer class="rst-page__foot" ><details class="rst-dropdown" name="rst-fixture-menus12"><summary>Language</summary><div class="rst-dropdown__menu" ><a href="#a" aria-current="true">English</a></div></details><details class="rst-dropdown" name="rst-fixture-menus12"><summary>Colour scheme</summary><div class="rst-dropdown__menu" ><a href="#a">Dark</a></div></details></footer></div><div class="rst-page" ><p>Content</p><footer class="rst-page__foot" >
+ </footer></div>
<div class="rst-shell-sidebar rst-shell-sidebar--index" ><div class="rst-shell__back" ><a href="#a">Sections</a></div><div class="rst-shell__rail" ><h1 class="rst-shell__title" ><span><a class="rst-shell__brand" href="#a">App</a></span><span>App</span></h1><a class="rst-shell__brand" href="#a">App</a><nav class="rst-shell__nav" ><p class="rst-shell__group" >Work</p><a href="#a">One</a><a href="#a">Two</a><p class="rst-shell__group" >More</p><a href="#a">Three</a></nav><div class="rst-shell__rail-foot" ><details class="rst-dropdown rst-shell__profile" name="rst-fixture-menus11"><summary><span class="rst-shell__avatar" ><span class="rst-person__av" aria-hidden="true">A</span><span class="rst-person__name" >Ana</span></span></summary><div class="rst-dropdown__menu" ><div class="rst-shell__who" ><span class="rst-person__name" >Ana</span><span class="rst-person__email" >ana@example.com</span></div><a href="#a">Sign out</a></div></details></div></div><main class="rst-shell__main" >Main</main></div>
<div class="rst-shell-sidebar rst-shell-sidebar--page" ><div class="rst-shell__back" ><a href="#a">Sections</a></div><div class="rst-shell__rail" ><h1 class="rst-shell__title" >App</h1><nav class="rst-shell__nav" ><a href="#a">One</a></nav></div><main class="rst-shell__main" >Main</main></div>
<div class="rst-shell-console rst-shell-console--index" ><div class="rst-shell__bar" ><h1 class="rst-shell__title" ><span><a class="rst-shell__brand" href="#a">App</a></span><span>App</span></h1><a class="rst-shell__brand" href="#a">App</a><details class="rst-shell__menu" name="rst-fixture-menus9" open><summary><span class="rst-shell__menu-label" >Menu</span><span class="rst-shell__avatar" ><span class="rst-person__av rst-person__av--empty rst-person__av--placeholder" aria-hidden="true"></span><span class="rst-person__name" >Account</span></span></summary></details><div class="rst-shell__tail" ><div class="rst-shell__who" ><span class="rst-person__av rst-person__av--empty rst-person__av--placeholder" aria-hidden="true"></span><span class="rst-person__name" >Account</span></div><details class="rst-shell__account rst-dropdown" name="rst-fixture-menus10"><summary class="rst-btn" >Ana</summary><div class="rst-dropdown__menu" ><a href="#a">Sign out</a></div></details></div></div><div class="rst-shell__back" ><a href="#a">Sections</a></div><div class="rst-shell__rail" ><nav class="rst-shell__nav" ><a href="#a">One</a><a href="#a">Two</a></nav></div><main class="rst-shell__main" >Main</main></div>
diff --git a/css/tokens.css b/css/tokens.css
index fb0ee03..34ba896 100644
--- a/css/tokens.css
+++ b/css/tokens.css
@@ -682,6 +682,48 @@ body {
max-inline-size: 44rem;
}
+/* ── page-foot: the column's footer row ───────────────────────────── */
+/* The column's last child, for the language and colour-scheme menus.
+ Above the title a menu pushed the page down and read as the page's
+ first action; here it is out of the way and on every page. Quiet:
+ a hairline, muted text one step down, and summaries with no face
+ (the face rules below exclude it, as they do a shell's tail).
+ Packed at the inline end, so one menu sits at the end and two sit
+ side by side, Language first, with whatever else an app puts in the
+ row before them; it wraps on a phone rather than scroll sideways.
+
+ position: relative, and static on its dropdowns, is the fallback
+ where nothing can anchor a panel. A panel then hangs from its
+ <details>, and Language is not at the row's end: its panel, at least
+ 176px wide, ran past the opposite edge of a narrow page. With the
+ row as the containing block every panel opens under the row, flush
+ with its inline end, with or without anchor.js. Where anchor
+ positioning applies the panel is fixed to its own summary and none
+ of this matters. The document grows to hold an open panel, so no
+ drop-up rule: the rail foot needs one because its rail is the
+ window's height, and a page's foot is not. */
+.rst-page__foot, [rst-page-foot] {
+ align-items: center;
+ border-block-start: 1px solid var(--rst-line);
+ color: var(--rst-text-muted);
+ display: flex;
+ flex-wrap: wrap;
+ font-size: var(--rst-fs-sm);
+ gap: var(--rst-sp-2) var(--rst-sp-4);
+ justify-content: flex-end;
+ margin-block-start: var(--rst-sp-6);
+ padding-block-start: var(--rst-sp-3);
+ position: relative;
+}
+/* Standing alone, as in a component example, the margin above it
+ would be a blank band over nothing. */
+.rst-page__foot:first-child, [rst-page-foot]:first-child { margin-block-start: 0; }
+/* An app with one language and no scheme menu may render the row with
+ only template whitespace in it, which would draw a hairline under
+ the page for nothing. :empty counts that whitespace as content. */
+.rst-page__foot:not(:has(> *)), [rst-page-foot]:not(:has(> *)) { display: none; }
+.rst-page__foot .rst-dropdown, [rst-page-foot] [rst-dropdown] { position: static; }
+
/* ── The list card, its toolbar, and its search field ─────────────── */
/* No padding here, on purpose: a list card holds rows, and each row pads
itself. A form, prose or a strip of links does NOT go in here — it
@@ -1644,18 +1686,19 @@ a.rst-nm:hover { color: var(--rst-accent); }
theme that paints no face (A15). Not every summary: the topbar and
console account menus and the sidebar profile are their own
controls, the menus in a shell's tail are flat beside the nav they
- sit with (in the bar and as rows of the phone card), a dropdown
+ sit with (in the bar and as rows of the phone card), the footer
+ row's are a quiet line under the page, not buttons, a dropdown
nested in a menu is one of that menu's rows, the bulk bar's summary
draws its own small face, and a summary written as a button (the
dropdown partial's) already has the whole button language, press
and sheen included, which a second face at a higher weight would
overpaint. The exclusions are in every state's selector, so an
excluded summary never regains a face on hover or press. */
-.rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn), [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]) { background: var(--rst-summary-face, transparent); border-radius: var(--rst-summary-radius, 0); box-shadow: 0 0 0 1px var(--rst-summary-edge, transparent), inset var(--rst-hl-x) 1px 0 var(--rst-depth-hl, transparent), var(--rst-shadow-x) var(--rst-shadow-y) 1px var(--rst-depth-contact, transparent), calc(var(--rst-shadow-x) * 2) calc(var(--rst-shadow-y) * 1.5) 3px var(--rst-depth-ambient, transparent); }
-.rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):active, [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):active { box-shadow: 0 0 0 1px var(--rst-summary-edge, transparent), inset 0 1px 2px var(--rst-depth-contact, transparent); }
+.rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn), [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]) { background: var(--rst-summary-face, transparent); border-radius: var(--rst-summary-radius, 0); box-shadow: 0 0 0 1px var(--rst-summary-edge, transparent), inset var(--rst-hl-x) 1px 0 var(--rst-depth-hl, transparent), var(--rst-shadow-x) var(--rst-shadow-y) 1px var(--rst-depth-contact, transparent), calc(var(--rst-shadow-x) * 2) calc(var(--rst-shadow-y) * 1.5) 3px var(--rst-depth-ambient, transparent); }
+.rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):active, [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):active { box-shadow: 0 0 0 1px var(--rst-summary-edge, transparent), inset 0 1px 2px var(--rst-depth-contact, transparent); }
@media (hover: hover) {
- .rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover, [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover { background: var(--rst-summary-face-hover, transparent); box-shadow: 0 0 0 1px var(--rst-summary-edge-hover, transparent), inset var(--rst-hl-x) 1px 0 var(--rst-depth-hl, transparent), var(--rst-shadow-x) var(--rst-shadow-y) 1px var(--rst-depth-contact, transparent), calc(var(--rst-shadow-x) * 2) calc(var(--rst-shadow-y) * 1.5) 3px var(--rst-depth-ambient, transparent); }
- .rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover:active, [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover:active { box-shadow: 0 0 0 1px var(--rst-summary-edge-hover, transparent), inset 0 1px 2px var(--rst-depth-contact, transparent); }
+ .rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover, [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover { background: var(--rst-summary-face-hover, transparent); box-shadow: 0 0 0 1px var(--rst-summary-edge-hover, transparent), inset var(--rst-hl-x) 1px 0 var(--rst-depth-hl, transparent), var(--rst-shadow-x) var(--rst-shadow-y) 1px var(--rst-depth-contact, transparent), calc(var(--rst-shadow-x) * 2) calc(var(--rst-shadow-y) * 1.5) 3px var(--rst-depth-ambient, transparent); }
+ .rst-dropdown:not(.rst-shell__account, .rst-shell__profile, .rst-shell__tail .rst-dropdown, .rst-page__foot .rst-dropdown, .rst-bulkbar .rst-dropdown, .rst-dropdown__menu .rst-dropdown) > summary:not(.rst-btn):hover:active, [rst-dropdown]:not([rst-shell-account], [rst-shell-profile], [rst-shell-tail] [rst-dropdown], [rst-page-foot] [rst-dropdown], [rst-bulkbar] [rst-dropdown], [rst-dropdown-menu] [rst-dropdown]) > summary:not([rst-btn]):hover:active { box-shadow: 0 0 0 1px var(--rst-summary-edge-hover, transparent), inset 0 1px 2px var(--rst-depth-contact, transparent); }
}
.rst-dropdown > summary:hover, [rst-dropdown] > summary:hover, .rst-dropdown[open] > summary, [rst-dropdown][open] > summary { color: var(--rst-text); }
.rst-dropdown__menu, [rst-dropdown-menu] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inset-inline-end: 0; margin-top: 4px; min-width: 176px; padding: 0.25rem; position: absolute; top: 100%; z-index: 30; }
diff --git a/examples/examples.json b/examples/examples.json
index 99d4374..d4fbbbb 100644
--- a/examples/examples.json
+++ b/examples/examples.json
@@ -1572,6 +1572,7 @@
"label": "",
"sampleURLs": [
"/",
+ "/_theme",
"/ar/",
"/bn/",
"/en/",
diff --git a/examples/layout-column/default.html b/examples/layout-column/default.html
index fd58b94..94a5ce1 100644
--- a/examples/layout-column/default.html
+++ b/examples/layout-column/default.html
@@ -21,7 +21,6 @@
<body>
<a rst-skip href="#main">Skip to content</a>
<main rst-page id="main">
-<details rst-dropdown rst-locale name="rst-menus"><summary>Language<span rst-caret aria-hidden="true"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></summary><div rst-dropdown-menu><a href="/en/" lang="en" dir="ltr" aria-current="true">English</a><a href="/ga/" lang="ga" dir="ltr">Gaeilge</a><a href="/zh-Hans/" lang="zh-Hans" dir="ltr">简体中文</a><a href="/es/" lang="es" dir="ltr">Español</a><a href="/hi/" lang="hi" dir="ltr">हिन्दी</a><a href="/pt-PT/" lang="pt-PT" dir="ltr">Português</a><a href="/bn/" lang="bn" dir="ltr">বাংলা</a><a href="/ru/" lang="ru" dir="ltr">Русский</a><a href="/ja/" lang="ja" dir="ltr">日本語</a><a href="/yue/" lang="yue" dir="ltr">廣東話</a><a href="/vi/" lang="vi" dir="ltr">Tiếng Việt</a><a href="/ar/" lang="ar" dir="rtl">العربية</a></div></details>
<!-- fiesta:slot -->
<header rst-page-header>
<div rst-page-header-titles>
@@ -40,6 +39,17 @@
</div>
<p rst-count-line>Displaying <strong>1–2</strong> of <strong>412</strong></p>
<!-- /fiesta:slot -->
+<footer rst-page-foot>
+<details rst-dropdown rst-locale name="rst-menus"><summary>Language<span rst-caret aria-hidden="true"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></summary><div rst-dropdown-menu><a href="/en/" lang="en" dir="ltr" aria-current="true">English</a><a href="/ga/" lang="ga" dir="ltr">Gaeilge</a><a href="/zh-Hans/" lang="zh-Hans" dir="ltr">简体中文</a><a href="/es/" lang="es" dir="ltr">Español</a><a href="/hi/" lang="hi" dir="ltr">हिन्दी</a><a href="/pt-PT/" lang="pt-PT" dir="ltr">Português</a><a href="/bn/" lang="bn" dir="ltr">বাংলা</a><a href="/ru/" lang="ru" dir="ltr">Русский</a><a href="/ja/" lang="ja" dir="ltr">日本語</a><a href="/yue/" lang="yue" dir="ltr">廣東話</a><a href="/vi/" lang="vi" dir="ltr">Tiếng Việt</a><a href="/ar/" lang="ar" dir="rtl">العربية</a></div></details>
+<details rst-dropdown rst-scheme name="rst-menus">
+ <summary>Colour scheme<span rst-caret aria-hidden="true"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></summary>
+ <div rst-dropdown-menu role="group" aria-label="Colour scheme">
+ <form method="post" action="/_theme"><input type="hidden" name="scheme" value="light"><input type="hidden" name="return" value="/"><button type="submit">Light</button></form>
+ <form method="post" action="/_theme"><input type="hidden" name="scheme" value="dark"><input type="hidden" name="return" value="/"><button type="submit">Dark</button></form>
+ <form method="post" action="/_theme"><input type="hidden" name="scheme" value="system"><input type="hidden" name="return" value="/"><button type="submit" aria-current="true">System</button></form>
+ </div>
+</details>
+</footer>
</main>
</body>
</html>
diff --git a/examples/prose.json b/examples/prose.json
index 018e0f0..7307968 100644
--- a/examples/prose.json
+++ b/examples/prose.json
@@ -214,7 +214,7 @@
}
},
"layout-column": {
- "blurb": "The plain centred page an app starts on: a skip link, a title, and the content column."
+ "blurb": "The plain centred page an app starts on: a skip link, a title, the content column, and a footer row for the language and colour-scheme menus."
},
"layout-console": {
"blurb": "A bar across the top and a navigation rail down the side at once, the shape most admin consoles are. On a phone, the bar's menu opens as a card and the rail is an index page. No JavaScript needed."
diff --git a/hack/test-weights.txt b/hack/test-weights.txt
index 78b2fa2..53175b8 100644
--- a/hack/test-weights.txt
+++ b/hack/test-weights.txt
@@ -65,6 +65,7 @@ browser check TestATblockHeadDragsLikeASwitch 4
browser check TestBothSpellingsComputeTheSameStyles 4
browser check TestReducedMotionIsTotal 4
browser check TestTheBulkBarNeverWrapsItsText 4
+browser check TestTheFootersMenusOpenOnScreen 4
browser check TestTheSlideKnowsWhichWayItIsGoing 4
browser check TestTheWholeRowIsTheTarget 4
browser check TestAFragmentLinkLandsBelowTheBackStrip 3
diff --git a/internal/gallery/frame_test.go b/internal/gallery/frame_test.go
index f812268..45bdc71 100644
--- a/internal/gallery/frame_test.go
+++ b/internal/gallery/frame_test.go
@@ -550,3 +550,57 @@ func TestALayoutsLanguageLinksNameTheStateItFrames(t *testing.T) {
}
}
}
+
+// TestTheColumnDemosMenusAreInItsFooter: the column demo, opened from
+// the Demos list in a tab of its own, shows its menus where the column
+// shell now puts them, in the footer row under the page, and its
+// language menu still works there: each of the twelve links opens the
+// column demo in its locale, and the page's own language is the one
+// marked. Nothing that drops down comes before the page header.
+func TestTheColumnDemosMenusAreInItsFooter(t *testing.T) {
+ files := render(t, "")
+ s := newTestSite(t, fiesta.FS(), "")
+ state := mustEntry(t, "layout-column").Default
+ for _, locale := range []string{"en", "ar"} {
+ name := "day/" + locale + "/frames/layout-column/" + state + ".html"
+ body, ok := files[name]
+ if !ok {
+ t.Fatalf("the render wrote no %s", name)
+ }
+ head := strings.Index(string(body), "<header rst-page-header")
+ if head < 0 {
+ t.Fatalf("%s has no page header", name)
+ }
+ if strings.Contains(string(body[:head]), "<details") {
+ t.Errorf("%s: a <details> comes before the page header; the title must be the first thing on the page", name)
+ }
+ doc := parse(t, body)
+ inFoot := func(n node) bool { return within(n, func(p node) bool { return hasAttr(p, "rst-page-foot") }) }
+ for _, menu := range []string{"rst-locale", "rst-scheme"} {
+ found := doc.find(func(n node) bool { return n.n.Data == "details" && hasAttr(n, menu) })
+ if len(found) != 1 || !inFoot(found[0]) {
+ t.Errorf("%s: %d %s menus, want one, inside rst-page-foot", name, len(found), menu)
+ }
+ }
+ langs := doc.find(func(n node) bool {
+ return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-locale") })
+ })
+ if len(langs) != len(Locales) {
+ t.Fatalf("%s: %d language links, want %d", name, len(langs), len(Locales))
+ }
+ for _, a := range langs {
+ lang, _ := a.attr("lang")
+ href, _ := a.attr("href")
+ if !inFoot(a) {
+ t.Errorf("%s: the %s link is not in the footer row", name, lang)
+ }
+ if want := s.href("day", lang, "frames/layout-column/"+state+".html"); href != want {
+ t.Errorf("%s: the %s link goes to %q, want %q", name, lang, href, want)
+ }
+ current, _ := a.attr("aria-current")
+ if want := map[bool]string{true: "true"}[lang == locale]; current != want {
+ t.Errorf("%s: the %s link has aria-current=%q, want %q", name, lang, current, want)
+ }
+ }
+ }
+}
diff --git a/internal/gallery/heights.go b/internal/gallery/heights.go
index e372d02..ce3bfa0 100644
--- a/internal/gallery/heights.go
+++ b/internal/gallery/heights.go
@@ -132,7 +132,7 @@ var previewHeights = map[string]int{
"job-status/failed": 80,
"job-status/finished": 80,
"job-status/running": 90,
- "layout-column/default": 590,
+ "layout-column/default": 920,
"layout-console-page/default": 680,
"layout-console/default": 680,
"layout-sidebar-page/default": 560,
@@ -207,7 +207,7 @@ var previewMobileHeights = map[string]int{
"callout/warning": 240,
"format-abbr/default": 220,
"format-numbers/default": 220,
- "layout-column/default": 980,
+ "layout-column/default": 1330,
"layout-console-page/default": 1130,
"layout-sidebar-page/default": 1000,
"layout-stage/default": 640,
@@ -275,7 +275,7 @@ var previewMobileLocaleHeights = map[string]map[string]int{
"form-layout/default": 510,
"format-abbr/default": 270,
"format-addresses/default": 320,
- "layout-column/default": 1040,
+ "layout-column/default": 1390,
"layout-console-page/default": 1240,
"layout-topbar/default": 1180,
"list-row-action/lead-marker-status-and-an-action": 310,
@@ -329,7 +329,7 @@ var previewMobileLocaleHeights = map[string]map[string]int{
"format-abbr/default": 270,
"format-addresses/default": 350,
"job-status/failed": 130,
- "layout-column/default": 1050,
+ "layout-column/default": 1400,
"layout-console-page/default": 1200,
"layout-topbar/default": 1140,
"list-row-action/lead-marker-status-and-an-action": 290,
diff --git a/internal/gallery/i18n/ar.json b/internal/gallery/i18n/ar.json
index a7c6156..8274514 100644
--- a/internal/gallery/i18n/ar.json
+++ b/internal/gallery/i18n/ar.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "علامة استفهام محاطة بإطار تصل إلى مقالة مساعدة. تلميح CSS زينة فقط، أما الرابط فيحمل تسميته الكاملة بجملة تامة.",
"examples.job-status.blurb": "تقدّم مهمة تعمل في الخلفية: مؤشر دوّار، وسطر نص، وشريط. تقول سمات ‎data-poll‎ من أين يمكن للصفحة جلب نسخة حديثة وكم مرة. لا يأتي Fiesta بأي سكربت يفعل ذلك، فاربطه بنفسك، واترك السمات حين تنتهي المهمة.",
"examples.job-status.states.running": "سمات ‎data-poll‎ هي المكان الذي سيجلب منه سكربتٌ نسخة حديثة، وكم مرة. ولا شيء في Fiesta يفعل ذلك، فتبقى على هذه الصفحة قيد التشغيل.",
- "examples.layout-column.blurb": "الصفحة البسيطة الموسّطة التي يبدأ بها التطبيق: رابط تخطٍّ، وعنوان، وعمود المحتوى.",
+ "examples.layout-column.blurb": "الصفحة البسيطة الموسّطة التي يبدأ بها التطبيق: رابط تخطٍّ، وعنوان، وعمود المحتوى، وصف تذييل لقائمتي اللغة ونظام الألوان.",
"examples.layout-console-page.blurb": "شريط في الأعلى وسكة تنقّل على الجانب معًا، وهو شكل معظم لوحات الإدارة. على الهاتف تُفتح قائمة الشريط كبطاقة، وتصبح السكة صفحة فهرس. لا حاجة إلى JavaScript.",
"examples.layout-console.blurb": "شريط في الأعلى وسكة تنقّل على الجانب معًا، وهو شكل معظم لوحات الإدارة. على الهاتف تُفتح قائمة الشريط كبطاقة، وتصبح السكة صفحة فهرس. لا حاجة إلى JavaScript.",
"examples.layout-sidebar-page.blurb": "سكة تنقّل بجانب الصفحة. على الهاتف تصبح السكة صفحة فهرس، ولكل صفحة أخرى عنصر للرجوع. لا حاجة إلى JavaScript.",
diff --git a/internal/gallery/i18n/bn.json b/internal/gallery/i18n/bn.json
index 2c49689..2fdbb23 100644
--- a/internal/gallery/i18n/bn.json
+++ b/internal/gallery/i18n/bn.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "সীমানাযুক্ত একটি প্রশ্নচিহ্ন, যা সহায়তা নিবন্ধে নিয়ে যায়। এর CSS টুলটিপ কেবল সাজসজ্জা; লিঙ্কটি নিজের পূর্ণ বাক্যের লেবেল বহন করে।",
"examples.job-status.blurb": "পটভূমির কাজের অগ্রগতি: একটি স্পিনার, এক লাইন লেখা আর একটি বার। data-poll বৈশিষ্ট্যগুলো বলে পাতা কোথা থেকে আর কত ঘন ঘন নতুন কপি আনতে পারে। Fiesta-তে এমন কোনো স্ক্রিপ্ট নেই, তাই নিজে জুড়ে নিন, আর কাজ শেষ হলে বৈশিষ্ট্যগুলো বাদ দিন।",
"examples.job-status.states.running": "data-poll বৈশিষ্ট্যগুলো বলে একটি স্ক্রিপ্ট কোথা থেকে আর কত ঘন ঘন নতুন কপি আনত। Fiesta-র কিছুই তা করে না, তাই এই পাতায় এটি চলতেই থাকে।",
- "examples.layout-column.blurb": "একটি সাদামাটা মাঝখানে রাখা পাতা, যেখান থেকে অ্যাপ শুরু হয়: একটি স্কিপ লিঙ্ক, একটি শিরোনাম আর বিষয়বস্তুর কলাম।",
+ "examples.layout-column.blurb": "একটি সাদামাটা মাঝখানে রাখা পাতা, যেখান থেকে অ্যাপ শুরু হয়: একটি স্কিপ লিঙ্ক, একটি শিরোনাম, বিষয়বস্তুর কলাম আর ভাষা ও রঙের ধরনের মেনুর জন্য একটি ফুটার সারি।",
"examples.layout-console-page.blurb": "উপরে একটি বার আর পাশে একটি নেভিগেশন রেল, দুটোই একসঙ্গে, বেশির ভাগ প্রশাসন কনসোল এই আকারেরই। ফোনে বারের মেনু একটি কার্ড হয়ে খোলে, আর রেলটি একটি সূচিপাতা। JavaScript লাগে না।",
"examples.layout-console.blurb": "উপরে একটি বার আর পাশে একটি নেভিগেশন রেল, দুটোই একসঙ্গে, বেশির ভাগ প্রশাসন কনসোল এই আকারেরই। ফোনে বারের মেনু একটি কার্ড হয়ে খোলে, আর রেলটি একটি সূচিপাতা। JavaScript লাগে না।",
"examples.layout-sidebar-page.blurb": "পাতার পাশে একটি নেভিগেশন রেল। ফোনে রেলটি একটি সূচিপাতা, আর বাকি প্রতিটি পাতায় ফিরে যাওয়ার একটি নিয়ন্ত্রণ থাকে। JavaScript লাগে না।",
diff --git a/internal/gallery/i18n/drafts.json b/internal/gallery/i18n/drafts.json
index b55b90a..ba883d5 100644
--- a/internal/gallery/i18n/drafts.json
+++ b/internal/gallery/i18n/drafts.json
@@ -448,7 +448,7 @@
]
},
"examples.layout-column.blurb": {
- "english": "The plain centred page an app starts on: a skip link, a title, and the content column.",
+ "english": "The plain centred page an app starts on: a skip link, a title, the content column, and a footer row for the language and colour-scheme menus.",
"locales": [
"ar",
"bn",
diff --git a/internal/gallery/i18n/es.json b/internal/gallery/i18n/es.json
index 344783e..23ef6c1 100644
--- a/internal/gallery/i18n/es.json
+++ b/internal/gallery/i18n/es.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "Un signo de interrogación con borde que enlaza a un artículo de ayuda. Su tooltip de CSS es decoración; el enlace lleva su propia etiqueta en una frase completa.",
"examples.job-status.blurb": "El progreso de una tarea en segundo plano: un indicador giratorio, una línea de texto y una barra. Los atributos data-poll dicen de dónde podría una página traer una copia nueva y cada cuánto. Fiesta no trae ningún script que lo haga, así que conéctalo tú, y quita los atributos cuando la tarea termine.",
"examples.job-status.states.running": "Los atributos data-poll indican de dónde traería un script una copia nueva, y cada cuánto. Nada en Fiesta lo hace, así que en esta página se queda en marcha.",
- "examples.layout-column.blurb": "La página sencilla y centrada con la que arranca una aplicación: un enlace para saltar, un título y la columna de contenido.",
+ "examples.layout-column.blurb": "La página sencilla y centrada con la que arranca una aplicación: un enlace para saltar, un título, la columna de contenido y una fila de pie para los menús de idioma y de esquema de color.",
"examples.layout-console-page.blurb": "Una barra arriba y un raíl de navegación al costado a la vez, la forma que tiene la mayoría de las consolas de administración. En un teléfono, el menú de la barra se abre como una tarjeta y el raíl es una página índice. No hace falta JavaScript.",
"examples.layout-console.blurb": "Una barra arriba y un raíl de navegación al costado a la vez, la forma que tiene la mayoría de las consolas de administración. En un teléfono, el menú de la barra se abre como una tarjeta y el raíl es una página índice. No hace falta JavaScript.",
"examples.layout-sidebar-page.blurb": "Un raíl de navegación junto a la página. En un teléfono, el raíl es una página índice y todas las demás páginas tienen un control para volver. No hace falta JavaScript.",
diff --git a/internal/gallery/i18n/ga.json b/internal/gallery/i18n/ga.json
index cab2834..4ea3c94 100644
--- a/internal/gallery/i18n/ga.json
+++ b/internal/gallery/i18n/ga.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "Comhartha ceiste le himlíne a nascann le halt cabhrach. Maisiúchán is ea a leid CSS; iompraíonn an nasc a lipéad iomlán féin.",
"examples.job-status.blurb": "Dul chun cinn poist chúlra: sníomhaire, líne téacs agus barra. Insíonn na haitreabúidí data-poll cá bhféadfadh leathanach cóip úr a fháil agus cé chomh minic. Níl aon script ag Fiesta a dhéanann é sin, mar sin ceangail suas féin é, agus fág na haitreabúidí amach nuair a bhíonn an post déanta.",
"examples.job-status.states.running": "Is iad na haitreabúidí data-poll an áit a bhfaigheadh script cóip úr, agus cé chomh minic. Ní dhéanann aon rud in Fiesta é, mar sin fanann sé ar siúl ar an leathanach seo.",
- "examples.layout-column.blurb": "An leathanach simplí láraithe a dtosaíonn aip air: nasc léime, teideal, agus colún an ábhair.",
+ "examples.layout-column.blurb": "An leathanach simplí láraithe a dtosaíonn aip air: nasc léime, teideal, colún an ábhair, agus ró buntásca do na roghchláir teanga agus scéime dathanna.",
"examples.layout-console-page.blurb": "Barra trasna an bharr agus ráille nascleanúna síos an taobh in éineacht, an cruth atá ar fhormhór na gconsól riaracháin. Ar fhón, osclaíonn roghchlár an bharra mar chárta agus is leathanach innéacs é an ráille. Níl gá le JavaScript.",
"examples.layout-console.blurb": "Barra trasna an bharr agus ráille nascleanúna síos an taobh in éineacht, an cruth atá ar fhormhór na gconsól riaracháin. Ar fhón, osclaíonn roghchlár an bharra mar chárta agus is leathanach innéacs é an ráille. Níl gá le JavaScript.",
"examples.layout-sidebar-page.blurb": "Ráille nascleanúna taobh leis an leathanach. Ar fhón, is leathanach innéacs é an ráille agus tá rialtán ar ais ar gach leathanach eile. Níl gá le JavaScript.",
diff --git a/internal/gallery/i18n/hi.json b/internal/gallery/i18n/hi.json
index c65bb00..84025a6 100644
--- a/internal/gallery/i18n/hi.json
+++ b/internal/gallery/i18n/hi.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "किनारेदार प्रश्नचिह्न, जो सहायता लेख से जुड़ता है। उसका CSS टूलटिप सजावट है; लिंक अपना पूरा वाक्य वाला लेबल खुद रखता है।",
"examples.job-status.blurb": "पृष्ठभूमि के काम की प्रगति: एक स्पिनर, एक पंक्ति का पाठ और एक पट्टी। data-poll विशेषताएँ बताती हैं कि पेज ताज़ा प्रति कहाँ से और कितनी बार ला सकता है। Fiesta में ऐसा करने वाली कोई स्क्रिप्ट नहीं है, इसलिए इसे खुद जोड़ें, और काम पूरा होने पर विशेषताएँ हटा दें।",
"examples.job-status.states.running": "data-poll विशेषताएँ बताती हैं कि कोई स्क्रिप्ट ताज़ा प्रति कहाँ से और कितनी बार लाती। Fiesta में कुछ भी ऐसा नहीं करता, इसलिए इस पेज पर यह चलता ही रहता है।",
- "examples.layout-column.blurb": "वह सादा, बीच में रखा पेज जिससे ऐप शुरू होता है: एक स्किप लिंक, एक शीर्षक और सामग्री का कॉलम।",
+ "examples.layout-column.blurb": "वह सादा, बीच में रखा पेज जिससे ऐप शुरू होता है: एक स्किप लिंक, एक शीर्षक, सामग्री का कॉलम और भाषा व रंग योजना के मेनू के लिए एक फ़ुटर पंक्ति।",
"examples.layout-console-page.blurb": "ऊपर एक पट्टी और बग़ल में एक नेविगेशन रेल, दोनों एक साथ, अधिकांश प्रशासन कंसोल इसी आकार के होते हैं। फ़ोन पर पट्टी का मेन्यू एक कार्ड की तरह खुलता है और रेल एक सूची-पृष्ठ होती है। JavaScript की ज़रूरत नहीं।",
"examples.layout-console.blurb": "ऊपर एक पट्टी और बग़ल में एक नेविगेशन रेल, दोनों एक साथ, अधिकांश प्रशासन कंसोल इसी आकार के होते हैं। फ़ोन पर पट्टी का मेन्यू एक कार्ड की तरह खुलता है और रेल एक सूची-पृष्ठ होती है। JavaScript की ज़रूरत नहीं।",
"examples.layout-sidebar-page.blurb": "पृष्ठ के बग़ल में एक नेविगेशन रेल। फ़ोन पर यह रेल एक सूची-पृष्ठ होती है और बाक़ी हर पृष्ठ पर वापस जाने का नियंत्रण होता है। JavaScript की ज़रूरत नहीं।",
diff --git a/internal/gallery/i18n/ja.json b/internal/gallery/i18n/ja.json
index 8bce3a4..0ed7226 100644
--- a/internal/gallery/i18n/ja.json
+++ b/internal/gallery/i18n/ja.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "ヘルプ記事にリンクする、枠付きの疑問符です。CSS のツールチップは飾りで、リンク自身が文としてのラベルを持っています。",
"examples.job-status.blurb": "バックグラウンド処理の進み具合です。スピナー、一行のテキスト、バー。data-poll 属性は、ページが新しいコピーをどこから、どのくらいの頻度で取りに行けるかを示します。Fiesta にはそれを行うスクリプトがないので、自分でつないでください。処理が終わったら属性は外します。",
"examples.job-status.states.running": "data-poll 属性は、スクリプトが新しいコピーをどこから、どのくらいの頻度で取りに行くかを示します。Fiesta にはそれを行うものがないので、このページでは実行中のままです。",
- "examples.layout-column.blurb": "アプリがまず立つ、素朴な中央寄せのページです。スキップリンク、タイトル、そして本文の列。",
+ "examples.layout-column.blurb": "アプリがまず立つ、素朴な中央寄せのページです。スキップリンク、タイトル、本文の列、そして言語と配色のメニューを置くフッター行。",
"examples.layout-console-page.blurb": "上にバー、横にナビゲーションのレール、その両方です。管理コンソールのほとんどはこの形をしています。スマートフォンではバーのメニューがカードとして開き、レールは目次ページになります。JavaScript は不要です。",
"examples.layout-console.blurb": "上にバー、横にナビゲーションのレール、その両方です。管理コンソールのほとんどはこの形をしています。スマートフォンではバーのメニューがカードとして開き、レールは目次ページになります。JavaScript は不要です。",
"examples.layout-sidebar-page.blurb": "ページの横に立つナビゲーションレールです。スマートフォンではレールが目次ページになり、ほかのどのページにも戻る操作があります。JavaScript は不要です。",
diff --git a/internal/gallery/i18n/pt-PT.json b/internal/gallery/i18n/pt-PT.json
index c0eb7b7..a691706 100644
--- a/internal/gallery/i18n/pt-PT.json
+++ b/internal/gallery/i18n/pt-PT.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "Um ponto de interrogação com contorno que liga a um artigo de ajuda. A sua dica de CSS é decoração; a ligação leva a sua própria etiqueta, em frase completa.",
"examples.job-status.blurb": "O progresso de uma tarefa em segundo plano: um indicador giratório, uma linha de texto e uma barra. Os atributos data-poll dizem de onde uma página poderia ir buscar uma cópia nova e com que frequência. O Fiesta não traz nenhum script que o faça, por isso ligue-o você, e retire os atributos quando a tarefa terminar.",
"examples.job-status.states.running": "Os atributos data-poll indicam de onde um script iria buscar uma cópia nova, e com que frequência. Nada no Fiesta o faz, por isso nesta página fica a correr.",
- "examples.layout-column.blurb": "A página simples e centrada com que uma aplicação começa: uma ligação para saltar, um título e a coluna de conteúdo.",
+ "examples.layout-column.blurb": "A página simples e centrada com que uma aplicação começa: uma ligação para saltar, um título, a coluna de conteúdo e uma linha de rodapé para os menus de idioma e de esquema de cores.",
"examples.layout-console-page.blurb": "Uma barra no topo e um carril de navegação ao lado ao mesmo tempo, a forma que a maioria das consolas de administração tem. Num telemóvel, o menu da barra abre como um cartão e o carril é uma página de índice. Não é preciso JavaScript.",
"examples.layout-console.blurb": "Uma barra no topo e um carril de navegação ao lado ao mesmo tempo, a forma que a maioria das consolas de administração tem. Num telemóvel, o menu da barra abre como um cartão e o carril é uma página de índice. Não é preciso JavaScript.",
"examples.layout-sidebar-page.blurb": "Um carril de navegação ao lado da página. Num telemóvel, o carril é uma página de índice e todas as outras páginas têm um controlo para voltar. Não é preciso JavaScript.",
diff --git a/internal/gallery/i18n/ru.json b/internal/gallery/i18n/ru.json
index baf18ac..0526407 100644
--- a/internal/gallery/i18n/ru.json
+++ b/internal/gallery/i18n/ru.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "Вопросительный знак в рамке, ведущий к статье справки. Его CSS-подсказка — украшение; ссылка несёт собственную подпись целым предложением.",
"examples.job-status.blurb": "Ход фоновой задачи: индикатор-вертушка, строка текста и полоса. Атрибуты data-poll говорят, откуда страница могла бы получать свежую копию и как часто. Скрипта, который это делает, в Fiesta нет, так что подключите его сами, а когда задача закончится, уберите атрибуты.",
"examples.job-status.states.running": "Атрибуты data-poll говорят, откуда скрипт получал бы свежую копию и как часто. В Fiesta этого ничто не делает, поэтому на этой странице задача так и остаётся выполняющейся.",
- "examples.layout-column.blurb": "Простая центрированная страница, с которой начинается приложение: ссылка пропуска, заголовок и колонка содержимого.",
+ "examples.layout-column.blurb": "Простая центрированная страница, с которой начинается приложение: ссылка пропуска, заголовок, колонка содержимого и строка подвала для меню языка и цветовой схемы.",
"examples.layout-console-page.blurb": "Полоса сверху и навигационная рейка сбоку одновременно: такую форму имеет большинство административных консолей. На телефоне меню полосы открывается карточкой, а рейка становится страницей-оглавлением. JavaScript не нужен.",
"examples.layout-console.blurb": "Полоса сверху и навигационная рейка сбоку одновременно: такую форму имеет большинство административных консолей. На телефоне меню полосы открывается карточкой, а рейка становится страницей-оглавлением. JavaScript не нужен.",
"examples.layout-sidebar-page.blurb": "Навигационная рейка сбоку от страницы. На телефоне рейка становится страницей-оглавлением, а на каждой другой странице есть кнопка «назад». JavaScript не нужен.",
diff --git a/internal/gallery/i18n/vi.json b/internal/gallery/i18n/vi.json
index 8d450b0..2e771b6 100644
--- a/internal/gallery/i18n/vi.json
+++ b/internal/gallery/i18n/vi.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "Một dấu hỏi có viền, dẫn tới bài trợ giúp. Chú giải CSS của nó chỉ là trang trí; bản thân liên kết mang nhãn là một câu đầy đủ.",
"examples.job-status.blurb": "Tiến độ của một tác vụ nền: một vòng xoay, một dòng chữ và một thanh. Các thuộc tính data-poll cho biết trang có thể lấy bản mới từ đâu và bao lâu một lần. Fiesta không kèm script nào làm việc đó, nên bạn tự nối vào, và bỏ các thuộc tính khi tác vụ xong.",
"examples.job-status.states.running": "Các thuộc tính data-poll là nơi một script sẽ lấy bản mới, và bao lâu một lần. Không có gì trong Fiesta làm việc đó, nên trên trang này nó cứ ở trạng thái đang chạy.",
- "examples.layout-column.blurb": "Trang đơn giản căn giữa mà một ứng dụng bắt đầu: một liên kết bỏ qua, một tiêu đề và cột nội dung.",
+ "examples.layout-column.blurb": "Trang đơn giản căn giữa mà một ứng dụng bắt đầu: một liên kết bỏ qua, một tiêu đề, cột nội dung và một hàng chân trang cho các menu ngôn ngữ và bảng màu.",
"examples.layout-console-page.blurb": "Một thanh ngang trên đỉnh và một dải điều hướng bên cạnh cùng lúc, hình dạng của phần lớn bảng điều khiển quản trị. Trên điện thoại, menu của thanh mở ra dưới dạng thẻ và dải điều hướng là một trang mục lục. Không cần JavaScript.",
"examples.layout-console.blurb": "Một thanh ngang trên đỉnh và một dải điều hướng bên cạnh cùng lúc, hình dạng của phần lớn bảng điều khiển quản trị. Trên điện thoại, menu của thanh mở ra dưới dạng thẻ và dải điều hướng là một trang mục lục. Không cần JavaScript.",
"examples.layout-sidebar-page.blurb": "Một dải điều hướng bên cạnh trang. Trên điện thoại, dải này là một trang mục lục và mọi trang khác đều có nút quay lại. Không cần JavaScript.",
diff --git a/internal/gallery/i18n/yue.json b/internal/gallery/i18n/yue.json
index ce681cd..da08366 100644
--- a/internal/gallery/i18n/yue.json
+++ b/internal/gallery/i18n/yue.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "一個有邊框嘅問號,連去說明文章。佢個 CSS 提示只係裝飾;個連結自己有完整句子嘅標籤。",
"examples.job-status.blurb": "背景工作嘅進度:一個轉圈、一行字同一條進度條。data-poll 屬性講明頁面可以喺邊度、幾耐攞一次新嘅版本。Fiesta 冇提供做呢樣嘢嘅腳本,所以要你自己駁上去,工作完成之後就唔好再放呢啲屬性。",
"examples.job-status.states.running": "data-poll 屬性係腳本會去邊度、幾耐攞一次新版本。Fiesta 入面冇嘢做呢樣嘢,所以喺呢一版佢會一直喺度行緊。",
- "examples.layout-column.blurb": "應用程式起步用嘅樸素置中頁面:一個跳過連結、一個標題,同內容欄。",
+ "examples.layout-column.blurb": "應用程式起步用嘅樸素置中頁面:一個跳過連結、一個標題、內容欄,同擺語言同配色選單嘅頁尾列。",
"examples.layout-console-page.blurb": "頂部一條欄,側邊一條導航軌,兩樣同時喺度,大部分管理控制台就係呢個形狀。喺手機上,頂欄嘅選單會以卡片形式打開,導航軌係一個目錄頁。唔使 JavaScript。",
"examples.layout-console.blurb": "頂部一條欄,側邊一條導航軌,兩樣同時喺度,大部分管理控制台就係呢個形狀。喺手機上,頂欄嘅選單會以卡片形式打開,導航軌係一個目錄頁。唔使 JavaScript。",
"examples.layout-sidebar-page.blurb": "喺頁面旁邊嘅導航軌。喺手機上,導航軌係一個目錄頁,其他每一頁都有一個返回控制。唔使 JavaScript。",
diff --git a/internal/gallery/i18n/zh-Hans.json b/internal/gallery/i18n/zh-Hans.json
index 7882375..541358e 100644
--- a/internal/gallery/i18n/zh-Hans.json
+++ b/internal/gallery/i18n/zh-Hans.json
@@ -60,7 +60,7 @@
"examples.help.blurb": "一个带边框的问号,链接到帮助文章。它的 CSS 提示只是装饰;链接自己带着完整句子的标签。",
"examples.job-status.blurb": "后台任务的进度:一个转圈、一行文字和一条进度条。data-poll 属性说明页面可以从哪里、多久取一次新的副本。Fiesta 不带做这件事的脚本,所以要你自己接上,任务完成后就去掉这些属性。",
"examples.job-status.states.running": "data-poll 属性是脚本会去哪里、多久取一次新副本。Fiesta 里没有东西做这件事,所以在这一页上它会一直处于运行中。",
- "examples.layout-column.blurb": "应用起步用的那张朴素居中页面:一个跳转链接、一个标题,和内容列。",
+ "examples.layout-column.blurb": "应用起步用的那张朴素居中页面:一个跳转链接、一个标题、内容列,和放语言与配色方案菜单的页脚行。",
"examples.layout-console-page.blurb": "顶部一条栏,侧边一条导航栏,两者同时在场,大多数管理控制台就是这个形状。在手机上,顶栏的菜单以卡片形式打开,导航栏是一个目录页。不需要 JavaScript。",
"examples.layout-console.blurb": "顶部一条栏,侧边一条导航栏,两者同时在场,大多数管理控制台就是这个形状。在手机上,顶栏的菜单以卡片形式打开,导航栏是一个目录页。不需要 JavaScript。",
"examples.layout-sidebar-page.blurb": "页面旁边的导航栏。在手机上,导航栏是一个目录页,其他每个页面都有一个返回控件。不需要 JavaScript。",