// Package sky is Calma's ambient nebula: a soft sky of every app's colour, // seeded, so each team's is its own. It lives where the suite meets you — // the sign-in, the team's home, the front page — and never inside an app // (AGENTS.md, decisions table, row 8). // // A sky is generated from its ID (generate.go): a far haze and near clouds // of every app's colour, a fine grain, and a few white filaments. Its // colour — the haze and clouds — is painted as the placeholder // (placeholder.go), 50x20 pixels inline, so it is there in the first frame; // the grain and filaments are two static SVGs served by Handler. Handler // still serves the haze and clouds SVGs too, for pages cached before they // stopped being linked. Light and dark each get their own placeholder; // sky.css shows the one matching the page's scheme, drifts the sky slowly, // and holds it still for anyone who asks their system for less motion. // No script. package sky import ( "embed" "html/template" "net/http" "regexp" "strings" ) //go:embed sky.css var css []byte //go:embed templates/*.html var templateFS embed.FS // CSS styles [estilo-sky] and [estilo-sky-host]. func CSS() []byte { return append([]byte(nil), css...) } // Templates holds "estilo-sky"; parse it into the page's template set. func Templates() embed.FS { return templateFS } // Prefix is the URL path Handler serves under. The version is the // generator's: a change to what a given ID draws is a new version, so // the year-long cache below never serves a stale sky. TestSkyGoldens holds // one sky's six files still; when it fails, the drawing changed — bump // the version here, then update the goldens. const Prefix = "/static/estilo/sky/v1/" // SuiteSeed is the sky of pages that belong to no one team: the front // page and the sign-in. const SuiteSeed = "oficina" // Layer is one image of a sky. type Layer struct { Name string // placeholder, grain or filaments (haze and clouds are served, no longer linked) Tone string // light, dark, or "" for both Src string // a URL under Prefix; empty for the placeholder // Inline is the placeholder's data: URI, typed so html/template // passes it (a data: URI in a plain string is sanitised away). Built // only by Placeholder, never from input. Inline template.URL } // View is the data for the "estilo-sky" template. type View struct{ Layers []Layer } // For is the sky for seed — a team's id, or SuiteSeed. func For(seed string) View { id := ID(seed) base := Prefix + id + "/" return View{Layers: []Layer{ {Name: "placeholder", Tone: "light", Inline: template.URL(Placeholder(id, Light))}, {Name: "placeholder", Tone: "dark", Inline: template.URL(Placeholder(id, Dark))}, {Name: "grain", Src: base + "grain.svg"}, {Name: "filaments", Src: base + "filaments.svg"}, }} } var idShape = regexp.MustCompile(`^[0-9a-f]{16}$`) // draw returns the SVG for one layer file of sky id, or "" when the name // is not one of the six. func draw(id, file string) string { switch file { case "haze-light.svg": return Haze(id, Light) case "haze-dark.svg": return Haze(id, Dark) case "clouds-light.svg": return Clouds(id, Light) case "clouds-dark.svg": return Clouds(id, Dark) case "grain.svg": return Grain(id) case "filaments.svg": return Filaments(id) } return "" } // Handler serves GET and HEAD for Prefix + "/.svg". Mount it // at Prefix. The ID must be the shape ID produces and the layer one of // the five; anything else is 404 before anything is drawn. Skies are // public artwork and need no session; an ID's drawing never changes // within a version, so it is cached for a year. func Handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rest, ok := strings.CutPrefix(r.URL.Path, Prefix) id, file, _ := strings.Cut(rest, "/") if !ok || !idShape.MatchString(id) || strings.Contains(file, "/") { http.NotFound(w, r) return } body := draw(id, file) if body == "" { http.NotFound(w, r) return } if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Allow", "GET, HEAD") http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("X-Content-Type-Options", "nosniff") if r.Method == http.MethodHead { return } _, _ = w.Write([]byte(body)) }) }