~bigbes/sr-ht-dolt

51a5acf935a1f7fbcf879fb9f748331e5a27a559 — Eugene Blikh 30 days ago 76bf70d
style(web): restyle the beads parade in the todo.sr.ht idiom

The board still read as a rounded-card kanban from another app. Re-skin
every surface in sourcehut's own issue-tracker language (todo.sr.ht):
flat and square — no border-radius, no shadows, no tints — hairline gray
borders, monospace ids, striped-on-hover rows instead of cards, and
squared bordered labels (the sourcehut .label: 1px border, no radius).
The summary becomes a flat hairline strip and each lane a squared column
with a 2px accent cap. The Mardi Gras hues survive only as small square
swatches and the lane cap, never as fills, so the view looks like a
native sourcehut tracker in both the light and dark themes. Verified via
a headless light/dark render. Test asserts the renamed summary class.
2 files changed, 96 insertions(+), 109 deletions(-)

M web/beads_test.go
M web/templates/beads.html
M web/beads_test.go => web/beads_test.go +1 -1
@@ 299,7 299,7 @@ func TestBeadsHandleViewBoard(t *testing.T) {
		t.Fatalf("board: got %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{"Rolling", "Lined Up", "Stalled", "Past Stand", "Ready to roll", "beads-marquee"} {
	for _, want := range []string{"Rolling", "Lined Up", "Stalled", "Past Stand", "Ready to roll", "beads-summary"} {
		if !strings.Contains(body, want) {
			t.Errorf("board body missing %q", want)
		}

M web/templates/beads.html => web/templates/beads.html +95 -108
@@ 1,22 1,26 @@
{{define "content" -}}
<style>
/* 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
   The look deliberately mimics todo.sr.ht (sourcehut's own issue tracker): flat
   and square (no radius, no shadows, no gradients), hairline gray borders,
   monospace ids, and squared bordered labels. Colours come from CSS variables
   that mirror core.sr.ht's Bootstrap palette for 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. */
   host theme. The Mardi Gras lanes survive only as muted accent swatches and a
   2px lane cap — never saturated fills — so it reads as sourcehut, not a
   separate app. */
.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;
  --bd-bg: #ffffff; --bd-panel: #f2f3f5; --bd-fg: #212529;
  --bd-muted: #6c757d; --bd-border: #ced4da; --bd-stripe: rgba(0,0,0,.05);
  --bd-danger: #b52a2a;
}
@media (prefers-color-scheme: dark) {
  .beads {
    --bd-bg: #1c1f24; --bd-panel: #16181b; --bd-fg: #f1f3f5;
    --bd-muted: #adb5bd; --bd-border: #3a4046; --bd-danger: #ff6b6b;
    --bd-bg: #212529; --bd-panel: #343a40; --bd-fg: #dee2e6;
    --bd-muted: #adb5bd; --bd-border: #6c757d; --bd-stripe: rgba(255,255,255,.05);
    --bd-danger: #ff6b6b;
  }
}
.beads .rolling    { --lane: var(--lane-rolling); }


@@ 25,84 29,68 @@
.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 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;
/* a small square colour chip standing in for the lane's Mardi Gras hue */
.beads .swatch {
  display: inline-block; width: .7rem; height: .7rem; flex: 0 0 auto;
  background: var(--lane); border: 1px solid rgba(0,0,0,.25); vertical-align: -1px;
}
.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); }

/* 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; 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: 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;
/* summary: one flat hairline-bordered strip, cells divided by hairlines */
.beads-summary { display: flex; flex-wrap: wrap; border: 1px solid var(--bd-border); margin-bottom: 1rem; }
.beads-summary .stat {
  flex: 1 1 7rem; min-width: 6.5rem; padding: .35rem .6rem;
  border-left: 1px solid var(--bd-border);
}
.bead-card {
  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: 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; }
.beads-summary .stat:first-child { border-left: none; }
.beads-summary .stat .n { font-size: 1.35rem; font-weight: 700; line-height: 1.1; }
.beads-summary .stat .l { display: flex; align-items: center; gap: .35rem; font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--bd-muted); }

