rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package gallery
2
3import (
4 "html/template"
5 "strings"
6
7 "amadan.net/rastrillo/fiesta"
8)
9
10// The rail's model: one section per page kind with the entries the
11// page anchors, read off the same data the page renders from, so an
12// entry pointing at nothing and a section with no way to reach it are
13// both impossible. The rail is the same on every page apart from the
14// current marks: one list of the whole vocabulary rather than a table
15// of contents per page, which TestTheRailIsTheSameOnEveryPage holds
16// literally. The phone index is read off the rail, so the rows and
17// the tree cannot list different sections.
18
19// navItem is one line in the rail. Code marks an entry whose label is
20// an identifier (an example's name, an icon's slug), drawn in the mono
21// face. Blank marks an entry that leaves the page and opens in a new
22// tab. Aria is the accessible name where it has to differ from the
23// visible label; Terms the synonyms the filter matches beside it.
24// Current marks the entry that is the page being rendered: an
25// example's own line on its page.
26type navItem struct {
27 Label, Href string
28 Code, Blank, Current bool
29 Aria, Terms string
30}
31
32// searchTerms are the words people call an example by that its name
33// does not contain, keyed by example name and matched by the filter
34// beside the name. Rastrillo keys them by anchor id; Fiesta's rail
35// entries are example pages, so the key is the page's stem.
36// TestEverySearchTermNamesAnAnchor holds every key to a real example
37// and every rail link to its terms.
38var searchTerms = map[string]string{
39 "field-check": "checkbox toggle switch",
40 "modal": "dialog popup",
41 "choice-field": "radio",
42 "list-grid": "table",
43 "box": "card panel",
44 "person": "avatar user",
45 "button": "button submit cta",
46 "snippet": "code copy clipboard",
47 "side-nav": "sidebar navigation sections",
48}
49
50// shellNames are the shells the Demos group opens, in the order the
51// layout examples are read: the five whose frame document is an app
52// laid out whole. The -page pair members are the content pages of the
53// sidebar and console shells and are reached from inside those.
54var shellNames = []string{"column", "topbar", "sidebar", "console", "stage"}
55
56// navSection is one group in the rail: a page of the directory, or
57// the Demos. Href is the section's own page and Current says whether
58// that page is the one being rendered.
59type navSection struct {
60 Title, Href, Kind string
61 Current bool
62 Items []navItem
63}
64
65// indexRow is one row of the phone index: a section's page, by title,
66// carrying the id its pages' back controls name.
67type indexRow struct{ ID, Title, Href string }
68
69// localeLink is one entry of the language menu: the locale's code, its
70// name in its own language, its direction, this page in that locale,
71// and whether it is the page's own.
72type localeLink struct {
73 Code, Name, Dir, Href string
74 Current bool
75}
76
77// controls is one copy of the switchers: the theme tabs, the scheme
78// menu (Fiesta's, lifted from the scheme-menu example, so fiesta.js
79// owns the scheme) and the language menu. Every page writes them for
80// the bar and, on a phone, for the display settings menu, from this
81// one value, so the two cannot differ.
82type controls struct {
83 Themes []link
84 Scheme template.HTML
85 Locales []localeLink
86 LocaleName string
87}
88
89// tokenNav is one entry per token group on the Tokens page, in page
90// order: the theme's palette groups, then the structure every theme
91// shares.
92func (s *site) tokenNav(p page) []navItem {
93 file := s.href(p.Theme, p.Locale, s.fileOf("tokens"))
94 var items []navItem
95 for _, g := range s.tokenGroups(p.Theme) {
96 items = append(items, navItem{Label: s.say(p.Locale, "gallery.tokens.group."+g.Key), Href: file + "#" + anchorID("tokens", g.Key)})
97 }
98 return items
99}
100
101// iconNav is one entry per icon, by slug, in the order the page draws
102// them.
103func (s *site) iconNav(p page) []navItem {
104 file := s.href(p.Theme, p.Locale, s.fileOf("icons"))
105 var items []navItem
106 for _, ic := range s.icons {
107 items = append(items, navItem{Label: ic.Slug, Href: file + "#" + anchorID("icon", ic.Slug), Code: true})
108 }
109 return items
110}
111
112// familyNav is one family's entries: its examples, by name, each the
113// link to its own page. It closes over the key because the rail is the
114// same on every page: every family's section lists that family's
115// examples wherever the reader is standing.
116func (s *site) familyNav(key string) func(p page) []navItem {
117 return func(p page) []navItem {
118 var items []navItem
119 for _, f := range s.families {
120 if f.Key != key {
121 continue
122 }
123 for _, e := range f.Examples {
124 items = append(items, navItem{Label: e.Name, Href: s.href(p.Theme, p.Locale, e.Name+".html"), Code: true})
125 }
126 }
127 return items
128 }
129}
130
131// controls builds the switchers for one page: this page in every
132// theme, the scheme menu, and this page in every locale, each language
133// named in itself. The links carry no fragment; the script adds the
134// reader's place at the click.
135func (s *site) controls(p page) controls {
136 c := controls{Scheme: s.schemeIn(p.Locale), LocaleName: s.say(p.Locale, "gallery.locale.name")}
137 for _, theme := range fiesta.ThemeNames() {
138 c.Themes = append(c.Themes, link{Href: s.href(theme, p.Locale, p.File), Label: theme, Current: theme == p.Theme})
139 }
140 for _, code := range Locales {
141 c.Locales = append(c.Locales, localeLink{
142 Code: code,
143 Name: endonym(code),
144 Dir: Dir(code),
145 Href: s.href(p.Theme, code, p.File),
146 Current: code == p.Locale,
147 })
148 }
149 return c
150}
151
152// galleryNav is the rail for one page: one section per page kind, in
153// table order, each with its overview item first and then the entries
154// its page anchors, with the search terms on the entries that have
155// them; then the Demos. The section whose kind is the page's is
156// Current (on an example page that is its family's), and on an
157// example page the example's own entry is Current too.
158func (s *site) galleryNav(p page) []navSection {
159 self := s.href(p.Theme, p.Locale, p.File)
160 out := make([]navSection, 0, len(s.pageKinds())+1)
161 for _, pk := range s.pageKinds() {
162 section := navSection{
163 Title: s.titleOf(p.Locale, pk),
164 Href: s.href(p.Theme, p.Locale, pk.File),
165 Kind: pk.Kind,
166 Current: pk.Kind == p.Kind,
167 }
168 if pk.Nav != nil {
169 // The section overview link, first, before the entries. A
170 // section that lists anything draws its title as a
171 // <summary>, which discloses rather than navigates, so
172 // without this the section would have no way to its own
173 // page. A section with nothing to list is already a plain
174 // link to its page and needs no second one under it.
175 section.Items = append([]navItem{s.sectionOverview(p, section)}, pk.Nav(p)...)
176 for i := 1; i < len(section.Items); i++ {
177 item := &section.Items[i]
178 item.Terms = searchTerms[stemOf(item.Href)]
179 item.Current = item.Href == self
180 }
181 }
182 out = append(out, section)
183 }
184 return append(out, s.demos(p))
185}
186
187// stemOf is the file name a page link ends in, without its extension
188// and fragment: the example's name for an example page's link, and ""
189// for anything else.
190func stemOf(href string) string {
191 file := href[strings.LastIndex(href, "/")+1:]
192 if i := strings.Index(file, "#"); i >= 0 {
193 return ""
194 }
195 return strings.TrimSuffix(file, ".html")
196}
197
198// sectionOverview is one section's route to the top of its own page:
199// the first item under it, labelled Overview, which is also the word
200// the rail's filter matches on, and named for a screen reader by the
201// section it belongs to.
202//
203// The visible label is deliberately the same on every section and the
204// accessible name deliberately is not. Twelve links reading "Overview"
205// in one navigation landmark are unambiguous nested under their
206// headings and ambiguous read out in a list, so the accessible name
207// carries the section: "Tokens overview". It contains the visible
208// label, so WCAG 2.2 SC 2.5.3 Label in Name is satisfied without
209// changing the word a sighted reader sees or types into the filter.
210func (s *site) sectionOverview(p page, section navSection) navItem {
211 return navItem{
212 Label: s.say(p.Locale, "gallery.nav.overview"),
213 Href: section.Href,
214 Aria: s.say(p.Locale, "gallery.nav.overview.aria", "section", section.Title),
215 }
216}
217
218// demos is the group at the foot of the rail, every entry opening in a
219// tab of its own: the five shells, each the layout example's default
220// frame document, which is the shell laid out whole as the app it
221// stands for; then the modal route and the demo application.
222func (s *site) demos(p page) navSection {
223 out := navSection{Title: s.say(p.Locale, "gallery.nav.demos")}
224 for _, shell := range shellNames {
225 out.Items = append(out.Items, navItem{
226 Label: s.say(p.Locale, "gallery.demos.shell", "shell", shell),
227 Href: s.layoutHref(p.Theme, p.Locale, "layout-"+shell),
228 Blank: true,
229 })
230 }
231 out.Items = append(out.Items,
232 navItem{Label: s.say(p.Locale, "gallery.demos.modal"), Href: s.modalHref(p.Theme, p.Locale), Blank: true},
233 navItem{Label: s.say(p.Locale, "gallery.demos.app"), Href: s.demoHref(p.Theme, p.Locale), Blank: true},
234 )
235 return out
236}
237
238// indexRows reads the phone index off the rail, so the rows and the
239// tree cannot list different sections: one row per page kind but the
240// Overview, which is the index itself, and the Demos section as is.
241// Rows are plain links, not the tree's disclosures, because shell.js
242// returns focus to the first nav link whose path is the page just
243// left, and in the tree that link is inside a closed <details> that
244// cannot take focus. anchorID never writes a nav- id, and the kinds
245// are unique, so the ids cannot collide with anything else on the
246// page.
247func indexRows(nav []navSection) ([]indexRow, navSection) {
248 rows := make([]indexRow, 0, len(nav))
249 for _, s := range nav[:len(nav)-1] {
250 if s.Kind == "overview" {
251 continue
252 }
253 rows = append(rows, indexRow{ID: "nav-" + s.Kind, Title: s.Title, Href: s.Href})
254 }
255 return rows, nav[len(nav)-1]
256}
257
258// phoneIndex sets the Overview's phone index, Rows and Demos, on the
259// page being built: the Overview is the shell's index page below
260// 800px, and every other page a content page with a way back to its
261// row, so only the Overview carries them.
262func (s *site) phoneIndex(p *page) {
263 if p.Kind != "overview" {
264 return
265 }
266 p.Rows, p.Demos = indexRows(p.Nav)
267}
268