clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / internal / app / assets / assets.go
assets.go 5.0K
1// Package assets holds the application's own chrome: the stylesheet system2// and the hypermedia runtime, embedded in the binary.3//4// This is not the seam's tree. The seam's built bundle arrives from whatever5// host mounted it and is served under /static, which answers 404 when a host6// mounted nothing -- the application is required to build, serve and work7// without the seam present (issue #5 §3.2). What is here is the part of the8// page that is the application's own and must be there whatever else is not:9// the chrome a reader sees, and the runtime that swaps it.10//11// # The stylesheet system12//13// css/ is the token-first, one-file-per-region system carried over from the14// reference implementation as an asset (issue #5 §9). The files are the15// reference's own, unedited; where the new DOM demands a different selector16// the difference lives in chrome.css alone, so the delta between the carried17// system and this application's markup is one file long and readable in one18// sitting.19//20// The files are concatenated in the order below rather than chained by21// @import, because thirty-three imports is thirty-three round trips for a22// stylesheet that is forty kilobytes whole. The order is the cascade and is23// load-bearing: a handful of files deliberately override earlier ones.24//25// # The runtime26//27// htmx.min.js is htmx 4.0.0-beta6, vendored, and hx-sse.min.js is its28// server-sent-events extension, which is how the page hears that the library29// moved (§4.6). Both are vendored rather than linked because the offline30// invariant is not only about bundles: an atlas opens on a machine with no31// network, forever, and a page that fetches its own runtime from a CDN is a32// page that does not (issue #5 §2). The licence travels beside them as33// htmx.LICENSE.34package assets3536import (37 "bytes"38 "embed"39 "io/fs"40 "sync"41)4243//go:embed css/*.css htmx.min.js hx-sse.min.js htmx.LICENSE44var files embed.FS4546// Files is the embedded tree, for a host that would rather serve it whole.47func Files() fs.FS { return files }4849// Reloadable is what the development loop watches: the names, relative to a50// working copy of this directory, whose contents this package serves.51func Reloadable() []string { return append([]string(nil), stylesheets...) }5253// stylesheets is the cascade, in order. One line per region, mirroring the54// one-file-per-region template set; chrome.css is last because its whole job55// is to have the last word.56var stylesheets = []string{57 "css/tokens.css",58 "css/base.css",59 "css/shell.css",60 "css/topbar.css",61 "css/brand.css",62 "css/map-pickers.css",63 "css/icon-button.css",64 "css/topbar-collapse.css",65 "css/sidebar.css",66 "css/search.css",67 "css/legend-toolbar.css",68 "css/tooltip.css",69 "css/layers.css",70 "css/layer-header.css",71 "css/solo-chip.css",72 "css/layer-switch.css",73 "css/text-symbol.css",74 "css/feature-index.css",75 "css/category-rows.css",76 "css/sidebar-footer.css",77 "css/map-panel.css",78 "css/globe.css",79 "css/map-loading.css",80 "css/map-corner.css",81 "css/overview.css",82 "css/zoom-controls.css",83 "css/grid-navigator.css",84 "css/map-hint.css",85 "css/dock.css",86 "css/pin-detail.css",87 "css/fatal-error.css",88 "css/empty-state.css",89 "css/responsive.css",90 "css/chrome.css",91}9293var (94 mu sync.RWMutex95 tree fs.FS = files96 sheet []byte97 loaded bool98)99100// Reload points the assets at another tree, for the development loop: `atlas101// dev` watches the working copy so a stylesheet edit is visible on the next102// request. A shipped binary never calls it and serves what is embedded.103func Reload(other fs.FS) {104 mu.Lock()105 defer mu.Unlock()106 tree = other107 sheet, loaded = nil, false108 runtime, runtimeLoaded = nil, false109}110111func read(name string) ([]byte, error) {112 mu.RLock()113 held := tree114 mu.RUnlock()115 return fs.ReadFile(held, name)116}117118// Stylesheet is the whole cascade as one document. It is assembled on first119// ask from bytes that are already in the binary, and again after a Reload.120func Stylesheet() []byte {121 mu.RLock()122 if loaded {123 defer mu.RUnlock()124 return sheet125 }126 mu.RUnlock()127128 var out bytes.Buffer129 for _, name := range stylesheets {130 body, err := read(name)131 if err != nil {132 // The list and the tree are both in this package and both in133 // the binary; a name that will not read is a build mistake,134 // and skipping it loses a region's styling rather than the135 // page.136 continue137 }138 out.WriteString("/* " + name + " */\n")139 out.Write(body)140 out.WriteString("\n")141 }142 mu.Lock()143 defer mu.Unlock()144 sheet, loaded = out.Bytes(), true145 return sheet146}147148var (149 runtime []byte150 runtimeLoaded bool151)152153// Runtime is the hypermedia runtime: htmx and the one extension the154// application uses, in that order, as one script.155func Runtime() []byte {156 mu.RLock()157 if runtimeLoaded {158 defer mu.RUnlock()159 return runtime160 }161 mu.RUnlock()162163 var out bytes.Buffer164 for _, name := range []string{"htmx.min.js", "hx-sse.min.js"} {165 body, err := read(name)166 if err != nil {167 continue168 }169 out.Write(body)170 out.WriteString("\n")171 }172 mu.Lock()173 defer mu.Unlock()174 runtime, runtimeLoaded = out.Bytes(), true175 return runtime176}