## 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).
73 lines
3.3 KiB
Cheetah
73 lines
3.3 KiB
Cheetah
{{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}}
|