rastrillo / fiesta Public

A dropdown sits in a row beside what it acts on: the rule, the check, and the examples that show it

A menu alone on a line reads as a control for nothing in particular,
and one above a page's title pushes the title down and reads as the
page's first action. DESIGN.md now says so: the Row Rule in
Navigation, a matching Don't, and a Layout line for the column's
footer row. The words are the approved English.

TestEveryDropdownSitsInARow holds every state of every example to the
rule, bodies and layouts whole. A dropdown's nearest ancestor that
lays something out (a plain <form> or <search> only groups a row's
controls) must be one of eight rows, a page header must hold its h1
and a section head its heading, and nothing outside the shell's own
chrome may come before the page header. Markup is respelled first,
so a class-spelled menu or row is read as its attribute twin. The
checker is a pure function with a table of exact messages, so each
rule is held on its own and not only by the examples that happen to
exist; a walk that finds fewer than ten menus fails, since it would
pass whatever the rule said.

Against the tree before this commit the walk named the five
component states and nothing else; at 04d01e1, before the footer
row, it also named layout-column/default twice (in main[rst-page],
and above the page header).

The five states gain a row around the component and keep their ids,
labels and the component's bytes: the Sort menu in a section head
under "Posts" (a sample phrase the catalogs already translate), the
Filter menu and its chip in a list's toolbar, and the language and
scheme menus in the footer row, where Fiesta now puts them; a page
header around them would teach the placement the column shell just
moved away from. TestEachRowedStateHoldsOneComponent pins how a
reader takes each component back out: Element for four, Contents of
the toolbar for the menu idiom, and no action on the standalone
scheme state's forms.

The dropdown blurb gains the rule, as approved. Its eleven
translations are machine drafts from the approved English, not
reviewed, recorded in drafts.json. The frame heights already hold
the five states, open and at rest, so heights.go does not change.

