Part 3's design-system addresses redirect to their pages
Part 4 moved every design-system page under /design-system/<theme>/<locale>/, so part 3's published addresses (/design-system/day/button.html, its right-to-left twin /design-system/day/rtl/button.html, the theme indexes and every frame) would have answered 404 once it ships. Each now answers a 301 to its page: a theme's page to the same page under en/, a twin to the same page under ar/, an index to its directory. The gallery owns the rules (gallery.Redirects, from the pages it renders); www.Redirects hands them over and site.Build writes them into _redirects after the release rewrites, refusing a rule that is not one line or that takes the file past CARLOS's limits. 99 rules for the design system, 100 with the release rewrite, 8,352 bytes: literal theme directories, :theme placeholders for the index, frames and right-to-left pages, and one rule per English page, because <theme>/:page would also match <theme>/en. The www test server now answers as CARLOS's edge does: its _redirects grammar and matching are mirrored from internal/edge/redirects.go (301 with Location, :name, :splat, shadowing, forced rules, first match, the slash form of a directory), and a line `carlos ship` would refuse is refused. Tests: - TestServeSiteRedirectsLikeTheEdge, TestServeSiteRefusesARuleCarlosWouldRefuse (new): red first by assertion against the old 200-only parse. - TestThePagesOwnRulesFollowTheReleaseRewrites, TestARuleThatIsNotOneLineIsRefused (new, internal/site): red first by assertion, the rules dropped and every bad rule accepted. - TestEveryPart3AddressStillReachesAPage (new): every one of 1,429 part 3 addresses (testdata/part3-addresses.txt, generated from 6af218e, regeneration in its header) plus each index's directory form: 1,434 answer 301 to their page, which answers 200, one hop, no fragment in the Location; the two that are still pages answer 200. A sample of six pages keeps every id part 3 had (testdata/part3-ids.txt). Red first by assertion: every address 404. - TestNoRedirectShadowsAPage, TestNoRedirectLeadsToAnotherRule (new): no rule matches any file or directory the site serves, none is forced, and no destination can match a source. With /design-system/:theme/* -> /design-system/:theme/en/:splat added, the first named 9,329 served paths and the second every redirect as a second hop. - TestTheRedirectsFileIsWithinCarlosLimits (new): parses under the mirrored grammar, within 2,000 rules, 256 KiB and 2,048 bytes a line. carlos ship has no dry run for a static tree; CARLOS's own parseRedirects, run on the built file in a scratch module, also accepted all 100 rules. - TestSiteWritesWhatBuildReturns now expects the rules in _redirects; red first by assertion before fiesta site passed them. The www browser drives (16) pass through the new server. The four new www tests are weighed in hack/test-weights.txt; the two www slices deal to 39s each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 files changed,
+2482
−78
Makefile+4 −3cmd/fiesta/main.go+10 −9cmd/fiesta/release_test.go+6 −4hack/test-weights.txt+4 −0internal/gallery/redirects.go+93 −0internal/site/site.go+25 −6internal/site/site_test.go+54 −0internal/www/legacy_test.go+270 −0internal/www/redirects_test.go+394 −0internal/www/serve_test.go+151 −56internal/www/testdata/part3-addresses.txt+1439 −0internal/www/testdata/part3-ids.txt+16 −0internal/www/www.go+16 −0
CI passed — run details
CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
go: downloading go1.26.10 (linux/arm64)
ok amadan.net/rastrillo/fiesta 1.240s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN TestTheOneLinkIsUnder25KiBGzipped
build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.03s)
PASS
ok amadan.net/rastrillo/fiesta/internal/build 0.033s
=== history ===
history: 32 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 5.481s
ok amadan.net/rastrillo/fiesta/check 9.292s
ok amadan.net/rastrillo/fiesta/example 0.737s
? amadan.net/rastrillo/fiesta/hack/seed/english [no test files]
ok amadan.net/rastrillo/fiesta/internal/axe 0.368s
ok amadan.net/rastrillo/fiesta/internal/build 2.756s
ok amadan.net/rastrillo/fiesta/internal/codeview 0.004s
? amadan.net/rastrillo/fiesta/internal/harness [no test files]
ok amadan.net/rastrillo/fiesta/internal/markup 0.011s
ok amadan.net/rastrillo/fiesta/internal/nodetest 0.633s
ok amadan.net/rastrillo/fiesta/internal/site 5.017s
ok amadan.net/rastrillo/fiesta/internal/tokens 0.008s
suite: root took 73s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.830000s 0m4.590000s
--- 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 104 test tests
ok amadan.net/rastrillo/fiesta/internal/gallery 141.078s
suite: test.internal.gallery~1of3 took 174s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m53.750000s 0m2.070000s
--- 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 34 of 104 test tests
ok amadan.net/rastrillo/fiesta/internal/gallery 134.008s
suite: test.internal.gallery~2of3 took 169s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.320000s 0m2.090000s
--- 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 35 of 104 test tests
ok amadan.net/rastrillo/fiesta/internal/gallery 126.050s
suite: test.internal.gallery~3of3 took 162s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.880000s 0m1.740000s
--- 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 116.805s
suite: test.cmd.fiesta~1of2 took 154s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.670000s 0m2.850000s
--- 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 112.856s
suite: test.cmd.fiesta~2of2 took 147s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.390000s 0m3.100000s
--- 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 122.876s
suite: test.internal.www~1of2 took 177s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m2.250000s 0m3.760000s
--- 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 128.793s
suite: test.internal.www~2of2 took 179s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m58.920000s 0m3.530000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
go: downloading honnef.co/go/tools v0.8.2
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading golang.org/x/tools v0.51.0
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
suite: staticcheck took 85s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.830000s 0m5.870000s
--- shard 9 of 48 ---
==> shard 9 of 48: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
go: downloading golang.org/x/vuln v1.8.0
go: downloading golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518
go: downloading golang.org/x/sync v0.23.0
go: downloading golang.org/x/tools v0.50.0
go: downloading golang.org/x/mod v0.41.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 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.390000s 0m2.620000s
--- 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' .
go: downloading github.com/zricethezav/gitleaks/v8 v8.30.1
go: downloading github.com/fatih/semgroup v1.2.0
go: downloading github.com/gitleaks/go-gitdiff v0.9.1
go: downloading github.com/Masterminds/sprig/v3 v3.3.0
go: downloading github.com/mholt/archives v0.1.2
go: downloading github.com/BobuSumisu/aho-corasick v1.0.3
go: downloading golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
go: downloading github.com/hashicorp/go-version v1.7.0
go: downloading github.com/spf13/viper v1.19.0
go: downloading github.com/rs/zerolog v1.33.0
go: downloading github.com/charmbracelet/lipgloss v0.5.0
go: downloading github.com/spf13/cobra v1.9.1
go: downloading github.com/h2non/filetype v1.1.3
go: downloading golang.org/x/sync v0.11.0
go: downloading golang.org/x/text v0.22.0
go: downloading github.com/klauspost/pgzip v1.2.6
go: downloading github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3
go: downloading github.com/minio/minlz v1.0.0
go: downloading github.com/nwaples/rardecode/v2 v2.1.0
go: downloading github.com/bodgit/sevenzip v1.6.0
go: downloading github.com/klauspost/compress v1.17.11
go: downloading github.com/sorairolake/lzip-go v0.3.5
go: downloading github.com/therootcompany/xz v1.0.1
go: downloading github.com/ulikunitz/xz v0.5.12
go: downloading github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
go: downloading github.com/STARRY-S/zip v0.2.1
go: downloading github.com/pierrec/lz4/v4 v4.1.21
go: downloading github.com/spf13/afero v1.12.0
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/fsnotify/fsnotify v1.8.0
go: downloading github.com/spf13/cast v1.7.1
go: downloading github.com/sagikazarmark/slog-shim v0.1.0
go: downloading github.com/spf13/pflag v1.0.6
go: downloading github.com/Masterminds/goutils v1.1.1
go: downloading dario.cat/mergo v1.0.1
go: downloading github.com/Masterminds/semver/v3 v3.3.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/huandu/xstrings v1.5.0
go: downloading github.com/mitchellh/copystructure v1.2.0
go: downloading github.com/shopspring/decimal v1.4.0
go: downloading golang.org/x/crypto v0.35.0
go: downloading github.com/subosito/gotenv v1.6.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading gopkg.in/ini.v1 v1.67.0
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/pelletier/go-toml/v2 v2.2.3
go: downloading github.com/magiconair/properties v1.8.9
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
go: downloading github.com/mattn/go-runewidth v0.0.14
go: downloading github.com/muesli/termenv v0.15.1
go: downloading github.com/mattn/go-colorable v0.1.14
go: downloading github.com/bodgit/plumbing v1.3.0
go: downloading github.com/bodgit/windows v1.0.1
go: downloading github.com/hashicorp/go-multierror v1.1.1
go: downloading go4.org v0.0.0-20230225012048-214862532bf5
go: downloading github.com/mitchellh/reflectwalk v1.0.2
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7
go: downloading golang.org/x/sys v0.30.0
go: downloading github.com/hashicorp/errwrap v1.1.0
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
[90m9:03PM[0m [32mINF[0m [1mUnknown SCM platform. Use --platform to include links in findings.[0m [36mhost=[0mamadan.net
[90m9:03PM[0m [32mINF[0m [1m30 commits scanned.[0m
[90m9:03PM[0m [32mINF[0m [1mscanned ~7881895 bytes (7.88 MB) in 5.88s[0m
[90m9:03PM[0m [32mINF[0m [1mno leaks found[0m
suite: gitleaks took 73s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.340000s 0m4.020000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-21-140, 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 18.956s
suite: browser.internal.harness took 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.630000s 0m3.290000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 5 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 135.529s
suite: browser.check~1of13 took 204s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.570000s 0m10.020000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 9 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 119.498s
suite: browser.check~2of13 took 186s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.880000s 0m9.710000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 10 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 133.680s
suite: browser.check~3of13 took 201s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.960000s 0m10.550000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 11 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 139.530s
suite: browser.check~4of13 took 206s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m56.320000s 0m13.340000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 16 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 84.778s
suite: browser.check~5of13 took 153s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.290000s 0m9.030000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 16 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 110.720s
suite: browser.check~6of13 took 178s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.790000s 0m12.030000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 16 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 108.293s
suite: browser.check~7of13 took 175s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.520000s 0m8.890000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 17 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 99.118s
suite: browser.check~8of13 took 168s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.240000s 0m10.990000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 18 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 103.629s
suite: browser.check~9of13 took 171s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.150000s 0m13.040000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 17 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 124.352s
suite: browser.check~10of13 took 190s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.640000s 0m13.970000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 18 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 93.773s
suite: browser.check~11of13 took 160s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.500000s 0m9.020000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 18 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 107.960s
suite: browser.check~12of13 took 174s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.460000s 0m12.000000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 18 of 189 browser tests
ok amadan.net/rastrillo/fiesta/check 92.949s
suite: browser.check~13of13 took 161s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m29.430000s 0m9.840000s
--- 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-21-140, 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 67.533s
suite: browser.internal.gallery~1of18 took 132s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.580000s 0m5.470000s
--- 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-21-140, 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 82.171s
suite: browser.internal.gallery~2of18 took 148s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.490000s 0m6.310000s
--- 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-21-140, 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 101.801s
suite: browser.internal.gallery~3of18 took 168s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.460000s 0m6.370000s
--- 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-21-140, 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 107.221s
suite: browser.internal.gallery~4of18 took 173s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.120000s 0m7.120000s
--- 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-21-140, 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 113.538s
suite: browser.internal.gallery~5of18 took 177s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m29.950000s 0m6.710000s
--- 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-21-140, 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 86.557s
suite: browser.internal.gallery~6of18 took 153s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m26.730000s 0m5.140000s
--- 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-21-140, 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 80.994s
suite: browser.internal.gallery~7of18 took 147s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m27.870000s 0m4.510000s
--- 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-21-140, 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 103.437s
suite: browser.internal.gallery~8of18 took 170s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.240000s 0m8.050000s
--- 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-21-140, 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 121.797s
suite: browser.internal.gallery~9of18 took 186s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.010000s 0m9.350000s
--- 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-21-140, 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 85.267s
suite: browser.internal.gallery~10of18 took 150s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m29.630000s 0m4.990000s
--- 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-21-140, 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 89.806s
suite: browser.internal.gallery~11of18 took 155s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.670000s 0m5.250000s
--- 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-21-140, 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 123.057s
suite: browser.internal.gallery~12of18 took 189s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.720000s 0m7.050000s
--- 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-21-140, 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 122.873s
suite: browser.internal.gallery~13of18 took 188s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.640000s 0m7.580000s
--- 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-21-140, 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 126.921s
suite: browser.internal.gallery~14of18 took 192s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.750000s 0m8.140000s
--- 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-21-140, 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 126.978s
suite: browser.internal.gallery~15of18 took 191s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.610000s 0m8.470000s
--- 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-21-140, 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 117.067s
suite: browser.internal.gallery~16of18 took 184s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m27.550000s 0m5.560000s
--- 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-21-140, 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 123.654s
suite: browser.internal.gallery~17of18 took 189s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.680000s 0m8.280000s
--- 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-21-140, 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 122.446s
suite: browser.internal.gallery~18of18 took 189s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.470000s 0m7.520000s
--- 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-21-140, 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 70.559s
suite: browser.internal.gallery.history took 128s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.930000s 0m3.760000s
--- 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-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok amadan.net/rastrillo/fiesta/internal/www 106.083s
suite: browser.internal.www~1of4 took 168s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.760000s 0m3.850000s
--- 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-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok amadan.net/rastrillo/fiesta/internal/www 110.738s
suite: browser.internal.www~2of4 took 174s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.430000s 0m4.180000s
--- 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-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok amadan.net/rastrillo/fiesta/internal/www 101.407s
suite: browser.internal.www~3of4 took 166s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.240000s 0m5.200000s
--- 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-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok amadan.net/rastrillo/fiesta/internal/www 101.102s
suite: browser.internal.www~4of4 took 162s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.000000s 0m4.280000s
Intent
diff --git a/Makefile b/Makefile| index 53690ab..f9f6c00 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -256,9 +256,10 @@ heights-sweep: | $(BIN)/tmp |
| # site assembles fiesta.rastrillo.org into .build/site/public from the |
| # committed releases and the site source: each release under its exact |
| -# version, _redirects sending /M/* to the newest M.y.z, and at the root |
| -# the pages internal/www assembles from site/, the landing page, the |
| -# design system under /design-system/ and site/static/. The tree is |
| +# version, _redirects sending /M/* to the newest M.y.z and the design |
| +# system's former addresses to their pages, and at the root the pages |
| +# internal/www assembles from site/, the landing page, the design |
| +# system under /design-system/ and site/static/. The tree is |
| # .build/site/public; the command's own .fiesta stamp sits above it in |
| # .build/site, so nothing hidden is in the shipped tree. Nothing in ci |
| # depends on this target, since the releases are committed and checked |
diff --git a/cmd/fiesta/main.go b/cmd/fiesta/main.go| index 1c50a1f..4777f7a 100644 |
| --- a/cmd/fiesta/main.go |
| +++ b/cmd/fiesta/main.go |
| @@ -34,11 +34,12 @@ |
| // assembles the site from the releases and the site source, exactly |
| // what internal/site.Build returns, into DIR/public/: each release |
| // under its exact version, _redirects sending /M/* to the newest |
| -// M.y.z, and the pages at the root. The pages are what internal/www |
| -// assembles from the site source: the landing page, the design system |
| -// under /design-system/, and the source's static/ files. DIR/public/ |
| -// is the tree to ship, and the stamp sits above it in DIR, so nothing |
| -// hidden is ever deployed. |
| +// M.y.z and the design system's former addresses to their pages, and |
| +// the pages at the root. The pages are what internal/www assembles |
| +// from the site source: the landing page, the design system under |
| +// /design-system/, and the source's static/ files. DIR/public/ is the |
| +// tree to ship, and the stamp sits above it in DIR, so nothing hidden |
| +// is ever deployed. |
| // |
| // Each command owns the directory it writes. It empties it before it |
| // writes, so what is there afterwards is the output and only the |
| @@ -205,15 +206,15 @@ func writeGallery(out, mount string) error { |
| const shipDir = "public" |
| // writeSite assembles the pages from the site source, then the site |
| -// from the releases and those pages, and puts it in out/public/, after |
| -// claiming out. A fault in the site source is reported on its own, |
| -// before the releases are read. |
| +// from the releases, those pages and their rules, and puts it in |
| +// out/public/, after claiming out. A fault in the site source is |
| +// reported on its own, before the releases are read. |
| func writeSite(releases, siteDir, out string) error { |
| pages, err := www.Pages(fiesta.FS(), os.DirFS(siteDir)) |
| if err != nil { |
| return err |
| } |
| - files, err := site.Build(os.DirFS(releases), www.FS(pages)) |
| + files, err := site.Build(os.DirFS(releases), www.FS(pages), www.Redirects(pages)...) |
| if err != nil { |
| return err |
| } |
diff --git a/cmd/fiesta/release_test.go b/cmd/fiesta/release_test.go| index 3e3464d..58d5542 100644 |
| --- a/cmd/fiesta/release_test.go |
| +++ b/cmd/fiesta/release_test.go |
| @@ -361,7 +361,8 @@ func TestAnUnreachableOriginRefusesACandidate(t *testing.T) { |
| // -releases R -site S -out DIR` writes site.Build's tree into |
| // DIR/public, file for file, stamps DIR itself as its own, and leaves |
| // nothing hidden in the tree to ship. The pages are what www.Pages |
| -// assembles from S, so the expected tree is computed the same way. |
| +// assembles from S and their rules www.Redirects, so the expected tree |
| +// is computed the same way. |
| func TestSiteWritesWhatBuildReturns(t *testing.T) { |
| root := t.TempDir() |
| releases := filepath.Join(root, "releases") |
| @@ -412,7 +413,7 @@ func TestSiteWritesWhatBuildReturns(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - want, err := site.Build(os.DirFS(releases), www.FS(pages)) |
| + want, err := site.Build(os.DirFS(releases), www.FS(pages), www.Redirects(pages)...) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -431,8 +432,9 @@ func TestSiteWritesWhatBuildReturns(t *testing.T) { |
| } |
| } |
| major, _, _ := strings.Cut(fiesta.Version, ".") |
| - if got := string(want["_redirects"]); got != "/"+major+"/* /"+fiesta.Version+"/:splat 200\n" { |
| - t.Errorf("_redirects is %q", got) |
| + rules := "/" + major + "/* /" + fiesta.Version + "/:splat 200\n" + strings.Join(www.Redirects(pages), "\n") + "\n" |
| + if got := string(want["_redirects"]); got != rules { |
| + t.Errorf("_redirects is %q, want the release rewrite and then the design system's rules", got) |
| } |
| if b, err := os.ReadFile(filepath.Join(out, stampName)); err != nil || string(b) != stampBody("fiesta site") { |
| t.Errorf("the stamp is not fiesta site's, or is not in -out itself: %q, %v", b, err) |
diff --git a/hack/test-weights.txt b/hack/test-weights.txt| index af558a0..de71d83 100644 |
| --- a/hack/test-weights.txt |
| +++ b/hack/test-weights.txt |
| @@ -197,7 +197,11 @@ test internal/www TestEveryLinkInTheSiteResolves 10 |
| test internal/www TestSiteBuildAcceptsThePages 6 |
| test internal/www TestEveryLanguageHasAPage 5 |
| test internal/www TestEveryPageLinksEveryLanguage 5 |
| +test internal/www TestEveryPart3AddressStillReachesAPage 5 |
| +test internal/www TestNoRedirectLeadsToAnotherRule 5 |
| +test internal/www TestNoRedirectShadowsAPage 5 |
| test internal/www TestPagesHasARootAndTheDesignSystem 5 |
| test internal/www TestStaticFilesAreCopied 5 |
| test internal/www TestTheDesignSystemIsMounted 5 |
| +test internal/www TestTheRedirectsFileIsWithinCarlosLimits 5 |
| test internal/www TestAHiddenSiteFileIsRefused 4 |
diff --git a/internal/gallery/redirects.go b/internal/gallery/redirects.go| new file mode 100644 |
| index 0000000..7a516e7 |
| --- /dev/null |
| +++ b/internal/gallery/redirects.go |
| @@ -0,0 +1,93 @@ |
| +package gallery |
| + |
| +import ( |
| + "sort" |
| + "strings" |
| + |
| + "amadan.net/rastrillo/fiesta" |
| +) |
| + |
| +// formerRTL is the directory a theme's right-to-left twin had before |
| +// every page moved under a locale: <theme>/rtl/<page>, beside the |
| +// English <theme>/<page>. Those addresses were published, so they are |
| +// redirected rather than left to 404. |
| +const formerRTL = "rtl" |
| + |
| +// Redirects are the _redirects rules, for a tree rendered for mount, |
| +// that send each address the gallery had before its pages moved under |
| +// a locale to the page that is there now, with a 301: |
| +// |
| +// <theme>/ <theme>/en/ |
| +// <theme>/index.html <theme>/en/ |
| +// <theme>/<page>.html <theme>/en/<page>.html |
| +// <theme>/frames/<example>/<state> <theme>/en/frames/<example>/<state> |
| +// <theme>/rtl/... the same under <theme>/ar/ |
| +// |
| +// An index goes to its directory, the address the edge serves it at, |
| +// since the edge sends .../index.html on to ./ and that would be a |
| +// second hop. The rules are written so that no source can match a |
| +// file of the tree, and no destination can match a source, so none |
| +// shadows a page and none leads to another: every file of today's tree |
| +// sits under <theme>/<locale>/ or assets/, and no locale is "frames", |
| +// "rtl", "index.html" or a page's name. So <theme>/rtl/:page can be one |
| +// rule, but an English page is named rule by rule: <theme>/:page would |
| +// also match <theme>/en, the English tree's own directory. The pages |
| +// named are the ones tree holds at <theme>/en/ in every theme, so each |
| +// destination is a page whatever the theme. The order is fixed, so the |
| +// same tree gives the same rules. |
| +func Redirects(mount string, tree map[string][]byte) []string { |
| + m := cleanMount(mount) |
| + en, ar := Locales[0], rtlLocale() |
| + var out []string |
| + rule := func(from, to string) { out = append(out, m+from+" "+m+to+" 301") } |
| + |
| + themes := fiesta.ThemeNames() |
| + for _, theme := range themes { |
| + rule("/"+theme, "/"+theme+"/"+en+"/") |
| + } |
| + rule("/:theme/index.html", "/:theme/"+en+"/") |
| + rule("/:theme/frames/:example/:state", "/:theme/"+en+"/frames/:example/:state") |
| + for _, page := range everyThemesPages(tree, themes, en) { |
| + rule("/:theme/"+page, "/:theme/"+en+"/"+page) |
| + } |
| + |
| + twin := "/:theme/" + formerRTL |
| + rule(twin, "/:theme/"+ar+"/") |
| + rule(twin+"/index.html", "/:theme/"+ar+"/") |
| + rule(twin+"/frames/:example/:state", "/:theme/"+ar+"/frames/:example/:state") |
| + rule(twin+"/:page", "/:theme/"+ar+"/:page") |
| + return out |
| +} |
| + |
| +// rtlLocale is the locale that renders right to left, where the former |
| +// twins now live. |
| +func rtlLocale() string { |
| + for _, l := range Locales { |
| + if Dir(l) == "rtl" { |
| + return l |
| + } |
| + } |
| + panic("gallery: no locale renders right to left") |
| +} |
| + |
| +// everyThemesPages are the names of the HTML pages directly under |
| +// <theme>/<locale>/ for every theme, the index aside, sorted. |
| +func everyThemesPages(tree map[string][]byte, themes []string, locale string) []string { |
| + count := map[string]int{} |
| + for name := range tree { |
| + for _, theme := range themes { |
| + page, ok := strings.CutPrefix(name, theme+"/"+locale+"/") |
| + if ok && !strings.Contains(page, "/") && strings.HasSuffix(page, ".html") && page != "index.html" { |
| + count[page]++ |
| + } |
| + } |
| + } |
| + var pages []string |
| + for page, n := range count { |
| + if n == len(themes) { |
| + pages = append(pages, page) |
| + } |
| + } |
| + sort.Strings(pages) |
| + return pages |
| +} |
diff --git a/internal/site/site.go b/internal/site/site.go| index 29889e3..97d2689 100644 |
| --- a/internal/site/site.go |
| +++ b/internal/site/site.go |
| @@ -86,13 +86,15 @@ func parseVersion(name string) (version, bool) { |
| } |
| // The edge's limits on one _redirects file, from CARLOS's |
| -// internal/edge/redirects.go: 2,000 rules and 256 KiB (and 2,048 bytes |
| -// a line, which one rule per major cannot reach once every component |
| -// fits an int). A site past either assembles here and is refused by the |
| -// packer, so it is refused here first, naming the figure. |
| +// internal/edge/redirects.go: 2,000 rules, 256 KiB and 2,048 bytes a |
| +// line (which one rule per major cannot reach once every component |
| +// fits an int, but a page's rule could). A site past any of them |
| +// assembles here and is refused by the packer, so it is refused here |
| +// first, naming the figure. |
| const ( |
| redirectsMaxRules = 2000 |
| redirectsMaxBytes = 256 << 10 |
| + redirectsMaxLine = 2048 |
| ) |
| // less orders versions numerically, so 1.10.0 is newer than 1.9.3. |
| @@ -114,6 +116,8 @@ func (v version) less(w version) bool { |
| // against the release's SHA256SUMS first (Release); |
| // - _redirects, one rule per major, newest major first, sending |
| // /M/* to that major's newest release: `/M/* /M.y.z/:splat 200`; |
| +// then own, the pages' rules, one a line as given, so a page's |
| +// rule never decides a /M/ URL first; |
| // - every page, at the path it has under pages. |
| // |
| // Anything else is an error, and the first error is the whole answer: |
| @@ -124,8 +128,9 @@ func (v version) less(w version) bool { |
| // under an all-digit first segment would answer for /M/*, released or |
| // not, and a page under a version-shaped segment would answer for that |
| // release. Both are refused, and so is a page named _redirects, which |
| -// is the rules file. |
| -func Build(releases fs.FS, pages fs.FS) (map[string][]byte, error) { |
| +// is the rules file. So is a rule that is not one line of it, or |
| +// rules that take it past the edge's limits. |
| +func Build(releases fs.FS, pages fs.FS, own ...string) (map[string][]byte, error) { |
| out := map[string][]byte{} |
| entries, err := fs.ReadDir(releases, ".") |
| if err != nil { |
| @@ -166,10 +171,24 @@ func Build(releases fs.FS, pages fs.FS) (map[string][]byte, error) { |
| if len(majors) > redirectsMaxRules { |
| return nil, fmt.Errorf("%s would hold %d rules, one per major, and the edge accepts %d", redirectsName, len(majors), redirectsMaxRules) |
| } |
| + if n := len(majors) + len(own); n > redirectsMaxRules { |
| + return nil, fmt.Errorf("%s would hold %d rules, %d for the releases and %d for the pages, and the edge accepts %d", redirectsName, n, len(majors), len(own), redirectsMaxRules) |
| + } |
| var rules bytes.Buffer |
| for _, m := range majors { |
| fmt.Fprintf(&rules, "/%d/* /%s/:splat 200\n", m, newest[m].name) |
| } |
| + for _, r := range own { |
| + switch { |
| + case strings.ContainsAny(r, "\n\r"): |
| + return nil, fmt.Errorf("the %s rule %q is not one line", redirectsName, r) |
| + case strings.TrimSpace(r) == "": |
| + return nil, fmt.Errorf("a %s rule is empty", redirectsName) |
| + case len(r) > redirectsMaxLine: |
| + return nil, fmt.Errorf("the %s rule %.40q... is %d bytes, and the edge accepts %d a line", redirectsName, r, len(r), redirectsMaxLine) |
| + } |
| + rules.WriteString(r + "\n") |
| + } |
| if rules.Len() > redirectsMaxBytes { |
| return nil, fmt.Errorf("%s would be %d bytes, and the edge accepts %d", redirectsName, rules.Len(), redirectsMaxBytes) |
| } |
diff --git a/internal/site/site_test.go b/internal/site/site_test.go| index 07bf9f0..f275207 100644 |
| --- a/internal/site/site_test.go |
| +++ b/internal/site/site_test.go |
| @@ -468,3 +468,57 @@ func TestTheRedirectsStayUnderTheEdgesLimits(t *testing.T) { |
| t.Errorf("the refusal does not give both figures: %v", err) |
| } |
| } |
| + |
| +// TestThePagesOwnRulesFollowTheReleaseRewrites: the rules Build is |
| +// handed go into _redirects as they are, one a line, in the order |
| +// given, after every release rewrite, so /M/ is decided first. |
| +func TestThePagesOwnRulesFollowTheReleaseRewrites(t *testing.T) { |
| + out, err := site.Build(releasesOf("1.0.0", "2.0.0"), pagesOf(nil), |
| + "/old/:page /new/:page 301", |
| + "/gone.html /here.html 301") |
| + if err != nil { |
| + t.Fatalf("Build: %v", err) |
| + } |
| + const want = "/2/* /2.0.0/:splat 200\n" + |
| + "/1/* /1.0.0/:splat 200\n" + |
| + "/old/:page /new/:page 301\n" + |
| + "/gone.html /here.html 301\n" |
| + if got := string(out["_redirects"]); got != want { |
| + t.Errorf("_redirects is %q, want %q", got, want) |
| + } |
| +} |
| + |
| +// TestARuleThatIsNotOneLineIsRefused: a rule is one line of |
| +// _redirects, so a rule with a line break in it, an empty one, and one |
| +// longer than the edge's 2,048 bytes a line are refused by name, and so |
| +// are rules that take the file past 2,000 rules or 256 KiB with the |
| +// release rewrites, rather than assembling a site the packer rejects. |
| +func TestARuleThatIsNotOneLineIsRefused(t *testing.T) { |
| + many := make([]string, 2000) |
| + for i := range many { |
| + many[i] = "/a" + strconv.Itoa(i) + " /b 301" |
| + } |
| + wide := make([]string, 130) |
| + for i := range wide { |
| + wide[i] = "/" + strconv.Itoa(i) + strings.Repeat("a", 2030) + " /b 301" |
| + } |
| + for _, c := range []struct { |
| + name string |
| + rules []string |
| + want string |
| + }{ |
| + {"a line break", []string{"/a /b 301\n/c /d 301"}, "one line"}, |
| + {"a carriage return", []string{"/a /b 301\r"}, "one line"}, |
| + {"an empty rule", []string{""}, "empty"}, |
| + {"a rule over 2048 bytes", []string{"/" + strings.Repeat("a", 2048) + " /b 301"}, "2048"}, |
| + {"2001 rules with the release's", many, "2000"}, |
| + {"over 256 KiB", wide, "bytes"}, |
| + } { |
| + _, err := site.Build(releasesOf("1.0.0"), pagesOf(nil), c.rules...) |
| + if err == nil { |
| + t.Errorf("%s: Build accepted it", c.name) |
| + } else if !strings.Contains(err.Error(), c.want) { |
| + t.Errorf("%s: refused, but the error does not say %q: %v", c.name, c.want, err) |
| + } |
| + } |
| +} |
diff --git a/internal/www/legacy_test.go b/internal/www/legacy_test.go| new file mode 100644 |
| index 0000000..e54c2f1 |
| --- /dev/null |
| +++ b/internal/www/legacy_test.go |
| @@ -0,0 +1,270 @@ |
| +package www_test |
| + |
| +import ( |
| + "bufio" |
| + "net/http" |
| + "net/http/httptest" |
| + "os" |
| + "path" |
| + "strings" |
| + "testing" |
| + |
| + xhtml "golang.org/x/net/html" |
| +) |
| + |
| +// lines reads a testdata file: one entry a line, # comments and blank |
| +// lines skipped. |
| +func lines(t *testing.T, name string) []string { |
| + t.Helper() |
| + f, err := os.Open("testdata/" + name) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer f.Close() |
| + var out []string |
| + sc := bufio.NewScanner(f) |
| + for sc.Scan() { |
| + if l := strings.TrimSpace(sc.Text()); l != "" && !strings.HasPrefix(l, "#") { |
| + out = append(out, l) |
| + } |
| + } |
| + if err := sc.Err(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(out) == 0 { |
| + t.Fatalf("testdata/%s lists nothing", name) |
| + } |
| + return out |
| +} |
| + |
| +// part3Home is where a part 3 page lives now, as a URL: a theme's page |
| +// under its English tree, a right-to-left twin under its Arabic one, |
| +// same name, and an index as its directory, which is the address the |
| +// edge serves an index at (it sends .../index.html to ./, so a |
| +// Location naming the file would be a second hop). |
| +func part3Home(old string) string { |
| + rest, ok := strings.CutPrefix(old, "design-system/") |
| + if !ok { |
| + return "" |
| + } |
| + theme, rest, ok := strings.Cut(rest, "/") |
| + if !ok { |
| + return "/" + old // the design system's own index, which stayed |
| + } |
| + locale := "en" |
| + if r, ok := strings.CutPrefix(rest, "rtl/"); ok { |
| + locale, rest = "ar", r |
| + } |
| + to := "/design-system/" + theme + "/" + locale + "/" + rest |
| + if d, ok := strings.CutSuffix(to, "/index.html"); ok { |
| + to = d + "/" |
| + } |
| + return to |
| +} |
| + |
| +// serve answers one GET through the test server's handler. |
| +func serve(h http.Handler, target string) *httptest.ResponseRecorder { |
| + rec := httptest.NewRecorder() |
| + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil)) |
| + return rec |
| +} |
| + |
| +// TestEveryPart3AddressStillReachesAPage: every page part 3 published |
| +// under /design-system/ (testdata/part3-addresses.txt), and an index's |
| +// directory address too, answers either with the page itself, where |
| +// the path is still a page, or with a 301 whose Location is that |
| +// page's part 4 address and answers 200 with HTML: one hop, never two. |
| +// The Location carries no fragment, so the browser keeps the one the |
| +// old link had; and on a sample of pages |
| +// (testdata/part3-ids.txt) the page it lands on has every id part 3's |
| +// page had, so the fragment still finds its place. |
| +func TestEveryPart3AddressStillReachesAPage(t *testing.T) { |
| + tree := builtSite(t) |
| + h := newSiteHandler(t, tree) |
| + redirected, stayed := 0, 0 |
| + for _, old := range lines(t, "part3-addresses.txt") { |
| + want := part3Home(old) |
| + if want == "" { |
| + t.Errorf("%s is not under design-system/; the inventory is part 3's design system", old) |
| + continue |
| + } |
| + asked := []string{"/" + old} |
| + if d, ok := strings.CutSuffix(old, "/index.html"); ok { |
| + asked = append(asked, "/"+d+"/") |
| + } |
| + for _, a := range asked { |
| + res := serve(h, a) |
| + if _, ok := tree[old]; ok { |
| + if res.Code != http.StatusOK { |
| + t.Errorf("%s is still a page and answers %d", a, res.Code) |
| + } |
| + stayed++ |
| + continue |
| + } |
| + loc := res.Header().Get("Location") |
| + if res.Code != http.StatusMovedPermanently || loc != want { |
| + t.Errorf("%s answers %d, Location %q; want 301 to %s", a, res.Code, loc, want) |
| + continue |
| + } |
| + if strings.Contains(loc, "#") { |
| + t.Errorf("%s: the Location %s has a fragment, which replaces the one the link had", a, loc) |
| + } |
| + there := serve(h, loc) |
| + if there.Code != http.StatusOK || !strings.HasPrefix(there.Header().Get("Content-Type"), "text/html") { |
| + t.Errorf("%s redirects to %s, which answers %d %s; one hop must reach the page", a, loc, there.Code, there.Header().Get("Content-Type")) |
| + } |
| + redirected++ |
| + } |
| + } |
| + t.Logf("%d part 3 addresses redirect to their pages in one hop, %d are still pages", redirected, stayed) |
| + |
| + for _, l := range lines(t, "part3-ids.txt") { |
| + f := strings.Fields(l) |
| + old, ids := f[0], f[1:] |
| + loc := serve(h, "/"+old).Header().Get("Location") |
| + if loc == "" { |
| + t.Errorf("%s does not redirect, so its ids cannot be compared", old) |
| + continue |
| + } |
| + res := serve(h, loc) |
| + if res.Code != http.StatusOK { |
| + t.Errorf("%s: %s answers %d", old, loc, res.Code) |
| + continue |
| + } |
| + have := map[string]bool{} |
| + walk(parse(t, res.Body.Bytes()), func(n *xhtml.Node) { |
| + if id := attr(n, "id"); id != "" { |
| + have[id] = true |
| + } |
| + }) |
| + for _, id := range ids { |
| + if !have[id] { |
| + t.Errorf("%s#%s lands on %s#%s, and that page has no element with that id", old, id, loc, id) |
| + } |
| + } |
| + } |
| +} |
| + |
| +// TestNoRedirectShadowsAPage: no rule in the site's _redirects matches |
| +// the path of anything the site serves, a file or an index's directory, |
| +// with or without its slash; and none is forced. The edge serves a file |
| +// before any rule not forced, so a rule that matched a page would be |
| +// dead today, but it is proved here not to match at all, so no page's |
| +// address depends on that order. Every page then answers 200 through |
| +// the test server. |
| +func TestNoRedirectShadowsAPage(t *testing.T) { |
| + tree := builtSite(t) |
| + rules, err := parseRedirects(tree["_redirects"]) |
| + if err != nil { |
| + t.Fatalf("_redirects: %v", err) |
| + } |
| + if len(rules) < 2 { |
| + t.Fatalf("_redirects holds %d rules; the release rewrite and the design system's moved pages are at least two", len(rules)) |
| + } |
| + for _, r := range rules { |
| + if r.force { |
| + t.Errorf("_redirects line %d is forced, so it would answer for a file", r.line) |
| + } |
| + } |
| + h := newSiteHandler(t, tree) |
| + for name := range tree { |
| + if name == "_redirects" { |
| + continue |
| + } |
| + paths := []string{"/" + name} |
| + if d, ok := strings.CutSuffix("/"+name, "/index.html"); ok { |
| + paths = append(paths, d+"/") |
| + } |
| + for _, p := range paths { |
| + for _, r := range rules { |
| + if _, ok := r.match("", segments(path.Clean(p))); ok { |
| + t.Errorf("_redirects line %d matches %s, which the site serves", r.line, p) |
| + } |
| + } |
| + } |
| + if res := serve(h, "/"+name); res.Code != http.StatusOK { |
| + t.Errorf("/%s answers %d", name, res.Code) |
| + } |
| + } |
| +} |
| + |
| +// TestNoRedirectLeadsToAnotherRule: no redirect's destination, whatever |
| +// its placeholders are given, can match the source of any rule, so no |
| +// address, part 3's or any other, is redirected twice or in a loop. |
| +// A placeholder in a destination stands for any one segment and :splat |
| +// for any number, so this holds for every request, not only the |
| +// addresses the inventory lists. |
| +func TestNoRedirectLeadsToAnotherRule(t *testing.T) { |
| + rules, err := parseRedirects(builtSite(t)["_redirects"]) |
| + if err != nil { |
| + t.Fatalf("_redirects: %v", err) |
| + } |
| + for _, from := range rules { |
| + if !isRedirect(from.status) || from.prefix != "" { |
| + continue |
| + } |
| + dest := segments(path.Clean("/" + from.to)) |
| + for _, to := range rules { |
| + if overlaps(dest, to) { |
| + t.Errorf("_redirects line %d redirects to %s, which line %d can match: a second hop", from.line, from.to, to.line) |
| + } |
| + } |
| + } |
| +} |
| + |
| +// overlaps reports whether some path fits both dest, a destination's |
| +// segments in which a segment holding a placeholder stands for any |
| +// segment and a final :splat for any number of them, and the source |
| +// of r. |
| +func overlaps(dest []string, r rule) bool { |
| + wild := func(seg string) bool { return strings.Contains(seg, ":") } |
| + tail := len(dest) > 0 && dest[len(dest)-1] == ":splat" |
| + fixed := dest |
| + if tail { |
| + fixed = dest[:len(dest)-1] |
| + } |
| + // Where both have a segment, both must be able to hold the same one. |
| + n := min(len(fixed), len(r.segs)) |
| + for i := 0; i < n; i++ { |
| + if !wild(fixed[i]) && !strings.HasPrefix(r.segs[i], ":") && fixed[i] != r.segs[i] { |
| + return false |
| + } |
| + } |
| + switch { |
| + case len(fixed) == len(r.segs): |
| + return true |
| + case len(fixed) > len(r.segs): |
| + return r.splat |
| + default: |
| + // The source wants more segments than dest fixes: only a |
| + // splat in dest can supply them. |
| + return tail |
| + } |
| +} |
| + |
| +// TestTheRedirectsFileIsWithinCarlosLimits: the site's _redirects is |
| +// one `carlos ship` would pack: every line parses under the edge's |
| +// grammar, and it holds no more than 2,000 rules, 256 KiB and 2,048 |
| +// bytes a line. carlos ship has no dry run for a static tree, so the |
| +// packer's parse is mirrored in redirects_test.go. |
| +func TestTheRedirectsFileIsWithinCarlosLimits(t *testing.T) { |
| + data := builtSite(t)["_redirects"] |
| + rules, err := parseRedirects(data) |
| + if err != nil { |
| + t.Errorf("carlos ship would refuse _redirects: %v", err) |
| + } |
| + if len(rules) > redirectsMaxRules { |
| + t.Errorf("_redirects holds %d rules; the edge accepts %d", len(rules), redirectsMaxRules) |
| + } |
| + if len(data) > redirectsMaxBytes { |
| + t.Errorf("_redirects is %d bytes; the edge accepts %d", len(data), redirectsMaxBytes) |
| + } |
| + longest := 0 |
| + for i, l := range strings.Split(string(data), "\n") { |
| + longest = max(longest, len(l)) |
| + if len(l) > redirectsMaxLine { |
| + t.Errorf("_redirects line %d is %d bytes; the edge accepts %d", i+1, len(l), redirectsMaxLine) |
| + } |
| + } |
| + t.Logf("_redirects: %d rules, %d bytes, longest line %d bytes", len(rules), len(data), longest) |
| +} |
diff --git a/internal/www/redirects_test.go b/internal/www/redirects_test.go| new file mode 100644 |
| index 0000000..d8fc6b6 |
| --- /dev/null |
| +++ b/internal/www/redirects_test.go |
| @@ -0,0 +1,394 @@ |
| +package www_test |
| + |
| +// The _redirects grammar and matching of CARLOS's static edge, mirrored |
| +// from internal/edge/redirects.go in carlos/platform (origin/main, |
| +// a8b52609) so the test server answers as the edge does and the tests |
| +// can refuse a file `carlos ship` would refuse. Only the edge's own |
| +// policy around the rules (its probe guard) is left out. When the edge |
| +// changes its grammar, this changes with it. |
| + |
| +import ( |
| + "errors" |
| + "fmt" |
| + "net" |
| + "net/url" |
| + "path" |
| + "strconv" |
| + "strings" |
| +) |
| + |
| +// The edge's limits on one _redirects file. |
| +const ( |
| + redirectsMaxBytes = 256 << 10 |
| + redirectsMaxRules = 2000 |
| + redirectsMaxLine = 2048 |
| +) |
| + |
| +// rule is one parsed line of a _redirects file. |
| +type rule struct { |
| + line int // 1-based line number |
| + host string // lowercased; "" matches any host |
| + segs []string // source path segments; ":name" captures one |
| + splat bool // the source ends in "*" |
| + prefix string // a URL destination's "scheme://host", never substituted |
| + to string // the destination template after prefix |
| + status int |
| + force bool // "!": applies even when a file exists at the request path |
| +} |
| + |
| +// isRedirect reports whether status is one a redirect rule may answer with. |
| +func isRedirect(status int) bool { |
| + switch status { |
| + case 301, 302, 303, 307, 308: |
| + return true |
| + } |
| + return false |
| +} |
| + |
| +// parseRedirects parses a _redirects file as `carlos ship` does: every |
| +// line it cannot use is an error, named by number. |
| +func parseRedirects(data []byte) ([]rule, error) { |
| + if len(data) > redirectsMaxBytes { |
| + return nil, fmt.Errorf("_redirects is %d bytes; the limit is %d", len(data), redirectsMaxBytes) |
| + } |
| + var rules []rule |
| + var errs []error |
| + text := strings.TrimPrefix(string(data), "\ufeff") |
| + for i, line := range strings.Split(text, "\n") { |
| + n := i + 1 |
| + line = strings.TrimSpace(strings.TrimSuffix(line, "\r")) |
| + if line == "" || strings.HasPrefix(line, "#") { |
| + continue |
| + } |
| + if len(line) > redirectsMaxLine { |
| + errs = append(errs, fmt.Errorf("line %d: longer than %d characters", n, redirectsMaxLine)) |
| + continue |
| + } |
| + r, err := parseRedirectLine(line) |
| + if err != nil { |
| + errs = append(errs, fmt.Errorf("line %d: %v", n, err)) |
| + continue |
| + } |
| + if len(rules) == redirectsMaxRules { |
| + errs = append(errs, fmt.Errorf("line %d: more than %d rules", n, redirectsMaxRules)) |
| + continue |
| + } |
| + r.line = n |
| + rules = append(rules, r) |
| + } |
| + return rules, errors.Join(errs...) |
| +} |
| + |
| +func parseRedirectLine(line string) (rule, error) { |
| + var fields []string |
| + for _, f := range strings.Fields(line) { |
| + if strings.HasPrefix(f, "#") { |
| + break |
| + } |
| + fields = append(fields, f) |
| + } |
| + var r rule |
| + switch { |
| + case len(fields) < 2: |
| + return r, errors.New("want a source and a destination") |
| + case len(fields) > 3: |
| + if strings.Contains(fields[3], "=") { |
| + return r, fmt.Errorf("conditions such as %q are not supported", fields[3]) |
| + } |
| + return r, fmt.Errorf("unexpected %q after the status", fields[3]) |
| + } |
| + |
| + r.status = 301 |
| + if len(fields) == 3 { |
| + st := fields[2] |
| + if s, ok := strings.CutSuffix(st, "!"); ok { |
| + r.force, st = true, s |
| + } |
| + code, err := strconv.Atoi(st) |
| + if err != nil || !(isRedirect(code) || code == 200 || code == 404) { |
| + return r, fmt.Errorf("status %q: want 301, 302, 303, 307, 308, 200 or 404, optionally followed by !", fields[2]) |
| + } |
| + r.status = code |
| + } |
| + |
| + src := fields[0] |
| + if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { |
| + u, err := url.Parse(src) |
| + if err != nil || u.Host == "" || u.User != nil || u.Port() != "" || u.RawQuery != "" || u.Fragment != "" { |
| + return r, fmt.Errorf("source %q: want a URL with only a host and a path", src) |
| + } |
| + r.host = hostName(u.Hostname()) |
| + src = u.Path |
| + if src == "" { |
| + src = "/" |
| + } |
| + } |
| + if !strings.HasPrefix(src, "/") { |
| + return r, fmt.Errorf("source %q: want a path starting with / or an http(s) URL", fields[0]) |
| + } |
| + if strings.ContainsAny(src, "?#") { |
| + return r, fmt.Errorf("source %q: query strings and fragments are not matched", fields[0]) |
| + } |
| + names := map[string]bool{} |
| + trimmed := strings.Trim(src, "/") |
| + for _, seg := range strings.Split(trimmed, "/") { |
| + switch { |
| + case seg == "": |
| + if trimmed != "" { |
| + return r, fmt.Errorf("source %q: empty path segment", fields[0]) |
| + } |
| + case r.splat: |
| + return r, fmt.Errorf("source %q: * must be the last segment", fields[0]) |
| + case seg == "*": |
| + r.splat = true |
| + case strings.Contains(seg, "*"): |
| + return r, fmt.Errorf("source %q: * must be a whole segment", fields[0]) |
| + case strings.HasPrefix(seg, ":"): |
| + name := seg[1:] |
| + if !validPlaceholder(name) || name == "splat" { |
| + return r, fmt.Errorf("source %q: bad placeholder %q", fields[0], seg) |
| + } |
| + if names[name] { |
| + return r, fmt.Errorf("source %q: placeholder %q appears twice", fields[0], seg) |
| + } |
| + names[name] = true |
| + r.segs = append(r.segs, seg) |
| + default: |
| + dec, err := url.PathUnescape(seg) |
| + if err != nil || strings.Contains(dec, "/") { |
| + return r, fmt.Errorf("source %q: bad percent-encoding in %q", fields[0], seg) |
| + } |
| + if strings.HasPrefix(dec, ":") { |
| + return r, fmt.Errorf("source %q: %q would read as a placeholder", fields[0], seg) |
| + } |
| + if dec == "." || dec == ".." { |
| + return r, fmt.Errorf("source %q: dot segments never match", fields[0]) |
| + } |
| + r.segs = append(r.segs, dec) |
| + } |
| + } |
| + |
| + dst := fields[1] |
| + switch { |
| + case strings.HasPrefix(dst, "http://") || strings.HasPrefix(dst, "https://"): |
| + if !isRedirect(r.status) { |
| + return r, fmt.Errorf("destination %q: a %d serves a file from this site, so it takes a path, not a URL", dst, r.status) |
| + } |
| + u, err := url.Parse(dst) |
| + if err != nil || u.Host == "" || u.User != nil { |
| + return r, fmt.Errorf("destination %q: not a usable URL", dst) |
| + } |
| + i := len(u.Scheme) + len("://") + len(u.Host) |
| + if !strings.EqualFold(dst[:i], u.Scheme+"://"+u.Host) { |
| + return r, fmt.Errorf("destination %q: not a usable URL", dst) |
| + } |
| + r.prefix, r.to = dst[:i], dst[i:] |
| + if r.to != "" && !strings.ContainsAny(r.to[:1], "/?#") { |
| + return r, fmt.Errorf("destination %q: not a usable URL", dst) |
| + } |
| + case strings.HasPrefix(dst, "//") || strings.HasPrefix(dst, "/\\"): |
| + return r, fmt.Errorf("destination %q: name another host with https://", dst) |
| + case strings.HasPrefix(dst, "/"): |
| + r.to = dst |
| + default: |
| + return r, fmt.Errorf("destination %q: want a path starting with / or an http(s) URL", dst) |
| + } |
| + return r, nil |
| +} |
| + |
| +// validPlaceholder: a placeholder name is letters, digits and underscores. |
| +func validPlaceholder(name string) bool { |
| + if name == "" { |
| + return false |
| + } |
| + for _, c := range name { |
| + if !(c == '_' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| +// hostName is a request's host as the edge compares it: lowercased, |
| +// with no port and no trailing dot. The edge also folds an |
| +// internationalised name to ASCII, which no host here needs. |
| +func hostName(h string) string { |
| + if host, _, err := net.SplitHostPort(h); err == nil { |
| + h = host |
| + } |
| + return strings.TrimSuffix(strings.ToLower(h), ".") |
| +} |
| + |
| +// segments splits a cleaned, rooted path into its segments; "/" has none. |
| +func segments(p string) []string { |
| + if p == "/" { |
| + return nil |
| + } |
| + return strings.Split(p[1:], "/") |
| +} |
| + |
| +// match reports whether the rule applies to a request for host whose |
| +// cleaned path splits into reqSegs, and returns what the source captured. |
| +func (r *rule) match(host string, reqSegs []string) (map[string]string, bool) { |
| + if r.host != "" && r.host != host { |
| + return nil, false |
| + } |
| + if len(reqSegs) < len(r.segs) || !r.splat && len(reqSegs) != len(r.segs) { |
| + return nil, false |
| + } |
| + caps := map[string]string{} |
| + for i, seg := range r.segs { |
| + if strings.HasPrefix(seg, ":") { |
| + caps[seg[1:]] = reqSegs[i] |
| + } else if seg != reqSegs[i] { |
| + return nil, false |
| + } |
| + } |
| + if r.splat { |
| + caps["splat"] = strings.Join(reqSegs[len(r.segs):], "/") |
| + } |
| + return caps, true |
| +} |
| + |
| +// expand substitutes the captures into the destination template, each |
| +// percent-encoded for a Location when escape is set, raw for a |
| +// rewrite's file lookup. A ":name" the source did not capture stays. |
| +func expand(t string, caps map[string]string, escape bool) string { |
| + var b strings.Builder |
| + inQuery := false |
| + for i := 0; i < len(t); { |
| + c := t[i] |
| + if c == '?' || c == '#' { |
| + inQuery = true |
| + } |
| + if c != ':' { |
| + b.WriteByte(c) |
| + i++ |
| + continue |
| + } |
| + j := i + 1 |
| + for j < len(t) && validPlaceholder(t[j:j+1]) { |
| + j++ |
| + } |
| + v, ok := caps[t[i+1:j]] |
| + if !ok { |
| + b.WriteString(t[i:j]) |
| + i = j |
| + continue |
| + } |
| + switch { |
| + case !escape: |
| + b.WriteString(v) |
| + case inQuery: |
| + b.WriteString(url.QueryEscape(v)) |
| + default: |
| + parts := strings.Split(v, "/") |
| + for k, p := range parts { |
| + parts[k] = url.PathEscape(p) |
| + } |
| + b.WriteString(strings.Join(parts, "/")) |
| + } |
| + i = j |
| + } |
| + return b.String() |
| +} |
| + |
| +// action is what a matched rule asks for: a redirect's status and |
| +// Location, or a rewrite's status and file. |
| +type action struct { |
| + status int |
| + location string |
| + file string |
| +} |
| + |
| +// resolve finds the first rule that applies to host and the cleaned |
| +// path p. exists is whether the tree has a file at p, which shadows |
| +// every rule not forced. rawPath and rawQuery are the request's own, so |
| +// a redirect to the URL asked for is skipped rather than looped. |
| +func resolve(rules []rule, host, p, rawPath, rawQuery string, exists bool) (action, bool) { |
| + reqSegs := segments(p) |
| + for i := range rules { |
| + r := &rules[i] |
| + if exists && !r.force { |
| + continue |
| + } |
| + caps, ok := r.match(host, reqSegs) |
| + if !ok { |
| + continue |
| + } |
| + if sp := caps["splat"]; sp != "" && strings.HasSuffix(rawPath, "/") { |
| + caps["splat"] = sp + "/" |
| + } |
| + if !isRedirect(r.status) { |
| + to := r.to |
| + if k := strings.IndexAny(to, "?#"); k >= 0 { |
| + to = to[:k] |
| + } |
| + return action{status: r.status, file: path.Clean("/" + expand(to, caps, false))}, true |
| + } |
| + loc := expand(r.to, caps, true) |
| + frag := "" |
| + if k := strings.IndexByte(loc, '#'); k >= 0 { |
| + loc, frag = loc[:k], loc[k:] |
| + } |
| + if r.prefix == "" { |
| + pth, q, hasQ := strings.Cut(loc, "?") |
| + trailing := strings.HasSuffix(pth, "/") |
| + pth = path.Clean("/" + pth) |
| + if trailing && pth != "/" { |
| + pth += "/" |
| + } |
| + loc = pth |
| + if hasQ { |
| + loc += "?" + q |
| + } |
| + } |
| + if !strings.Contains(loc, "?") && rawQuery != "" { |
| + loc += "?" + rawQuery |
| + } |
| + if r.prefix == "" || hostOf(r.prefix) == host { |
| + pth, q, _ := strings.Cut(loc, "?") |
| + if pth == "" { |
| + pth = "/" |
| + } |
| + if u, err := url.Parse(pth); err == nil && u.Path == rawPath && q == rawQuery { |
| + continue |
| + } |
| + } |
| + return action{status: r.status, location: r.prefix + loc + frag}, true |
| + } |
| + return action{}, false |
| +} |
| + |
| +// names reports whether some rule names the cleaned path p outright: |
| +// no splat and no ":name", its segments p's. |
| +func names(rules []rule, host, p string) bool { |
| + reqSegs := segments(p) |
| +outer: |
| + for i := range rules { |
| + r := &rules[i] |
| + if r.splat || len(r.segs) != len(reqSegs) || r.host != "" && r.host != host { |
| + continue |
| + } |
| + for j, seg := range r.segs { |
| + if strings.HasPrefix(seg, ":") || seg != reqSegs[j] { |
| + continue outer |
| + } |
| + } |
| + return true |
| + } |
| + return false |
| +} |
| + |
| +// redirectsSlashForm reports whether the rules would redirect the slash |
| +// form of rawPath, given that the directory there exists. |
| +func redirectsSlashForm(rules []rule, host, p, rawPath, rawQuery string) bool { |
| + act, ok := resolve(rules, host, p, rawPath+"/", rawQuery, true) |
| + return ok && act.location != "" |
| +} |
| + |
| +// hostOf is the host of a "scheme://host[:port]" prefix. |
| +func hostOf(prefix string) string { |
| + _, h, _ := strings.Cut(prefix, "://") |
| + return hostName(h) |
| +} |
diff --git a/internal/www/serve_test.go b/internal/www/serve_test.go| index 64bf114..55ceae1 100644 |
| --- a/internal/www/serve_test.go |
| +++ b/internal/www/serve_test.go |
| @@ -30,36 +30,32 @@ func siteSource(t *testing.T) *os.Root { |
| } |
| // builtSite is the whole site as the edge would receive it: site.Build |
| -// over the committed releases and the pages www.Pages assembles from |
| -// the real site source. |
| +// over the committed releases, the pages www.Pages assembles from the |
| +// real site source and their rules, as fiesta site builds it. |
| func builtSite(t *testing.T) map[string][]byte { |
| t.Helper() |
| pages, err := www.Pages(fiesta.FS(), siteSource(t).FS()) |
| if err != nil { |
| t.Fatalf("Pages: %v", err) |
| } |
| - tree, err := site.Build(os.DirFS("../../releases"), www.FS(pages)) |
| + tree, err := site.Build(os.DirFS("../../releases"), www.FS(pages), www.Redirects(pages)...) |
| if err != nil { |
| t.Fatalf("site.Build: %v", err) |
| } |
| return tree |
| } |
| -// rule is one line of _redirects: a from pattern, a to path, and the |
| -// status. Only what the edge applies to this site is understood: a |
| -// from ending in /* with a :splat in to, served as a 200 rewrite. |
| -type rule struct { |
| - from, to string |
| - status int |
| -} |
| - |
| -// siteHandler serves a site.Build tree the way the static edge does. |
| -// A directory URL serves its index.html; a real file wins over any |
| -// rewrite; a _redirects rule with status 200 is applied as a rewrite; |
| -// anything else is a 404; and any method other than GET or HEAD is a |
| -// 405, as on the edge, so a form a page should never submit cannot |
| -// pass a test by accident. It records every request's path and can be |
| -// told to delay one path, for the first-paint drive. |
| +// siteHandler serves a site.Build tree the way CARLOS's static edge |
| +// does (serveTree in internal/edge/static.go). A directory URL serves |
| +// its index.html, and one asked for without its slash is sent to the |
| +// slash form; a real file wins over every _redirects rule not forced; |
| +// the first rule that matches applies, a 200 or 404 serving another |
| +// file of the tree and a 301, 302, 303, 307 or 308 answering with its |
| +// Location (redirects_test.go); anything else is a 404; and any method |
| +// other than GET or HEAD is a 405, as on the edge, so a form a page |
| +// should never submit cannot pass a test by accident. It records every |
| +// request's path and can be told to delay one path, for the |
| +// first-paint drive. |
| type siteHandler struct { |
| tree map[string][]byte |
| rules []rule |
| @@ -72,16 +68,11 @@ type siteHandler struct { |
| func newSiteHandler(t *testing.T, tree map[string][]byte) *siteHandler { |
| t.Helper() |
| h := &siteHandler{tree: tree, delays: map[string]time.Duration{}} |
| - for _, line := range strings.Split(string(tree["_redirects"]), "\n") { |
| - f := strings.Fields(line) |
| - if len(f) == 0 || strings.HasPrefix(f[0], "#") { |
| - continue |
| - } |
| - if len(f) != 3 || f[2] != "200" { |
| - t.Fatalf("_redirects has a rule this server does not understand: %q", line) |
| - } |
| - h.rules = append(h.rules, rule{from: f[0], to: f[1], status: 200}) |
| + rules, err := parseRedirects(tree["_redirects"]) |
| + if err != nil { |
| + t.Fatalf("_redirects: %v", err) |
| } |
| + h.rules = rules |
| return h |
| } |
| @@ -106,32 +97,23 @@ func (h *siteHandler) Seen(reset bool) []string { |
| return out |
| } |
| -// file finds the tree entry for a request path: the file itself, or |
| -// the directory's index. |
| -func (h *siteHandler) file(p string) ([]byte, string, bool) { |
| - name := strings.TrimPrefix(p, "/") |
| - if strings.HasSuffix(p, "/") { |
| - name += "index.html" |
| - } |
| - data, ok := h.tree[name] |
| - return data, name, ok |
| -} |
| - |
| -// rewrite applies the first matching rule to p, or returns it as is. |
| -func (h *siteHandler) rewrite(p string) (string, bool) { |
| - for _, r := range h.rules { |
| - prefix, ok := strings.CutSuffix(r.from, "/*") |
| - if !ok { |
| - if p == r.from { |
| - return r.to, true |
| - } |
| - continue |
| - } |
| - if rest, ok := strings.CutPrefix(p, prefix+"/"); ok { |
| - return strings.Replace(r.to, ":splat", rest, 1), true |
| - } |
| +// file finds the tree entry for a cleaned, rooted path: the file |
| +// itself, or the index of the directory there, and whether it was a |
| +// directory. The rules file answers nothing. |
| +func (h *siteHandler) file(p string) (data []byte, name string, dir, ok bool) { |
| + name = strings.TrimPrefix(p, "/") |
| + if name == "_redirects" { |
| + return nil, "", false, false |
| + } |
| + if data, ok = h.tree[name]; ok && name != "" { |
| + return data, name, false, true |
| } |
| - return p, false |
| + index := "index.html" |
| + if name != "" { |
| + index = name + "/index.html" |
| + } |
| + data, ok = h.tree[index] |
| + return data, index, ok, ok |
| } |
| func (h *siteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| @@ -144,11 +126,28 @@ func (h *siteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
| return |
| } |
| - data, name, ok := h.file(r.URL.Path) |
| - if !ok { |
| - if to, matched := h.rewrite(r.URL.Path); matched { |
| - data, name, ok = h.file(to) |
| + host := hostName(r.Host) |
| + p := path.Clean("/" + r.URL.Path) |
| + status := http.StatusOK |
| + data, name, dir, ok := h.file(p) |
| + if act, matched := resolve(h.rules, host, p, r.URL.Path, r.URL.RawQuery, ok); matched { |
| + if act.location != "" { |
| + http.Redirect(w, r, act.location, act.status) |
| + return |
| } |
| + // A rewrite is looked up once: the rules are not consulted |
| + // again, and a destination with no file is a plain 404. |
| + status = act.status |
| + data, name, _, ok = h.file(act.file) |
| + } else if ok && dir && !strings.HasSuffix(r.URL.Path, "/") && |
| + !names(h.rules, host, p) && !redirectsSlashForm(h.rules, host, p, r.URL.Path, r.URL.RawQuery) { |
| + loc := "./" + path.Base(r.URL.EscapedPath()) + "/" |
| + if q := r.URL.RawQuery; q != "" { |
| + loc += "?" + q |
| + } |
| + w.Header().Set("Location", loc) |
| + w.WriteHeader(http.StatusMovedPermanently) |
| + return |
| } |
| if !ok { |
| http.NotFound(w, r) |
| @@ -163,6 +162,7 @@ func (h *siteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| } |
| w.Header().Set("Content-Type", ct) |
| w.Header().Set("Cache-Control", "no-store") |
| + w.WriteHeader(status) |
| w.Write(data) |
| } |
| @@ -225,3 +225,98 @@ func TestServeSiteServesLikeTheEdge(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// TestServeSiteRedirectsLikeTheEdge holds the test server's redirects |
| +// to CARLOS's (internal/edge/redirects.go): a 301 with its Location, a |
| +// :name capturing one segment, a final * capturing the rest ("" too) |
| +// into :splat, a trailing slash ignored in matching and kept on a |
| +// non-empty splat, the first matching rule winning, a file at the path |
| +// winning over every rule but a forced one, a directory with an index |
| +// counting as that file, the query carried over, and a redirect to the |
| +// very URL asked for skipped. A test of the test double, not of the |
| +// site. |
| +func TestServeSiteRedirectsLikeTheEdge(t *testing.T) { |
| + h := newSiteHandler(t, map[string][]byte{ |
| + "_redirects": []byte("# moved pages\n" + |
| + "/1/* /1.0.0/:splat 200\n" + |
| + "/old/first.html /new/first.html 301\n" + |
| + "/old/first.html /never.html 301\n" + |
| + "/old/:page /new/:page\n" + |
| + "/deep/:a/:b /new/:b/:a.html 302\n" + |
| + "/tree/* /new/tree/:splat 301\n" + |
| + "/kept.html /new/kept.html 301\n" + |
| + "/forced.html /new/forced.html 301!\n" + |
| + "/dir /new/dir/ 301\n" + |
| + "/same/* /same/:splat 301\n"), |
| + "1.0.0/fiesta.css": []byte("newest"), |
| + "kept.html": []byte("kept"), |
| + "forced.html": []byte("forced"), |
| + "dir/index.html": []byte("dir"), |
| + "new/first.html": []byte("first"), |
| + "new/second.html": []byte("second"), |
| + "new/kept.html": []byte("new kept"), |
| + "new/tree/index.html": []byte("tree"), |
| + }) |
| + for _, c := range []struct { |
| + path string |
| + status int |
| + location string |
| + }{ |
| + {"/1/fiesta.css", 200, ""}, |
| + {"/old/first.html", 301, "/new/first.html"}, |
| + {"/old/second.html", 301, "/new/second.html"}, |
| + {"/old/second.html?x=1", 301, "/new/second.html?x=1"}, |
| + {"/old/", 404, ""}, |
| + {"/old/a/b", 404, ""}, |
| + {"/deep/one/two", 302, "/new/two/one.html"}, |
| + {"/tree", 301, "/new/tree/"}, |
| + {"/tree/", 301, "/new/tree/"}, |
| + {"/tree/a/b.html", 301, "/new/tree/a/b.html"}, |
| + {"/tree/a/", 301, "/new/tree/a/"}, |
| + {"/kept.html", 200, ""}, |
| + {"/forced.html", 301, "/new/forced.html"}, |
| + {"/dir/", 200, ""}, |
| + {"/same/x", 404, ""}, |
| + } { |
| + rec := httptest.NewRecorder() |
| + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, c.path, nil)) |
| + if loc := rec.Header().Get("Location"); rec.Code != c.status || loc != c.location { |
| + t.Errorf("GET %s: %d, Location %q; want %d, Location %q", c.path, rec.Code, loc, c.status, c.location) |
| + } |
| + } |
| +} |
| + |
| +// TestServeSiteRefusesARuleCarlosWouldRefuse: the test server reads |
| +// _redirects with CARLOS's grammar, so a line `carlos ship` would |
| +// refuse is refused here too, by line, and a file the edge would serve |
| +// is not mistaken for one it would not. |
| +func TestServeSiteRefusesARuleCarlosWouldRefuse(t *testing.T) { |
| + for _, line := range []string{ |
| + "/only-a-source", |
| + "/a /b 418", |
| + "/a /b 301 Country=ie", |
| + "/a/*/b /c", |
| + "/a/:x/:x /c", |
| + "/a?q=1 /b", |
| + "relative /b", |
| + "/a //elsewhere.example/", |
| + "/a https://example.com/ 200", |
| + "/a /" + strings.Repeat("b", 2048), |
| + } { |
| + if _, err := parseRedirects([]byte(line + "\n")); err == nil { |
| + t.Errorf("%q parsed; CARLOS refuses it", line) |
| + } |
| + } |
| + for _, line := range []string{ |
| + "/a /b", |
| + "/a/:x /b/:x 302", |
| + "/a/* /b/:splat 200", |
| + "/a https://example.com/b 301!", |
| + "/a /b 404 # a comment", |
| + "# a comment alone", |
| + } { |
| + if _, err := parseRedirects([]byte(line + "\n")); err != nil { |
| + t.Errorf("%q was refused (%v); CARLOS accepts it", line, err) |
| + } |
| + } |
| +} |
diff --git a/internal/www/testdata/part3-addresses.txt b/internal/www/testdata/part3-addresses.txt| new file mode 100644 |
| index 0000000..d976682 |
| --- /dev/null |
| +++ b/internal/www/testdata/part3-addresses.txt |
| @@ -0,0 +1,1439 @@ |
| +# Every HTML page part 3 of Fiesta published under /design-system/ on |
| +# fiesta.rastrillo.org, one tree path a line: the site built from |
| +# 6af218e, main when part 3 shipped. TestEveryPart3AddressStillReachesAPage |
| +# holds the site to every one of them. To regenerate, from a checkout |
| +# of this repository: |
| +# |
| +# d=$(mktemp -d) && git archive 6af218e | tar -x -C "$d" && |
| +# make -C "$d" site >/dev/null && |
| +# (cd "$d/.build/site/public" && find design-system -name '*.html' | LC_ALL=C sort) && |
| +# rm -rf "$d" |
| +design-system/day/actor.html |
| +design-system/day/api-tokens.html |
| +design-system/day/back-nav.html |
| +design-system/day/badge.html |
| +design-system/day/box.html |
| +design-system/day/bulk-bar.html |
| +design-system/day/button.html |
| +design-system/day/callout.html |
| +design-system/day/choice-field.html |
| +design-system/day/confirm-form.html |
| +design-system/day/detail-list.html |
| +design-system/day/dropdown.html |
| +design-system/day/empty-state.html |
| +design-system/day/error-page.html |
| +design-system/day/field-check.html |
| +design-system/day/field-date.html |
| +design-system/day/field-daterange.html |
| +design-system/day/field-datetime.html |
| +design-system/day/field-money.html |
| +design-system/day/field-select.html |
| +design-system/day/field-text.html |
| +design-system/day/field-textarea.html |
| +design-system/day/field-time.html |
| +design-system/day/field-url.html |
| +design-system/day/field.html |
| +design-system/day/form-error.html |
| +design-system/day/form-foot.html |
| +design-system/day/form-layout.html |
| +design-system/day/format-abbr.html |
| +design-system/day/format-addresses.html |
| +design-system/day/format-dates.html |
| +design-system/day/format-durations.html |
| +design-system/day/format-filesizes.html |
| +design-system/day/format-identifiers.html |
| +design-system/day/format-money.html |
| +design-system/day/format-numbers.html |
| +design-system/day/format-output.html |
| +design-system/day/format-people.html |
| +design-system/day/format-ratios.html |
| +design-system/day/frames/actor/a-person.html |
| +design-system/day/frames/actor/an-agent-acting-for-a-person.html |
| +design-system/day/frames/api-tokens/four-tokens-two-of-them-live.html |
| +design-system/day/frames/api-tokens/just-created-the-secret-shown-once.html |
| +design-system/day/frames/api-tokens/no-tokens-yet.html |
| +design-system/day/frames/back-nav/back-to-a-record.html |
| +design-system/day/frames/badge/negative.html |
| +design-system/day/frames/badge/neutral.html |
| +design-system/day/frames/badge/no-tone-quiet-default.html |
| +design-system/day/frames/badge/positive.html |
| +design-system/day/frames/badge/warning.html |
| +design-system/day/frames/box/default.html |
| +design-system/day/frames/bulk-bar/three-rows-selected.html |
| +design-system/day/frames/button/default.html |
| +design-system/day/frames/callout/info.html |
| +design-system/day/frames/callout/negative-announced-as-an-alert.html |
| +design-system/day/frames/callout/negative.html |
| +design-system/day/frames/callout/positive.html |
| +design-system/day/frames/callout/warning.html |
| +design-system/day/frames/choice-field/pick-any.html |
| +design-system/day/frames/choice-field/pick-one.html |
| +design-system/day/frames/confirm-form/destructive.html |
| +design-system/day/frames/confirm-form/ordinary-default-cancel-wording.html |
| +design-system/day/frames/detail-list/four-facts-one-machine-ish.html |
| +design-system/day/frames/dropdown/a-filter-menu-with-one-choice-applied.html |
| +design-system/day/frames/dropdown/menu-idiom.html |
| +design-system/day/frames/empty-state/sentence-only.html |
| +design-system/day/frames/empty-state/with-a-post-call-to-action.html |
| +design-system/day/frames/error-page/403-forbidden.html |
| +design-system/day/frames/error-page/404-not-found.html |
| +design-system/day/frames/error-page/422-unprocessable.html |
| +design-system/day/frames/error-page/500-server-error.html |
| +design-system/day/frames/error-page/503-unavailable.html |
| +design-system/day/frames/error-page/anything-else-the-generic-pair.html |
| +design-system/day/frames/field-check/disabled.html |
| +design-system/day/frames/field-check/off.html |
| +design-system/day/frames/field-check/on.html |
| +design-system/day/frames/field-date/bounded.html |
| +design-system/day/frames/field-date/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/day/frames/field-date/plain-the-bare-native-input.html |
| +design-system/day/frames/field-date/required-with-an-error.html |
| +design-system/day/frames/field-daterange/date-and-time-seeded.html |
| +design-system/day/frames/field-daterange/dates-only-with-an-error-on-the-end.html |
| +design-system/day/frames/field-datetime/bounded.html |
| +design-system/day/frames/field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/day/frames/field-datetime/plain-the-bare-native-input.html |
| +design-system/day/frames/field-datetime/required-with-an-error.html |
| +design-system/day/frames/field-money/bare.html |
| +design-system/day/frames/field-money/with-a-hint.html |
| +design-system/day/frames/field-money/with-an-error.html |
| +design-system/day/frames/field-select/hand-written-with-optgroups.html |
| +design-system/day/frames/field-select/required-with-an-error.html |
| +design-system/day/frames/field-select/three-options-a-native-select.html |
| +design-system/day/frames/field-select/twelve-options-plain-the-enhancement-opted-out.html |
| +design-system/day/frames/field-select/twelve-options-the-filterable-combobox.html |
| +design-system/day/frames/field-text/bare.html |
| +design-system/day/frames/field-text/required.html |
| +design-system/day/frames/field-text/the-headline-field.html |
| +design-system/day/frames/field-text/with-a-hint.html |
| +design-system/day/frames/field-text/with-an-error.html |
| +design-system/day/frames/field-textarea/bare.html |
| +design-system/day/frames/field-textarea/required.html |
| +design-system/day/frames/field-textarea/with-a-hint.html |
| +design-system/day/frames/field-textarea/with-an-error.html |
| +design-system/day/frames/field-time/bounded.html |
| +design-system/day/frames/field-time/enhanced-input-type-half-seven-or-noon.html |
| +design-system/day/frames/field-time/plain-the-bare-native-input.html |
| +design-system/day/frames/field-time/required-with-an-error.html |
| +design-system/day/frames/field-url/bare.html |
| +design-system/day/frames/field-url/with-a-hint.html |
| +design-system/day/frames/field-url/with-an-error.html |
| +design-system/day/frames/field/bare.html |
| +design-system/day/frames/field/required.html |
| +design-system/day/frames/field/short.html |
| +design-system/day/frames/field/the-headline-field.html |
| +design-system/day/frames/field/with-a-hint.html |
| +design-system/day/frames/field/with-an-error.html |
| +design-system/day/frames/field/with-help-under-the-control.html |
| +design-system/day/frames/form-error/a-rejected-submission.html |
| +design-system/day/frames/form-foot/idle-the-button-before-anything-happens.html |
| +design-system/day/frames/form-foot/save-and-cancel.html |
| +design-system/day/frames/form-foot/submit-only.html |
| +design-system/day/frames/form-foot/working-what-busy-js-writes-on-the-way-out.html |
| +design-system/day/frames/form-layout/default.html |
| +design-system/day/frames/format-abbr/default.html |
| +design-system/day/frames/format-addresses/default.html |
| +design-system/day/frames/format-dates/default.html |
| +design-system/day/frames/format-durations/default.html |
| +design-system/day/frames/format-filesizes/default.html |
| +design-system/day/frames/format-identifiers/default.html |
| +design-system/day/frames/format-money/default.html |
| +design-system/day/frames/format-numbers/default.html |
| +design-system/day/frames/format-output/default.html |
| +design-system/day/frames/format-people/default.html |
| +design-system/day/frames/format-ratios/default.html |
| +design-system/day/frames/help/default.html |
| +design-system/day/frames/job-status/failed.html |
| +design-system/day/frames/job-status/finished.html |
| +design-system/day/frames/job-status/running.html |
| +design-system/day/frames/layout-column/default.html |
| +design-system/day/frames/layout-console-page/default.html |
| +design-system/day/frames/layout-console/default.html |
| +design-system/day/frames/layout-sidebar-page/default.html |
| +design-system/day/frames/layout-sidebar/default.html |
| +design-system/day/frames/layout-stage/default.html |
| +design-system/day/frames/layout-topbar/default.html |
| +design-system/day/frames/list-bar-search/empty-accessible-name-from-the-catalog.html |
| +design-system/day/frames/list-bar-search/with-a-query-on-page-4.html |
| +design-system/day/frames/list-bar-search/with-a-query.html |
| +design-system/day/frames/list-bar/search-with-a-filter-dropdown.html |
| +design-system/day/frames/list-grid/default.html |
| +design-system/day/frames/list-row-action/lead-marker-status-and-an-action.html |
| +design-system/day/frames/list-row-action/name-and-meta-line-only.html |
| +design-system/day/frames/list-search-submit/default-label.html |
| +design-system/day/frames/locale-menu/twelve-languages-current-highlighted.html |
| +design-system/day/frames/media-grid/screenshots-a-logo-and-a-banner.html |
| +design-system/day/frames/media-grid/select-mode-one-tile-selected.html |
| +design-system/day/frames/meter/empty.html |
| +design-system/day/frames/meter/full.html |
| +design-system/day/frames/meter/nearly-full.html |
| +design-system/day/frames/meter/over-budget.html |
| +design-system/day/frames/meter/part-way.html |
| +design-system/day/frames/modal/default.html |
| +design-system/day/frames/notice/a-saved-message.html |
| +design-system/day/frames/page-header/title-only.html |
| +design-system/day/frames/page-header/title-subhead-and-a-primary-action.html |
| +design-system/day/frames/pagination/first-page-default-accessible-name.html |
| +design-system/day/frames/pagination/middle-of-a-long-list.html |
| +design-system/day/frames/person/large-the-show-header-size.html |
| +design-system/day/frames/person/name-and-email.html |
| +design-system/day/frames/person/no-initial.html |
| +design-system/day/frames/row-menu/links-a-form-and-a-destructive-item.html |
| +design-system/day/frames/row-menu/links-only-for-a-list-inside-a-selection-form.html |
| +design-system/day/frames/scheme-menu/standalone.html |
| +design-system/day/frames/scheme-menu/three-choices-dark-current.html |
| +design-system/day/frames/seg-tabs/two-sections.html |
| +design-system/day/frames/selbox/default.html |
| +design-system/day/frames/shell-sidebar/default.html |
| +design-system/day/frames/shell-topbar/default.html |
| +design-system/day/frames/signin/a-passkey.html |
| +design-system/day/frames/signin/after-the-link-is-sent-in-another-browser.html |
| +design-system/day/frames/signin/after-the-link-is-sent.html |
| +design-system/day/frames/signin/an-address-that-does-not-look-right.html |
| +design-system/day/frames/signin/asking-for-an-address.html |
| +design-system/day/frames/signin/coming-back-with-an-email-link.html |
| +design-system/day/frames/signin/coming-back-with-keymail.html |
| +design-system/day/frames/signin/email-and-password.html |
| +design-system/day/frames/signin/google-apple-and-the-rest.html |
| +design-system/day/frames/signin/on-the-way-to-keymail.html |
| +design-system/day/frames/signin/when-keymail-could-not-confirm-it.html |
| +design-system/day/frames/signin/when-keymail-was-offered-and-a-link-went-out.html |
| +design-system/day/frames/stat-band/default.html |
| +design-system/day/frames/stat/a-shortened-number.html |
| +design-system/day/frames/stat/a-smaller-number.html |
| +design-system/day/frames/stat/the-big-number.html |
| +design-system/day/frames/stat/when-going-down-is-good.html |
| +design-system/day/frames/stat/when-going-up-is-bad.html |
| +design-system/day/frames/stat/when-you-cannot-say.html |
| +design-system/day/frames/status-pill/negative.html |
| +design-system/day/frames/status-pill/neutral.html |
| +design-system/day/frames/status-pill/positive.html |
| +design-system/day/frames/status-pill/warning.html |
| +design-system/day/frames/tblock/default.html |
| +design-system/day/help.html |
| +design-system/day/icons.html |
| +design-system/day/index.html |
| +design-system/day/job-status.html |
| +design-system/day/layout-column.html |
| +design-system/day/layout-console-page.html |
| +design-system/day/layout-console.html |
| +design-system/day/layout-sidebar-page.html |
| +design-system/day/layout-sidebar.html |
| +design-system/day/layout-stage.html |
| +design-system/day/layout-topbar.html |
| +design-system/day/list-bar-search.html |
| +design-system/day/list-bar.html |
| +design-system/day/list-grid.html |
| +design-system/day/list-row-action.html |
| +design-system/day/list-search-submit.html |
| +design-system/day/locale-menu.html |
| +design-system/day/media-grid.html |
| +design-system/day/meter.html |
| +design-system/day/modal.html |
| +design-system/day/notice.html |
| +design-system/day/page-header.html |
| +design-system/day/pagination.html |
| +design-system/day/person.html |
| +design-system/day/row-menu.html |
| +design-system/day/rtl/actor.html |
| +design-system/day/rtl/api-tokens.html |
| +design-system/day/rtl/back-nav.html |
| +design-system/day/rtl/badge.html |
| +design-system/day/rtl/box.html |
| +design-system/day/rtl/bulk-bar.html |
| +design-system/day/rtl/button.html |
| +design-system/day/rtl/callout.html |
| +design-system/day/rtl/choice-field.html |
| +design-system/day/rtl/confirm-form.html |
| +design-system/day/rtl/detail-list.html |
| +design-system/day/rtl/dropdown.html |
| +design-system/day/rtl/empty-state.html |
| +design-system/day/rtl/error-page.html |
| +design-system/day/rtl/field-check.html |
| +design-system/day/rtl/field-date.html |
| +design-system/day/rtl/field-daterange.html |
| +design-system/day/rtl/field-datetime.html |
| +design-system/day/rtl/field-money.html |
| +design-system/day/rtl/field-select.html |
| +design-system/day/rtl/field-text.html |
| +design-system/day/rtl/field-textarea.html |
| +design-system/day/rtl/field-time.html |
| +design-system/day/rtl/field-url.html |
| +design-system/day/rtl/field.html |
| +design-system/day/rtl/form-error.html |
| +design-system/day/rtl/form-foot.html |
| +design-system/day/rtl/form-layout.html |
| +design-system/day/rtl/format-abbr.html |
| +design-system/day/rtl/format-addresses.html |
| +design-system/day/rtl/format-dates.html |
| +design-system/day/rtl/format-durations.html |
| +design-system/day/rtl/format-filesizes.html |
| +design-system/day/rtl/format-identifiers.html |
| +design-system/day/rtl/format-money.html |
| +design-system/day/rtl/format-numbers.html |
| +design-system/day/rtl/format-output.html |
| +design-system/day/rtl/format-people.html |
| +design-system/day/rtl/format-ratios.html |
| +design-system/day/rtl/frames/actor/a-person.html |
| +design-system/day/rtl/frames/actor/an-agent-acting-for-a-person.html |
| +design-system/day/rtl/frames/api-tokens/four-tokens-two-of-them-live.html |
| +design-system/day/rtl/frames/api-tokens/just-created-the-secret-shown-once.html |
| +design-system/day/rtl/frames/api-tokens/no-tokens-yet.html |
| +design-system/day/rtl/frames/back-nav/back-to-a-record.html |
| +design-system/day/rtl/frames/badge/negative.html |
| +design-system/day/rtl/frames/badge/neutral.html |
| +design-system/day/rtl/frames/badge/no-tone-quiet-default.html |
| +design-system/day/rtl/frames/badge/positive.html |
| +design-system/day/rtl/frames/badge/warning.html |
| +design-system/day/rtl/frames/box/default.html |
| +design-system/day/rtl/frames/bulk-bar/three-rows-selected.html |
| +design-system/day/rtl/frames/button/default.html |
| +design-system/day/rtl/frames/callout/info.html |
| +design-system/day/rtl/frames/callout/negative-announced-as-an-alert.html |
| +design-system/day/rtl/frames/callout/negative.html |
| +design-system/day/rtl/frames/callout/positive.html |
| +design-system/day/rtl/frames/callout/warning.html |
| +design-system/day/rtl/frames/choice-field/pick-any.html |
| +design-system/day/rtl/frames/choice-field/pick-one.html |
| +design-system/day/rtl/frames/confirm-form/destructive.html |
| +design-system/day/rtl/frames/confirm-form/ordinary-default-cancel-wording.html |
| +design-system/day/rtl/frames/detail-list/four-facts-one-machine-ish.html |
| +design-system/day/rtl/frames/dropdown/a-filter-menu-with-one-choice-applied.html |
| +design-system/day/rtl/frames/dropdown/menu-idiom.html |
| +design-system/day/rtl/frames/empty-state/sentence-only.html |
| +design-system/day/rtl/frames/empty-state/with-a-post-call-to-action.html |
| +design-system/day/rtl/frames/error-page/403-forbidden.html |
| +design-system/day/rtl/frames/error-page/404-not-found.html |
| +design-system/day/rtl/frames/error-page/422-unprocessable.html |
| +design-system/day/rtl/frames/error-page/500-server-error.html |
| +design-system/day/rtl/frames/error-page/503-unavailable.html |
| +design-system/day/rtl/frames/error-page/anything-else-the-generic-pair.html |
| +design-system/day/rtl/frames/field-check/disabled.html |
| +design-system/day/rtl/frames/field-check/off.html |
| +design-system/day/rtl/frames/field-check/on.html |
| +design-system/day/rtl/frames/field-date/bounded.html |
| +design-system/day/rtl/frames/field-date/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/day/rtl/frames/field-date/plain-the-bare-native-input.html |
| +design-system/day/rtl/frames/field-date/required-with-an-error.html |
| +design-system/day/rtl/frames/field-daterange/date-and-time-seeded.html |
| +design-system/day/rtl/frames/field-daterange/dates-only-with-an-error-on-the-end.html |
| +design-system/day/rtl/frames/field-datetime/bounded.html |
| +design-system/day/rtl/frames/field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/day/rtl/frames/field-datetime/plain-the-bare-native-input.html |
| +design-system/day/rtl/frames/field-datetime/required-with-an-error.html |
| +design-system/day/rtl/frames/field-money/bare.html |
| +design-system/day/rtl/frames/field-money/with-a-hint.html |
| +design-system/day/rtl/frames/field-money/with-an-error.html |
| +design-system/day/rtl/frames/field-select/hand-written-with-optgroups.html |
| +design-system/day/rtl/frames/field-select/required-with-an-error.html |
| +design-system/day/rtl/frames/field-select/three-options-a-native-select.html |
| +design-system/day/rtl/frames/field-select/twelve-options-plain-the-enhancement-opted-out.html |
| +design-system/day/rtl/frames/field-select/twelve-options-the-filterable-combobox.html |
| +design-system/day/rtl/frames/field-text/bare.html |
| +design-system/day/rtl/frames/field-text/required.html |
| +design-system/day/rtl/frames/field-text/the-headline-field.html |
| +design-system/day/rtl/frames/field-text/with-a-hint.html |
| +design-system/day/rtl/frames/field-text/with-an-error.html |
| +design-system/day/rtl/frames/field-textarea/bare.html |
| +design-system/day/rtl/frames/field-textarea/required.html |
| +design-system/day/rtl/frames/field-textarea/with-a-hint.html |
| +design-system/day/rtl/frames/field-textarea/with-an-error.html |
| +design-system/day/rtl/frames/field-time/bounded.html |
| +design-system/day/rtl/frames/field-time/enhanced-input-type-half-seven-or-noon.html |
| +design-system/day/rtl/frames/field-time/plain-the-bare-native-input.html |
| +design-system/day/rtl/frames/field-time/required-with-an-error.html |
| +design-system/day/rtl/frames/field-url/bare.html |
| +design-system/day/rtl/frames/field-url/with-a-hint.html |
| +design-system/day/rtl/frames/field-url/with-an-error.html |
| +design-system/day/rtl/frames/field/bare.html |
| +design-system/day/rtl/frames/field/required.html |
| +design-system/day/rtl/frames/field/short.html |
| +design-system/day/rtl/frames/field/the-headline-field.html |
| +design-system/day/rtl/frames/field/with-a-hint.html |
| +design-system/day/rtl/frames/field/with-an-error.html |
| +design-system/day/rtl/frames/field/with-help-under-the-control.html |
| +design-system/day/rtl/frames/form-error/a-rejected-submission.html |
| +design-system/day/rtl/frames/form-foot/idle-the-button-before-anything-happens.html |
| +design-system/day/rtl/frames/form-foot/save-and-cancel.html |
| +design-system/day/rtl/frames/form-foot/submit-only.html |
| +design-system/day/rtl/frames/form-foot/working-what-busy-js-writes-on-the-way-out.html |
| +design-system/day/rtl/frames/form-layout/default.html |
| +design-system/day/rtl/frames/format-abbr/default.html |
| +design-system/day/rtl/frames/format-addresses/default.html |
| +design-system/day/rtl/frames/format-dates/default.html |
| +design-system/day/rtl/frames/format-durations/default.html |
| +design-system/day/rtl/frames/format-filesizes/default.html |
| +design-system/day/rtl/frames/format-identifiers/default.html |
| +design-system/day/rtl/frames/format-money/default.html |
| +design-system/day/rtl/frames/format-numbers/default.html |
| +design-system/day/rtl/frames/format-output/default.html |
| +design-system/day/rtl/frames/format-people/default.html |
| +design-system/day/rtl/frames/format-ratios/default.html |
| +design-system/day/rtl/frames/help/default.html |
| +design-system/day/rtl/frames/job-status/failed.html |
| +design-system/day/rtl/frames/job-status/finished.html |
| +design-system/day/rtl/frames/job-status/running.html |
| +design-system/day/rtl/frames/layout-column/default.html |
| +design-system/day/rtl/frames/layout-console-page/default.html |
| +design-system/day/rtl/frames/layout-console/default.html |
| +design-system/day/rtl/frames/layout-sidebar-page/default.html |
| +design-system/day/rtl/frames/layout-sidebar/default.html |
| +design-system/day/rtl/frames/layout-stage/default.html |
| +design-system/day/rtl/frames/layout-topbar/default.html |
| +design-system/day/rtl/frames/list-bar-search/empty-accessible-name-from-the-catalog.html |
| +design-system/day/rtl/frames/list-bar-search/with-a-query-on-page-4.html |
| +design-system/day/rtl/frames/list-bar-search/with-a-query.html |
| +design-system/day/rtl/frames/list-bar/search-with-a-filter-dropdown.html |
| +design-system/day/rtl/frames/list-grid/default.html |
| +design-system/day/rtl/frames/list-row-action/lead-marker-status-and-an-action.html |
| +design-system/day/rtl/frames/list-row-action/name-and-meta-line-only.html |
| +design-system/day/rtl/frames/list-search-submit/default-label.html |
| +design-system/day/rtl/frames/locale-menu/twelve-languages-current-highlighted.html |
| +design-system/day/rtl/frames/media-grid/screenshots-a-logo-and-a-banner.html |
| +design-system/day/rtl/frames/media-grid/select-mode-one-tile-selected.html |
| +design-system/day/rtl/frames/meter/empty.html |
| +design-system/day/rtl/frames/meter/full.html |
| +design-system/day/rtl/frames/meter/nearly-full.html |
| +design-system/day/rtl/frames/meter/over-budget.html |
| +design-system/day/rtl/frames/meter/part-way.html |
| +design-system/day/rtl/frames/modal/default.html |
| +design-system/day/rtl/frames/notice/a-saved-message.html |
| +design-system/day/rtl/frames/page-header/title-only.html |
| +design-system/day/rtl/frames/page-header/title-subhead-and-a-primary-action.html |
| +design-system/day/rtl/frames/pagination/first-page-default-accessible-name.html |
| +design-system/day/rtl/frames/pagination/middle-of-a-long-list.html |
| +design-system/day/rtl/frames/person/large-the-show-header-size.html |
| +design-system/day/rtl/frames/person/name-and-email.html |
| +design-system/day/rtl/frames/person/no-initial.html |
| +design-system/day/rtl/frames/row-menu/links-a-form-and-a-destructive-item.html |
| +design-system/day/rtl/frames/row-menu/links-only-for-a-list-inside-a-selection-form.html |
| +design-system/day/rtl/frames/scheme-menu/standalone.html |
| +design-system/day/rtl/frames/scheme-menu/three-choices-dark-current.html |
| +design-system/day/rtl/frames/seg-tabs/two-sections.html |
| +design-system/day/rtl/frames/selbox/default.html |
| +design-system/day/rtl/frames/shell-sidebar/default.html |
| +design-system/day/rtl/frames/shell-topbar/default.html |
| +design-system/day/rtl/frames/signin/a-passkey.html |
| +design-system/day/rtl/frames/signin/after-the-link-is-sent-in-another-browser.html |
| +design-system/day/rtl/frames/signin/after-the-link-is-sent.html |
| +design-system/day/rtl/frames/signin/an-address-that-does-not-look-right.html |
| +design-system/day/rtl/frames/signin/asking-for-an-address.html |
| +design-system/day/rtl/frames/signin/coming-back-with-an-email-link.html |
| +design-system/day/rtl/frames/signin/coming-back-with-keymail.html |
| +design-system/day/rtl/frames/signin/email-and-password.html |
| +design-system/day/rtl/frames/signin/google-apple-and-the-rest.html |
| +design-system/day/rtl/frames/signin/on-the-way-to-keymail.html |
| +design-system/day/rtl/frames/signin/when-keymail-could-not-confirm-it.html |
| +design-system/day/rtl/frames/signin/when-keymail-was-offered-and-a-link-went-out.html |
| +design-system/day/rtl/frames/stat-band/default.html |
| +design-system/day/rtl/frames/stat/a-shortened-number.html |
| +design-system/day/rtl/frames/stat/a-smaller-number.html |
| +design-system/day/rtl/frames/stat/the-big-number.html |
| +design-system/day/rtl/frames/stat/when-going-down-is-good.html |
| +design-system/day/rtl/frames/stat/when-going-up-is-bad.html |
| +design-system/day/rtl/frames/stat/when-you-cannot-say.html |
| +design-system/day/rtl/frames/status-pill/negative.html |
| +design-system/day/rtl/frames/status-pill/neutral.html |
| +design-system/day/rtl/frames/status-pill/positive.html |
| +design-system/day/rtl/frames/status-pill/warning.html |
| +design-system/day/rtl/frames/tblock/default.html |
| +design-system/day/rtl/help.html |
| +design-system/day/rtl/icons.html |
| +design-system/day/rtl/index.html |
| +design-system/day/rtl/job-status.html |
| +design-system/day/rtl/layout-column.html |
| +design-system/day/rtl/layout-console-page.html |
| +design-system/day/rtl/layout-console.html |
| +design-system/day/rtl/layout-sidebar-page.html |
| +design-system/day/rtl/layout-sidebar.html |
| +design-system/day/rtl/layout-stage.html |
| +design-system/day/rtl/layout-topbar.html |
| +design-system/day/rtl/list-bar-search.html |
| +design-system/day/rtl/list-bar.html |
| +design-system/day/rtl/list-grid.html |
| +design-system/day/rtl/list-row-action.html |
| +design-system/day/rtl/list-search-submit.html |
| +design-system/day/rtl/locale-menu.html |
| +design-system/day/rtl/media-grid.html |
| +design-system/day/rtl/meter.html |
| +design-system/day/rtl/modal.html |
| +design-system/day/rtl/notice.html |
| +design-system/day/rtl/page-header.html |
| +design-system/day/rtl/pagination.html |
| +design-system/day/rtl/person.html |
| +design-system/day/rtl/row-menu.html |
| +design-system/day/rtl/scheme-menu.html |
| +design-system/day/rtl/seg-tabs.html |
| +design-system/day/rtl/selbox.html |
| +design-system/day/rtl/shell-sidebar.html |
| +design-system/day/rtl/shell-topbar.html |
| +design-system/day/rtl/signin.html |
| +design-system/day/rtl/stat-band.html |
| +design-system/day/rtl/stat.html |
| +design-system/day/rtl/status-pill.html |
| +design-system/day/rtl/tblock.html |
| +design-system/day/rtl/tokens.html |
| +design-system/day/scheme-menu.html |
| +design-system/day/seg-tabs.html |
| +design-system/day/selbox.html |
| +design-system/day/shell-sidebar.html |
| +design-system/day/shell-topbar.html |
| +design-system/day/signin.html |
| +design-system/day/stat-band.html |
| +design-system/day/stat.html |
| +design-system/day/status-pill.html |
| +design-system/day/tblock.html |
| +design-system/day/tokens.html |
| +design-system/index.html |
| +design-system/plain/actor.html |
| +design-system/plain/api-tokens.html |
| +design-system/plain/back-nav.html |
| +design-system/plain/badge.html |
| +design-system/plain/box.html |
| +design-system/plain/bulk-bar.html |
| +design-system/plain/button.html |
| +design-system/plain/callout.html |
| +design-system/plain/choice-field.html |
| +design-system/plain/confirm-form.html |
| +design-system/plain/detail-list.html |
| +design-system/plain/dropdown.html |
| +design-system/plain/empty-state.html |
| +design-system/plain/error-page.html |
| +design-system/plain/field-check.html |
| +design-system/plain/field-date.html |
| +design-system/plain/field-daterange.html |
| +design-system/plain/field-datetime.html |
| +design-system/plain/field-money.html |
| +design-system/plain/field-select.html |
| +design-system/plain/field-text.html |
| +design-system/plain/field-textarea.html |
| +design-system/plain/field-time.html |
| +design-system/plain/field-url.html |
| +design-system/plain/field.html |
| +design-system/plain/form-error.html |
| +design-system/plain/form-foot.html |
| +design-system/plain/form-layout.html |
| +design-system/plain/format-abbr.html |
| +design-system/plain/format-addresses.html |
| +design-system/plain/format-dates.html |
| +design-system/plain/format-durations.html |
| +design-system/plain/format-filesizes.html |
| +design-system/plain/format-identifiers.html |
| +design-system/plain/format-money.html |
| +design-system/plain/format-numbers.html |
| +design-system/plain/format-output.html |
| +design-system/plain/format-people.html |
| +design-system/plain/format-ratios.html |
| +design-system/plain/frames/actor/a-person.html |
| +design-system/plain/frames/actor/an-agent-acting-for-a-person.html |
| +design-system/plain/frames/api-tokens/four-tokens-two-of-them-live.html |
| +design-system/plain/frames/api-tokens/just-created-the-secret-shown-once.html |
| +design-system/plain/frames/api-tokens/no-tokens-yet.html |
| +design-system/plain/frames/back-nav/back-to-a-record.html |
| +design-system/plain/frames/badge/negative.html |
| +design-system/plain/frames/badge/neutral.html |
| +design-system/plain/frames/badge/no-tone-quiet-default.html |
| +design-system/plain/frames/badge/positive.html |
| +design-system/plain/frames/badge/warning.html |
| +design-system/plain/frames/box/default.html |
| +design-system/plain/frames/bulk-bar/three-rows-selected.html |
| +design-system/plain/frames/button/default.html |
| +design-system/plain/frames/callout/info.html |
| +design-system/plain/frames/callout/negative-announced-as-an-alert.html |
| +design-system/plain/frames/callout/negative.html |
| +design-system/plain/frames/callout/positive.html |
| +design-system/plain/frames/callout/warning.html |
| +design-system/plain/frames/choice-field/pick-any.html |
| +design-system/plain/frames/choice-field/pick-one.html |
| +design-system/plain/frames/confirm-form/destructive.html |
| +design-system/plain/frames/confirm-form/ordinary-default-cancel-wording.html |
| +design-system/plain/frames/detail-list/four-facts-one-machine-ish.html |
| +design-system/plain/frames/dropdown/a-filter-menu-with-one-choice-applied.html |
| +design-system/plain/frames/dropdown/menu-idiom.html |
| +design-system/plain/frames/empty-state/sentence-only.html |
| +design-system/plain/frames/empty-state/with-a-post-call-to-action.html |
| +design-system/plain/frames/error-page/403-forbidden.html |
| +design-system/plain/frames/error-page/404-not-found.html |
| +design-system/plain/frames/error-page/422-unprocessable.html |
| +design-system/plain/frames/error-page/500-server-error.html |
| +design-system/plain/frames/error-page/503-unavailable.html |
| +design-system/plain/frames/error-page/anything-else-the-generic-pair.html |
| +design-system/plain/frames/field-check/disabled.html |
| +design-system/plain/frames/field-check/off.html |
| +design-system/plain/frames/field-check/on.html |
| +design-system/plain/frames/field-date/bounded.html |
| +design-system/plain/frames/field-date/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/plain/frames/field-date/plain-the-bare-native-input.html |
| +design-system/plain/frames/field-date/required-with-an-error.html |
| +design-system/plain/frames/field-daterange/date-and-time-seeded.html |
| +design-system/plain/frames/field-daterange/dates-only-with-an-error-on-the-end.html |
| +design-system/plain/frames/field-datetime/bounded.html |
| +design-system/plain/frames/field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/plain/frames/field-datetime/plain-the-bare-native-input.html |
| +design-system/plain/frames/field-datetime/required-with-an-error.html |
| +design-system/plain/frames/field-money/bare.html |
| +design-system/plain/frames/field-money/with-a-hint.html |
| +design-system/plain/frames/field-money/with-an-error.html |
| +design-system/plain/frames/field-select/hand-written-with-optgroups.html |
| +design-system/plain/frames/field-select/required-with-an-error.html |
| +design-system/plain/frames/field-select/three-options-a-native-select.html |
| +design-system/plain/frames/field-select/twelve-options-plain-the-enhancement-opted-out.html |
| +design-system/plain/frames/field-select/twelve-options-the-filterable-combobox.html |
| +design-system/plain/frames/field-text/bare.html |
| +design-system/plain/frames/field-text/required.html |
| +design-system/plain/frames/field-text/the-headline-field.html |
| +design-system/plain/frames/field-text/with-a-hint.html |
| +design-system/plain/frames/field-text/with-an-error.html |
| +design-system/plain/frames/field-textarea/bare.html |
| +design-system/plain/frames/field-textarea/required.html |
| +design-system/plain/frames/field-textarea/with-a-hint.html |
| +design-system/plain/frames/field-textarea/with-an-error.html |
| +design-system/plain/frames/field-time/bounded.html |
| +design-system/plain/frames/field-time/enhanced-input-type-half-seven-or-noon.html |
| +design-system/plain/frames/field-time/plain-the-bare-native-input.html |
| +design-system/plain/frames/field-time/required-with-an-error.html |
| +design-system/plain/frames/field-url/bare.html |
| +design-system/plain/frames/field-url/with-a-hint.html |
| +design-system/plain/frames/field-url/with-an-error.html |
| +design-system/plain/frames/field/bare.html |
| +design-system/plain/frames/field/required.html |
| +design-system/plain/frames/field/short.html |
| +design-system/plain/frames/field/the-headline-field.html |
| +design-system/plain/frames/field/with-a-hint.html |
| +design-system/plain/frames/field/with-an-error.html |
| +design-system/plain/frames/field/with-help-under-the-control.html |
| +design-system/plain/frames/form-error/a-rejected-submission.html |
| +design-system/plain/frames/form-foot/idle-the-button-before-anything-happens.html |
| +design-system/plain/frames/form-foot/save-and-cancel.html |
| +design-system/plain/frames/form-foot/submit-only.html |
| +design-system/plain/frames/form-foot/working-what-busy-js-writes-on-the-way-out.html |
| +design-system/plain/frames/form-layout/default.html |
| +design-system/plain/frames/format-abbr/default.html |
| +design-system/plain/frames/format-addresses/default.html |
| +design-system/plain/frames/format-dates/default.html |
| +design-system/plain/frames/format-durations/default.html |
| +design-system/plain/frames/format-filesizes/default.html |
| +design-system/plain/frames/format-identifiers/default.html |
| +design-system/plain/frames/format-money/default.html |
| +design-system/plain/frames/format-numbers/default.html |
| +design-system/plain/frames/format-output/default.html |
| +design-system/plain/frames/format-people/default.html |
| +design-system/plain/frames/format-ratios/default.html |
| +design-system/plain/frames/help/default.html |
| +design-system/plain/frames/job-status/failed.html |
| +design-system/plain/frames/job-status/finished.html |
| +design-system/plain/frames/job-status/running.html |
| +design-system/plain/frames/layout-column/default.html |
| +design-system/plain/frames/layout-console-page/default.html |
| +design-system/plain/frames/layout-console/default.html |
| +design-system/plain/frames/layout-sidebar-page/default.html |
| +design-system/plain/frames/layout-sidebar/default.html |
| +design-system/plain/frames/layout-stage/default.html |
| +design-system/plain/frames/layout-topbar/default.html |
| +design-system/plain/frames/list-bar-search/empty-accessible-name-from-the-catalog.html |
| +design-system/plain/frames/list-bar-search/with-a-query-on-page-4.html |
| +design-system/plain/frames/list-bar-search/with-a-query.html |
| +design-system/plain/frames/list-bar/search-with-a-filter-dropdown.html |
| +design-system/plain/frames/list-grid/default.html |
| +design-system/plain/frames/list-row-action/lead-marker-status-and-an-action.html |
| +design-system/plain/frames/list-row-action/name-and-meta-line-only.html |
| +design-system/plain/frames/list-search-submit/default-label.html |
| +design-system/plain/frames/locale-menu/twelve-languages-current-highlighted.html |
| +design-system/plain/frames/media-grid/screenshots-a-logo-and-a-banner.html |
| +design-system/plain/frames/media-grid/select-mode-one-tile-selected.html |
| +design-system/plain/frames/meter/empty.html |
| +design-system/plain/frames/meter/full.html |
| +design-system/plain/frames/meter/nearly-full.html |
| +design-system/plain/frames/meter/over-budget.html |
| +design-system/plain/frames/meter/part-way.html |
| +design-system/plain/frames/modal/default.html |
| +design-system/plain/frames/notice/a-saved-message.html |
| +design-system/plain/frames/page-header/title-only.html |
| +design-system/plain/frames/page-header/title-subhead-and-a-primary-action.html |
| +design-system/plain/frames/pagination/first-page-default-accessible-name.html |
| +design-system/plain/frames/pagination/middle-of-a-long-list.html |
| +design-system/plain/frames/person/large-the-show-header-size.html |
| +design-system/plain/frames/person/name-and-email.html |
| +design-system/plain/frames/person/no-initial.html |
| +design-system/plain/frames/row-menu/links-a-form-and-a-destructive-item.html |
| +design-system/plain/frames/row-menu/links-only-for-a-list-inside-a-selection-form.html |
| +design-system/plain/frames/scheme-menu/standalone.html |
| +design-system/plain/frames/scheme-menu/three-choices-dark-current.html |
| +design-system/plain/frames/seg-tabs/two-sections.html |
| +design-system/plain/frames/selbox/default.html |
| +design-system/plain/frames/shell-sidebar/default.html |
| +design-system/plain/frames/shell-topbar/default.html |
| +design-system/plain/frames/signin/a-passkey.html |
| +design-system/plain/frames/signin/after-the-link-is-sent-in-another-browser.html |
| +design-system/plain/frames/signin/after-the-link-is-sent.html |
| +design-system/plain/frames/signin/an-address-that-does-not-look-right.html |
| +design-system/plain/frames/signin/asking-for-an-address.html |
| +design-system/plain/frames/signin/coming-back-with-an-email-link.html |
| +design-system/plain/frames/signin/coming-back-with-keymail.html |
| +design-system/plain/frames/signin/email-and-password.html |
| +design-system/plain/frames/signin/google-apple-and-the-rest.html |
| +design-system/plain/frames/signin/on-the-way-to-keymail.html |
| +design-system/plain/frames/signin/when-keymail-could-not-confirm-it.html |
| +design-system/plain/frames/signin/when-keymail-was-offered-and-a-link-went-out.html |
| +design-system/plain/frames/stat-band/default.html |
| +design-system/plain/frames/stat/a-shortened-number.html |
| +design-system/plain/frames/stat/a-smaller-number.html |
| +design-system/plain/frames/stat/the-big-number.html |
| +design-system/plain/frames/stat/when-going-down-is-good.html |
| +design-system/plain/frames/stat/when-going-up-is-bad.html |
| +design-system/plain/frames/stat/when-you-cannot-say.html |
| +design-system/plain/frames/status-pill/negative.html |
| +design-system/plain/frames/status-pill/neutral.html |
| +design-system/plain/frames/status-pill/positive.html |
| +design-system/plain/frames/status-pill/warning.html |
| +design-system/plain/frames/tblock/default.html |
| +design-system/plain/help.html |
| +design-system/plain/icons.html |
| +design-system/plain/index.html |
| +design-system/plain/job-status.html |
| +design-system/plain/layout-column.html |
| +design-system/plain/layout-console-page.html |
| +design-system/plain/layout-console.html |
| +design-system/plain/layout-sidebar-page.html |
| +design-system/plain/layout-sidebar.html |
| +design-system/plain/layout-stage.html |
| +design-system/plain/layout-topbar.html |
| +design-system/plain/list-bar-search.html |
| +design-system/plain/list-bar.html |
| +design-system/plain/list-grid.html |
| +design-system/plain/list-row-action.html |
| +design-system/plain/list-search-submit.html |
| +design-system/plain/locale-menu.html |
| +design-system/plain/media-grid.html |
| +design-system/plain/meter.html |
| +design-system/plain/modal.html |
| +design-system/plain/notice.html |
| +design-system/plain/page-header.html |
| +design-system/plain/pagination.html |
| +design-system/plain/person.html |
| +design-system/plain/row-menu.html |
| +design-system/plain/rtl/actor.html |
| +design-system/plain/rtl/api-tokens.html |
| +design-system/plain/rtl/back-nav.html |
| +design-system/plain/rtl/badge.html |
| +design-system/plain/rtl/box.html |
| +design-system/plain/rtl/bulk-bar.html |
| +design-system/plain/rtl/button.html |
| +design-system/plain/rtl/callout.html |
| +design-system/plain/rtl/choice-field.html |
| +design-system/plain/rtl/confirm-form.html |
| +design-system/plain/rtl/detail-list.html |
| +design-system/plain/rtl/dropdown.html |
| +design-system/plain/rtl/empty-state.html |
| +design-system/plain/rtl/error-page.html |
| +design-system/plain/rtl/field-check.html |
| +design-system/plain/rtl/field-date.html |
| +design-system/plain/rtl/field-daterange.html |
| +design-system/plain/rtl/field-datetime.html |
| +design-system/plain/rtl/field-money.html |
| +design-system/plain/rtl/field-select.html |
| +design-system/plain/rtl/field-text.html |
| +design-system/plain/rtl/field-textarea.html |
| +design-system/plain/rtl/field-time.html |
| +design-system/plain/rtl/field-url.html |
| +design-system/plain/rtl/field.html |
| +design-system/plain/rtl/form-error.html |
| +design-system/plain/rtl/form-foot.html |
| +design-system/plain/rtl/form-layout.html |
| +design-system/plain/rtl/format-abbr.html |
| +design-system/plain/rtl/format-addresses.html |
| +design-system/plain/rtl/format-dates.html |
| +design-system/plain/rtl/format-durations.html |
| +design-system/plain/rtl/format-filesizes.html |
| +design-system/plain/rtl/format-identifiers.html |
| +design-system/plain/rtl/format-money.html |
| +design-system/plain/rtl/format-numbers.html |
| +design-system/plain/rtl/format-output.html |
| +design-system/plain/rtl/format-people.html |
| +design-system/plain/rtl/format-ratios.html |
| +design-system/plain/rtl/frames/actor/a-person.html |
| +design-system/plain/rtl/frames/actor/an-agent-acting-for-a-person.html |
| +design-system/plain/rtl/frames/api-tokens/four-tokens-two-of-them-live.html |
| +design-system/plain/rtl/frames/api-tokens/just-created-the-secret-shown-once.html |
| +design-system/plain/rtl/frames/api-tokens/no-tokens-yet.html |
| +design-system/plain/rtl/frames/back-nav/back-to-a-record.html |
| +design-system/plain/rtl/frames/badge/negative.html |
| +design-system/plain/rtl/frames/badge/neutral.html |
| +design-system/plain/rtl/frames/badge/no-tone-quiet-default.html |
| +design-system/plain/rtl/frames/badge/positive.html |
| +design-system/plain/rtl/frames/badge/warning.html |
| +design-system/plain/rtl/frames/box/default.html |
| +design-system/plain/rtl/frames/bulk-bar/three-rows-selected.html |
| +design-system/plain/rtl/frames/button/default.html |
| +design-system/plain/rtl/frames/callout/info.html |
| +design-system/plain/rtl/frames/callout/negative-announced-as-an-alert.html |
| +design-system/plain/rtl/frames/callout/negative.html |
| +design-system/plain/rtl/frames/callout/positive.html |
| +design-system/plain/rtl/frames/callout/warning.html |
| +design-system/plain/rtl/frames/choice-field/pick-any.html |
| +design-system/plain/rtl/frames/choice-field/pick-one.html |
| +design-system/plain/rtl/frames/confirm-form/destructive.html |
| +design-system/plain/rtl/frames/confirm-form/ordinary-default-cancel-wording.html |
| +design-system/plain/rtl/frames/detail-list/four-facts-one-machine-ish.html |
| +design-system/plain/rtl/frames/dropdown/a-filter-menu-with-one-choice-applied.html |
| +design-system/plain/rtl/frames/dropdown/menu-idiom.html |
| +design-system/plain/rtl/frames/empty-state/sentence-only.html |
| +design-system/plain/rtl/frames/empty-state/with-a-post-call-to-action.html |
| +design-system/plain/rtl/frames/error-page/403-forbidden.html |
| +design-system/plain/rtl/frames/error-page/404-not-found.html |
| +design-system/plain/rtl/frames/error-page/422-unprocessable.html |
| +design-system/plain/rtl/frames/error-page/500-server-error.html |
| +design-system/plain/rtl/frames/error-page/503-unavailable.html |
| +design-system/plain/rtl/frames/error-page/anything-else-the-generic-pair.html |
| +design-system/plain/rtl/frames/field-check/disabled.html |
| +design-system/plain/rtl/frames/field-check/off.html |
| +design-system/plain/rtl/frames/field-check/on.html |
| +design-system/plain/rtl/frames/field-date/bounded.html |
| +design-system/plain/rtl/frames/field-date/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/plain/rtl/frames/field-date/plain-the-bare-native-input.html |
| +design-system/plain/rtl/frames/field-date/required-with-an-error.html |
| +design-system/plain/rtl/frames/field-daterange/date-and-time-seeded.html |
| +design-system/plain/rtl/frames/field-daterange/dates-only-with-an-error-on-the-end.html |
| +design-system/plain/rtl/frames/field-datetime/bounded.html |
| +design-system/plain/rtl/frames/field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/plain/rtl/frames/field-datetime/plain-the-bare-native-input.html |
| +design-system/plain/rtl/frames/field-datetime/required-with-an-error.html |
| +design-system/plain/rtl/frames/field-money/bare.html |
| +design-system/plain/rtl/frames/field-money/with-a-hint.html |
| +design-system/plain/rtl/frames/field-money/with-an-error.html |
| +design-system/plain/rtl/frames/field-select/hand-written-with-optgroups.html |
| +design-system/plain/rtl/frames/field-select/required-with-an-error.html |
| +design-system/plain/rtl/frames/field-select/three-options-a-native-select.html |
| +design-system/plain/rtl/frames/field-select/twelve-options-plain-the-enhancement-opted-out.html |
| +design-system/plain/rtl/frames/field-select/twelve-options-the-filterable-combobox.html |
| +design-system/plain/rtl/frames/field-text/bare.html |
| +design-system/plain/rtl/frames/field-text/required.html |
| +design-system/plain/rtl/frames/field-text/the-headline-field.html |
| +design-system/plain/rtl/frames/field-text/with-a-hint.html |
| +design-system/plain/rtl/frames/field-text/with-an-error.html |
| +design-system/plain/rtl/frames/field-textarea/bare.html |
| +design-system/plain/rtl/frames/field-textarea/required.html |
| +design-system/plain/rtl/frames/field-textarea/with-a-hint.html |
| +design-system/plain/rtl/frames/field-textarea/with-an-error.html |
| +design-system/plain/rtl/frames/field-time/bounded.html |
| +design-system/plain/rtl/frames/field-time/enhanced-input-type-half-seven-or-noon.html |
| +design-system/plain/rtl/frames/field-time/plain-the-bare-native-input.html |
| +design-system/plain/rtl/frames/field-time/required-with-an-error.html |
| +design-system/plain/rtl/frames/field-url/bare.html |
| +design-system/plain/rtl/frames/field-url/with-a-hint.html |
| +design-system/plain/rtl/frames/field-url/with-an-error.html |
| +design-system/plain/rtl/frames/field/bare.html |
| +design-system/plain/rtl/frames/field/required.html |
| +design-system/plain/rtl/frames/field/short.html |
| +design-system/plain/rtl/frames/field/the-headline-field.html |
| +design-system/plain/rtl/frames/field/with-a-hint.html |
| +design-system/plain/rtl/frames/field/with-an-error.html |
| +design-system/plain/rtl/frames/field/with-help-under-the-control.html |
| +design-system/plain/rtl/frames/form-error/a-rejected-submission.html |
| +design-system/plain/rtl/frames/form-foot/idle-the-button-before-anything-happens.html |
| +design-system/plain/rtl/frames/form-foot/save-and-cancel.html |
| +design-system/plain/rtl/frames/form-foot/submit-only.html |
| +design-system/plain/rtl/frames/form-foot/working-what-busy-js-writes-on-the-way-out.html |
| +design-system/plain/rtl/frames/form-layout/default.html |
| +design-system/plain/rtl/frames/format-abbr/default.html |
| +design-system/plain/rtl/frames/format-addresses/default.html |
| +design-system/plain/rtl/frames/format-dates/default.html |
| +design-system/plain/rtl/frames/format-durations/default.html |
| +design-system/plain/rtl/frames/format-filesizes/default.html |
| +design-system/plain/rtl/frames/format-identifiers/default.html |
| +design-system/plain/rtl/frames/format-money/default.html |
| +design-system/plain/rtl/frames/format-numbers/default.html |
| +design-system/plain/rtl/frames/format-output/default.html |
| +design-system/plain/rtl/frames/format-people/default.html |
| +design-system/plain/rtl/frames/format-ratios/default.html |
| +design-system/plain/rtl/frames/help/default.html |
| +design-system/plain/rtl/frames/job-status/failed.html |
| +design-system/plain/rtl/frames/job-status/finished.html |
| +design-system/plain/rtl/frames/job-status/running.html |
| +design-system/plain/rtl/frames/layout-column/default.html |
| +design-system/plain/rtl/frames/layout-console-page/default.html |
| +design-system/plain/rtl/frames/layout-console/default.html |
| +design-system/plain/rtl/frames/layout-sidebar-page/default.html |
| +design-system/plain/rtl/frames/layout-sidebar/default.html |
| +design-system/plain/rtl/frames/layout-stage/default.html |
| +design-system/plain/rtl/frames/layout-topbar/default.html |
| +design-system/plain/rtl/frames/list-bar-search/empty-accessible-name-from-the-catalog.html |
| +design-system/plain/rtl/frames/list-bar-search/with-a-query-on-page-4.html |
| +design-system/plain/rtl/frames/list-bar-search/with-a-query.html |
| +design-system/plain/rtl/frames/list-bar/search-with-a-filter-dropdown.html |
| +design-system/plain/rtl/frames/list-grid/default.html |
| +design-system/plain/rtl/frames/list-row-action/lead-marker-status-and-an-action.html |
| +design-system/plain/rtl/frames/list-row-action/name-and-meta-line-only.html |
| +design-system/plain/rtl/frames/list-search-submit/default-label.html |
| +design-system/plain/rtl/frames/locale-menu/twelve-languages-current-highlighted.html |
| +design-system/plain/rtl/frames/media-grid/screenshots-a-logo-and-a-banner.html |
| +design-system/plain/rtl/frames/media-grid/select-mode-one-tile-selected.html |
| +design-system/plain/rtl/frames/meter/empty.html |
| +design-system/plain/rtl/frames/meter/full.html |
| +design-system/plain/rtl/frames/meter/nearly-full.html |
| +design-system/plain/rtl/frames/meter/over-budget.html |
| +design-system/plain/rtl/frames/meter/part-way.html |
| +design-system/plain/rtl/frames/modal/default.html |
| +design-system/plain/rtl/frames/notice/a-saved-message.html |
| +design-system/plain/rtl/frames/page-header/title-only.html |
| +design-system/plain/rtl/frames/page-header/title-subhead-and-a-primary-action.html |
| +design-system/plain/rtl/frames/pagination/first-page-default-accessible-name.html |
| +design-system/plain/rtl/frames/pagination/middle-of-a-long-list.html |
| +design-system/plain/rtl/frames/person/large-the-show-header-size.html |
| +design-system/plain/rtl/frames/person/name-and-email.html |
| +design-system/plain/rtl/frames/person/no-initial.html |
| +design-system/plain/rtl/frames/row-menu/links-a-form-and-a-destructive-item.html |
| +design-system/plain/rtl/frames/row-menu/links-only-for-a-list-inside-a-selection-form.html |
| +design-system/plain/rtl/frames/scheme-menu/standalone.html |
| +design-system/plain/rtl/frames/scheme-menu/three-choices-dark-current.html |
| +design-system/plain/rtl/frames/seg-tabs/two-sections.html |
| +design-system/plain/rtl/frames/selbox/default.html |
| +design-system/plain/rtl/frames/shell-sidebar/default.html |
| +design-system/plain/rtl/frames/shell-topbar/default.html |
| +design-system/plain/rtl/frames/signin/a-passkey.html |
| +design-system/plain/rtl/frames/signin/after-the-link-is-sent-in-another-browser.html |
| +design-system/plain/rtl/frames/signin/after-the-link-is-sent.html |
| +design-system/plain/rtl/frames/signin/an-address-that-does-not-look-right.html |
| +design-system/plain/rtl/frames/signin/asking-for-an-address.html |
| +design-system/plain/rtl/frames/signin/coming-back-with-an-email-link.html |
| +design-system/plain/rtl/frames/signin/coming-back-with-keymail.html |
| +design-system/plain/rtl/frames/signin/email-and-password.html |
| +design-system/plain/rtl/frames/signin/google-apple-and-the-rest.html |
| +design-system/plain/rtl/frames/signin/on-the-way-to-keymail.html |
| +design-system/plain/rtl/frames/signin/when-keymail-could-not-confirm-it.html |
| +design-system/plain/rtl/frames/signin/when-keymail-was-offered-and-a-link-went-out.html |
| +design-system/plain/rtl/frames/stat-band/default.html |
| +design-system/plain/rtl/frames/stat/a-shortened-number.html |
| +design-system/plain/rtl/frames/stat/a-smaller-number.html |
| +design-system/plain/rtl/frames/stat/the-big-number.html |
| +design-system/plain/rtl/frames/stat/when-going-down-is-good.html |
| +design-system/plain/rtl/frames/stat/when-going-up-is-bad.html |
| +design-system/plain/rtl/frames/stat/when-you-cannot-say.html |
| +design-system/plain/rtl/frames/status-pill/negative.html |
| +design-system/plain/rtl/frames/status-pill/neutral.html |
| +design-system/plain/rtl/frames/status-pill/positive.html |
| +design-system/plain/rtl/frames/status-pill/warning.html |
| +design-system/plain/rtl/frames/tblock/default.html |
| +design-system/plain/rtl/help.html |
| +design-system/plain/rtl/icons.html |
| +design-system/plain/rtl/index.html |
| +design-system/plain/rtl/job-status.html |
| +design-system/plain/rtl/layout-column.html |
| +design-system/plain/rtl/layout-console-page.html |
| +design-system/plain/rtl/layout-console.html |
| +design-system/plain/rtl/layout-sidebar-page.html |
| +design-system/plain/rtl/layout-sidebar.html |
| +design-system/plain/rtl/layout-stage.html |
| +design-system/plain/rtl/layout-topbar.html |
| +design-system/plain/rtl/list-bar-search.html |
| +design-system/plain/rtl/list-bar.html |
| +design-system/plain/rtl/list-grid.html |
| +design-system/plain/rtl/list-row-action.html |
| +design-system/plain/rtl/list-search-submit.html |
| +design-system/plain/rtl/locale-menu.html |
| +design-system/plain/rtl/media-grid.html |
| +design-system/plain/rtl/meter.html |
| +design-system/plain/rtl/modal.html |
| +design-system/plain/rtl/notice.html |
| +design-system/plain/rtl/page-header.html |
| +design-system/plain/rtl/pagination.html |
| +design-system/plain/rtl/person.html |
| +design-system/plain/rtl/row-menu.html |
| +design-system/plain/rtl/scheme-menu.html |
| +design-system/plain/rtl/seg-tabs.html |
| +design-system/plain/rtl/selbox.html |
| +design-system/plain/rtl/shell-sidebar.html |
| +design-system/plain/rtl/shell-topbar.html |
| +design-system/plain/rtl/signin.html |
| +design-system/plain/rtl/stat-band.html |
| +design-system/plain/rtl/stat.html |
| +design-system/plain/rtl/status-pill.html |
| +design-system/plain/rtl/tblock.html |
| +design-system/plain/rtl/tokens.html |
| +design-system/plain/scheme-menu.html |
| +design-system/plain/seg-tabs.html |
| +design-system/plain/selbox.html |
| +design-system/plain/shell-sidebar.html |
| +design-system/plain/shell-topbar.html |
| +design-system/plain/signin.html |
| +design-system/plain/stat-band.html |
| +design-system/plain/stat.html |
| +design-system/plain/status-pill.html |
| +design-system/plain/tblock.html |
| +design-system/plain/tokens.html |
| +design-system/signal/actor.html |
| +design-system/signal/api-tokens.html |
| +design-system/signal/back-nav.html |
| +design-system/signal/badge.html |
| +design-system/signal/box.html |
| +design-system/signal/bulk-bar.html |
| +design-system/signal/button.html |
| +design-system/signal/callout.html |
| +design-system/signal/choice-field.html |
| +design-system/signal/confirm-form.html |
| +design-system/signal/detail-list.html |
| +design-system/signal/dropdown.html |
| +design-system/signal/empty-state.html |
| +design-system/signal/error-page.html |
| +design-system/signal/field-check.html |
| +design-system/signal/field-date.html |
| +design-system/signal/field-daterange.html |
| +design-system/signal/field-datetime.html |
| +design-system/signal/field-money.html |
| +design-system/signal/field-select.html |
| +design-system/signal/field-text.html |
| +design-system/signal/field-textarea.html |
| +design-system/signal/field-time.html |
| +design-system/signal/field-url.html |
| +design-system/signal/field.html |
| +design-system/signal/form-error.html |
| +design-system/signal/form-foot.html |
| +design-system/signal/form-layout.html |
| +design-system/signal/format-abbr.html |
| +design-system/signal/format-addresses.html |
| +design-system/signal/format-dates.html |
| +design-system/signal/format-durations.html |
| +design-system/signal/format-filesizes.html |
| +design-system/signal/format-identifiers.html |
| +design-system/signal/format-money.html |
| +design-system/signal/format-numbers.html |
| +design-system/signal/format-output.html |
| +design-system/signal/format-people.html |
| +design-system/signal/format-ratios.html |
| +design-system/signal/frames/actor/a-person.html |
| +design-system/signal/frames/actor/an-agent-acting-for-a-person.html |
| +design-system/signal/frames/api-tokens/four-tokens-two-of-them-live.html |
| +design-system/signal/frames/api-tokens/just-created-the-secret-shown-once.html |
| +design-system/signal/frames/api-tokens/no-tokens-yet.html |
| +design-system/signal/frames/back-nav/back-to-a-record.html |
| +design-system/signal/frames/badge/negative.html |
| +design-system/signal/frames/badge/neutral.html |
| +design-system/signal/frames/badge/no-tone-quiet-default.html |
| +design-system/signal/frames/badge/positive.html |
| +design-system/signal/frames/badge/warning.html |
| +design-system/signal/frames/box/default.html |
| +design-system/signal/frames/bulk-bar/three-rows-selected.html |
| +design-system/signal/frames/button/default.html |
| +design-system/signal/frames/callout/info.html |
| +design-system/signal/frames/callout/negative-announced-as-an-alert.html |
| +design-system/signal/frames/callout/negative.html |
| +design-system/signal/frames/callout/positive.html |
| +design-system/signal/frames/callout/warning.html |
| +design-system/signal/frames/choice-field/pick-any.html |
| +design-system/signal/frames/choice-field/pick-one.html |
| +design-system/signal/frames/confirm-form/destructive.html |
| +design-system/signal/frames/confirm-form/ordinary-default-cancel-wording.html |
| +design-system/signal/frames/detail-list/four-facts-one-machine-ish.html |
| +design-system/signal/frames/dropdown/a-filter-menu-with-one-choice-applied.html |
| +design-system/signal/frames/dropdown/menu-idiom.html |
| +design-system/signal/frames/empty-state/sentence-only.html |
| +design-system/signal/frames/empty-state/with-a-post-call-to-action.html |
| +design-system/signal/frames/error-page/403-forbidden.html |
| +design-system/signal/frames/error-page/404-not-found.html |
| +design-system/signal/frames/error-page/422-unprocessable.html |
| +design-system/signal/frames/error-page/500-server-error.html |
| +design-system/signal/frames/error-page/503-unavailable.html |
| +design-system/signal/frames/error-page/anything-else-the-generic-pair.html |
| +design-system/signal/frames/field-check/disabled.html |
| +design-system/signal/frames/field-check/off.html |
| +design-system/signal/frames/field-check/on.html |
| +design-system/signal/frames/field-date/bounded.html |
| +design-system/signal/frames/field-date/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/signal/frames/field-date/plain-the-bare-native-input.html |
| +design-system/signal/frames/field-date/required-with-an-error.html |
| +design-system/signal/frames/field-daterange/date-and-time-seeded.html |
| +design-system/signal/frames/field-daterange/dates-only-with-an-error-on-the-end.html |
| +design-system/signal/frames/field-datetime/bounded.html |
| +design-system/signal/frames/field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/signal/frames/field-datetime/plain-the-bare-native-input.html |
| +design-system/signal/frames/field-datetime/required-with-an-error.html |
| +design-system/signal/frames/field-money/bare.html |
| +design-system/signal/frames/field-money/with-a-hint.html |
| +design-system/signal/frames/field-money/with-an-error.html |
| +design-system/signal/frames/field-select/hand-written-with-optgroups.html |
| +design-system/signal/frames/field-select/required-with-an-error.html |
| +design-system/signal/frames/field-select/three-options-a-native-select.html |
| +design-system/signal/frames/field-select/twelve-options-plain-the-enhancement-opted-out.html |
| +design-system/signal/frames/field-select/twelve-options-the-filterable-combobox.html |
| +design-system/signal/frames/field-text/bare.html |
| +design-system/signal/frames/field-text/required.html |
| +design-system/signal/frames/field-text/the-headline-field.html |
| +design-system/signal/frames/field-text/with-a-hint.html |
| +design-system/signal/frames/field-text/with-an-error.html |
| +design-system/signal/frames/field-textarea/bare.html |
| +design-system/signal/frames/field-textarea/required.html |
| +design-system/signal/frames/field-textarea/with-a-hint.html |
| +design-system/signal/frames/field-textarea/with-an-error.html |
| +design-system/signal/frames/field-time/bounded.html |
| +design-system/signal/frames/field-time/enhanced-input-type-half-seven-or-noon.html |
| +design-system/signal/frames/field-time/plain-the-bare-native-input.html |
| +design-system/signal/frames/field-time/required-with-an-error.html |
| +design-system/signal/frames/field-url/bare.html |
| +design-system/signal/frames/field-url/with-a-hint.html |
| +design-system/signal/frames/field-url/with-an-error.html |
| +design-system/signal/frames/field/bare.html |
| +design-system/signal/frames/field/required.html |
| +design-system/signal/frames/field/short.html |
| +design-system/signal/frames/field/the-headline-field.html |
| +design-system/signal/frames/field/with-a-hint.html |
| +design-system/signal/frames/field/with-an-error.html |
| +design-system/signal/frames/field/with-help-under-the-control.html |
| +design-system/signal/frames/form-error/a-rejected-submission.html |
| +design-system/signal/frames/form-foot/idle-the-button-before-anything-happens.html |
| +design-system/signal/frames/form-foot/save-and-cancel.html |
| +design-system/signal/frames/form-foot/submit-only.html |
| +design-system/signal/frames/form-foot/working-what-busy-js-writes-on-the-way-out.html |
| +design-system/signal/frames/form-layout/default.html |
| +design-system/signal/frames/format-abbr/default.html |
| +design-system/signal/frames/format-addresses/default.html |
| +design-system/signal/frames/format-dates/default.html |
| +design-system/signal/frames/format-durations/default.html |
| +design-system/signal/frames/format-filesizes/default.html |
| +design-system/signal/frames/format-identifiers/default.html |
| +design-system/signal/frames/format-money/default.html |
| +design-system/signal/frames/format-numbers/default.html |
| +design-system/signal/frames/format-output/default.html |
| +design-system/signal/frames/format-people/default.html |
| +design-system/signal/frames/format-ratios/default.html |
| +design-system/signal/frames/help/default.html |
| +design-system/signal/frames/job-status/failed.html |
| +design-system/signal/frames/job-status/finished.html |
| +design-system/signal/frames/job-status/running.html |
| +design-system/signal/frames/layout-column/default.html |
| +design-system/signal/frames/layout-console-page/default.html |
| +design-system/signal/frames/layout-console/default.html |
| +design-system/signal/frames/layout-sidebar-page/default.html |
| +design-system/signal/frames/layout-sidebar/default.html |
| +design-system/signal/frames/layout-stage/default.html |
| +design-system/signal/frames/layout-topbar/default.html |
| +design-system/signal/frames/list-bar-search/empty-accessible-name-from-the-catalog.html |
| +design-system/signal/frames/list-bar-search/with-a-query-on-page-4.html |
| +design-system/signal/frames/list-bar-search/with-a-query.html |
| +design-system/signal/frames/list-bar/search-with-a-filter-dropdown.html |
| +design-system/signal/frames/list-grid/default.html |
| +design-system/signal/frames/list-row-action/lead-marker-status-and-an-action.html |
| +design-system/signal/frames/list-row-action/name-and-meta-line-only.html |
| +design-system/signal/frames/list-search-submit/default-label.html |
| +design-system/signal/frames/locale-menu/twelve-languages-current-highlighted.html |
| +design-system/signal/frames/media-grid/screenshots-a-logo-and-a-banner.html |
| +design-system/signal/frames/media-grid/select-mode-one-tile-selected.html |
| +design-system/signal/frames/meter/empty.html |
| +design-system/signal/frames/meter/full.html |
| +design-system/signal/frames/meter/nearly-full.html |
| +design-system/signal/frames/meter/over-budget.html |
| +design-system/signal/frames/meter/part-way.html |
| +design-system/signal/frames/modal/default.html |
| +design-system/signal/frames/notice/a-saved-message.html |
| +design-system/signal/frames/page-header/title-only.html |
| +design-system/signal/frames/page-header/title-subhead-and-a-primary-action.html |
| +design-system/signal/frames/pagination/first-page-default-accessible-name.html |
| +design-system/signal/frames/pagination/middle-of-a-long-list.html |
| +design-system/signal/frames/person/large-the-show-header-size.html |
| +design-system/signal/frames/person/name-and-email.html |
| +design-system/signal/frames/person/no-initial.html |
| +design-system/signal/frames/row-menu/links-a-form-and-a-destructive-item.html |
| +design-system/signal/frames/row-menu/links-only-for-a-list-inside-a-selection-form.html |
| +design-system/signal/frames/scheme-menu/standalone.html |
| +design-system/signal/frames/scheme-menu/three-choices-dark-current.html |
| +design-system/signal/frames/seg-tabs/two-sections.html |
| +design-system/signal/frames/selbox/default.html |
| +design-system/signal/frames/shell-sidebar/default.html |
| +design-system/signal/frames/shell-topbar/default.html |
| +design-system/signal/frames/signin/a-passkey.html |
| +design-system/signal/frames/signin/after-the-link-is-sent-in-another-browser.html |
| +design-system/signal/frames/signin/after-the-link-is-sent.html |
| +design-system/signal/frames/signin/an-address-that-does-not-look-right.html |
| +design-system/signal/frames/signin/asking-for-an-address.html |
| +design-system/signal/frames/signin/coming-back-with-an-email-link.html |
| +design-system/signal/frames/signin/coming-back-with-keymail.html |
| +design-system/signal/frames/signin/email-and-password.html |
| +design-system/signal/frames/signin/google-apple-and-the-rest.html |
| +design-system/signal/frames/signin/on-the-way-to-keymail.html |
| +design-system/signal/frames/signin/when-keymail-could-not-confirm-it.html |
| +design-system/signal/frames/signin/when-keymail-was-offered-and-a-link-went-out.html |
| +design-system/signal/frames/stat-band/default.html |
| +design-system/signal/frames/stat/a-shortened-number.html |
| +design-system/signal/frames/stat/a-smaller-number.html |
| +design-system/signal/frames/stat/the-big-number.html |
| +design-system/signal/frames/stat/when-going-down-is-good.html |
| +design-system/signal/frames/stat/when-going-up-is-bad.html |
| +design-system/signal/frames/stat/when-you-cannot-say.html |
| +design-system/signal/frames/status-pill/negative.html |
| +design-system/signal/frames/status-pill/neutral.html |
| +design-system/signal/frames/status-pill/positive.html |
| +design-system/signal/frames/status-pill/warning.html |
| +design-system/signal/frames/tblock/default.html |
| +design-system/signal/help.html |
| +design-system/signal/icons.html |
| +design-system/signal/index.html |
| +design-system/signal/job-status.html |
| +design-system/signal/layout-column.html |
| +design-system/signal/layout-console-page.html |
| +design-system/signal/layout-console.html |
| +design-system/signal/layout-sidebar-page.html |
| +design-system/signal/layout-sidebar.html |
| +design-system/signal/layout-stage.html |
| +design-system/signal/layout-topbar.html |
| +design-system/signal/list-bar-search.html |
| +design-system/signal/list-bar.html |
| +design-system/signal/list-grid.html |
| +design-system/signal/list-row-action.html |
| +design-system/signal/list-search-submit.html |
| +design-system/signal/locale-menu.html |
| +design-system/signal/media-grid.html |
| +design-system/signal/meter.html |
| +design-system/signal/modal.html |
| +design-system/signal/notice.html |
| +design-system/signal/page-header.html |
| +design-system/signal/pagination.html |
| +design-system/signal/person.html |
| +design-system/signal/row-menu.html |
| +design-system/signal/rtl/actor.html |
| +design-system/signal/rtl/api-tokens.html |
| +design-system/signal/rtl/back-nav.html |
| +design-system/signal/rtl/badge.html |
| +design-system/signal/rtl/box.html |
| +design-system/signal/rtl/bulk-bar.html |
| +design-system/signal/rtl/button.html |
| +design-system/signal/rtl/callout.html |
| +design-system/signal/rtl/choice-field.html |
| +design-system/signal/rtl/confirm-form.html |
| +design-system/signal/rtl/detail-list.html |
| +design-system/signal/rtl/dropdown.html |
| +design-system/signal/rtl/empty-state.html |
| +design-system/signal/rtl/error-page.html |
| +design-system/signal/rtl/field-check.html |
| +design-system/signal/rtl/field-date.html |
| +design-system/signal/rtl/field-daterange.html |
| +design-system/signal/rtl/field-datetime.html |
| +design-system/signal/rtl/field-money.html |
| +design-system/signal/rtl/field-select.html |
| +design-system/signal/rtl/field-text.html |
| +design-system/signal/rtl/field-textarea.html |
| +design-system/signal/rtl/field-time.html |
| +design-system/signal/rtl/field-url.html |
| +design-system/signal/rtl/field.html |
| +design-system/signal/rtl/form-error.html |
| +design-system/signal/rtl/form-foot.html |
| +design-system/signal/rtl/form-layout.html |
| +design-system/signal/rtl/format-abbr.html |
| +design-system/signal/rtl/format-addresses.html |
| +design-system/signal/rtl/format-dates.html |
| +design-system/signal/rtl/format-durations.html |
| +design-system/signal/rtl/format-filesizes.html |
| +design-system/signal/rtl/format-identifiers.html |
| +design-system/signal/rtl/format-money.html |
| +design-system/signal/rtl/format-numbers.html |
| +design-system/signal/rtl/format-output.html |
| +design-system/signal/rtl/format-people.html |
| +design-system/signal/rtl/format-ratios.html |
| +design-system/signal/rtl/frames/actor/a-person.html |
| +design-system/signal/rtl/frames/actor/an-agent-acting-for-a-person.html |
| +design-system/signal/rtl/frames/api-tokens/four-tokens-two-of-them-live.html |
| +design-system/signal/rtl/frames/api-tokens/just-created-the-secret-shown-once.html |
| +design-system/signal/rtl/frames/api-tokens/no-tokens-yet.html |
| +design-system/signal/rtl/frames/back-nav/back-to-a-record.html |
| +design-system/signal/rtl/frames/badge/negative.html |
| +design-system/signal/rtl/frames/badge/neutral.html |
| +design-system/signal/rtl/frames/badge/no-tone-quiet-default.html |
| +design-system/signal/rtl/frames/badge/positive.html |
| +design-system/signal/rtl/frames/badge/warning.html |
| +design-system/signal/rtl/frames/box/default.html |
| +design-system/signal/rtl/frames/bulk-bar/three-rows-selected.html |
| +design-system/signal/rtl/frames/button/default.html |
| +design-system/signal/rtl/frames/callout/info.html |
| +design-system/signal/rtl/frames/callout/negative-announced-as-an-alert.html |
| +design-system/signal/rtl/frames/callout/negative.html |
| +design-system/signal/rtl/frames/callout/positive.html |
| +design-system/signal/rtl/frames/callout/warning.html |
| +design-system/signal/rtl/frames/choice-field/pick-any.html |
| +design-system/signal/rtl/frames/choice-field/pick-one.html |
| +design-system/signal/rtl/frames/confirm-form/destructive.html |
| +design-system/signal/rtl/frames/confirm-form/ordinary-default-cancel-wording.html |
| +design-system/signal/rtl/frames/detail-list/four-facts-one-machine-ish.html |
| +design-system/signal/rtl/frames/dropdown/a-filter-menu-with-one-choice-applied.html |
| +design-system/signal/rtl/frames/dropdown/menu-idiom.html |
| +design-system/signal/rtl/frames/empty-state/sentence-only.html |
| +design-system/signal/rtl/frames/empty-state/with-a-post-call-to-action.html |
| +design-system/signal/rtl/frames/error-page/403-forbidden.html |
| +design-system/signal/rtl/frames/error-page/404-not-found.html |
| +design-system/signal/rtl/frames/error-page/422-unprocessable.html |
| +design-system/signal/rtl/frames/error-page/500-server-error.html |
| +design-system/signal/rtl/frames/error-page/503-unavailable.html |
| +design-system/signal/rtl/frames/error-page/anything-else-the-generic-pair.html |
| +design-system/signal/rtl/frames/field-check/disabled.html |
| +design-system/signal/rtl/frames/field-check/off.html |
| +design-system/signal/rtl/frames/field-check/on.html |
| +design-system/signal/rtl/frames/field-date/bounded.html |
| +design-system/signal/rtl/frames/field-date/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/signal/rtl/frames/field-date/plain-the-bare-native-input.html |
| +design-system/signal/rtl/frames/field-date/required-with-an-error.html |
| +design-system/signal/rtl/frames/field-daterange/date-and-time-seeded.html |
| +design-system/signal/rtl/frames/field-daterange/dates-only-with-an-error-on-the-end.html |
| +design-system/signal/rtl/frames/field-datetime/bounded.html |
| +design-system/signal/rtl/frames/field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks.html |
| +design-system/signal/rtl/frames/field-datetime/plain-the-bare-native-input.html |
| +design-system/signal/rtl/frames/field-datetime/required-with-an-error.html |
| +design-system/signal/rtl/frames/field-money/bare.html |
| +design-system/signal/rtl/frames/field-money/with-a-hint.html |
| +design-system/signal/rtl/frames/field-money/with-an-error.html |
| +design-system/signal/rtl/frames/field-select/hand-written-with-optgroups.html |
| +design-system/signal/rtl/frames/field-select/required-with-an-error.html |
| +design-system/signal/rtl/frames/field-select/three-options-a-native-select.html |
| +design-system/signal/rtl/frames/field-select/twelve-options-plain-the-enhancement-opted-out.html |
| +design-system/signal/rtl/frames/field-select/twelve-options-the-filterable-combobox.html |
| +design-system/signal/rtl/frames/field-text/bare.html |
| +design-system/signal/rtl/frames/field-text/required.html |
| +design-system/signal/rtl/frames/field-text/the-headline-field.html |
| +design-system/signal/rtl/frames/field-text/with-a-hint.html |
| +design-system/signal/rtl/frames/field-text/with-an-error.html |
| +design-system/signal/rtl/frames/field-textarea/bare.html |
| +design-system/signal/rtl/frames/field-textarea/required.html |
| +design-system/signal/rtl/frames/field-textarea/with-a-hint.html |
| +design-system/signal/rtl/frames/field-textarea/with-an-error.html |
| +design-system/signal/rtl/frames/field-time/bounded.html |
| +design-system/signal/rtl/frames/field-time/enhanced-input-type-half-seven-or-noon.html |
| +design-system/signal/rtl/frames/field-time/plain-the-bare-native-input.html |
| +design-system/signal/rtl/frames/field-time/required-with-an-error.html |
| +design-system/signal/rtl/frames/field-url/bare.html |
| +design-system/signal/rtl/frames/field-url/with-a-hint.html |
| +design-system/signal/rtl/frames/field-url/with-an-error.html |
| +design-system/signal/rtl/frames/field/bare.html |
| +design-system/signal/rtl/frames/field/required.html |
| +design-system/signal/rtl/frames/field/short.html |
| +design-system/signal/rtl/frames/field/the-headline-field.html |
| +design-system/signal/rtl/frames/field/with-a-hint.html |
| +design-system/signal/rtl/frames/field/with-an-error.html |
| +design-system/signal/rtl/frames/field/with-help-under-the-control.html |
| +design-system/signal/rtl/frames/form-error/a-rejected-submission.html |
| +design-system/signal/rtl/frames/form-foot/idle-the-button-before-anything-happens.html |
| +design-system/signal/rtl/frames/form-foot/save-and-cancel.html |
| +design-system/signal/rtl/frames/form-foot/submit-only.html |
| +design-system/signal/rtl/frames/form-foot/working-what-busy-js-writes-on-the-way-out.html |
| +design-system/signal/rtl/frames/form-layout/default.html |
| +design-system/signal/rtl/frames/format-abbr/default.html |
| +design-system/signal/rtl/frames/format-addresses/default.html |
| +design-system/signal/rtl/frames/format-dates/default.html |
| +design-system/signal/rtl/frames/format-durations/default.html |
| +design-system/signal/rtl/frames/format-filesizes/default.html |
| +design-system/signal/rtl/frames/format-identifiers/default.html |
| +design-system/signal/rtl/frames/format-money/default.html |
| +design-system/signal/rtl/frames/format-numbers/default.html |
| +design-system/signal/rtl/frames/format-output/default.html |
| +design-system/signal/rtl/frames/format-people/default.html |
| +design-system/signal/rtl/frames/format-ratios/default.html |
| +design-system/signal/rtl/frames/help/default.html |
| +design-system/signal/rtl/frames/job-status/failed.html |
| +design-system/signal/rtl/frames/job-status/finished.html |
| +design-system/signal/rtl/frames/job-status/running.html |
| +design-system/signal/rtl/frames/layout-column/default.html |
| +design-system/signal/rtl/frames/layout-console-page/default.html |
| +design-system/signal/rtl/frames/layout-console/default.html |
| +design-system/signal/rtl/frames/layout-sidebar-page/default.html |
| +design-system/signal/rtl/frames/layout-sidebar/default.html |
| +design-system/signal/rtl/frames/layout-stage/default.html |
| +design-system/signal/rtl/frames/layout-topbar/default.html |
| +design-system/signal/rtl/frames/list-bar-search/empty-accessible-name-from-the-catalog.html |
| +design-system/signal/rtl/frames/list-bar-search/with-a-query-on-page-4.html |
| +design-system/signal/rtl/frames/list-bar-search/with-a-query.html |
| +design-system/signal/rtl/frames/list-bar/search-with-a-filter-dropdown.html |
| +design-system/signal/rtl/frames/list-grid/default.html |
| +design-system/signal/rtl/frames/list-row-action/lead-marker-status-and-an-action.html |
| +design-system/signal/rtl/frames/list-row-action/name-and-meta-line-only.html |
| +design-system/signal/rtl/frames/list-search-submit/default-label.html |
| +design-system/signal/rtl/frames/locale-menu/twelve-languages-current-highlighted.html |
| +design-system/signal/rtl/frames/media-grid/screenshots-a-logo-and-a-banner.html |
| +design-system/signal/rtl/frames/media-grid/select-mode-one-tile-selected.html |
| +design-system/signal/rtl/frames/meter/empty.html |
| +design-system/signal/rtl/frames/meter/full.html |
| +design-system/signal/rtl/frames/meter/nearly-full.html |
| +design-system/signal/rtl/frames/meter/over-budget.html |
| +design-system/signal/rtl/frames/meter/part-way.html |
| +design-system/signal/rtl/frames/modal/default.html |
| +design-system/signal/rtl/frames/notice/a-saved-message.html |
| +design-system/signal/rtl/frames/page-header/title-only.html |
| +design-system/signal/rtl/frames/page-header/title-subhead-and-a-primary-action.html |
| +design-system/signal/rtl/frames/pagination/first-page-default-accessible-name.html |
| +design-system/signal/rtl/frames/pagination/middle-of-a-long-list.html |
| +design-system/signal/rtl/frames/person/large-the-show-header-size.html |
| +design-system/signal/rtl/frames/person/name-and-email.html |
| +design-system/signal/rtl/frames/person/no-initial.html |
| +design-system/signal/rtl/frames/row-menu/links-a-form-and-a-destructive-item.html |
| +design-system/signal/rtl/frames/row-menu/links-only-for-a-list-inside-a-selection-form.html |
| +design-system/signal/rtl/frames/scheme-menu/standalone.html |
| +design-system/signal/rtl/frames/scheme-menu/three-choices-dark-current.html |
| +design-system/signal/rtl/frames/seg-tabs/two-sections.html |
| +design-system/signal/rtl/frames/selbox/default.html |
| +design-system/signal/rtl/frames/shell-sidebar/default.html |
| +design-system/signal/rtl/frames/shell-topbar/default.html |
| +design-system/signal/rtl/frames/signin/a-passkey.html |
| +design-system/signal/rtl/frames/signin/after-the-link-is-sent-in-another-browser.html |
| +design-system/signal/rtl/frames/signin/after-the-link-is-sent.html |
| +design-system/signal/rtl/frames/signin/an-address-that-does-not-look-right.html |
| +design-system/signal/rtl/frames/signin/asking-for-an-address.html |
| +design-system/signal/rtl/frames/signin/coming-back-with-an-email-link.html |
| +design-system/signal/rtl/frames/signin/coming-back-with-keymail.html |
| +design-system/signal/rtl/frames/signin/email-and-password.html |
| +design-system/signal/rtl/frames/signin/google-apple-and-the-rest.html |
| +design-system/signal/rtl/frames/signin/on-the-way-to-keymail.html |
| +design-system/signal/rtl/frames/signin/when-keymail-could-not-confirm-it.html |
| +design-system/signal/rtl/frames/signin/when-keymail-was-offered-and-a-link-went-out.html |
| +design-system/signal/rtl/frames/stat-band/default.html |
| +design-system/signal/rtl/frames/stat/a-shortened-number.html |
| +design-system/signal/rtl/frames/stat/a-smaller-number.html |
| +design-system/signal/rtl/frames/stat/the-big-number.html |
| +design-system/signal/rtl/frames/stat/when-going-down-is-good.html |
| +design-system/signal/rtl/frames/stat/when-going-up-is-bad.html |
| +design-system/signal/rtl/frames/stat/when-you-cannot-say.html |
| +design-system/signal/rtl/frames/status-pill/negative.html |
| +design-system/signal/rtl/frames/status-pill/neutral.html |
| +design-system/signal/rtl/frames/status-pill/positive.html |
| +design-system/signal/rtl/frames/status-pill/warning.html |
| +design-system/signal/rtl/frames/tblock/default.html |
| +design-system/signal/rtl/help.html |
| +design-system/signal/rtl/icons.html |
| +design-system/signal/rtl/index.html |
| +design-system/signal/rtl/job-status.html |
| +design-system/signal/rtl/layout-column.html |
| +design-system/signal/rtl/layout-console-page.html |
| +design-system/signal/rtl/layout-console.html |
| +design-system/signal/rtl/layout-sidebar-page.html |
| +design-system/signal/rtl/layout-sidebar.html |
| +design-system/signal/rtl/layout-stage.html |
| +design-system/signal/rtl/layout-topbar.html |
| +design-system/signal/rtl/list-bar-search.html |
| +design-system/signal/rtl/list-bar.html |
| +design-system/signal/rtl/list-grid.html |
| +design-system/signal/rtl/list-row-action.html |
| +design-system/signal/rtl/list-search-submit.html |
| +design-system/signal/rtl/locale-menu.html |
| +design-system/signal/rtl/media-grid.html |
| +design-system/signal/rtl/meter.html |
| +design-system/signal/rtl/modal.html |
| +design-system/signal/rtl/notice.html |
| +design-system/signal/rtl/page-header.html |
| +design-system/signal/rtl/pagination.html |
| +design-system/signal/rtl/person.html |
| +design-system/signal/rtl/row-menu.html |
| +design-system/signal/rtl/scheme-menu.html |
| +design-system/signal/rtl/seg-tabs.html |
| +design-system/signal/rtl/selbox.html |
| +design-system/signal/rtl/shell-sidebar.html |
| +design-system/signal/rtl/shell-topbar.html |
| +design-system/signal/rtl/signin.html |
| +design-system/signal/rtl/stat-band.html |
| +design-system/signal/rtl/stat.html |
| +design-system/signal/rtl/status-pill.html |
| +design-system/signal/rtl/tblock.html |
| +design-system/signal/rtl/tokens.html |
| +design-system/signal/scheme-menu.html |
| +design-system/signal/seg-tabs.html |
| +design-system/signal/selbox.html |
| +design-system/signal/shell-sidebar.html |
| +design-system/signal/shell-topbar.html |
| +design-system/signal/signin.html |
| +design-system/signal/stat-band.html |
| +design-system/signal/stat.html |
| +design-system/signal/status-pill.html |
| +design-system/signal/tblock.html |
| +design-system/signal/tokens.html |
diff --git a/internal/www/testdata/part3-ids.txt b/internal/www/testdata/part3-ids.txt| new file mode 100644 |
| index 0000000..cb55099 |
| --- /dev/null |
| +++ b/internal/www/testdata/part3-ids.txt |
| @@ -0,0 +1,16 @@ |
| +# The element ids on a sample of part 3's example, family, Tokens and |
| +# Icons pages, in each theme, left to right and right to left, one page |
| +# a line: its tree path, then its ids, sorted. A part 3 link with a |
| +# fragment keeps it through the redirect, so the page it lands on has |
| +# to carry every one. From the same build of 6af218e as |
| +# part3-addresses.txt; to regenerate, in that build's public/: |
| +# |
| +# for p in <the paths below>; do |
| +# printf '%s' "$p"; grep -o ' id="[^"]*"' "$p" | sed 's/^ id="\(.*\)"$/ \1/' | LC_ALL=C sort -u | tr -d '\n'; echo |
| +# done |
| +design-system/day/signin.html a-passkey after-the-link-is-sent after-the-link-is-sent-in-another-browser an-address-that-does-not-look-right asking-for-an-address coming-back-with-an-email-link coming-back-with-keymail email-and-password google-apple-and-the-rest main on-the-way-to-keymail when-keymail-could-not-confirm-it when-keymail-was-offered-and-a-link-went-out |
| +design-system/day/rtl/tokens.html main tokens-accent tokens-controls tokens-depth tokens-radius tokens-spacing tokens-status-tones tokens-surfaces-and-lines tokens-text tokens-type-choices tokens-type-family tokens-type-scale |
| +design-system/signal/icons.html icon-alert-triangle icon-check icon-check-circle icon-chevron-down icon-help-circle icon-info icon-kebab icon-menu icon-plus icon-search icon-x icon-x-circle main |
| +design-system/signal/rtl/field.html bare main required short the-headline-field with-a-hint with-an-error with-help-under-the-control |
| +design-system/plain/stat.html a-shortened-number a-smaller-number main the-big-number when-going-down-is-good when-going-up-is-bad when-you-cannot-say |
| +design-system/plain/rtl/error-page.html 403-forbidden 404-not-found 422-unprocessable 500-server-error 503-unavailable anything-else-the-generic-pair main |
diff --git a/internal/www/www.go b/internal/www/www.go| index 21579b3..38b58c1 100644 |
| --- a/internal/www/www.go |
| +++ b/internal/www/www.go |
| @@ -122,3 +122,19 @@ func Pages(src fs.FS, siteDir fs.FS) (map[string][]byte, error) { |
| } |
| return out, nil |
| } |
| + |
| +// Redirects are the _redirects rules the pages need beside the files: |
| +// the design system's (gallery.Redirects), which send the addresses |
| +// its pages had before they moved under a locale to the pages there |
| +// now. pages is what Pages returned; site.Build writes the rules after |
| +// the release rewrites. |
| +func Redirects(pages map[string][]byte) []string { |
| + prefix := strings.TrimPrefix(Mount, "/") + "/" |
| + ds := map[string][]byte{} |
| + for name, data := range pages { |
| + if rest, ok := strings.CutPrefix(name, prefix); ok { |
| + ds[rest] = data |
| + } |
| + } |
| + return gallery.Redirects(Mount, ds) |
| +} |