diff --git a/internal/web/securityheaders_test.go b/internal/web/securityheaders_test.go index 61d0b56..c8b5e17 100644 --- a/internal/web/securityheaders_test.go +++ b/internal/web/securityheaders_test.go @@ -16,7 +16,7 @@ func TestSecurityHeaders(t *testing.T) { h := SecurityHeaders(pages) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) - wantCSP := "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'" + wantCSP := "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'" if got := rec.Header().Get("Content-Security-Policy"); got != wantCSP { t.Errorf("CSP = %q, want %q", got, wantCSP) } diff --git a/internal/web/static/app.css b/internal/web/static/app.css index d125f73..bdae956 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -4,6 +4,8 @@ --muted: #7A6A9E; --muted-fg: #C0B2DE; --fg: #F2EDF8; --accent: #C4A8F0; --border: #42355C; --radius-lg: 20px; --radius: 10px; --radius-sm: 999px; + --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23C8C8D4' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); /* #136: select chevron stroke (dark presets) */ + --chevron-fg: #C8C8D4; --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-mono: ui-monospace, "JetBrains Mono", "Fira Code", monospace; } @@ -18,6 +20,8 @@ --accent: #7A7796; --border: #DAD7E6; --ok: #456F45; --warn: #7A5E1B; --err: #9E4054; --on-accent: #F6F5FA; + --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23555560' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); /* #136: light presets use a dark stroke */ + --chevron-fg: #555560; } [data-preset="pastel-lavender"] { --bg: #e6e0f5; --surface: #f1edfa; --surface-2: #cbb8e7; @@ -25,6 +29,8 @@ --accent: #806bb8; --border: #c4b6e0; --ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054; --on-accent: #f1edfa; + --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23555560' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); /* #136: light presets use a dark stroke */ + --chevron-fg: #555560; } [data-preset="pastel-peach"] { --bg: #ffe0d6; --surface: #fff0ea; --surface-2: #ffc4a8; @@ -32,6 +38,8 @@ --accent: #f9826c; --border: #ffc9b5; --ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054; --on-accent: #4F2318; + --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23555560' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); /* #136: light presets use a dark stroke */ + --chevron-fg: #555560; } [data-preset="pastel-cloud"] { --bg: #fff0f6; --surface: #fff7fb; --surface-2: #ffc8dd; @@ -39,6 +47,8 @@ --accent: #a2d2ff; --border: #ffccd9; --ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054; --on-accent: #274a6b; + --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23555560' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); /* #136: light presets use a dark stroke */ + --chevron-fg: #555560; } /* matching light/dark counterparts: same hue family, flipped lightness roles */ /* Midnight Light: light counterpart of midnight (violet family) */ @@ -48,6 +58,8 @@ --accent: #7B5FC0; --border: #D8CEEC; --ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054; --on-accent: #FAF8FD; + --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23555560' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); /* #136: light presets use a dark stroke */ + --chevron-fg: #555560; } /* Smooth Dark: dark counterpart of smooth (muted slate-violet family) */ [data-preset="smooth-dark"] { @@ -144,7 +156,7 @@ body { border: none; outline: none; background: transparent; color: var(--fg); font: inherit; font-size: 23.2px; flex: 1; } .editor-head select { - border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); + border: 1px solid var(--border); background-color: var(--surface-2); color: var(--muted-fg); border-radius: var(--radius); padding: 4px 10px; font: inherit; font-size: 21.6px; cursor: pointer; } /* shared code line metrics (#50): gutter + code must share one line box */ @@ -602,7 +614,7 @@ select { -webkit-appearance: none; border: 1px solid var(--border); border-radius: var(--radius); - background: var(--bg) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 10px center; + background: var(--bg) var(--chevron) no-repeat right 10px center; /* #136: themed chevron */ color: var(--fg); font: inherit; padding: 4px 30px 4px 12px; diff --git a/internal/web/web.go b/internal/web/web.go index 40a6fc2..2f9a3b1 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -390,7 +390,7 @@ func SecurityHeaders(next http.Handler) http.Handler { // is harmless and arguably desirable. h := w.Header() h.Set("Content-Security-Policy", - "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'") + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'") h.Set("Referrer-Policy", "no-referrer") h.Set("X-Content-Type-Options", "nosniff") next.ServeHTTP(w, r)