| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "html/template" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // The Icons page: one chip per icon, drawn the way a component draws |
| 10 | // it, with its slug, the file to copy and where the glyph comes from. |
| 11 | // Nothing in this file names an icon or counts them: the chips are |
| 12 | // s.icons, read off icons/*.svg, and both numbers in the sentence |
| 13 | // under the grid are counted from the set. The one list here is the |
| 14 | // names Fiesta gives glyphs that Lucide names otherwise. |
| 15 | |
| 16 | // lucideHome is the one address in this tree that is not in this tree: |
| 17 | // where the glyphs come from, and where a reader goes to see the rest |
| 18 | // of the set. outboundLinks is the gate's side of the same decision. |
| 19 | const lucideHome = "https://lucide.dev" |
| 20 | |
| 21 | // lucideNames is Fiesta's slug to the name lucide.dev publishes, for |
| 22 | // the slugs where the two differ; every other slug is Lucide's own |
| 23 | // name. Each was checked at lucide.dev/icons/<name> on 2026-10-29. |
| 24 | // help-circle's published name is circle-question-mark: the address |
| 25 | // lucide.dev/icons/circle-help now lands on that page. |
| 26 | var lucideNames = map[string]string{ |
| 27 | "kebab": "ellipsis-vertical", |
| 28 | "alert-triangle": "triangle-alert", |
| 29 | "check-circle": "circle-check", |
| 30 | "help-circle": "circle-question-mark", |
| 31 | "x-circle": "circle-x", |
| 32 | } |
| 33 | |
| 34 | // outboundLinks is the whole list of addresses the tree may point at |
| 35 | // that are not in it, each with the reason it is there. The table is |
| 36 | // exact strings, not prefixes, and every entry has to be linked by some |
| 37 | // page; TestEveryOutboundLinkIsAllowedAndUsed holds both halves, so it |
| 38 | // cannot become a licence for the next link somebody feels like adding. |
| 39 | var outboundLinks = map[string]string{ |
| 40 | lucideHome: "where the glyphs come from: the set the icons are copied from, under the ISC licence, and the only place a reader can see the rest of it", |
| 41 | } |
| 42 | |
| 43 | // lucideName is what lucide.dev calls the icon Fiesta calls slug. |
| 44 | func lucideName(slug string) string { |
| 45 | if name, ok := lucideNames[slug]; ok { |
| 46 | return name |
| 47 | } |
| 48 | return slug |
| 49 | } |
| 50 | |
| 51 | // iconView is one chip: its anchor, its slug, the glyph, and its |
| 52 | // provenance, an address rather than a sentence (lucide.dev/icons/…) |
| 53 | // so it needs no translation and a reader can type it. |
| 54 | type iconView struct { |
| 55 | ID, Slug, Provenance string |
| 56 | Markup template.HTML |
| 57 | } |
| 58 | |
| 59 | // iconsView is the page's data. Total and Renamed are counted from |
| 60 | // the set rather than written into the sentence, so the sentence |
| 61 | // cannot be wrong about the set it describes. Lucide is the line that |
| 62 | // links the set, with the link in it. |
| 63 | type iconsView struct { |
| 64 | List []iconView |
| 65 | Total, Renamed string |
| 66 | Lucide template.HTML |
| 67 | } |
| 68 | |
| 69 | // buildIcons reads the set for one locale's page. |
| 70 | func (s *site) buildIcons(locale string) iconsView { |
| 71 | out := iconsView{Total: fmt.Sprint(len(s.icons))} |
| 72 | renamed := 0 |
| 73 | for _, ic := range s.icons { |
| 74 | name := lucideName(ic.Slug) |
| 75 | if name != ic.Slug { |
| 76 | renamed++ |
| 77 | } |
| 78 | out.List = append(out.List, iconView{ |
| 79 | ID: anchorID("icon", ic.Slug), |
| 80 | Slug: ic.Slug, |
| 81 | Provenance: "lucide.dev/icons/" + name, |
| 82 | Markup: ic.Markup, |
| 83 | }) |
| 84 | } |
| 85 | out.Renamed = fmt.Sprint(renamed) |
| 86 | link := `<a href="` + lucideHome + `" target="_blank" rel="noopener">lucide.dev<span class="rst-sr-only"> (` + |
| 87 | template.HTMLEscapeString(s.say(locale, "gallery.newtab")) + `)</span></a>` |
| 88 | out.Lucide = template.HTML(strings.Replace(template.HTMLEscapeString(s.say(locale, "gallery.icons.lucide")), "{link}", link, 1)) |
| 89 | return out |
| 90 | } |
| 91 | |
| 92 | // iconsBody is every slug, drawn at the size tokens.css gives .icon, |
| 93 | // each with its file and its lucide.dev address; then the names that |
| 94 | // are Fiesta's own, with the count; then where the glyphs come from, |
| 95 | // with the one link that leaves the tree. |
| 96 | const iconsBody = `{{define "ds-body-icons"}}<p class="ds-lead">{{say "gallery.icons.lead" "total" .Icons.Total}}</p> |
| 97 | <ul class="ds-toks ds-icons">{{range .Icons.List}} |
| 98 | <li class="ds-tok" id="{{.ID}}" data-ds-anchor><span class="ds-chip ds-chip--icon">{{.Markup}}</span><span class="ds-tok__text rst-mono"><span class="ds-tok__name">{{.Slug}}</span><span class="ds-tok__value">icons/{{.Slug}}.svg</span><span class="ds-tok__value">{{.Provenance}}</span></span></li>{{end}} |
| 99 | </ul> |
| 100 | |
| 101 | <h2 class="ds-sub">{{say "gallery.icons.names.heading"}}</h2> |
| 102 | <p class="ds-lead">{{say "gallery.icons.names.body" "renamed" .Icons.Renamed "total" .Icons.Total}}</p> |
| 103 | <p class="ds-note">{{say "gallery.icons.kebab"}}</p> |
| 104 | |
| 105 | <h2 class="ds-sub">{{say "gallery.icons.source"}}</h2> |
| 106 | <p class="ds-lead">{{say "gallery.icons.source.body"}}</p> |
| 107 | <p class="ds-note">{{.Icons.Lucide}}</p> |
| 108 | {{end}}` |
| 109 | |
| 110 | // iconsBody renders the Icons page's body for p. |
| 111 | func (s *site) iconsBody(p page) (template.HTML, error) { |
| 112 | return s.body(p.Locale, "ds-body-icons", struct { |
| 113 | page |
| 114 | Icons iconsView |
| 115 | }{p, s.buildIcons(p.Locale)}) |
| 116 | } |
| 117 | |
| 118 | // iconMarkup is one of Fiesta's icons, inline, for the gallery's own |
| 119 | // furniture; the icon template func. |
| 120 | func (s *site) iconMarkup(slug string) template.HTML { |
| 121 | for _, ic := range s.icons { |
| 122 | if ic.Slug == slug { |
| 123 | return ic.Markup |
| 124 | } |
| 125 | } |
| 126 | return "" |
| 127 | } |
| 128 | |
| 129 | func init() { registerTemplate(iconsBody) } |
| 130 | |