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