New tests, each red by assertion first: TestTheRowCheckNamesEachBreach
against a stub returning nil (all eight breach cases);
TestEveryDropdownSitsInARow against the old examples;
TestEachRowedStateHoldsOneComponent against them ("no element
carries rst-lbar"). Eight one-line mutations of the checker, each
caught by name: no respelling, forms and searches not skipped, a
styled form skipped too, no shell-chrome exception, a heading inside
the menu counted, no h1 needed, no heading needed, nothing checked
above the title.

Size, gzip level 6, unchanged: fiesta.css 23,146, fiesta-plain.css
22,431, fiesta-signal.css 22,542 (no CSS in this commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d49fca4962ce102ebfc308f1cb5ad86ac500ab68 parent 4306038
21 files changed, +328 −12
  • DESIGN.md +5 −0
  • check/rows_rule_test.go +242 −0
  • example/example_test.go +43 −0
  • examples/dropdown/a-filter-menu-with-one-choice-applied.html +2 −0
  • examples/dropdown/menu-idiom.html +2 −0
  • examples/locale-menu/twelve-languages-current-highlighted.html +2 −0
  • examples/prose.json +1 −1
  • examples/scheme-menu/standalone.html +2 −0
  • examples/scheme-menu/three-choices-dark-current.html +2 −0
  • internal/gallery/i18n/ar.json +1 −1
  • internal/gallery/i18n/bn.json +1 −1
  • internal/gallery/i18n/drafts.json +16 −0
  • 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.224s
=== 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: 22 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.646s
ok  	amadan.net/rastrillo/fiesta/check	7.453s
ok  	amadan.net/rastrillo/fiesta/example	0.181s
?   	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	0.876s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.009s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.012s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.344s
ok  	amadan.net/rastrillo/fiesta/internal/site	2.272s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.030s
suite: root took 17s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m11.340000s 0m2.010000s
--- 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 36 of 107 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.902s
suite: test.internal.gallery~1of3 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.630000s 0m1.270000s
--- 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 107 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.791s
suite: test.internal.gallery~2of3 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.410000s 0m1.350000s
--- 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 107 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	55.687s
suite: test.internal.gallery~3of3 took 63s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.520000s 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	47.027s
suite: test.cmd.fiesta~1of2 took 53s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.320000s 0m2.060000s
--- 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	49.939s
suite: test.cmd.fiesta~2of2 took 56s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.040000s 0m2.340000s
--- 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.798s
suite: test.internal.www~1of2 took 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.310000s 0m1.610000s
--- 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	58.067s
suite: test.internal.www~2of2 took 65s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.230000s 0m1.550000s
--- 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 11s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.040000s 0m0.960000s
--- 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 7s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m3.080000s 0m0.330000s
--- 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' .
4:10AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
4:10AM INF 22 commits scanned.
4:10AM INF scanned ~8278667 bytes (8.28 MB) in 6.78s
4: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
0m7.170000s 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-31-141, 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	4.846s
suite: browser.internal.harness took 12s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m1.590000s 0m1.310000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-31-141, 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.228s
suite: browser.check~1of13 took 97s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.930000s 0m13.370000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-31-141, 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	91.110s
suite: browser.check~2of13 took 102s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.010000s 0m13.360000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-31-141, 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	89.361s
suite: browser.check~3of13 took 100s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.990000s 0m10.950000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-31-141, 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	85.362s
suite: browser.check~4of13 took 96s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m53.010000s 0m15.580000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-31-141, 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	77.455s
suite: browser.check~5of13 took 88s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.580000s 0m13.200000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-31-141, 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	75.815s
suite: browser.check~6of13 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.710000s 0m14.740000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-31-141, 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	84.050s
suite: browser.check~7of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.950000s 0m14.750000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-31-141, 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	88.340s
suite: browser.check~8of13 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.060000s 0m15.090000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-31-141, 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	78.261s
suite: browser.check~9of13 took 89s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.470000s 0m9.200000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-31-141, 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.520s
suite: browser.check~10of13 took 93s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.590000s 0m12.210000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-31-141, 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	81.597s
suite: browser.check~11of13 took 92s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.600000s 0m14.950000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-31-141, 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	83.124s
suite: browser.check~12of13 took 93s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.680000s 0m16.060000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-31-141, 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	80.245s
suite: browser.check~13of13 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.190000s 0m10.720000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	39.406s
suite: browser.internal.gallery~1of18 took 49s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.020000s 0m3.810000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	49.719s
suite: browser.internal.gallery~2of18 took 59s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.830000s 0m2.790000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.214s
suite: browser.internal.gallery~3of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.340000s 0m4.170000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	50.515s
suite: browser.internal.gallery~4of18 took 60s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.210000s 0m2.850000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.335s
suite: browser.internal.gallery~5of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.410000s 0m5.270000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	58.470s
suite: browser.internal.gallery~6of18 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.440000s 0m4.090000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	56.739s
suite: browser.internal.gallery~7of18 took 66s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.140000s 0m2.810000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	58.640s
suite: browser.internal.gallery~8of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.060000s 0m4.830000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.556s
suite: browser.internal.gallery~9of18 took 76s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m23.020000s 0m7.600000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	60.856s
suite: browser.internal.gallery~10of18 took 70s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.920000s 0m2.710000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	62.387s
suite: browser.internal.gallery~11of18 took 72s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.200000s 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.353s
suite: browser.internal.gallery~12of18 took 73s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.650000s 0m4.740000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 4 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	64.585s
suite: browser.internal.gallery~13of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.210000s 0m5.470000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.493s
suite: browser.internal.gallery~14of18 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.650000s 0m5.410000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.443s
suite: browser.internal.gallery~15of18 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.800000s 0m5.560000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.831s
suite: browser.internal.gallery~16of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.490000s 0m4.370000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.245s
suite: browser.internal.gallery~17of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.220000s 0m5.790000s
--- 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-31-141, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.384s
suite: browser.internal.gallery~18of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.460000s 0m6.510000s
--- 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-31-141, 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.434s
suite: browser.internal.gallery.history took 40s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.200000s 0m2.090000s
--- 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-31-141, 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	67.745s
suite: browser.internal.www~1of4 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.220000s 0m2.150000s
--- 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-31-141, 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.296s
suite: browser.internal.www~2of4 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.030000s 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-31-141, 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.700s
suite: browser.internal.www~3of4 took 90s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.780000s 0m2.720000s
--- 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-31-141, 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	77.434s
suite: browser.internal.www~4of4 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.770000s 0m3.090000s
diff --git a/DESIGN.md b/DESIGN.md
index 79a73b0..62f7b4a 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -346,6 +346,8 @@ A single centred content column, capped at **64rem** with `1.5rem 1rem
2.5rem` of padding, holding every screen. The column is markup an app
emits (`rst-page`); no partial brings its own.
+The column ends with a footer row (`rst-page-foot`) for the language and colour-scheme menus: muted, under a hairline, with the menus at the inline end. The page header stays the first thing on screen.
+
**Spacing** is a six-step rem scale — 0.25 / 0.5 / 0.75 / 1 / 1.5 /
2.5rem — and screens stack vertically in reading order: page header,
then section head plus card, then the next. Horizontal arrangement is
@@ -515,6 +517,8 @@ half-raised preview. Below 800px the rail collapses into a
`<details name="rst-menus">`, so opening one closes the rest with no
script; `fiesta.js` adds outside-click and Escape as enhancement.
+**The Row Rule.** A dropdown never sits on a line of its own. Put it in a row, beside what it acts on: a page header, a section head, a list's toolbar, a bar, or the footer row. A menu above a page's title pushes the title down and reads as the page's first action.
+
### Status Pill
A pill-radius tinted chip carrying a foreground/background tone pair and
@@ -573,3 +577,4 @@ because a falling number is good about as often as it is bad.
- **Don't** compose a heading, a paragraph and a button side by side in a
flex row. Screens stack vertically; horizontal arrangement is reserved
for the idioms that ship it.
+- **Don't** put a dropdown on a line of its own, or above a page's title. Put it in a row beside what it acts on.
diff --git a/check/rows_rule_test.go b/check/rows_rule_test.go
new file mode 100644
index 0000000..910fd84
--- /dev/null
+++ b/check/rows_rule_test.go
@@ -0,0 +1,242 @@
+package check
+
+import (
+ "slices"
+ "strings"
+ "testing"
+
+ xhtml "golang.org/x/net/html"
+
+ "amadan.net/rastrillo/fiesta"
+ "amadan.net/rastrillo/fiesta/internal/markup"
+)
+
+// The Row Rule: a dropdown never sits on a line of its own. It sits in
+// a row beside what it acts on. A menu alone on a line reads as a
+// control for nothing in particular, and one above a page's title
+// pushes the title down and reads as the page's first action. DESIGN.md
+// states the rule; these tests hold every example to it, so a gallery
+// page can never teach the placement the rule forbids.
+
+// dropdownRows are the rows a dropdown may sit in, each with why it is
+// a row: a dropdown's nearest ancestor that lays something out must
+// carry one of these.
+var dropdownRows = map[string]string{
+ "rst-page-header": "the page header: a title and its actions",
+ "rst-box-head": "a section head: a heading and its actions",
+ "rst-lbar": "a list's toolbar",
+ "rst-bulkbar": "the bulk bar",
+ "rst-shell-tail": "the shell's bar",
+ "rst-shell-rail-foot": "the rail's foot",
+ "rst-page-foot": "the column's footer row",
+ "rst-dropdown-menu": "a dropdown inside a menu is one of that menu's rows",
+}
+
+// shellChrome is the shell's own chrome. A dropdown there is laid out
+// beside the page, not above its title, so it may come before the page
+// header in document order; anything else that does is above the title.
+var shellChrome = []string{"rst-shell-tail", "rst-shell-rail-foot"}
+
+// dropdownsOutOfRow reads one piece of markup and returns one message
+// per breach of the Row Rule, in document order. It is a pure function
+// so its own test can hold each rule to an exact message; the walk over
+// every example is TestEveryDropdownSitsInARow.
+//
+// The markup is respelled first, so a class-spelled dropdown is visited
+// and a class-spelled row counts as its attribute twin.
+func dropdownsOutOfRow(src string) []string {
+ respelled, notes := markup.Respell([]byte(src))
+ var out []string
+ // A class list the respeller could not read is a dropdown or a row
+ // this check might not see; it is a breach of the check, not a pass.
+ for _, n := range notes {
+ out = append(out, "the markup could not be respelled: "+n.String())
+ }
+ doc, err := xhtml.Parse(strings.NewReader(string(respelled)))
+ if err != nil {
+ return append(out, "the markup does not parse: "+err.Error())
+ }
+ var order []*xhtml.Node
+ var walk func(*xhtml.Node)
+ walk = func(n *xhtml.Node) {
+ if n.Type == xhtml.ElementNode {
+ order = append(order, n)
+ }
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ walk(c)
+ }
+ }
+ walk(doc)
+ header := slices.IndexFunc(order, func(n *xhtml.Node) bool { return nodeCarries(n, "rst-page-header") })
+ for at, n := range order {
+ if !isDropdown(n) {
+ continue
+ }
+ row := n.Parent
+ for row != nil && groupsControls(row) {
+ row = row.Parent
+ }
+ switch {
+ case row == nil || !isRow(row):
+ out = append(out, "a dropdown sits in "+describe(row)+", not in a row")
+ case nodeCarries(row, "rst-page-header") && !holds(row, "h1"):
+ out = append(out, "a page header holds a dropdown and no h1")
+ case nodeCarries(row, "rst-box-head") && !holds(row, "h2", "h3", "h4", "h5", "h6"):
+ out = append(out, "a section head holds a dropdown and no heading")
+ }
+ if header >= 0 && at < header && !inShellChrome(n) {
+ out = append(out, "a dropdown comes before the page header")
+ }
+ }
+ return out
+}
+
+func isDropdown(n *xhtml.Node) bool { return n.Data == "details" && nodeCarries(n, "rst-dropdown") }
+
+// nodeCarries reports whether n has the attribute name, whatever its value.
+func nodeCarries(n *xhtml.Node, name string) bool {
+ return slices.ContainsFunc(n.Attr, func(a xhtml.Attribute) bool { return a.Key == name })
+}
+
+// groupsControls is a <form> or <search> with no rst- attribute: it
+// groups a row's controls and lays nothing out, so the row is above it.
+// One that carries an rst- attribute is styled, and is what the menu
+// sits in.
+func groupsControls(n *xhtml.Node) bool {
+ if n.Type != xhtml.ElementNode || (n.Data != "form" && n.Data != "search") {
+ return false
+ }
+ return !slices.ContainsFunc(n.Attr, func(a xhtml.Attribute) bool { return strings.HasPrefix(a.Key, "rst-") })
+}
+
+func isRow(n *xhtml.Node) bool {
+ return slices.ContainsFunc(n.Attr, func(a xhtml.Attribute) bool { _, ok := dropdownRows[a.Key]; return ok })
+}
+
+// describe names an element as a failure shows it: its tag and its rst-
+// attributes, main[rst-page], so the reader can find it in the markup.
+func describe(n *xhtml.Node) string {
+ if n == nil || n.Type != xhtml.ElementNode {
+ return "the document"
+ }
+ s := n.Data
+ for _, a := range n.Attr {
+ if strings.HasPrefix(a.Key, "rst-") {
+ s += "[" + a.Key + "]"
+ }
+ }
+ return s
+}
+
+// holds reports whether row has one of the tags among its descendants,
+// outside any dropdown: a heading inside a menu's panel titles nothing
+// in the row, so a header holding only that is still a menu on its own.
+func holds(row *xhtml.Node, tags ...string) bool {
+ for c := row.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type != xhtml.ElementNode || isDropdown(c) {
+ continue
+ }
+ if slices.Contains(tags, c.Data) || holds(c, tags...) {
+ return true
+ }
+ }
+ return false
+}
+
+func inShellChrome(n *xhtml.Node) bool {
+ for p := n.Parent; p != nil; p = p.Parent {
+ if slices.ContainsFunc(shellChrome, func(name string) bool { return nodeCarries(p, name) }) {
+ return true
+ }
+ }
+ return false
+}
+
+// TestTheRowCheckNamesEachBreach: each way a dropdown can break the
+// rule is named by its own message, in both spellings, and each shape
+// the rule allows passes. A checker that only counted breaches would
+// pass a page that broke the rule one way and was fixed another.
+func TestTheRowCheckNamesEachBreach(t *testing.T) {
+ const header = `<header rst-page-header><h1>T</h1></header>`
+ cases := []struct {
+ name, markup string
+ want []string
+ }{
+ {"alone at the top of the page, above its title",
+ `<main rst-page><details rst-dropdown></details>` + header + `</main>`,
+ []string{"a dropdown sits in main[rst-page], not in a row", "a dropdown comes before the page header"}},
+ {"in a bare div",
+ `<div><details rst-dropdown></details></div>`,
+ []string{"a dropdown sits in div, not in a row"}},
+ {"in a toolbar above the page's title",
+ `<main rst-page><div rst-lbar><details rst-dropdown></details></div>` + header + `</main>`,
+ []string{"a dropdown comes before the page header"}},
+ {"a page header with no title",
+ `<header rst-page-header><details rst-dropdown></details></header>`,
+ []string{"a page header holds a dropdown and no h1"}},
+ {"a section head with no heading",
+ `<div rst-box-head><details rst-dropdown></details></div>`,
+ []string{"a section head holds a dropdown and no heading"}},
+ {"class-spelled, alone in the body",
+ `<body><details class="rst-dropdown"></details></body>`,
+ []string{"a dropdown sits in body, not in a row"}},
+ {"in a form that lays itself out",
+ `<div rst-lbar><form rst-search><details rst-dropdown></details></form></div>`,
+ []string{"a dropdown sits in form[rst-search], not in a row"}},
+ {"a heading inside the menu is not the section's",
+ `<div rst-box-head><details rst-dropdown><summary>S</summary><div rst-dropdown-menu><h2>P</h2></div></details></div>`,
+ []string{"a section head holds a dropdown and no heading"}},
+
+ {"class-spelled, in a section head with its heading",
+ `<div class="rst-box-head"><h2>P</h2><details rst-dropdown></details></div>`, nil},
+ {"in a toolbar, inside a form, after the toolbar's search",
+ `<div rst-lbar><search><form rst-search method="get" action="/posts"><input type="search" name="q"></form></search>` +
+ `<form method="get" action="/posts"><details rst-dropdown><summary>S</summary></details></form></div>`, nil},
+ {"in a toolbar, inside a search's form",
+ `<div rst-lbar><search><form method="get"><details rst-dropdown></details></form></search></div>`, nil},
+ {"in the shell's bar, before the page header",
+ `<div rst-shell-tail><details rst-dropdown></details></div><main rst-page>` + header + `</main>`, nil},
+ {"in a menu in the rail's foot, before the page header",
+ `<div rst-shell-rail-foot><details rst-dropdown><summary>S</summary><div rst-dropdown-menu><details rst-dropdown></details></div></details></div><main rst-page>` + header + `</main>`, nil},
+ {"inside another dropdown's menu",
+ `<div rst-box-head><h2>P</h2><details rst-dropdown><summary>S</summary><div rst-dropdown-menu><details rst-dropdown></details></div></details></div>`, nil},
+ {"in the page header beside its title",
+ `<main rst-page><header rst-page-header><div><h1>T</h1></div><details rst-dropdown></details></header></main>`, nil},
+ {"in the footer row, after the page header",
+ `<main rst-page>` + header + `<footer rst-page-foot><details rst-dropdown></details></footer></main>`, nil},
+ }
+ for _, c := range cases {
+ if got := dropdownsOutOfRow(c.markup); !slices.Equal(got, c.want) {
+ t.Errorf("%s:\n got %q\n want %q", c.name, got, c.want)
+ }
+ }
+}
+
+// TestEveryDropdownSitsInARow: every state of every example, bodies and
+// layouts whole, keeps the Row Rule. The gallery is where a reader
+// learns where a menu goes, so a state that shows one alone teaches the
+// placement the rule forbids.
+func TestEveryDropdownSitsInARow(t *testing.T) {
+ states := everyState(t, fiesta.FS())
+ menus := 0
+ for _, s := range states {
+ if strings.Contains(s.Markup, "rst-dropdown") {
+ menus++
+ }
+ for _, msg := range dropdownsOutOfRow(s.Markup) {
+ t.Errorf("%s/%s: %s", s.Name, s.State, msg)
+ }
+ }
+ // A walk that finds no menus passes whatever the rule says.
+ if menus < 10 {
+ t.Fatalf("%d states hold a dropdown, want at least 10; the walk is reading the wrong markup", menus)
+ }
+ if t.Failed() {
+ var rows []string
+ for row, why := range dropdownRows {
+ rows = append(rows, row+" ("+why+")")
+ }
+ slices.Sort(rows)
+ t.Logf("put each menu in one of these rows: %s", strings.Join(rows, ", "))
+ }
+}
diff --git a/example/example_test.go b/example/example_test.go
index f1c3ba3..b1f0dcf 100644
--- a/example/example_test.go
+++ b/example/example_test.go
@@ -723,3 +723,46 @@ func TestANestedCarrierCountsOnce(t *testing.T) {
t.Fatalf("Element returned\n%s\nwant the outer dropdown\n%s", got, outer)
}
}
+
+// TestEachRowedStateHoldsOneComponent: the five component states that
+// show their menu in a row still give a reader the bare component, the
+// way part 2's golden tests and the gallery take it out: Element for
+// four, and for the menu idiom Contents of its toolbar, since the idiom
+// is the menu and its applied-filter chip together. The standalone
+// scheme state's component is the partial with its forms' action
+// removed, as the seeder made it, so it must carry no action.
+func TestEachRowedStateHoldsOneComponent(t *testing.T) {
+ cases := []struct {
+ name, state, attribute string
+ read func(markup, attribute string) (string, error)
+ }{
+ {"dropdown", "a-filter-menu-with-one-choice-applied", "rst-dropdown", example.Element},
+ {"dropdown", "menu-idiom", "rst-lbar", example.Contents},
+ {"locale-menu", "twelve-languages-current-highlighted", "rst-locale", example.Element},
+ {"scheme-menu", "three-choices-dark-current", "rst-scheme", example.Element},
+ {"scheme-menu", "standalone", "rst-scheme", example.Element},
+ }
+ for _, c := range cases {
+ m, err := example.Markup(fiesta.FS(), c.name, c.state)
+ if err != nil {
+ t.Fatal(err)
+ }
+ component, err := c.read(m, c.attribute)
+ if err != nil {
+ t.Errorf("%s/%s: %v", c.name, c.state, err)
+ continue
+ }
+ switch c.name + "/" + c.state {
+ case "dropdown/menu-idiom":
+ for _, part := range []string{"rst-dropdown", "rst-ftok"} {
+ if _, err := example.Element(component, part); err != nil {
+ t.Errorf("dropdown/menu-idiom: the toolbar should hold one %s: %v", part, err)
+ }
+ }
+ case "scheme-menu/standalone":
+ if strings.Contains(component, " action=") {
+ t.Error("scheme-menu/standalone: the component's forms name an action")
+ }
+ }
+ }
+}
diff --git a/examples/dropdown/a-filter-menu-with-one-choice-applied.html b/examples/dropdown/a-filter-menu-with-one-choice-applied.html
index aa6d600..923b620 100644
--- a/examples/dropdown/a-filter-menu-with-one-choice-applied.html
+++ b/examples/dropdown/a-filter-menu-with-one-choice-applied.html
@@ -11,6 +11,7 @@
<script defer src="../../js/anchor.js"></script>
</head>
<body>
+<div rst-box-head><h2>Posts</h2>
<details rst-dropdown name="rst-menus">
<summary rst-btn aria-label="Sort posts: newest first">Newest <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></summary>
<div rst-dropdown-menu>
@@ -19,5 +20,6 @@
<a href="/posts?sort=title">By title</a>
</div>
</details>
+</div>
</body>
</html>
diff --git a/examples/dropdown/menu-idiom.html b/examples/dropdown/menu-idiom.html
index 19f9c44..a9c3e9f 100644
--- a/examples/dropdown/menu-idiom.html
+++ b/examples/dropdown/menu-idiom.html
@@ -10,6 +10,7 @@
<script defer src="../../js/anchor.js"></script>
</head>
<body>
+<div rst-list><div rst-lbar>
<details rst-dropdown name="rst-menus">
<summary>Filter<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><span class="rst-sr-only">Filter orders: Paid</span></summary>
<div rst-dropdown-menu>
@@ -18,5 +19,6 @@
</div>
</details>
<span rst-ftok><span rst-ftok-k>Paid</span><a href="/orders" aria-label="Remove filter Paid">✕</a></span>
+</div></div>
</body>
</html>
diff --git a/examples/locale-menu/twelve-languages-current-highlighted.html b/examples/locale-menu/twelve-languages-current-highlighted.html
index 644a5ec..eabf808 100644
--- a/examples/locale-menu/twelve-languages-current-highlighted.html
+++ b/examples/locale-menu/twelve-languages-current-highlighted.html
@@ -10,6 +10,7 @@
<script defer src="../../js/anchor.js"></script>
</head>
<body>
+<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>
@@ -27,5 +28,6 @@
<form method="post" action="/_locale"><input type="hidden" name="locale" value="ar"><input type="hidden" name="return" value="/"><button type="submit" lang="ar">العربية</button></form>
</div>
</details>
+</footer>
</body>
</html>
diff --git a/examples/prose.json b/examples/prose.json
index 7307968..7ddf345 100644
--- a/examples/prose.json
+++ b/examples/prose.json
@@ -92,7 +92,7 @@
"blurb": "Definition list about a record on a detail screen. Mono marks for machine-ish values."
},
"dropdown": {
- "blurb": "A zero-JavaScript disclosure: a summary button over a panel of plain links.",
+ "blurb": "A zero-JavaScript disclosure: a summary button over a panel of plain links. Put it in a row beside what it acts on, never on a line of its own.",
"states": {
"menu-idiom": "The details/summary menu behind header overflow menus and a list bar's filter, plus an applied filter as a removable chip."
}
diff --git a/examples/scheme-menu/standalone.html b/examples/scheme-menu/standalone.html
index eb71b55..6a1d3ef 100644
--- a/examples/scheme-menu/standalone.html
+++ b/examples/scheme-menu/standalone.html
@@ -10,6 +10,7 @@
<script defer src="../../js/anchor.js"></script>
</head>
<body>
+<footer rst-page-foot>
<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">
@@ -18,5 +19,6 @@
<form method="post"><input type="hidden" name="scheme" value="system"><input type="hidden" name="return" value="/"><button type="submit">System</button></form>
</div>
</details>
+</footer>
</body>
</html>
diff --git a/examples/scheme-menu/three-choices-dark-current.html b/examples/scheme-menu/three-choices-dark-current.html
index 56370fe..95bb69d 100644
--- a/examples/scheme-menu/three-choices-dark-current.html
+++ b/examples/scheme-menu/three-choices-dark-current.html
@@ -10,6 +10,7 @@
<script defer src="../../js/anchor.js"></script>
</head>
<body>
+<footer rst-page-foot>
<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">
@@ -18,5 +19,6 @@
<form method="post" action="/_theme"><input type="hidden" name="scheme" value="system"><input type="hidden" name="return" value="/"><button type="submit">System</button></form>
</div>
</details>
+</footer>
</body>
</html>
diff --git a/internal/gallery/i18n/ar.json b/internal/gallery/i18n/ar.json
index 8274514..cfea78d 100644
--- a/internal/gallery/i18n/ar.json
+++ b/internal/gallery/i18n/ar.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "صف الإجراءات في صفحة التأكيد. للإجراءات المُتلِفة مسار خاص بها لا مربّع حوار، فلا شيء يحتاج إلى برمجة.",
"examples.confirm-form.states.ordinary-default-cancel-wording": "يقول رابط الإلغاء «إلغاء»، وهي الصياغة المعتادة. غيّرها حين تكون كلمات النموذج نفسه أوضح.",
"examples.detail-list.blurb": "قائمة تعريفات عن سجل في شاشة التفاصيل. علامة mono للقيم ذات الطابع الآلي.",
- "examples.dropdown.blurb": "عنصر يُفتح ويُغلق بلا أي JavaScript: زر summary فوق لوحة من روابط عادية.",
+ "examples.dropdown.blurb": "عنصر يُفتح ويُغلق بلا أي JavaScript: زر summary فوق لوحة من روابط عادية. ضعه في صف بجانب ما يعمل عليه، ولا تضعه أبدًا في سطر وحده.",
"examples.dropdown.states.menu-idiom": "قائمة ‎details/summary‎ التي تقف خلف قوائم الفائض في الترويسة وخلف تصفية شريط القائمة، ومعها تصفية مطبَّقة في هيئة شارة يمكن إزالتها.",
"examples.empty-state.blurb": "بطاقة الحالة الفارغة. حالة فارغة مع مداخل للخطوات الأولى.",
"examples.error-page.blurb": "جسد استجابة الخطأ كاملًا: الحالة، وجملة صادقة واحدة، وطريق للعودة، ومراجع الخطأ 500 التي يفيد منها المشرف.",
diff --git a/internal/gallery/i18n/bn.json b/internal/gallery/i18n/bn.json
index 2fdbb23..389876c 100644
--- a/internal/gallery/i18n/bn.json
+++ b/internal/gallery/i18n/bn.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "নিশ্চিতকরণ পাতার কাজের সারি। ধ্বংসাত্মক কাজের জন্য ডায়ালগ নয়, নিজস্ব রুট থাকে, তাই স্ক্রিপ্ট লেখার কিছুই থাকে না।",
"examples.confirm-form.states.ordinary-default-cancel-wording": "বাতিল লিঙ্কে লেখা থাকে «বাতিল», যা সাধারণ কথা। ফর্মের নিজের কথা বেশি স্পষ্ট হলে বদলে দিন।",
"examples.detail-list.blurb": "বিস্তারিত স্ক্রিনে কোনও রেকর্ড সম্পর্কে ডেফিনিশন লিস্ট। মেশিনঘেঁষা মানের জন্য মনো চিহ্ন।",
- "examples.dropdown.blurb": "JavaScript ছাড়াই একটি ডিসক্লোজার: একটি summary বোতাম, তার নিচে সাধারণ লিঙ্কের প্যানেল।",
+ "examples.dropdown.blurb": "JavaScript ছাড়াই একটি ডিসক্লোজার: একটি summary বোতাম, তার নিচে সাধারণ লিঙ্কের প্যানেল। যার ওপর এটি কাজ করে, একটি সারিতে তার পাশে রাখুন, কখনও আলাদা একটি লাইনে একা রাখবেন না।",
"examples.dropdown.states.menu-idiom": "হেডারের বাড়তি মেনু আর তালিকা বারের ফিল্টারের পিছনে থাকা details/summary মেনু, সঙ্গে প্রয়োগ করা ফিল্টার একটি সরানো-যায় এমন চিপ হিসেবে।",
"examples.empty-state.blurb": "খালি-অবস্থার কার্ড। খালি অবস্থা, সঙ্গে শুরু করার পথ।",
"examples.error-page.blurb": "ত্রুটির উত্তরের পুরো অংশ: স্ট্যাটাস, একটি সৎ বাক্য, ফিরে যাওয়ার পথ, আর 500-এর রেফারেন্স যা একজন প্রশাসক কাজে লাগাতে পারেন।",
diff --git a/internal/gallery/i18n/drafts.json b/internal/gallery/i18n/drafts.json
index ba883d5..53512ca 100644
--- a/internal/gallery/i18n/drafts.json
+++ b/internal/gallery/i18n/drafts.json
@@ -111,6 +111,22 @@
"zh-Hans"
]
},
+ "examples.dropdown.blurb": {
+ "english": "A zero-JavaScript disclosure: a summary button over a panel of plain links. Put it in a row beside what it acts on, never on a line of its own.",
+ "locales": [
+ "ar",
+ "bn",
+ "es",
+ "ga",
+ "hi",
+ "ja",
+ "pt-PT",
+ "ru",
+ "vi",
+ "yue",
+ "zh-Hans"
+ ]
+ },
"examples.error-page.states.anything-else-the-generic-pair": {
"english": "Any status that has no wording of its own gets this generic pair: a title that says something is wrong and a body that says to try again.",
"locales": [
diff --git a/internal/gallery/i18n/es.json b/internal/gallery/i18n/es.json
index 23ef6c1..f042602 100644
--- a/internal/gallery/i18n/es.json
+++ b/internal/gallery/i18n/es.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "La fila de acciones de la página de confirmación. Las acciones destructivas tienen su propia ruta en vez de un diálogo, así que no hay nada que programar.",
"examples.confirm-form.states.ordinary-default-cancel-wording": "El enlace de cancelar dice Cancelar, que es lo habitual. Cámbialo cuando las palabras del propio formulario sean más claras.",
"examples.detail-list.blurb": "Lista de definiciones sobre un registro en una pantalla de detalle. La marca mono es para valores de aspecto técnico.",
- "examples.dropdown.blurb": "Un desplegable sin nada de JavaScript: un botón summary sobre un panel de enlaces normales.",
+ "examples.dropdown.blurb": "Un desplegable sin nada de JavaScript: un botón summary sobre un panel de enlaces normales. Ponlo en una fila junto a aquello sobre lo que actúa, nunca solo en una línea.",
"examples.dropdown.states.menu-idiom": "El menú details/summary que hay detrás de los menús de desbordamiento de la cabecera y del filtro de una barra de lista, más un filtro aplicado como etiqueta que se puede quitar.",
"examples.empty-state.blurb": "La tarjeta de estado vacío. Un estado vacío con vías para dar los primeros pasos.",
"examples.error-page.blurb": "El cuerpo completo de una respuesta de error: el estado, una frase honesta, una vía de vuelta y referencias de 500 que un administrador puede usar.",
diff --git a/internal/gallery/i18n/ga.json b/internal/gallery/i18n/ga.json
index 4ea3c94..a968d5c 100644
--- a/internal/gallery/i18n/ga.json
+++ b/internal/gallery/i18n/ga.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "Ró gnímh an leathanaigh deimhnithe. Faigheann gníomhartha millteacha a mbealach féin seachas dialóg, mar sin níl aon rud le scriptiú.",
"examples.confirm-form.states.ordinary-default-cancel-wording": "Deir an nasc cealaithe Cealaigh, an gnáthfhoclaíocht. Athraigh é nuair is soiléire focail na foirme féin.",
"examples.detail-list.blurb": "Liosta sainmhínithe faoi thaifead ar scáileán sonraí. Marcálann mono luachanna atá cosúil le sonraí meaisín.",
- "examples.dropdown.blurb": "Nochtadh gan aon JavaScript: cnaipe summary os cionn painéil de ghnáthnaisc.",
+ "examples.dropdown.blurb": "Nochtadh gan aon JavaScript: cnaipe summary os cionn painéil de ghnáthnaisc. Cuir i ró é in aice leis an rud a n-oibríonn sé air, agus ná cuir ar líne leis féin riamh é.",
"examples.dropdown.states.menu-idiom": "An roghchlár details/summary atá taobh thiar de roghchláir bharr an leathanaigh agus de scagaire barra liosta, mar aon le scagaire i bhfeidhm mar shlis inbhainte.",
"examples.empty-state.blurb": "An cárta staid fholaimh. Staid fholamh a thaispeánann na chéad chéimeanna.",
"examples.error-page.blurb": "Corp iomlán freagra earráide: an stádas, abairt mhacánta amháin, bealach ar ais, agus tagairtí 500 is féidir le riarthóir a úsáid.",
diff --git a/internal/gallery/i18n/hi.json b/internal/gallery/i18n/hi.json
index 84025a6..f27d0ae 100644
--- a/internal/gallery/i18n/hi.json
+++ b/internal/gallery/i18n/hi.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "पुष्टि पृष्ठ की क्रिया पंक्ति। विनाशकारी क्रियाओं का अपना रूट होता है, कोई डायलॉग नहीं, इसलिए स्क्रिप्ट लिखने को कुछ बचता ही नहीं।",
"examples.confirm-form.states.ordinary-default-cancel-wording": "रद्द लिंक पर «रद्द करें» लिखा होता है, जो आम शब्द है। जब फ़ॉर्म के अपने शब्द ज़्यादा साफ़ हों, तो इसे बदल दें।",
"examples.detail-list.blurb": "विवरण स्क्रीन पर किसी रिकॉर्ड की डेफ़िनिशन लिस्ट। मोनो चिह्न मशीन जैसे मानों के लिए।",
- "examples.dropdown.blurb": "बिना JavaScript का डिस्क्लोज़र: एक summary बटन, उसके नीचे सादे लिंक का पैनल।",
+ "examples.dropdown.blurb": "बिना JavaScript का डिस्क्लोज़र: एक summary बटन, उसके नीचे सादे लिंक का पैनल। इसे एक पंक्ति में उसके बगल में रखें जिस पर यह काम करता है, कभी अकेली पंक्ति में नहीं।",
"examples.dropdown.states.menu-idiom": "हेडर के अतिरिक्त मेन्यू और सूची पट्टी के फ़िल्टर के पीछे बैठा details/summary मेन्यू, और साथ में लागू फ़िल्टर एक हटाने योग्य चिप के रूप में।",
"examples.empty-state.blurb": "खाली-स्थिति कार्ड। खाली स्थिति, शुरुआती कदमों के रास्तों के साथ।",
"examples.error-page.blurb": "त्रुटि प्रतिक्रिया का पूरा हिस्सा: स्थिति, एक सच्चा वाक्य, वापस जाने का रास्ता, और 500 के संदर्भ जो एक प्रशासक काम में ले सके।",
diff --git a/internal/gallery/i18n/ja.json b/internal/gallery/i18n/ja.json
index 0ed7226..d657fd1 100644
--- a/internal/gallery/i18n/ja.json
+++ b/internal/gallery/i18n/ja.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "確認ページの操作行です。取り消せない操作にはダイアログではなく専用のルートがあるので、書くべきスクリプトがありません。",
"examples.confirm-form.states.ordinary-default-cancel-wording": "キャンセルリンクの文言は「キャンセル」で、これが普通の言い方です。フォーム自身の言葉のほうが分かりやすいときは変えてください。",
"examples.detail-list.blurb": "詳細画面でレコードを説明する定義リスト。機械寄りの値には等幅の印を付けます。",
- "examples.dropdown.blurb": "JavaScript を一行も使わない開閉です。summary のボタンの下に、ただのリンクを並べたパネルがあります。",
+ "examples.dropdown.blurb": "JavaScript を一行も使わない開閉です。summary のボタンの下に、ただのリンクを並べたパネルがあります。操作する対象と同じ行に、その隣に置いてください。単独で一行を占めさせることはしません。",
"examples.dropdown.states.menu-idiom": "ヘッダーのはみ出しメニューや一覧バーの絞り込みの正体である details/summary のメニューと、適用中の絞り込みを表す取り外せるチップです。",
"examples.empty-state.blurb": "空の状態のカード。空の状態と、最初の一歩への入り口。",
"examples.error-page.blurb": "エラー応答の本体すべて。ステータス、正直な一文、戻る道、そして管理者が使える 500 の参照番号。",
diff --git a/internal/gallery/i18n/pt-PT.json b/internal/gallery/i18n/pt-PT.json
index a691706..43e03d5 100644
--- a/internal/gallery/i18n/pt-PT.json
+++ b/internal/gallery/i18n/pt-PT.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "A linha de ações da página de confirmação. As ações destrutivas têm rota própria em vez de um diálogo, por isso não há nada para programar.",
"examples.confirm-form.states.ordinary-default-cancel-wording": "A ligação de cancelar diz Cancelar, que é o habitual. Mude-a quando as palavras do próprio formulário forem mais claras.",
"examples.detail-list.blurb": "Lista de definições sobre um registo num ecrã de detalhe. A marca mono é para valores de aspeto técnico.",
- "examples.dropdown.blurb": "Um desdobrável sem JavaScript nenhum: um botão summary sobre um painel de ligações simples.",
+ "examples.dropdown.blurb": "Um desdobrável sem JavaScript nenhum: um botão summary sobre um painel de ligações simples. Ponha-o numa fila, ao lado daquilo sobre que atua, nunca sozinho numa linha.",
"examples.dropdown.states.menu-idiom": "O menu details/summary por trás dos menus de excedente do cabeçalho e do filtro de uma barra de lista, mais um filtro aplicado como etiqueta removível.",
"examples.empty-state.blurb": "O cartão de estado vazio. Um estado vazio com caminhos para os primeiros passos.",
"examples.error-page.blurb": "O corpo inteiro de uma resposta de erro: o estado, uma frase honesta, um caminho de volta e referências de 500 que um administrador pode usar.",
diff --git a/internal/gallery/i18n/ru.json b/internal/gallery/i18n/ru.json
index 0526407..0ad24ed 100644
--- a/internal/gallery/i18n/ru.json
+++ b/internal/gallery/i18n/ru.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "Строка действий на странице подтверждения. У необратимых действий свой маршрут, а не диалог, поэтому писать скрипт не для чего.",
"examples.confirm-form.states.ordinary-default-cancel-wording": "Ссылка отмены говорит «Отмена», это обычная формулировка. Измените её, когда собственные слова формы понятнее.",
"examples.detail-list.blurb": "Список определений о записи на экране подробностей. Моноширинная пометка — для машинных значений.",
- "examples.dropdown.blurb": "Раскрывающийся блок вовсе без JavaScript: кнопка summary над панелью обычных ссылок.",
+ "examples.dropdown.blurb": "Раскрывающийся блок вовсе без JavaScript: кнопка summary над панелью обычных ссылок. Ставьте его в ряд рядом с тем, на что он действует, и никогда не на отдельной строке.",
"examples.dropdown.states.menu-idiom": "Меню на details/summary, стоящее за меню переполнения в шапке и за фильтром панели списка, плюс применённый фильтр в виде снимаемой метки.",
"examples.empty-state.blurb": "Карточка пустого состояния. Пустое состояние с подсказками, с чего начать.",
"examples.error-page.blurb": "Всё тело ответа с ошибкой: статус, одна честная фраза, путь назад и ссылки для 500, которыми может воспользоваться администратор.",
diff --git a/internal/gallery/i18n/vi.json b/internal/gallery/i18n/vi.json
index 2e771b6..3b162ed 100644
--- a/internal/gallery/i18n/vi.json
+++ b/internal/gallery/i18n/vi.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "Hàng hành động của trang xác nhận. Hành động phá hủy có tuyến riêng chứ không phải một hộp thoại, nên chẳng có gì để viết kịch bản.",
"examples.confirm-form.states.ordinary-default-cancel-wording": "Liên kết hủy ghi Hủy, là cách nói thông thường. Hãy đổi khi chính lời của biểu mẫu rõ ràng hơn.",
"examples.detail-list.blurb": "Danh sách định nghĩa về một bản ghi trên màn hình chi tiết. Dấu mono dành cho các giá trị thiên về máy.",
- "examples.dropdown.blurb": "Một khối bung mở không dùng chút JavaScript nào: một nút summary trên một bảng gồm các liên kết thường.",
+ "examples.dropdown.blurb": "Một khối bung mở không dùng chút JavaScript nào: một nút summary trên một bảng gồm các liên kết thường. Hãy đặt nó trong một hàng, cạnh thứ nó tác động tới, đừng bao giờ để nó đứng riêng một dòng.",
"examples.dropdown.states.menu-idiom": "Trình đơn details/summary nằm sau các trình đơn tràn ở đầu trang và bộ lọc của thanh danh sách, cùng một bộ lọc đã áp dụng dưới dạng chip gỡ được.",
"examples.empty-state.blurb": "Thẻ trạng thái trống. Trạng thái trống kèm lối vào cho các bước bắt đầu.",
"examples.error-page.blurb": "Toàn bộ phần thân của một phản hồi lỗi: mã trạng thái, một câu nói thật, một lối quay lại, và mã tham chiếu 500 mà quản trị viên dùng được.",
diff --git a/internal/gallery/i18n/yue.json b/internal/gallery/i18n/yue.json
index da08366..7dd81ac 100644
--- a/internal/gallery/i18n/yue.json
+++ b/internal/gallery/i18n/yue.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "確認版嘅動作行。破壞性動作有自己嘅路由,唔係一個對話框,所以根本冇嘢要寫腳本。",
"examples.confirm-form.states.ordinary-default-cancel-wording": "取消連結寫住「取消」,係平時嘅講法。如果表格自己嘅字眼更清楚,就改佢。",
"examples.detail-list.blurb": "詳情畫面上關於某筆記錄嘅定義清單。等寬標記用喺偏機器嘅值。",
- "examples.dropdown.blurb": "一個完全冇 JavaScript 嘅展開塊:一個 summary 掣,下面係一板普通連結。",
+ "examples.dropdown.blurb": "一個完全冇 JavaScript 嘅展開塊:一個 summary 掣,下面係一板普通連結。要放喺一行入面,貼住佢作用嘅嘢,唔好自己獨佔一行。",
"examples.dropdown.states.menu-idiom": "匿喺頁首溢出選單同清單列篩選背後嘅 details/summary 選單,加埋一個可以摘走嘅已套用篩選標籤。",
"examples.empty-state.blurb": "空狀態卡。空狀態,同埋開始嘅入口。",
"examples.error-page.blurb": "錯誤回應嘅整個主體:狀態、一句老實話、一條返轉頭嘅路,同管理員用得着嘅 500 參考編號。",
diff --git a/internal/gallery/i18n/zh-Hans.json b/internal/gallery/i18n/zh-Hans.json
index 541358e..bf46f9e 100644
--- a/internal/gallery/i18n/zh-Hans.json
+++ b/internal/gallery/i18n/zh-Hans.json
@@ -16,7 +16,7 @@
"examples.confirm-form.blurb": "确认页的操作行。破坏性操作有自己的路由而不是一个对话框,所以没有什么需要写脚本。",
"examples.confirm-form.states.ordinary-default-cancel-wording": "取消链接写的是「取消」,这是通常的措辞。当表单自己的说法更清楚时,就改掉它。",
"examples.detail-list.blurb": "详情页上关于某条记录的定义列表。等宽标记用于偏机器的值。",
- "examples.dropdown.blurb": "一个零 JavaScript 的折叠块:一个 summary 按钮,下面是一板普通链接。",
+ "examples.dropdown.blurb": "一个零 JavaScript 的折叠块:一个 summary 按钮,下面是一板普通链接。把它放在一行里,挨着它作用的对象,不要让它独占一行。",
"examples.dropdown.states.menu-idiom": "藏在页眉溢出菜单和列表工具栏筛选背后的 details/summary 菜单,外加一枚可移除的已应用筛选标签。",
"examples.empty-state.blurb": "空状态卡片。空状态,并给出开始的入口。",
"examples.error-page.blurb": "错误响应的整个主体:状态码、一句实话、一条回头的路,以及管理员用得上的 500 参考编号。",