~bigbes/sr-ht-dolt

76bf70dd30426965ce5f6a4ce8d4a6dc9fe7bb7c — Eugene Blikh 30 days ago 8f5ae3f
fix(web): make the beads view inherit the SourceHut theme

The beads board shipped its own Mardi Gras palette — a purple→green
marquee gradient and saturated solid lane fills — that clashed with
SourceHut's flat, utilitarian dark theme, and the detail pane's field
labels were a hardcoded dark purple with no dark-mode override, leaving
"Created by / Owner / Description" unreadable on the dark background.

Drive everything from CSS variables that mirror core.sr.ht's Bootstrap
palette for both the light default and the prefers-color-scheme: dark
variant sourcehut ships (the same media query it uses), so the view
inherits the host theme instead of fighting it. Lane hues become muted
Mardi Gras accents (gold/green/violet/gray) used only as thin borders and
low-alpha tints — never as body text — so contrast holds on either theme.
The marquee is now a flat stat row, lanes an understated panel with an
accent underline, and detail labels a readable muted caption. Verified
light and dark via a headless render.
2 files changed, 107 insertions(+), 72 deletions(-)

M web/beads.go
M web/templates/beads.html
M web/beads.go => web/beads.go +11 -7
@@ 274,10 274,14 @@ func (v *beadsView) Build(ctx context.Context, sess BrowseSession, _ *core.Repo,
	data := &BeadsData{
		Mode: "board",
		Lanes: []BeadsLane{
			{Name: "Rolling", Slug: "rolling", Accent: "#f5c518", Issues: rolling},
			{Name: "Lined Up", Slug: "lined-up", Accent: "#2e9e4f", Issues: linedUp},
			{Name: "Stalled", Slug: "stalled", Accent: "#7b2ff7", Issues: stalled},
			{Name: "Past Stand", Slug: "past-stand", Accent: "#8a8d91", Issues: pastStand},
			// Accents are muted Mardi Gras hues (gold / green / violet / gray)
			// chosen to read on both the light and dark SourceHut themes. They
			// are applied by the template as thin accents (card border, lane
			// underline, tinted chips), never as body text, so contrast holds.
			{Name: "Rolling", Slug: "rolling", Accent: "#c9930a", Issues: rolling},
			{Name: "Lined Up", Slug: "lined-up", Accent: "#2f9e44", Issues: linedUp},
			{Name: "Stalled", Slug: "stalled", Accent: "#9c36b5", Issues: stalled},
			{Name: "Past Stand", Slug: "past-stand", Accent: "#868e96", Issues: pastStand},
		},
		Counts: BeadsCounts{
			Rolling:   len(rolling),


@@ 473,11 477,11 @@ func statusCategory(status string, catByStatus map[string]string) string {
func laneForCategory(cat string) (name, accent string) {
	switch cat {
	case "closed":
		return "Past Stand", "#8a8d91"
		return "Past Stand", "#868e96"
	case "in_progress":
		return "Rolling", "#f5c518"
		return "Rolling", "#c9930a"
	default:
		return "Lined Up", "#2e9e4f"
		return "Lined Up", "#2f9e44"
	}
}


M web/templates/beads.html => web/templates/beads.html +96 -65
@@ 1,81 1,113 @@
{{define "content" -}}
<style>
/* Scoped Mardi Gras palette — inlined here because the scss bundle is not
   rebuilt in dev. Everything is prefixed .beads so it never leaks. */
.beads-marquee {
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: stretch;
  margin-bottom: 1rem;
  border-radius: .4rem; overflow: hidden;
  border: 2px solid #7b2ff7;
  background: linear-gradient(90deg, #2b0a52 0%, #3a1466 50%, #1f5c34 100%);
/* Scoped beads styles — inlined because the scss bundle is not rebuilt in dev.
   Everything is under .beads and driven by CSS variables that mirror the
   core.sr.ht theme (Bootstrap 4) for BOTH the light default and the
   prefers-color-scheme: dark variant sourcehut ships, so the view inherits the
   host theme and stays legible instead of shipping its own palette. Lane hues
   are muted Mardi Gras accents used only as borders/tints — never as body
   text — so contrast holds on either theme. */
.beads {
  --lane-rolling: #c9930a; --lane-linedup: #2f9e44;
  --lane-stalled: #9c36b5; --lane-past: #868e96;
  --bd-bg: #ffffff; --bd-panel: #f8f9fa; --bd-fg: #212529;
  --bd-muted: #6c757d; --bd-border: #dee2e6; --bd-danger: #e03131;
}
@media (prefers-color-scheme: dark) {
  .beads {
    --bd-bg: #1c1f24; --bd-panel: #16181b; --bd-fg: #f1f3f5;
    --bd-muted: #adb5bd; --bd-border: #3a4046; --bd-danger: #ff6b6b;
  }
}
.beads .rolling    { --lane: var(--lane-rolling); }
.beads .lined-up   { --lane: var(--lane-linedup); }
.beads .stalled    { --lane: var(--lane-stalled); }
.beads .past-stand { --lane: var(--lane-past); }
.beads .total      { --lane: var(--bd-muted); }

/* marquee: a flat row of stat cells, color-coded by a thin accent + faint tint */
.beads-marquee { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.25rem; }
.beads-marquee .bead-stat {
  flex: 1 1 auto; min-width: 7rem; padding: .5rem .75rem; color: #fdf6e3;
  text-align: center;
  flex: 1 1 7rem; min-width: 7rem; padding: .5rem .75rem; text-align: center;
  background: var(--bd-panel);
  background: color-mix(in srgb, var(--lane) 8%, var(--bd-panel));
  border: 1px solid var(--bd-border); border-left: 4px solid var(--lane);
  border-radius: .3rem;
}
.beads-marquee .bead-stat .n { display: block; font-size: 1.5rem; font-weight: 700; line-height: 1; }
.beads-marquee .bead-stat .l { display: block; font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; opacity: .85; }
.beads-marquee .bead-stat.total { background: rgba(245,197,24,.18); }
.beads-marquee .bead-stat .n.rolling { color: #f5c518; }
.beads-marquee .bead-stat .n.lined-up { color: #7be0a0; }
.beads-marquee .bead-stat .n.stalled { color: #c9a3ff; }
.beads-marquee .bead-stat .n.past-stand { color: #c9ccd1; }
.beads-marquee .bead-stat .n { display: block; font-size: 1.6rem; font-weight: 700; line-height: 1.1; color: var(--bd-fg); }
.beads-marquee .bead-stat .l { display: block; font-size: .7rem; text-transform: uppercase; letter-spacing: .06em; color: var(--bd-muted); }

.beads-lanes { display: flex; gap: 1rem; overflow-x: auto; padding-bottom: .5rem; }
/* lanes */
.beads-lanes { display: flex; gap: 1rem; overflow-x: auto; padding-bottom: .5rem; align-items: flex-start; }
.beads-lane { flex: 1 0 16rem; min-width: 16rem; }
.beads-lane .lane-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: .4rem .6rem; border-radius: .35rem .35rem 0 0;
  color: #fff; font-weight: 600;
}
.beads-lane .lane-body {
  border: 1px solid rgba(0,0,0,.12); border-top: none;
  border-radius: 0 0 .35rem .35rem; padding: .5rem;
  background: rgba(123,47,247,.03); min-height: 4rem;
  padding: .4rem .6rem; background: var(--bd-panel); color: var(--bd-fg); font-weight: 600;
  border: 1px solid var(--bd-border); border-bottom: 3px solid var(--lane);
  border-radius: .3rem .3rem 0 0;
}
.beads-lane .lane-count {
  background: rgba(255,255,255,.25); border-radius: 1rem;
  padding: 0 .5rem; font-size: .8rem;
  background: var(--lane); color: #fff; border-radius: 1rem;
  padding: 0 .5rem; font-size: .78rem; font-weight: 600; min-width: 1.4rem; text-align: center;
}
.beads-lane .lane-body {
  border: 1px solid var(--bd-border); border-top: none;
  border-radius: 0 0 .3rem .3rem; padding: .5rem; min-height: 3rem;
}
.bead-card {
  display: block; background: #fff; border: 1px solid rgba(0,0,0,.12);
  border-left: 4px solid #7b2ff7; border-radius: .3rem;
  padding: .5rem .6rem; margin-bottom: .5rem; text-decoration: none; color: inherit;
  display: block; background: var(--bd-bg); color: var(--bd-fg);
  border: 1px solid var(--bd-border); border-left: 3px solid var(--lane);
  border-radius: .25rem; padding: .5rem .6rem; margin-bottom: .5rem; text-decoration: none;
}
.bead-card:hover { border-color: #7b2ff7; box-shadow: 0 1px 4px rgba(123,47,247,.25); text-decoration: none; }
.bead-card .card-id { font-family: monospace; font-size: .75rem; color: #7b2ff7; }
.bead-card .card-title { display: block; font-weight: 600; margin: .1rem 0 .25rem; }
.bead-card:hover { border-color: var(--lane); box-shadow: 0 1px 4px rgba(0,0,0,.18); text-decoration: none; color: var(--bd-fg); }
.bead-card .card-id { font-family: monospace; font-size: .75rem; color: var(--bd-muted); }
.bead-card .card-title { display: block; font-weight: 600; margin: .1rem 0 .3rem; color: var(--bd-fg); }
.bead-card .card-meta { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }

/* pills — neutral by default; semantic variants use tints that resolve against
   the current theme's background so the text (var(--bd-fg)) always reads */
.bead-pill {
  font-size: .68rem; padding: .05rem .4rem; border-radius: 1rem;
  border: 1px solid rgba(0,0,0,.15); background: #f3f3f3;
  display: inline-block; font-size: .68rem; line-height: 1.5; padding: .02rem .45rem;
  border-radius: 1rem; white-space: nowrap;
  background: var(--bd-panel); border: 1px solid var(--bd-border); color: var(--bd-fg);
}
.bead-pill.prio { background: #f5c518; border-color: #d9ad0a; color: #3a2d00; font-weight: 700; }
.bead-pill.type { background: #ece3ff; border-color: #cbb6ff; color: #46248a; }
.bead-pill.assignee { background: #e3f4ea; border-color: #a7d9bd; color: #1d5c37; }
.bead-pill.label { background: #f0f0f0; }
.bead-pill.block { background: #ffe9e0; border-color: #f0b49b; color: #8a3311; }
.beads-empty { color: #888; font-size: .85rem; font-style: italic; padding: .25rem; }
.bead-pill.prio { background: var(--lane-rolling); border-color: var(--lane-rolling); color: #1a1400; font-weight: 700; }
.bead-pill.assignee {
  background: var(--bd-panel);
  background: color-mix(in srgb, var(--lane-linedup) 16%, var(--bd-bg));
  border-color: color-mix(in srgb, var(--lane-linedup) 45%, var(--bd-border));
}
.bead-pill.block {
  background: var(--bd-panel);
  background: color-mix(in srgb, var(--bd-danger) 16%, var(--bd-bg));
  border-color: color-mix(in srgb, var(--bd-danger) 45%, var(--bd-border));
}
.bead-pill.label { color: var(--bd-muted); }
.beads-empty { color: var(--bd-muted); font-size: .85rem; font-style: italic; padding: .25rem; }

.bead-detail .field-label { font-weight: 600; color: #46248a; }
/* detail pane */
.bead-detail .lane-pill {
  display: inline-block; font-size: .72rem; padding: .05rem .5rem; border-radius: 1rem; vertical-align: middle;
  border: 1px solid var(--lane); color: var(--bd-fg);
  background: color-mix(in srgb, var(--lane) 15%, transparent);
}
.bead-detail .field-label {
  color: var(--bd-muted); font-weight: 600; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
}
.bead-detail table td.field-label { white-space: nowrap; width: 9rem; vertical-align: middle; }
.bead-detail pre.field-body {
  white-space: pre-wrap; background: #faf7ff; border: 1px solid #e6dbff;
  border-radius: .3rem; padding: .5rem; margin: .25rem 0 1rem;
  white-space: pre-wrap; background: var(--bd-panel); border: 1px solid var(--bd-border);
  color: var(--bd-fg); border-radius: .25rem; padding: .6rem; margin: .25rem 0 1rem;
}
.bead-comment {
  border-left: 3px solid #2e9e4f; background: #f5faf7;
  background: var(--bd-panel); border: 1px solid var(--bd-border); border-left: 3px solid var(--lane-linedup);
  padding: .4rem .6rem; margin-bottom: .5rem; border-radius: .2rem;
}
.bead-comment .c-head { font-size: .8rem; color: #1d5c37; margin-bottom: .2rem; }
.bead-comment .c-body { white-space: pre-wrap; }
@media (prefers-color-scheme: dark) {
  .bead-card { background: #1e1e24; border-color: #333; color: #e6e6e6; }
  .bead-detail pre.field-body { background: #1e1626; border-color: #3a2a50; color: #e6e6e6; }
  .bead-comment { background: #16211a; }
  .bead-pill { background: #2a2a30; border-color: #444; color: #ddd; }
}
.bead-comment .c-head { font-size: .8rem; color: var(--bd-muted); margin-bottom: .2rem; }
.bead-comment .c-body { white-space: pre-wrap; color: var(--bd-fg); }
</style>

<div class="beads">
<h2><a href="/~{{.Repo.OwnerName}}/{{.Repo.Name}}">~{{.Repo.OwnerName}}/{{.Repo.Name}}</a> &middot; parade</h2>
{{template "viewtabs" (dict "Repo" .Repo "Views" .Views "Current" "beads" "Ref" .Ref)}}



@@ 86,12 118,12 @@
<div class="bead-detail">
  <h3>
    <code>{{.ID}}</code> {{.Title}}
    <span class="bead-pill" style="border-color: {{.Accent}}; color: {{.Accent}};">{{.Lane}}</span>
    <span class="lane-pill" style="--lane: {{.Accent}};">{{.Lane}}</span>
  </h3>
  <div class="card-meta mb-3">
    <span class="bead-pill">status: {{.Status}}</span>
    {{if .Priority}}<span class="bead-pill prio">P{{.Priority}}</span>{{end}}
    {{if .IssueType}}<span class="bead-pill type">{{.IssueType}}</span>{{end}}
    {{if .IssueType}}<span class="bead-pill">{{.IssueType}}</span>{{end}}
    {{if .Assignee}}<span class="bead-pill assignee">@{{.Assignee}}</span>{{end}}
    {{range .Labels}}<span class="bead-pill label">{{.}}</span>{{end}}
  </div>


@@ 162,30 194,28 @@
{{end}}

<div class="beads-marquee">
  <div class="bead-stat"><span class="n rolling">{{.Data.Counts.Rolling}}</span><span class="l">Rolling</span></div>
  <div class="bead-stat"><span class="n lined-up">{{.Data.Counts.LinedUp}}</span><span class="l">Lined Up</span></div>
  <div class="bead-stat"><span class="n stalled">{{.Data.Counts.Stalled}}</span><span class="l">Stalled</span></div>
  <div class="bead-stat"><span class="n past-stand">{{.Data.Counts.PastStand}}</span><span class="l">Past Stand</span></div>
  <div class="bead-stat rolling"><span class="n">{{.Data.Counts.Rolling}}</span><span class="l">Rolling</span></div>
  <div class="bead-stat lined-up"><span class="n">{{.Data.Counts.LinedUp}}</span><span class="l">Lined Up</span></div>
  <div class="bead-stat stalled"><span class="n">{{.Data.Counts.Stalled}}</span><span class="l">Stalled</span></div>
  <div class="bead-stat past-stand"><span class="n">{{.Data.Counts.PastStand}}</span><span class="l">Past Stand</span></div>
  <div class="bead-stat total"><span class="n">{{.Data.Counts.Total}}</span><span class="l">Total</span></div>
</div>

<div class="beads-lanes">
  {{range .Data.Lanes}}
  {{$accent := .Accent}}
  <div class="beads-lane">
    <div class="lane-head" style="background: {{.Accent}};">
  <div class="beads-lane {{.Slug}}">
    <div class="lane-head">
      <span>{{.Name}}</span>
      <span class="lane-count">{{len .Issues}}</span>
    </div>
    <div class="lane-body">
      {{range .Issues}}
      <a class="bead-card" style="border-left-color: {{$accent}};"
         href="/~{{$.Repo.OwnerName}}/{{$.Repo.Name}}/view/beads?issue={{.ID | urlquery}}">
      <a class="bead-card" href="/~{{$.Repo.OwnerName}}/{{$.Repo.Name}}/view/beads?issue={{.ID | urlquery}}">
        <span class="card-id">{{.ID}}</span>
        <span class="card-title">{{.Title}}</span>
        <span class="card-meta">
          {{if .PriorityLabel}}<span class="bead-pill prio">{{.PriorityLabel}}</span>{{end}}
          {{if .Type}}<span class="bead-pill type">{{.Type}}</span>{{end}}
          {{if .Type}}<span class="bead-pill">{{.Type}}</span>{{end}}
          {{if .Assignee}}<span class="bead-pill assignee">@{{.Assignee}}</span>{{end}}
          {{range .Labels}}<span class="bead-pill label">{{.}}</span>{{end}}
          {{if .BlockedBy}}<span class="bead-pill block">&#128679; {{.BlockedBy}}</span>{{end}}


@@ 200,4 230,5 @@
  {{end}}
</div>
{{end}}
</div>
{{- end}}