/* 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 {
  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: 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));
/* lanes: flat squared columns with a 2px accent cap */
.beads-lanes { display: flex; gap: .75rem; overflow-x: auto; padding-bottom: .5rem; align-items: flex-start; }
.beads-lane { flex: 1 0 15rem; min-width: 15rem; }
.beads-lane .lane-head {
  display: flex; align-items: center; gap: .4rem; padding: .3rem .5rem; font-weight: 700;
  background: var(--bd-panel); color: var(--bd-fg);
  border: 1px solid var(--bd-border); border-top: 2px solid var(--lane);
}
.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));
.beads-lane .lane-head .count { margin-left: auto; color: var(--bd-muted); font-weight: 400; font-size: .85rem; }
.beads-lane .lane-body { border: 1px solid var(--bd-border); border-top: none; }

/* issue rows: todo.sr.ht ticket-list feel — hairline separated, no radius */
.bead-row { display: block; padding: .3rem .5rem; text-decoration: none; color: var(--bd-fg); border-top: 1px solid var(--bd-border); }
.bead-row:first-child { border-top: none; }
.bead-row:hover { background: var(--bd-stripe); text-decoration: none; color: var(--bd-fg); }
.bead-row .r-id { font-family: monospace; font-size: .72rem; color: var(--bd-muted); }
.bead-row .r-title { display: block; margin: .05rem 0 .2rem; color: var(--bd-fg); }
.bead-row .r-meta { display: flex; flex-wrap: wrap; gap: .25rem; align-items: center; }
.beads-empty { color: var(--bd-muted); font-size: .8rem; font-style: italic; padding: .35rem .5rem; }

/* labels: sourcehut .label — squared, hairline-bordered, transparent */
.blabel {
  display: inline-block; font-size: .7rem; line-height: 1.5; padding: 0 .3rem; white-space: nowrap;
  border: 1px solid var(--bd-border); background: transparent; color: var(--bd-fg);
}
.bead-pill.label { color: var(--bd-muted); }
.beads-empty { color: var(--bd-muted); font-size: .85rem; font-style: italic; padding: .25rem; }
.blabel.prio { border-color: var(--lane-rolling); color: var(--bd-fg); font-weight: 700; }
.blabel.assignee { border-color: var(--lane-linedup); }
.blabel.block { border-color: var(--bd-danger); color: var(--bd-danger); }
.blabel.muted { color: var(--bd-muted); }

