Files
projax/web/templates/graph_svg.tmpl
mAi 5dcacff520 feat(phase 4b): dark/light theme toggle + file-upload permanently out-of-scope
## Slice A — explicit dark/light toggle

projax now ships with two palettes and a 1y cookie to remember the choice.
Dark is the new default; ☀ button in the header nav flips to light and
writes projax_theme=light. Server reads the cookie via themeFromRequest(r)
and injects Theme + ThemeColor into every template via the centralised
render(w, r, …) path, so first paint never flashes the wrong theme. Inline
JS in layout.tmpl handles the toggle without a server roundtrip.

Every panel colour now lives in a CSS variable under
:root[data-theme=dark|light]; the only hardcoded hex values left are
inside those two :root blocks. A future palette tweak is one edit, not
30 selectors. Graph node colours, kind-badges, highlights and warn/ok/bad
all have parallel dark/light values picked for contrast.

Standalone SVG download bakes the light palette inline because the
downloaded asset has no parent :root providing vars — m's existing
snapshots stay print-friendly regardless of his current cookie.

Login page keeps its embedded dark CSS — it's the gateway, intentionally
always dark.

Tests: TestThemeDefaultIsDark, TestThemeCookieRoundTrips,
TestThemeCookieUnknownFallsBackToDark, TestThemeTogglePagesShareSameTheme,
TestThemeToggleScriptPresent, TestThemeColorMetaHelper. Full suite green.

## Slice B — file-upload permanently out of scope (m, 2026-05-17)

docs/design.md moves "File uploads / in-projax storage" from the §3c
parked list to a permanent "Out of scope (decided 2026-05-17)" clause
with the rationale: PER is the cross-reference index, not the file
system. docs/standards/per.md gains the same explicit clause so future
shifts working from the PER standard see the constraint where they
look. Memory note filed so future workers don't re-propose multipart
uploads, attachments tables, or documents buckets.

## docs/design.md §13 Theming

Documents the toggle approach, cookie semantics, palette table, the
standalone-SVG carve-out, the login-page exception, and the 4b
out-of-scope (prefers-color-scheme detection, per-page overrides,
transitions on swap).
2026-05-17 18:14:08 +02:00

73 lines
3.3 KiB
Cheetah
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "graph-svg"}}<svg xmlns="http://www.w3.org/2000/svg"
class="graph-svg"
viewBox="0 0 {{printf "%.0f" .CanvasWidth}} {{printf "%.0f" .CanvasHeight}}"
width="{{printf "%.0f" .CanvasWidth}}" height="{{printf "%.0f" .CanvasHeight}}">
<defs>
<style>
{{/* Standalone SVG (?download=svg) must carry its own palette since no
outer page provides --foo. When embedded the outer :root wins. We
bake the light palette so downloaded snapshots stay print-friendly
regardless of the user's current theme cookie. */}}
{{if .Standalone}}
:root {
--fg: #1a1a1a; --muted: #6a6a6a; --surface: #ffffff;
--graph-mai: #2563eb; --graph-self: #15803d;
--graph-external: #ea580c; --graph-mixed: #7c3aed;
--graph-unmanaged: #9ca3af;
}
{{end}}
.gnode rect { fill: var(--surface); stroke-width: 2; }
.gnode.mgmt-mai rect { stroke: var(--graph-mai); }
.gnode.mgmt-self rect { stroke: var(--graph-self); }
.gnode.mgmt-external rect { stroke: var(--graph-external); }
.gnode.mgmt-mixed rect { stroke: var(--graph-mixed); stroke-dasharray: 4 2; }
.gnode.mgmt-unmanaged rect { stroke: var(--graph-unmanaged); }
.gnode text.slug { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 12px; fill: var(--fg); }
.gnode.dimmed { opacity: 0.15; }
.gnode .tag-pill { font-size: 9px; fill: var(--muted); }
.gnode .badge { font-size: 10px; fill: var(--graph-mai); font-weight: 600; }
.gedge { fill: none; stroke: var(--graph-unmanaged); stroke-width: 1.4; }
.gedge.dimmed { opacity: 0.1; }
</style>
</defs>
<g class="edges">
{{range .Edges}}
{{$dx := 0.0}}
{{$dy := 32.0}}
<path class="gedge" d="M {{printf "%.1f" .SourceX}} {{printf "%.1f" .SourceY}}
C {{printf "%.1f" .SourceX}} {{printf "%.1f" (addF .SourceY $dy)}},
{{printf "%.1f" .TargetX}} {{printf "%.1f" (subF .TargetY $dy)}},
{{printf "%.1f" .TargetX}} {{printf "%.1f" .TargetY}}"/>
{{end}}
</g>
<g class="nodes">
{{$NodeW := .NodeW}}
{{$NodeH := .NodeH}}
{{$isolate := .Isolate}}
{{range .Nodes}}
{{$dim := and (not .Matched) (not $isolate)}}
<a xlink:href="/i/{{.Path}}" href="/i/{{.Path}}">
<g class="gnode mgmt-{{.MgmtClass}} {{if $dim}}dimmed{{end}}"
transform="translate({{printf "%.1f" .Pos.X}} {{printf "%.1f" .Pos.Y}})"
opacity="{{printf "%.2f" .StatusOp}}">
<title>{{.Title}} — {{.Path}} · {{.Status}} · mgmt:{{join "," .Management}}</title>
<rect width="{{printf "%.0f" $NodeW}}" height="{{printf "%.0f" $NodeH}}" rx="6"/>
<text class="slug" x="8" y="18">{{.Slug}}</text>
{{if gt .PathCount 1}}
<text class="badge" x="{{subF $NodeW 22}}" y="14">×{{.PathCount}}</text>
{{end}}
{{$y := subF $NodeH 6}}
{{range $i, $t := .TagsShown}}
<text class="tag-pill" x="{{addF 8 (mulF 40 $i)}}" y="{{printf "%.0f" $y}}">{{$t}}</text>
{{end}}
{{if gt .TagOverflow 0}}
<text class="tag-pill" x="{{addF 8 (mulF 40 (len .TagsShown))}}" y="{{printf "%.0f" $y}}">+{{.TagOverflow}}</text>
{{end}}
</g>
</a>
{{end}}
</g>
</svg>{{end}}