D0 Part 1: vendored fonts + Lucide sprite + setup CSS fix
- Vendor Plus Jakarta Sans + JetBrains Mono as variable woff2 (latin + latin-ext) under internal/web/static/fonts/, embedded via go:embed and served at /static/fonts/ (font/woff2, immutable cache). Google Fonts @import replaced with @font-face rules preserving unicode-range — removes the CDN dependency that silently broke on offline nodes. - Add templates/icons.html: vendored Lucide sprite (30 icons, symbol ids i-<name>), included at the top of <body> in layout.html. - Fix setup wizard handleCSS: serve the embedded web.StyleCSS() instead of a dataDir-derived filesystem path that never exists in the container (production setup silently served minimalCSS). Fallback to minimalCSS only if the embedded read errors, with a WARN log. - Tests: font route + StyleCSS accessor (web), Scenario E embedded-CSS test (setup; red-proven against the pre-fix handler).
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSetupCSSServesEmbedded (Scenario E, TASK-D0): the setup wizard must serve
|
||||||
|
// the web package's EMBEDDED stylesheet. The pre-fix handleCSS read a filesystem
|
||||||
|
// path derived from dataDir, which does not exist inside the container image, so
|
||||||
|
// production setup mode silently served the minimalCSS fallback.
|
||||||
|
func TestSetupCSSServesEmbedded(t *testing.T) {
|
||||||
|
// dataDir whose derived legacy path (internal/web/templates/style.css
|
||||||
|
// relative to its grandparent) cannot exist.
|
||||||
|
dataDir := filepath.Join(t.TempDir(), "nonexistent-nested", "data")
|
||||||
|
logger := log.New(io.Discard, "", 0)
|
||||||
|
s := NewServer(&config.Config{}, dataDir, logger, "test")
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/static/style.css", nil)
|
||||||
|
s.Handler().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != 200 {
|
||||||
|
t.Fatalf("status = %d, want 200", rec.Code)
|
||||||
|
}
|
||||||
|
body := rec.Body.Bytes()
|
||||||
|
if len(body) <= 10000 {
|
||||||
|
t.Fatalf("body length = %d, want > 10000 (minimalCSS fallback served?)", len(body))
|
||||||
|
}
|
||||||
|
embedded, err := web.StyleCSS()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("web.StyleCSS() error: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(body, embedded) {
|
||||||
|
t.Error("served CSS differs from the embedded web stylesheet")
|
||||||
|
}
|
||||||
|
if strings.Contains(string(body), ".setup-container") {
|
||||||
|
t.Error("minimalCSS fallback was served instead of the embedded stylesheet")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,11 +189,12 @@ func (s *Server) handleFailed(w http.ResponseWriter, r *http.Request) {
|
|||||||
// --- Static Assets (reuse from web package embed) ---
|
// --- Static Assets (reuse from web package embed) ---
|
||||||
|
|
||||||
func (s *Server) handleCSS(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCSS(w http.ResponseWriter, r *http.Request) {
|
||||||
// Read the main style.css from the web package templates
|
// Serve the web package's embedded stylesheet. The previous implementation
|
||||||
cssPath := filepath.Join(filepath.Dir(s.dataDir), "..", "internal", "web", "templates", "style.css")
|
// read a filesystem path derived from dataDir, which does not exist inside
|
||||||
data, err := os.ReadFile(cssPath)
|
// the container image — setup mode silently fell back to minimalCSS.
|
||||||
|
data, err := web.StyleCSS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback: serve minimal CSS
|
s.logger.Printf("[WARN] Setup: embedded style.css unavailable (%v) — serving minimal fallback CSS", err)
|
||||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||||
w.Write([]byte(minimalCSS))
|
w.Write([]byte(minimalCSS))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,3 +7,13 @@ var templateFS embed.FS
|
|||||||
|
|
||||||
//go:embed static/chart.min.js
|
//go:embed static/chart.min.js
|
||||||
var chartJS []byte
|
var chartJS []byte
|
||||||
|
|
||||||
|
//go:embed static/fonts/*.woff2
|
||||||
|
var fontFS embed.FS
|
||||||
|
|
||||||
|
// StyleCSS returns the embedded dashboard stylesheet. Exported so the setup
|
||||||
|
// wizard can serve the same CSS instead of reading a filesystem path that
|
||||||
|
// does not exist inside the container image.
|
||||||
|
func StyleCSS() ([]byte, error) {
|
||||||
|
return templateFS.ReadFile("templates/style.css")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestServeFontHandler asserts the vendored woff2 files are embedded and served
|
||||||
|
// with the right content type + immutable caching (design system v2, D0 Part 1.1).
|
||||||
|
func TestServeFontHandler(t *testing.T) {
|
||||||
|
s := &Server{}
|
||||||
|
for _, name := range []string{"pjs-latin.woff2", "pjs-latin-ext.woff2", "jbm-latin.woff2", "jbm-latin-ext.woff2"} {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/static/fonts/"+name, nil)
|
||||||
|
s.serveFontHandler(rec, req, name)
|
||||||
|
if rec.Code != 200 {
|
||||||
|
t.Errorf("%s: status = %d, want 200", name, rec.Code)
|
||||||
|
}
|
||||||
|
if ct := rec.Header().Get("Content-Type"); ct != "font/woff2" {
|
||||||
|
t.Errorf("%s: Content-Type = %q, want font/woff2", name, ct)
|
||||||
|
}
|
||||||
|
if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "immutable") {
|
||||||
|
t.Errorf("%s: Cache-Control = %q, want immutable", name, cc)
|
||||||
|
}
|
||||||
|
// woff2 magic number: "wOF2"
|
||||||
|
if body := rec.Body.Bytes(); len(body) < 4 || string(body[:4]) != "wOF2" {
|
||||||
|
t.Errorf("%s: body is not a woff2 file (len=%d)", name, len(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown font → 404, and path traversal is neutralized by filepath.Base.
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
s.serveFontHandler(rec, httptest.NewRequest("GET", "/static/fonts/nope.woff2", nil), "nope.woff2")
|
||||||
|
if rec.Code != 404 {
|
||||||
|
t.Errorf("unknown font: status = %d, want 404", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStyleCSSAccessor asserts the exported accessor returns the embedded
|
||||||
|
// stylesheet (consumed by the setup wizard) with the vendored font-face rules
|
||||||
|
// and no CDN import.
|
||||||
|
func TestStyleCSSAccessor(t *testing.T) {
|
||||||
|
data, err := StyleCSS()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StyleCSS() error: %v", err)
|
||||||
|
}
|
||||||
|
css := string(data)
|
||||||
|
if len(css) < 10000 {
|
||||||
|
t.Fatalf("StyleCSS() length = %d, want > 10000", len(css))
|
||||||
|
}
|
||||||
|
if !strings.Contains(css, "@font-face") || !strings.Contains(css, "/static/fonts/pjs-latin.woff2") {
|
||||||
|
t.Error("StyleCSS() missing vendored @font-face rules")
|
||||||
|
}
|
||||||
|
if strings.Contains(css, "fonts.googleapis.com") {
|
||||||
|
t.Error("StyleCSS() still references the Google Fonts CDN")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -322,6 +322,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.serveCSSHandler(w, r)
|
s.serveCSSHandler(w, r)
|
||||||
case path == "/static/chart.min.js":
|
case path == "/static/chart.min.js":
|
||||||
s.serveChartJSHandler(w, r)
|
s.serveChartJSHandler(w, r)
|
||||||
|
case strings.HasPrefix(path, "/static/fonts/"):
|
||||||
|
s.serveFontHandler(w, r, strings.TrimPrefix(path, "/static/fonts/"))
|
||||||
case path == "/static/felhom-logo.svg":
|
case path == "/static/felhom-logo.svg":
|
||||||
s.serveLogoHandler(w, r)
|
s.serveLogoHandler(w, r)
|
||||||
case path == "/static/favicon.svg":
|
case path == "/static/favicon.svg":
|
||||||
@@ -489,6 +491,20 @@ func (s *Server) serveChartJSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(chartJS)
|
w.Write(chartJS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveFontHandler serves the vendored woff2 files embedded in the binary
|
||||||
|
// (self-hosted fonts — no Google Fonts CDN dependency on offline nodes).
|
||||||
|
func (s *Server) serveFontHandler(w http.ResponseWriter, r *http.Request, filename string) {
|
||||||
|
filename = filepath.Base(filename)
|
||||||
|
data, err := fontFS.ReadFile("static/fonts/" + filename)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "font/woff2")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) serveLogoHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) serveLogoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Try synced asset first (allows logo updates via Hub without rebuild)
|
// Try synced asset first (allows logo updates via Hub without rebuild)
|
||||||
if s.assetsSyncer != nil {
|
if s.assetsSyncer != nil {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,32 @@
|
|||||||
|
{{define "icon_sprite"}}<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||||
|
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||||
|
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||||
|
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||||
|
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||||
|
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||||
|
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||||
|
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||||
|
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||||
|
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||||
|
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||||
|
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||||
|
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||||
|
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||||
|
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||||
|
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||||
|
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||||
|
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||||
|
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||||
|
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||||
|
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||||
|
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||||
|
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||||
|
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||||
|
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||||
|
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||||
|
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||||
|
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||||
|
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||||
|
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||||
|
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||||
|
</svg>{{end}}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{{template "icon_sprite"}}
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="sidebar-logo">
|
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="sidebar-logo">
|
||||||
|
|||||||
@@ -1,4 +1,37 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
/* Vendored fonts — variable woff2 embedded in the binary, served from /static/fonts/.
|
||||||
|
No CDN dependency (offline customer nodes). latin-ext is mandatory for Hungarian ő/ű. */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Plus Jakarta Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/static/fonts/pjs-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Plus Jakarta Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/static/fonts/pjs-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/static/fonts/jbm-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/static/fonts/jbm-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0d1117;
|
--bg-primary: #0d1117;
|
||||||
|
|||||||
Reference in New Issue
Block a user