package gallery
import (
"html/template"
"strings"
"amadan.net/rastrillo/fiesta"
)
// The rail's model: one section per page kind with the entries the
// page anchors, read off the same data the page renders from, so an
// entry pointing at nothing and a section with no way to reach it are
// both impossible. The rail is the same on every page apart from the
// current marks: one list of the whole vocabulary rather than a table
// of contents per page, which TestTheRailIsTheSameOnEveryPage holds
// literally. The phone index is read off the rail, so the rows and
// the tree cannot list different sections.
// navItem is one line in the rail. Code marks an entry whose label is
// an identifier (an example's name, an icon's slug), drawn in the mono
// face. Blank marks an entry that leaves the page and opens in a new
// tab. Aria is the accessible name where it has to differ from the
// visible label; Terms the synonyms the filter matches beside it.
// Current marks the entry that is the page being rendered: an
// example's own line on its page.
type navItem struct {
Label, Href string
Code, Blank, Current bool
Aria, Terms string
}
// searchTerms are the words people call an example by that its name
// does not contain, keyed by example name and matched by the filter
// beside the name. Rastrillo keys them by anchor id; Fiesta's rail
// entries are example pages, so the key is the page's stem.
// TestEverySearchTermNamesAnAnchor holds every key to a real example
// and every rail link to its terms.
var searchTerms = map[string]string{
"field-check": "checkbox toggle switch",
"modal": "dialog popup",
"choice-field": "radio",
"list-grid": "table",
"box": "card panel",
"person": "avatar user",
"button": "button submit cta",
"snippet": "code copy clipboard",
"side-nav": "sidebar navigation sections",
}
// shellNames are the shells the Demos group opens, in the order the
// layout examples are read: the five whose frame document is an app
// laid out whole. The -page pair members are the content pages of the
// sidebar and console shells and are reached from inside those.
var shellNames = []string{"column", "topbar", "sidebar", "console", "stage"}
// navSection is one group in the rail: a page of the directory, or
// the Demos. Href is the section's own page and Current says whether
// that page is the one being rendered.
type navSection struct {
Title, Href, Kind string
Current bool
Items []navItem
}
// indexRow is one row of the phone index: a section's page, by title,
// carrying the id its pages' back controls name.
type indexRow struct{ ID, Title, Href string }
// localeLink is one entry of the language menu: the locale's code, its
// name in its own language, its direction, this page in that locale,
// and whether it is the page's own.
type localeLink struct {
Code, Name, Dir, Href string
Current bool
}
// controls is one copy of the switchers: the theme tabs, the scheme
// menu (Fiesta's, lifted from the scheme-menu example, so fiesta.js
// owns the scheme) and the language menu. Every page writes them for
// the bar and, on a phone, for the display settings menu, from this
// one value, so the two cannot differ.
type controls struct {
Themes []link
Scheme template.HTML
Locales []localeLink
LocaleName string
}
// tokenNav is one entry per token group on the Tokens page, in page
// order: the theme's palette groups, then the structure every theme
// shares.
func (s *site) tokenNav(p page) []navItem {
file := s.href(p.Theme, p.Locale, s.fileOf("tokens"))
var items []navItem
for _, g := range s.tokenGroups(p.Theme) {
items = append(items, navItem{Label: s.say(p.Locale, "gallery.tokens.group."+g.Key), Href: file + "#" + anchorID("tokens", g.Key)})
}
return items
}
// iconNav is one entry per icon, by slug, in the order the page draws
// them.
func (s *site) iconNav(p page) []navItem {
file := s.href(p.Theme, p.Locale, s.fileOf("icons"))
var items []navItem
for _, ic := range s.icons {
items = append(items, navItem{Label: ic.Slug, Href: file + "#" + anchorID("icon", ic.Slug), Code: true})
}
return items
}
// familyNav is one family's entries: its examples, by name, each the
// link to its own page. It closes over the key because the rail is the
// same on every page: every family's section lists that family's
// examples wherever the reader is standing.
func (s *site) familyNav(key string) func(p page) []navItem {
return func(p page) []navItem {
var items []navItem
for _, f := range s.families {
if f.Key != key {
continue
}
for _, e := range f.Examples {
items = append(items, navItem{Label: e.Name, Href: s.href(p.Theme, p.Locale, e.Name+".html"), Code: true})
}
}
return items
}
}
// controls builds the switchers for one page: this page in every
// theme, the scheme menu, and this page in every locale, each language
// named in itself. The links carry no fragment; the script adds the
// reader's place at the click.
func (s *site) controls(p page) controls {
c := controls{Scheme: s.schemeIn(p.Locale), LocaleName: s.say(p.Locale, "gallery.locale.name")}
for _, theme := range fiesta.ThemeNames() {
c.Themes = append(c.Themes, link{Href: s.href(theme, p.Locale, p.File), Label: theme, Current: theme == p.Theme})
}
for _, code := range Locales {
c.Locales = append(c.Locales, localeLink{
Code: code,
Name: endonym(code),
Dir: Dir(code),
Href: s.href(p.Theme, code, p.File),
Current: code == p.Locale,
})
}
return c
}
// galleryNav is the rail for one page: one section per page kind, in
// table order, each with its overview item first and then the entries
// its page anchors, with the search terms on the entries that have
// them; then the Demos. The section whose kind is the page's is
// Current (on an example page that is its family's), and on an
// example page the example's own entry is Current too.
func (s *site) galleryNav(p page) []navSection {
self := s.href(p.Theme, p.Locale, p.File)
out := make([]navSection, 0, len(s.pageKinds())+1)
for _, pk := range s.pageKinds() {
section := navSection{
Title: s.titleOf(p.Locale, pk),
Href: s.href(p.Theme, p.Locale, pk.File),
Kind: pk.Kind,
Current: pk.Kind == p.Kind,
}
if pk.Nav != nil {
// The section overview link, first, before the entries. A
// section that lists anything draws its title as a
// , which discloses rather than navigates, so
// without this the section would have no way to its own
// page. A section with nothing to list is already a plain
// link to its page and needs no second one under it.
section.Items = append([]navItem{s.sectionOverview(p, section)}, pk.Nav(p)...)
for i := 1; i < len(section.Items); i++ {
item := §ion.Items[i]
item.Terms = searchTerms[stemOf(item.Href)]
item.Current = item.Href == self
}
}
out = append(out, section)
}
return append(out, s.demos(p))
}
// stemOf is the file name a page link ends in, without its extension
// and fragment: the example's name for an example page's link, and ""
// for anything else.
func stemOf(href string) string {
file := href[strings.LastIndex(href, "/")+1:]
if i := strings.Index(file, "#"); i >= 0 {
return ""
}
return strings.TrimSuffix(file, ".html")
}
// sectionOverview is one section's route to the top of its own page:
// the first item under it, labelled Overview, which is also the word
// the rail's filter matches on, and named for a screen reader by the
// section it belongs to.
//
// The visible label is deliberately the same on every section and the
// accessible name deliberately is not. Twelve links reading "Overview"
// in one navigation landmark are unambiguous nested under their
// headings and ambiguous read out in a list, so the accessible name
// carries the section: "Tokens overview". It contains the visible
// label, so WCAG 2.2 SC 2.5.3 Label in Name is satisfied without
// changing the word a sighted reader sees or types into the filter.
func (s *site) sectionOverview(p page, section navSection) navItem {
return navItem{
Label: s.say(p.Locale, "gallery.nav.overview"),
Href: section.Href,
Aria: s.say(p.Locale, "gallery.nav.overview.aria", "section", section.Title),
}
}
// demos is the group at the foot of the rail, every entry opening in a
// tab of its own: the five shells, each the layout example's default
// frame document, which is the shell laid out whole as the app it
// stands for; then the modal route and the demo application.
func (s *site) demos(p page) navSection {
out := navSection{Title: s.say(p.Locale, "gallery.nav.demos")}
for _, shell := range shellNames {
out.Items = append(out.Items, navItem{
Label: s.say(p.Locale, "gallery.demos.shell", "shell", shell),
Href: s.layoutHref(p.Theme, p.Locale, "layout-"+shell),
Blank: true,
})
}
out.Items = append(out.Items,
navItem{Label: s.say(p.Locale, "gallery.demos.modal"), Href: s.modalHref(p.Theme, p.Locale), Blank: true},
navItem{Label: s.say(p.Locale, "gallery.demos.app"), Href: s.demoHref(p.Theme, p.Locale), Blank: true},
)
return out
}
// indexRows reads the phone index off the rail, so the rows and the
// tree cannot list different sections: one row per page kind but the
// Overview, which is the index itself, and the Demos section as is.
// Rows are plain links, not the tree's disclosures, because shell.js
// returns focus to the first nav link whose path is the page just
// left, and in the tree that link is inside a closed that
// cannot take focus. anchorID never writes a nav- id, and the kinds
// are unique, so the ids cannot collide with anything else on the
// page.
func indexRows(nav []navSection) ([]indexRow, navSection) {
rows := make([]indexRow, 0, len(nav))
for _, s := range nav[:len(nav)-1] {
if s.Kind == "overview" {
continue
}
rows = append(rows, indexRow{ID: "nav-" + s.Kind, Title: s.Title, Href: s.Href})
}
return rows, nav[len(nav)-1]
}
// phoneIndex sets the Overview's phone index, Rows and Demos, on the
// page being built: the Overview is the shell's index page below
// 800px, and every other page a content page with a way back to its
// row, so only the Overview carries them.
func (s *site) phoneIndex(p *page) {
if p.Kind != "overview" {
return
}
p.Rows, p.Demos = indexRows(p.Nav)
}