clone

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

feat/atlas-next-horizon
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/mint.css",79 "css/globe.css",80 "css/map-loading.css",81 "css/map-corner.css",82 "css/overview.css",83 "css/zoom-controls.css",84 "css/grid-navigator.css",85 "css/map-hint.css",86 "css/dock.css",87 "css/pin-detail.css",88 "css/fatal-error.css",89 "css/empty-state.css",90 "css/responsive.css",91 "css/chrome.css",92}9394var (95 mu sync.RWMutex96 tree fs.FS = files97 sheet []byte98 loaded bool99)100101// Reload points the assets at another tree, for the development loop: `atlas102// dev` watches the working copy so a stylesheet edit is visible on the next103// request. A shipped binary never calls it and serves what is embedded.104func Reload(other fs.FS) {105 mu.Lock()106 defer mu.Unlock()107 tree = other108 sheet, loaded = nil, false109 runtime, runtimeLoaded = nil, false110}111112func read(name string) ([]byte, error) {113 mu.RLock()114 held := tree115 mu.RUnlock()116 return fs.ReadFile(held, name)117}118119// Stylesheet is the whole cascade as one document. It is assembled on first120// ask from bytes that are already in the binary, and again after a Reload.121func Stylesheet() []byte {122 mu.RLock()123 if loaded {124 defer mu.RUnlock()125 return sheet126 }127 mu.RUnlock()128129 var out bytes.Buffer130 for _, name := range stylesheets {131 body, err := read(name)132 if err != nil {133 // The list and the tree are both in this package and both in134 // the binary; a name that will not read is a build mistake,135 // and skipping it loses a region's styling rather than the136 // page.137 continue138 }139 out.WriteString("/* " + name + " */\n")140 out.Write(body)141 out.WriteString("\n")142 }143 mu.Lock()144 defer mu.Unlock()145 sheet, loaded = out.Bytes(), true146 return sheet147}148149var (150 runtime []byte151 runtimeLoaded bool152)153154// Runtime is the hypermedia runtime: htmx and the one extension the155// application uses, in that order, as one script.156func Runtime() []byte {157 mu.RLock()158 if runtimeLoaded {159 defer mu.RUnlock()160 return runtime161 }162 mu.RUnlock()163164 var out bytes.Buffer165 for _, name := range []string{"htmx.min.js", "hx-sse.min.js"} {166 body, err := read(name)167 if err != nil {168 continue169 }170 out.Write(body)171 out.WriteString("\n")172 }173 mu.Lock()174 defer mu.Unlock()175 runtime, runtimeLoaded = out.Bytes(), true176 return runtime177}