/* 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 .ticket-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; margin: 0 0 .5rem; }
.bead-detail .ticket-title code { font-size: 1.1rem; }
.bead-detail .lane-label { border: 1px solid var(--lane); color: var(--bd-fg); padding: 0 .4rem; font-size: .75rem; }
.bead-detail .lane-label .swatch { margin-right: .3rem; }
.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: var(--bd-panel); border: 1px solid var(--bd-border);
  color: var(--bd-fg); border-radius: .25rem; padding: .6rem; margin: .25rem 0 1rem;
}
.bead-comment {
  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;
  color: var(--bd-fg); padding: .5rem; margin: .25rem 0 1rem;
}
.bead-deplist { list-style: none; padding-left: 0; margin-bottom: 0; }
.bead-deplist li { padding: .2rem 0; border-top: 1px solid var(--bd-border); }
.bead-deplist li:first-child { border-top: none; }
.bead-deplist code { font-size: .8rem; }
.bead-comment { border: 1px solid var(--bd-border); border-left: 2px solid var(--lane-linedup); padding: .4rem .6rem; margin-bottom: .5rem; }
.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>


@@ 116,16 104,16 @@
<p><a href="/~{{.Repo.OwnerName}}/{{.Repo.Name}}/view/beads">&larr; Back to the parade</a></p>
{{with .Data.Issue}}
<div class="bead-detail">
  <h3>
    <code>{{.ID}}</code> {{.Title}}
    <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">{{.IssueType}}</span>{{end}}
    {{if .Assignee}}<span class="bead-pill assignee">@{{.Assignee}}</span>{{end}}
    {{range .Labels}}<span class="bead-pill label">{{.}}</span>{{end}}
  <h2 class="ticket-title">
    <code>{{.ID}}</code> <span>{{.Title}}</span>
    <span class="lane-label" style="--lane: {{.Accent}};"><span class="swatch"></span>{{.Lane}}</span>
  </h2>
  <div class="r-meta mb-3">
    <span class="blabel muted">status: {{.Status}}</span>
    {{if .Priority}}<span class="blabel prio">P{{.Priority}}</span>{{end}}
    {{if .IssueType}}<span class="blabel">{{.IssueType}}</span>{{end}}
    {{if .Assignee}}<span class="blabel assignee">@{{.Assignee}}</span>{{end}}
    {{range .Labels}}<span class="blabel muted">{{.}}</span>{{end}}
  </div>
  <table class="table table-sm">
    <tbody>


@@ 146,13 134,13 @@
  <div class="col-md-6">
    <h4>Depends on</h4>
    {{if $.Data.DependsOn}}
    <ul>
    <ul class="bead-deplist">
      {{range $.Data.DependsOn}}
      <li>
        <a href="/~{{$.Repo.OwnerName}}/{{$.Repo.Name}}/view/beads?issue={{.IssueID | urlquery}}"><code>{{.IssueID}}</code></a>
        {{if .Title}}— {{.Title}}{{end}}
        <span class="bead-pill">{{.Type}}</span>
        {{if .Closed}}<span class="bead-pill">closed</span>{{else}}<span class="bead-pill block">{{.Status}}</span>{{end}}
        <span class="blabel muted">{{.Type}}</span>
        {{if .Closed}}<span class="blabel muted">closed</span>{{else}}<span class="blabel block">{{.Status}}</span>{{end}}
      </li>
      {{end}}
    </ul>


@@ 161,12 149,12 @@
  <div class="col-md-6">
    <h4>Depended on by</h4>
    {{if $.Data.DependedOnBy}}
    <ul>
    <ul class="bead-deplist">
      {{range $.Data.DependedOnBy}}
      <li>
        <a href="/~{{$.Repo.OwnerName}}/{{$.Repo.Name}}/view/beads?issue={{.IssueID | urlquery}}"><code>{{.IssueID}}</code></a>
        {{if .Title}}— {{.Title}}{{end}}
        <span class="bead-pill">{{.Type}}</span>
        <span class="blabel muted">{{.Type}}</span>
      </li>
      {{end}}
    </ul>


@@ 193,33 181,32 @@
<div class="alert alert-warning">Showing the first {{.Data.Total}} of {{.Data.ShownOf}} issues.</div>
{{end}}

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

<div class="beads-lanes">
  {{range .Data.Lanes}}
  <div class="beads-lane {{.Slug}}">
    <div class="lane-head">
      <span>{{.Name}}</span>
      <span class="lane-count">{{len .Issues}}</span>
      <span class="swatch"></span>{{.Name}}<span class="count">{{len .Issues}}</span>
    </div>
    <div class="lane-body">
      {{range .Issues}}
      <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}}</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}}
          {{if .Blocks}}<span class="bead-pill">blocks {{.Blocks}}</span>{{end}}
      <a class="bead-row" href="/~{{$.Repo.OwnerName}}/{{$.Repo.Name}}/view/beads?issue={{.ID | urlquery}}">
        <span class="r-id">{{.ID}}</span>
        <span class="r-title">{{.Title}}</span>
        <span class="r-meta">
          {{if .PriorityLabel}}<span class="blabel prio">{{.PriorityLabel}}</span>{{end}}
          {{if .Type}}<span class="blabel">{{.Type}}</span>{{end}}
          {{if .Assignee}}<span class="blabel assignee">@{{.Assignee}}</span>{{end}}
          {{range .Labels}}<span class="blabel muted">{{.}}</span>{{end}}
          {{if .BlockedBy}}<span class="blabel block">&#128679; {{.BlockedBy}}</span>{{end}}
          {{if .Blocks}}<span class="blabel muted">blocks {{.Blocks}}</span>{{end}}
        </span>
      </a>
      {{else}}