~bigbes/sr-ht-ecore

36315856aee2ed5ce5527a2c9682b4a25164106b — Eugene Blikh 10 days ago 0dfd1e5
chrome: repo-list partial and fixed-width brand

srht-repo-list renders project listings (repos, databases, spaces) as
the family's event-list cards from a neutral ListItem shape, encoding
the rules the services converged on: h4 title link, visibility as small
muted lowercase text on the right for non-public entries only,
description as a paragraph. The empty state is a parameter.

The brand in srht-nav gains min-width: 15rem so the service switcher
starts at the same x-coordinate on every service — without it the menu
shifts by the width of the red service label when hopping between
services.
3 files changed, 87 insertions(+), 1 deletions(-)

M chrome/chrome.go
M chrome/chrome_test.go
M chrome/templates/chrome.tmpl
M chrome/chrome.go => chrome/chrome.go +18 -0
@@ 139,6 139,24 @@ type Page struct {
	ContainerClass string
}

// ListItem is one project in a listing — a repository, a database, a space.
// Title is the display name ("~owner/name"); Visibility is the service's
// literal enum value ("PUBLIC"/"UNLISTED"/"PRIVATE", "" to render nothing —
// the srht-repo-list partial shows it lowercase, non-public only).
type ListItem struct {
	Href        string
	Title       string
	Visibility  string
	Description string
}

// RepoList is the dot for the srht-repo-list partial: the items, and the
// muted text shown when there are none.
type RepoList struct {
	Items []ListItem
	Empty string
}

// Service is the static half of the chrome, built once at startup. The
// exported fields may be adjusted between NewService and the first Page call
// (they are read, never written, by Page).

M chrome/chrome_test.go => chrome/chrome_test.go +33 -0
@@ 140,6 140,39 @@ func TestNavTemplateEmbeddedPage(t *testing.T) {
	assert.Contains(t, out, "Logged in as")
}

// renderList executes the srht-repo-list partial alone against v.
func renderList(t *testing.T, v RepoList) string {
	t.Helper()
	tpl := MustAttach(template.New("t"))
	tpl, err := tpl.Parse(`{{template "srht-repo-list" .}}`)
	require.NoError(t, err)
	var b strings.Builder
	require.NoError(t, tpl.Execute(&b, v))
	return b.String()
}

func TestRepoListCards(t *testing.T) {
	out := renderList(t, RepoList{Items: []ListItem{
		{Href: "/~a/pub", Title: "~a/pub", Visibility: "PUBLIC", Description: "described"},
		{Href: "/~a/unl", Title: "~a/unl", Visibility: "UNLISTED"},
		{Href: "/~a/prv", Title: "~a/prv", Visibility: "PRIVATE"},
		{Href: "/~a/none", Title: "~a/none"}, // service without visibility (spec spaces)
	}})

	assert.Equal(t, 4, strings.Count(out, `class="event"`))
	assert.Contains(t, out, "<p>described</p>")
	// PUBLIC and empty visibility render no label; the others render lowercase.
	assert.Equal(t, 1, strings.Count(out, ">unlisted</small>"))
	assert.Equal(t, 1, strings.Count(out, ">private</small>"))
	assert.Equal(t, 2, strings.Count(out, "pull-right"))
}

func TestRepoListEmptyState(t *testing.T) {
	out := renderList(t, RepoList{Empty: "No databases yet."})
	assert.NotContains(t, out, "event-list")
	assert.Contains(t, out, "No databases yet.")
}

func TestFuncs(t *testing.T) {
	assert.Equal(t, "12345678", ShortSHA("1234567890abcdef"))
	assert.Equal(t, "abc", ShortSHA("abc"))

M chrome/templates/chrome.tmpl => chrome/templates/chrome.tmpl +36 -1
@@ 16,8 16,43 @@
{{end}}
{{- end}}

{{/*
  srht-repo-list renders a listing of projects (repos, databases, spaces) as
  the family's event-list cards. Dot is a chrome.RepoList. The rules the four
  services converged on: h4 title link; visibility as small muted text on the
  right, non-public only, lowercase; description as a paragraph when present.
*/}}
{{define "srht-repo-list" -}}
{{if .Items}}
<div class="event-list">
  {{range .Items}}
  <div class="event">
    <h4>
      <a href="{{.Href}}">{{.Title}}</a>
      {{if and .Visibility (ne .Visibility "PUBLIC")}}
      <small class="pull-right">{{if eq .Visibility "UNLISTED"}}unlisted{{else}}private{{end}}</small>
      {{end}}
    </h4>
    {{if .Description}}
    <p>{{.Description}}</p>
    {{end}}
  </div>
  {{end}}
</div>
{{else}}
<p class="text-muted">{{.Empty}}</p>
{{end}}
{{- end}}

{{define "srht-nav" -}}
<span class="navbar-brand">
{{/*
  The brand carries a fixed min-width so the service switcher starts at the
  same x-coordinate on every service: without it the menu shifts by the width
  of the red service label ("dolt" vs "compare") when hopping between
  services. Sized for the longest label on the instance; inline because the
  services build their CSS from the shared core tree and ecore ships none.
*/}}
<span class="navbar-brand" style="min-width: 15rem">
  <span class="icon icon-circle" aria-hidden="true"><svg width="22" height="22" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"/></svg></span>
  <a class="navbar-brand" href="/">
    {{.SiteName}}